Indie Mac App DevOps With GitHub Actions
By: David Kramer
Tags: Software Development, Mac, Programming, Macos, DevOps

Continuous Deployment

The second piece of the automation we create is the continuous deployment: publishing our app after each release tag. We use the convention that any tag that begins with the letter “ v ” is a release tag — e.g. v1.2.0 .

We run xcodebuild install in the GitHub Actions environment on a macOS host to build and sign the project, and then we package our app up into a disk image, notarize it, upload it as a release asset, and finally update our Homebrew tap with the lastest version information.

By putting the following GitHub Actions workflow in our project workspace at .github/workflows/master_deploy.yml we enable continuous deployment:

There’s a lot more going with this workflow than the previous one, but the two start off similarly. This workflow runs every time we push new tags to the repository that start with the letter “ v ”. It includes a single job that runs on a macOS host. The job includes eleven steps.

As before, the first step checks out the project. This time, the tag that triggered the workflow is the tag that’s checked out.

Then, the default scheme of the project is determined and set as an environment variable, as before.

Next, additional build settings from the project are read using xcodebuild -showBuildSettings and set as environment variables for use by subsequent steps. This avoids the need to hard-code project details in the workflow and makes it easier to re-use the workflow with other projects.

The next step is to set up a macOS keychain on the action runner host that contains our app’s signing credentials because Xcode expects to find the signing identity in the default keychain. The step uses the SIGNING_CERTIFICATE_P12_DATA and SIGNING_CERTIFICATE_PASSWORD secrets and the security tool to import the signing certificate. (See below for information about how to set up the secrets.) This is the most complicated and least documented step of this process. The answers to this StackOverflow question provide some context.

Now we test the app. We never want to release a version of the app that fails tests, so we make sure the tests pass before we move on. If your project does not include tests, omit this step.

Once the app has been tested the next step rebuilds it for installation and then install the app into a distribution root ( DSTROOT ).

Upon successful completion of the install step, the app is packaged into a disk image using hdiutil .

The next step in the workflow notarizes the disk image using the notarize-cli tool. This is a tool I wrote to simplify notarization of Mac apps in the context of continuous deployment. It wraps two tools provided with Xcode — altool and stapler . It uses those tools to upload the disk image to Apple, wait for success, and then staple the notarization to the disk image. This step uses the NOTARIZE_USERNAME and NOTARIZE_PASSWORD secrets to authenticate to Apple. (See below for information about how to set up the secrets.)

The app is ready to be deployed. The next step is to deliver the notarized disk image to GitHub as a release asset. We use the softprops/action-gh-release action from the GitHub marketplace to perform this action for us. This step uses the GITHUB_TOKEN secret which is automatically set up for us by GitHub Actions. This token is scoped to allow us to make modifications only to the repository that hosts the running workflow.

At this point, the app has been delivered. Someone poking around on GitHub could find the released app and download it, but who has time for that? To finish deployment we still need to update a Homebrew tap. Popular apps can be hosted in the homebrew-cask repository, but if your app doesn’t have an audience yet it won’t be accepted for inclusion in the official tap — you’ll need to host your own. Fortunately, hosting your own tap is easy and Homebrew supports installing from 3rd-party taps seamlessly. (See below for information about how to set up a tap and cask for use with this workflow.)

The next step in the workflow checks out the tap. This step uses the CASK_REPO and CASK_REPO_TOKEN secrets. (See below for information about how to set up the secrets.) The cask repo token is saved in the checked-out workspace and is used to push changes back to the tap repository in the next step.

The final step is to update the cask with the app’s new version number and the hash value of the release asset. The version number is determined by the release tag that triggered the workflow, and the hash is calculated from the notarized disk image. The commit author is set to the user that triggered the workflow, and the changes are committed and pushed back to the tap repository.