Build and deploy your iOS app to TestFlight with GitHub Actions and Fastlane
Tags: Github Actions, Continuous Delivery, Testflight, iOS, Fastlane

Steps to follow in the post

Create App Store Connect API Setup Fastlane environment (create an App ID, an App in iTunes Connect..) Setup Github Actions (set secrets) Configure Github workflow yml file Trigger workflow

1. Using App Store Connect API with Fastlane Match

Starting February 2021, two-factor authentication or two-step verification will be required for all users to sign in to App Store Connect. This extra layer of security for your Apple ID helps ensure that you’re the only person who can access your account.

From Apple Support

Right now, Apple is enforcing two-factor authentication (2FA) for all accounts. If you don’t use any Continuous Integration (CI) and always submit your app via Xcode, you have nothing to worry about. You can check out Apple support and enabled 2FA if you don’t already have it enabled.

But if you use Fastlane with a username and password authentication for your CI, it is time for the change.

Apple announced the App Store Connect API back in WWDC18. It provides an official way to interact with App Store Connect, and Fastlane already supports this. Let’s see what we need to do to adopt it.

Requirements

To be able to use App Store Connect API, Fastlane needs three things.

Issuer ID. Key ID. Key file or Key content.

Creating an App Store Connect API Key

To generate keys, you must have Admin permission in App Store Connect. If you don’t have that permission, you can direct the relevant person to this article and follow the following instructions.

1 — Log in to App Store Connect.

2 — Select Users and Access.

3 — Select the API Keys tab.

4 — Click Generate API Key or the Add (+) button.

5 — Enter a name for the key. The name is for your reference only and is not part of the key itself.

6 — Under Access, select the role for the key. The roles that apply to keys are the same roles that apply to users on your team. See role permissions.

7 — Click Generate.

An API key’s access cannot be limited to specific apps.

The new key’s name, key ID, a download link, and other information appear on the page.

You can grab all three necessary information here.

<1> Issue ID.

<2> Key ID.

<3> Click “Download API Key” to download your API private key. The download link appears only if the private key has not yet been downloaded. Apple does not keep a copy of the private key. So you can download it only once.

🔴 Store your private key in a safe place. You should never share your keys, store keys in a code repository, or include keys in client-side code.

Using an App Store Connect API Key

The API Key file (p8 file that you download), the key id, and the issuer id are needed to create the JWT token for authorization. There are multiple ways that these pieces of information can be input into Fastlane using Fastlane’s new action, app_store_connect_api_key . You can learn other ways in Fastlane documentation. I show this method because I think it is the easiest way to work with most CI out there, where you can set environment variables.

Now we can manage Fastlane with the App Store Connect API key, great!

2. Setup Fastlane

1 — Initialize Fastlane for iOS

cd your_project_path

fastlane init

Select option number #2 (Distribution to TestFlight).

[01:00:00]: What would you like to use fastlane for?

1. 📸 Automate screenshots

2. 👩‍✈️ Automate beta distribution to TestFlight

3. 🚀 Automate App Store distribution

4. 🛠 Manual setup - manually setup your project to automate your tasks

Next, enter your Apple Developer account credentials.

[13:03:04]: Please enter your Apple ID developer credentials

[13:03:04]: Apple ID Username:

<YOUR_EMAIL>

[13:04:36]: Logging in...

If you haven’t an App ID created, Fastlane helps you



[00:56:27]: Checking if the app ‘your_bundle_id’ exists in your Apple Developer Portal…

[00:56:27]: It looks like the app ‘your_bundle_id’ isn’t available on the Apple Developer Portal

[00:56:27]: for the team ID ‘your_team_id’ on Apple ID ‘

[00:56:27]: Do you want fastlane to create the App ID for you on the Apple Developer Portal? (y/n) Logging in with your Apple ID was successful[00:56:27]: Checking if the app ‘your_bundle_id’ exists in your Apple Developer Portal…[00:56:27]: It looks like the app ‘your_bundle_id’ isn’t available on the Apple Developer Portal[00:56:27]: for the team ID ‘your_team_id’ on Apple ID ‘ y our_apple_id’[00:56:27]: Do you want fastlane to create the App ID for you on the Apple Developer Portal? (y/n)

