iOS TestFlight Deploy With GitHub Actions
I’ve just started a new iPhone project in my personal time, and I thought it’d be nice to get a CD pipeline setup. The idea is to get each PR merge out to my victims friends who are kindly testing my buggy code via TestFlight. I thought this would be pretty straightforward (it has been with Azure Pipelines in the past!), but for added fun I thought I’d do it with GitHub Actions. Easy, right?
Turns out… not so much! 😅
After a week and many hours cursing, I created a workflow that nailed it. 🎉 In the interest of giving back to the community, I’ve documented the steps (and the YAML
workflow I came up with) so you too can follow along and join in on the sweet DevOps goodness. The workflow makes a new build ready for release, increments the version number, uses the last PR merge commit message as the test release notes, and sends it off for distribution to a group we nominate.
Whilst my solution is for Xamarin.iOS, this should cover 95% of what you need to for any release procedure that needs to build an .ipa
package and release it via TestFlight.
How’d I do this
You’re going to need a few things:
- An Apple Developer Account with a paid Developer Program subscription
- An app registration setup in App Store Connect with at least one build that has been released externally already (that is, to an external testing group that you have defined)
- Access to a macOS device to generate distribution certificates with private keys, if you don’t have one already
- A GitHub repo - public or private, it’ll still work!
What you won’t need is a non-2FA Apple ID like most iOS CI approaches require, so that’s a nice bonus!
Prerequisite #1 - Distribution Certificate & Provisioning Profile
You will need to generate a .p12
Distribution Certificate and a App Store Distribution Provisioning Profile using the Developer Portal. There are plenty of guides on how to get a p12
export going - I used this guide to do that. Note down the passphrase you use to export the certificate - you’ll need it later.
Once you have the certificate export, you’ll need to export it as base64
for later.
openssl base64 -in Certificates.p12 -out Certificates.txt
If you haven’t got an App Store Distribution Profile, make one now, and ensure your distribution certificate is included. You won’t need to download it though.
Prerequisite #2 - App Store Connect API Key
Next you will need to generate an App Store Connect API key with the App Manager access role, if you don’t have one already. You can do that from the Users and Access - Keys section of App Store Connect.
Once made, you should make a note of the Issuer ID, Key ID and download the private key using the Download API Key link. You’ll need all three of these things later. If you run into trouble, the Apple docs on generating API keys are a good place to start.
Prerequisite #3 - App Specific Password
In order to not get 2FA prompts when our release is happening, we’ll need an app-specific password to upload builds to App Store Connect. You can set one up via Apple Account Management - and like the others, note it down for later.
Prerequisite #4 - App ‘Apple ID’
Last but not least, you’ll need your app’s Apple ID. No, not your own Apple ID in email form - a numerical number that identifies your application inside App Store Connect. Navigate to the Apps section in App Store Connect and select App Information on the left - you’ll find the Apple Id under the General Information pane. That’s the last thing you need!
Building the GitHub Action Workflow
As of time of writing, the App Connect API is a bit flaky. It has this habit of returning 403
2-7 times in a row before actually letting you in, even though your credentials are fine. I had to hack around this - forgive the clunkiness!
Phew. Now that we’ve collected all the required parts, you can start on the action itself. Firstly head to your repo’s Secrets section in GitHub, and add the following secrets in:
Name | Value |
---|---|
APPCONNECT_API_ISSUER | Issuer ID from #2 |
APPCONNECT_API_KEY_ID | Key ID from #2 |
APPCONNECT_API_KEY_PRIVATE | Contents of the .p8 file downloaded in #2 |
APPLE_ID | ID from #4 |
DIST_CERT_BASE64 | Contents of the .txt file generated in #1 |
DIST_CERT_P12_PASSWORD | Passphrase from #1 |
FASTLANE_APP_PASSWORD | The app-specific password in #3 |
FASTLANE_USERNAME | The email associated with the Apple Account you’ve been using |
To top it all off, create a workflow file in your repo that lives in .github/workflows/testflight.yaml
. I called it testflight
- you can call it whatever you like.
The workflow in words is: On push to master
, perform the following on the latest macOS agent:
- Checkout code with full history
- Make a temporary keychain to hold the distribution certificate
- Build a bearer token for the Connect API
- ‘Prime’ the API (see note above)
- Import the right provisioning profile
- Bump our build versions to use the build number
- Do the actual build. NB: This is Xamarin.iOS for me - replace with your build tool of choice
- Record the built
.ipa
as a build artefact in case we need it in the future - Generate release notes from our PR merge history
- Push the build to TestFlight
- Instruct TestFlight to release the build to our chosen external group
Step 11 assumes you have not set export compliance in your PList and you intend to nominate that you use non-exempt encryption. Read more here if you’re unsure.
Show me the YAML
Here it is, in all it’s glory. You should change the following to your own values:
bundle-id
GROUP_NAME
Here be dragons! 🐉🐉🐉 Be warned there’s a wide array of language use going on.
And that’s it! 🎉 Some of the above is pretty darn clunky, particularly the sprinkling of Python and Bash, but it works quite well. I’d like GitHub Actions to support more tasks out of the box like Azure Pipelines does, and overall the experience of the Actions Marketplace left a bit to be desired - but that’s for another post!
If you’d like more understanding of how the App Store Connect API works, I’d recommend checking out the API docs - it does more than just poking TestFlight! And lastly, any suggestions on how to improve the workflow would be more than welcome. ❤
Happy automating! 🚀