So you’ve built an iOS app and deployed a bunch of updates to your users manually from your laptop. Maybe your team is growing, and being “the release person” – the only one with the required keys and ability to archive and ship builds – is getting a bit old. Maybe your team is also realizing that it’s an unnecessary bottleneck and single point of failure and that it’s time to automate your build process so that anyone can create App Store builds, confidently and without breaking anything. Enter CI/CD.
But spinning up a build pipeline isn’t straightforward, and if you’re a busy, growing team you probably can’t afford to spend a week figuring out which CI provider is the best and poring over documentation to cobble together a functional workflow. We’re here to help! We’ve done this before, and have collected the out-of-the-box building blocks that will save you having to waste time figuring out all the pieces and reinventing the wheel.
Let’s get started!
First off, this tutorial will assume a few things:
By the end of this tutorial, you’ll be able to build & upload iOS binaries for your app by triggering a workflow in GitHub Actions with the click of a button (or by setting up automatic triggers)!
fastlane is a Ruby library created to automate common mobile development tasks. Using fastlane, you can configure custom “lanes” which bundle a series of “actions” that perform tasks that you’d normally perform using Xcode or xcodebuild. You can do a lot with fastlane, but for the purposes of this tutorial, we’ll be using only a handful of core actions.
Install fastlane using one of the recommended installation methods. Here we’ll use Bundler, running <code>brew install fastlane<code> from our main app project directory.
Then, we’ll run <code>fastlane init<code> and choose the “manual setup” option when prompted.
You’ll end up with a fastlane directory, a Fastfile, and an Appfile.
We’ll first modify our Appfile, filling in our app’s bundle identifier and removing everything else:
Next, we’ll set up some basic actions in our Fastfile.
First, we’ll need to load in our App Store Connect API key. This is how we’ll authenticate with the App Store Connect API so we can download provisioning profiles, upload binaries to TestFlight, and more. We’ll go over how to create your API key and add it to GitHub’s encrypted secrets later on in this tutorial.
Add the <code>:load_asc_api_key<code> lane to your Fastfile:
You’ll notice we’re passing in all the arguments as environment variables. We’ll set these up as GitHub environment secrets later.
Next, add the following lanes to your Fastfile:
Once you have these lanes set up, you can tie them all together in a single “build and upload” lane that we’ll call from the CI workflow:
Let’s break it down. The first thing we do is load the ASC API key that is used in all subsequent fastlane actions so we can authenticate and use the ASC API. Then, we’ll call the “prepare_signing” lane to check signing certificates and download provisioning profiles from the ASC API. Note that we’ll import the private key separately using GitHub Actions.
We’ll then increment the build number based on the latest build number found on TestFlight for the version. Finally we’re ready to build the app. “build_release” will build the Xcode project in the “Release” configuration using the distribution provisioning profiles & signing certificates, and the “app-store” distribution method.
Lastly, “upload_release” will take the <code>.ipa<code> file generated by the “build_release” action and upload it to TestFlight.
And that’s it! With these six actions, fastlane will handle bumping the build number, building, and uploading binaries to App Store Connect. Now, let’s hook it up to GitHub Actions.
In order to authenticate with the ASC API, we’ll need an API key along with a couple of other details. These are considered sensitive, which means we’ll need to store them securely in a place where they can be accessed by our GitHub workflows. Enter GitHub’s encrypted secrets: we’ll be storing all our sensitive keys in repository secrets, making them automatically accessible to our GitHub Actions workflows.
If you need to create a new ASC API key, follow these steps. We’ll be adding the Issuer ID, the Key ID, and the <code>p8<code> private key to GitHub’s encrypted secrets.
To add a new secret to GitHub’s encrypted secrets, first navigate to the iOS repo which you’ll be adding the GitHub workflow to. On the far right, click “Settings”.
Then, click “Secrets” from the list in the left menu.
From here, click “New repository secret” to add a new secret:
When you click “New repository secret”, you’ll see a form that will prompt you to enter a name for your new secret, and the value.
GitHub secrets only accepts strings, so for certain credentials (any <code>.p8<code> or <code>.p12<code> files for example), you’ll first need to convert the file to a base64-encoded string before adding it to GitHub secrets. You can do this from the command line:
This copies the resulting string to your clipboard, so you can paste it directly into a new repository secret on GitHub.
Let's create new repository secrets as follows:
Please note that you should store a backup copy of your secrets securely in another location (somewhere that is not GitHub encrypted secrets), as you won’t be able to export or access the credentials again from GitHub after you’ve added them.
With these secrets added to GitHub’s repository secrets, we can authenticate with the ASC API within the GitHub workflow we’ll create to run our builds.
In order to properly sign App Store distribution builds on CI, the workflow will need access to a valid App Store distribution certificate and private key pair. If you already use “fastlane match” to manage your App Store signing certificates, then you can skip this step and call fastlane match from your Fastfile instead. If you still manage your signing certificates manually, then you’ll need to add the App Store distribution signing certificate & private key (<code>.p12<code>) to your repository secrets:
With these secrets added to GitHub’s repository secrets, we’re ready to set up our GitHub Actions workflow to run our builds.
Let’s set up our iOS GitHub actions workflow <code>.yml<code> file – it’ll define the steps we’ll run as part of our workflow. Within these steps, we’ll call our fastlane lanes.
First let’s create the necessary folders. From your project’s root directory call:
Then, paste the following code into your newly created <code>build-upload-ios.yml<code> file:
Let’s break down what this workflow is doing:
This line defines the trigger for the workflow as a manual workflow trigger. GitHub Actions workflows support a number of triggers; for example, if you want the workflow to run any time a specific branch has code pushed up to it, you would define it like so:
Our workflow defines a single job, called “deploy”, which runs on the latest macOS machine version supported by GitHub workflows. The job has a series of steps, marked with a dash “-”. Some of these steps will use predefined “actions”, which can either be provided by GitHub (e.g. <code>actions/checkout@v2<code>), or available as open source actions created and supported by the community through GitHub’s Marketplace.
The first step is checking out the codebase using GitHub’s <code>checkout<code> action. Then, we install our dependencies. Since we’re using fastlane, which is a Ruby gem, we’ll define a Ruby environment with the option <code>bundler-cache: true<code>, which will automatically run <code>bundle install<code> and cache installed gems. <code>bundle install<code> will install fastlane and any other Ruby dependencies in your project.
Next, we’ll need to import our distribution code signing certificate into the machine’s keychain. We’ll do this using an open source GitHub action called “import-codesign-certs”, and passing in our base64-encoded App Store distribution code signing certificate (and its corresponding password) which are stored in GitHub’s repository secrets. This will allow us to properly sign the build we’ll be creating in the next step.
Finally, we can call our main fastlane action, “build_upload_testflight”, passing in the required ASC API key information (also stored in GitHub’s repository secrets), and the keychain path where the signing certificate was stored in the previous step. Our fastlane action will take care of downloading the necessary provisioning profiles, updating the build number, building the app, and uploading the resulting <code>.ipa<code> to TestFlight.
The last step in the workflow uploads the produced artifacts (the <code>.ipa<code> file and its associated dSYM files) so they can be viewed in GitHub as part of the workflow. If you don’t have any use for the build’s artifacts, you can remove this step from the workflow.
Now, you can commit and push up your newly created GitHub workflow file. It’ll show up on your repo under the “Actions” tab.
Once you’ve pushed up your GitHub workflow file, you’ll be able to trigger your workflow directly from GitHub’s UI. Simply find your workflow in the “Actions” tab, and click “Run workflow”:
Choose a base branch to run the workflow from, and click “Run workflow” to try out your workflow!
And that’s it!
Using fastlane combined with GitHub Actions, we’ve put together a simple CI pipeline that increments build numbers, creates a distribution <code>.ipa<code>, and uploads the <code>.ipa<code> to TestFlight. Anyone that has access to your GitHub repository can now trigger the workflow directly from GitHub, or you can set a workflow trigger to automatically kick off a workflow run any time pushes are made to certain branches. With a basic build & deploy CI pipeline now in place, anyone on the team is empowered to create and upload builds, removing a common bottleneck while also increasing reliability and consistency.
Questions or issues? Get in touch!