Press `y` to continue (if haven’t it) and the Fastlane prompt ask for the name, put it!

If you also haven’t an app created with bundle identifier on App Store Connect, Fastlane helps as again 😀:



[01:01:00]: Creating new app ‘your_project_name’ on the Apple Dev Center

[01:01:01]: Created app your_app_id

[01:01:02]: Finished creating new app ‘your_project_name’ on the Dev Center

[01:01:02]: ✅ Successfully created app

[01:01:02]: Checking if the app ‘your_bundle_id’ exists on App Store Connect…

[01:01:03]: Looks like the app ‘your_bundle_id’ isn’t available on App Store Connect

[01:01:03]: for the team ID ‘your_team_id’ on Apple ID ‘

[01:01:03]: Would you like fastlane to create the App on App Store Connect for you? (y/n) App Name: your_project_name[01:01:00]: Creating new app ‘your_project_name’ on the Apple Dev Center[01:01:01]: Created app your_app_id[01:01:02]: Finished creating new app ‘your_project_name’ on the Dev Center[01:01:02]: ✅ Successfully created app[01:01:02]: Checking if the app ‘your_bundle_id’ exists on App Store Connect…[01:01:03]: Looks like the app ‘your_bundle_id’ isn’t available on App Store Connect[01:01:03]: for the team ID ‘your_team_id’ on Apple ID ‘ y our_apple_id’[01:01:03]: Would you like fastlane to create the App on App Store Connect for you? (y/n)

Press `y` to continue (if haven’t it) and the Fastlane prompt ask for the name of the app, put it!

This will be the output:



[01:11:50]: Creating new app 'your_choosed_app_name' on App Store Connect

[01:11:50]: Sending language name is deprecated. 'English' has been mapped to 'en-US'.

[01:11:50]: Please enter one of available languages: ["ar-SA", "ca", "cs", "da", "de-DE", "el", "en-AU", "en-CA", "en-GB", "en-US", "es-ES", "es-MX", "fi", "fr-CA", "fr-FR", "he", "hi", "hr", "hu", "id", "it", "ja", "ko", "ms", "nl-NL", "no", "pl", "pt-BR", "pt-PT", "ro", "ru", "sk", "sv", "th", "tr", "uk", "vi", "zh-Hans", "zh-Hant"]

[01:11:56]: Ensuring version number

[01:11:56]: Successfully created new app 'your_choosed_app_name' on App Store Connect with ID your_id_

[01:11:56]: ✅ Successfully created app

[01:11:56]: It looks like your project isn't set up to do automatic version incrementing

[01:11:56]: To enable fastlane to handle automatic version incrementing for you, please follow this guide:

[01:11:56]:

[01:11:56]: Afterwards check out the fastlane docs on how to set up automatic build increments

[01:11:56]:

[01:11:56]: Installing dependencies for you... [01:11:35]: App Name: your_choosed_app_name[01:11:50]: Creating new app 'your_choosed_app_name' on App Store Connect[01:11:50]: Sending language name is deprecated. 'English' has been mapped to 'en-US'.[01:11:50]: Please enter one of available languages: ["ar-SA", "ca", "cs", "da", "de-DE", "el", "en-AU", "en-CA", "en-GB", "en-US", "es-ES", "es-MX", "fi", "fr-CA", "fr-FR", "he", "hi", "hr", "hu", "id", "it", "ja", "ko", "ms", "nl-NL", "no", "pl", "pt-BR", "pt-PT", "ro", "ru", "sk", "sv", "th", "tr", "uk", "vi", "zh-Hans", "zh-Hant"][01:11:56]: Ensuring version number[01:11:56]: Successfully created new app 'your_choosed_app_name' on App Store Connect with ID your_id_[01:11:56]: ✅ Successfully created app[01:11:56]: It looks like your project isn't set up to do automatic version incrementing[01:11:56]: To enable fastlane to handle automatic version incrementing for you, please follow this guide:[01:11:56]: https://developer.apple.com/library/content/qa/qa1827/_index.html [01:11:56]: Afterwards check out the fastlane docs on how to set up automatic build increments[01:11:56]: https://docs.fastlane.tools/getting-started/ios/beta-deployment/#best-practices [01:11:56]: Installing dependencies for you...

