Build and deploy your iOS app to TestFlight with GitHub Actions and Fastlane

Continuous Delivery for iOS using Fastlane and Github Actions

Lito
12 min readAug 29, 2021

Prerequisites

Before continuing with the tutorial…

  • Make sure you have Fastlane installed on your development machine.
  • iOS developer program membership.
  • Desire to read 😆…

Important about the price

https://github.com/features/actions

The service is ‘free’ up to the limit depending on the chosen machine.
We are going to use a macOS machine, you can see in the screenshot its price and limits (prices as of the creation of the tutorial, they could undergo changes in the future)

🔴 Once warned of requirements and prices, if you want we continue...

📣 In the post we are going to assume that we still do not have the app created in itunes connect, we do not have the certificates or anything from the bureaucracy that surrounds the apple ecosystem, everything will be managed by fastlane!

Let’s go to the mess 🧑🏽‍💻

Steps to follow in the post

  1. Create App Store Connect API
  2. Setup Fastlane environment (create an App ID, an App in iTunes Connect..)
  3. Setup Github Actions (set secrets)
  4. Configure Github workflow yml file
  5. 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.

  1. Issuer ID.
  2. Key ID.
  3. 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

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 ‘your_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 😀:

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 ‘your_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: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_urlshould be set to the https URL of the certificates repository. Optionally, you can also use SSH, but it requires a different steps to run.

# ios/Matchfile
git_url("https://github.com/gitusername/certificates")
storage_mode("git")
type("appstore")

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 projectin 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.

Configure Appfile

In ios/fastlane/Appfile, add the following.

app_identifier(ENV["DEVELOPER_APP_IDENTIFIER"])
apple_id(ENV["FASTLANE_APPLE_ID"])
itc_team_id(ENV["APP_STORE_CONNECT_TEAM_ID"])
team_id(ENV["DEVELOPER_PORTAL_TEAM_ID"])

3. Setup Github Actions

Configure Github secrets

Ever wonder where the values of the ENV are coming from? Well, it’s not a secret anymore - it’s from your project’s secret. 🤦

1. APP_STORE_CONNECT_TEAM_ID - the ID of your App Store Connect team in you’re in multiple teams

2. DEVELOPER_APP_ID - in App Store Connect, go to the app -> App Information -> Scroll down to the General Information section of your app and look for Apple ID.

3. DEVELOPER_APP_IDENTIFIER - your app’s bundle identifier

4. DEVELOPER_PORTAL_TEAM_ID - the ID of your Developer Portal team if you’re in multiple teams

5. FASTLANE_APPLE_ID - the Apple ID or developer email you use to manage the app

6. GIT_AUTHORIZATION - <YOUR_GITUSERNAME>:<YOUR_PERSONAL_ACCESS_TOKEN>, eg. joshuadeguzman:mysecretkeyyoudontwanttoknow

7. MATCH_PASSWORD - the passphrase that you assigned when initializing match, will be used for decrypting the certificates and provisioning profiles

8. PROVISIONING_PROFILE_SPECIFIER - match AppStore <YOUR_APP_BUNDLE_IDENTIFIER>, eg. match AppStore com.domain.blabla.demo.

9. TEMP_KEYCHAIN_USER & TEMP_KEYCHAIN_PASSWORD - assign a temp keychain user and password for your workflow

10. APPLE_KEY_ID — App Store Connect API Key 🔺Key ID

11. APPLE_ISSUER_ID — App Store Connect API Key 🔺Issuer ID

12. APPLE_KEY_CONTENT — App Store Connect API Key 🔺 Key file or Key content of .p8, check it

4. Configure Github workflow file

Create a Github workflow directory.

md .github/workflows

Inside the workflow folder, create a file named cd.ymland add the following.

You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs, refers here.

Our case dispatch when commit or pull request to the main branch, but the normal case should be protecting the main branch and only use for pull_requests.

💥 Important

If you facing the next issue, you need to specify on your project for increment_build_number to VERSIONING_SYSTEM, more info here

5. Trigger workflow

Create a branch

Make a commit or a pull request depending on how you have configured it, and you should see the active workflow in the repository.

Trigger the workflow

Push the new commits to the newly created branch to trigger the workflow.

After a few minutes, the build should be available in your App Store Connect dashboard.

Can deploy from local machine?

Yes, you can, and it is very easy.

Imagine that you have a private repository and you have used up the minutes of the free plan and you do not want to pay for new releases, or maybe you prefer to submit the application manually.

Let’s go for it

Ok, first we need to create in my_project_path/fastlane path a file called .env, just in the same path as Fastfile, to be able to create the same secret properties found in our Github, as below:

.env file for deploy from local machine

Now, you can go to the terminal and launch the Fastlane from your machine:

fastlane closed_beta

❌ Very important about the .env file, as we do not want to expose this data, we must add it in our .gitignore, something like that: ❌

fastlane/*.env

It should work the same as it happens from Github Actions on the remote machine but in our local machine. 🍻

Terminal execution: $ fastlane closed_beta

If you have come this far, my congratulations, now you have a fully automated process for your iOS apps with Fastlane and Github Actions.

Take note, the setup of jobs may vary depending on the tools or dependencies your project requires. Feel free to comment on any issues you encountered while setting up the CI/CD workflow for your project.

--

--

Lito

iOS developer at Kairos DS | iOS Lover | Valencian living on Madrid | Sometimes I try to be a writer