The application should appear on App Store Connect. Great!

This process should have generated an application ID and application from the appstore connect, if you already have it, these steps can be skipped

In your project you now have a fastlane folder and a generated Gemfile file needed to use Fastlane, we are going to modify them.

Configure Fastlane match

Fastlane match is a new approach to iOS’s codesigning. Fastlane match makes it easy for teams to manage the required certificates and provisioning profiles for your iOS apps.

Create a new private repository named certificates for example on your Github personal account or organization.

Initialize Fastlane match for your iOS app.

fastlane match init

Then select option #1 (Git Storage).

[01:00:00]: fastlane match supports multiple storage modes, please select the one you want to use:

1. git

2. google_cloud

3. s3

?

Assign the URL of the newly created repository.

[01:00:00]: Please create a new, private git repository to store the certificates and profiles there

[01:00:00]: URL of the Git Repo: <YOUR_CERTIFICATES_REPO_URL>

Now you have inside fastlane folder a file named Matchfile and git_url should be set to the https URL of the certificates repository. Optionally, you can also use SSH, but it requires a different steps to run.

Next, we go to generate the certificates and enter your credentials when asked with Fastlane Match.

You will be prompted to enter a passphrase. Remember it correctly because it will be used later by Github Actions to decrypt your certificates repository.

fastlane match appstore

If all went well, you should see something like that:

[01:40:52]: All required keys, certificates and provisioning profiles are installed 🙌

If you had any problem with Github and the necessary permissions, maybe this post will help you to generate authentication tokens for git.

Generated certificates and provisioning profiles are uploaded to the certificates repository resources

In my case, GitHub named the main branch as a `main`, and Fastlane seems that works with master name, you should change the default branch with master from Github settings repo in orther to prevent issues.

Repository resources

Lastly, open your project in XCode, and update the provisioning profile for the release configuration of your app.

Xcode choose profile

In your_project_path/fastlane/Fastfile , replace everything with the following.

Check the Fastfile file and make sure you put your data, eg:

...

increment_build_number(xcodeproj: “your_project_name.xcodeproj”)

... gym(

...

workspace: "your_project_workspace.xcworkspace",

...

Few things to note 💡

MATCH

For the CI/CD to import the certificates and provisioning profiles, it needs to have access to the certificates repository. You can do this by generating a personal access token (should be used before) that has the scope to access or read private repositories.

In Github, go to Settings -> Developer Settings -> Personal access tokens -> click Generate New Token -> tick the repo scope -> then click Generate token .

match(

...

git_basic_authorization: Base64.strict_encode64(ENV["GIT_AUTHORIZATION"]),

...

)

Have a copy of the personal access token generated. You will use it later for the environment variable GIT_AUTHORIZATION .

Keychains

Since you’ll be importing the certificates and provisioning profiles to the CI/CD’s macOS virtual machine, you need to create a keychain to store it.

def delete_temp_keychain(name)

delete_keychain(

name: name

) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db")

end def create_temp_keychain(name, password)

create_keychain(

name: name,

password: password,

unlock: false,

timeout: false

)

end def ensure_temp_keychain(name, password)

delete_temp_keychain(name)

create_temp_keychain(name, password)

end

You might be stuck on waiting for this step if you haven’t created a keychain because there will be a prompt that requires your input to create one for the CI/CD.

Build Processing

In Github Actions, you are billed based on the minutes you have used for running your CI/CD workflow. From experience, it takes about 15–30 minutes before a build can be processed in App Store Connect.

For private projects, the estimated cost per build can go up to $0.08/min x 30 mins = $2.4, or more, depending on the configuration or dependencies of your project.

If you share the same concerns for the pricing as I do for private projects, you can set the skip_waiting_for_build_processing to false .

pilot(

...

skip_waiting_for_build_processing: true,

..

)

What’s the catch? You have to manually update the compliance of your app in App Store Connect after the build has been processed, in order for you to distribute the build to your users.

This is just an optional parameter to update if you want to save on the build minutes for private projects. For free projects, this shouldn’t be a problem at all. See pricing.