Build and deploy the app to Testflight using Github Actions with Fastlane and App Distribution.

Continuous Integration and Delivery for iOS using Fastlane , Github Actions and App Distribution via Firebase

Smriti Gangele
12 min readNov 4, 2023
App distribution via different medium

Prerequisites

Before continuing with the tutorial…

  • Make sure you have Fastlane installed on your development machine.
  • Make sure you have iOS developer program membership.
  • Follow the steps…

📢 Here, we are going to assume that we have the app created in itunes connect in case you are new Create an app record , so moving forward we need to create the signing certificates, not familiar? to learn in depth you can visit Apple’s iOS Developer Library , if we do not have the certificates just don’t panic, everything can be managed by ‘fastlane’ Isn’t it great!!

Let’s dive in 👩‍💻

Steps to follow in the post

  1. Login > App Store Connect API
  2. Setup Github Actions Secrets
  3. Setup Fastlane environment
  4. Configure Github workflow .yml file
  5. Action using workflow
  6. Firebase App Distribution

1. Using App Store Connect API with Fastlane Match

Apple’s two-factor authentication is good way for all users to sign in to App Store Connect. This layer of security for your Apple ID helps you to ensure and secure your account.

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.

Earlier, to use Fastlane, we used to do username and password authentication for your CI, Now…

Apple’s App Store Connect API 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 use App Store Connect API, Fastlane needs three things.

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

4. Click Generate API Key or the Add (+) button, Make sure you have the admin rights.

5. Enter a name for the key. Can put anything as name.

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 on Generate.

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

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

You can check all the necessary information here.
From here, copy the Issuer ID, Key ID and 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.

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

2. Setup Github Actions

Configure Github secrets

Have you ever wonder where the values of the ENV variables are coming from? Well, it’s not a secret anymore, as it’s coming from your project’s secret.

  1. APPLE_KEY_ID — App Store Connect API Key ID
  2. APPLE_ISSUER_ID — App Store Connect Issuer ID
  3. APPLE_KEY_CONTENT — App Store Connect API Key Content , convert content of .p8 file to Base64.
  4. APP_STORE_CONNECT_TEAM_ID - the ID of your App Store Connect team in you’re in multiple teams

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

6. DEVELOPER_APP_IDENTIFIER - your app’s bundle identifier

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

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

9. GIT_AUTHORIZATION - <YOUR_GITHUB_USERNAME>:<YOUR_PERSONAL_ACCESS_TOKEN>

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

11. PROVISIONING_PROFILE_SPECIFIER - match AppStore <YOUR_APP_BUNDLE_IDENTIFIER>, eg. match AppStore com.domain.example.demo.

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

13. FIREBASE_APP_ID - firebase app id, inside project settings of your firebase account > General > scroll down to your apps > App ID.

14. FIREBASE_GROUP_ID - firebase group id, inside project settings of your firebase account > find your group or create one.

15. FIREBASE_TOKEN_CLI — firebase refresh token, you will while setting firebase on your machine (This is covered in point 6).

3. Setup Fastlane

1 — Initialize Fastlane for iOS

fastlane init

Select option number #2 (Distribution to TestFlight).

What would you like to use fastlane for?
📸 Automate screenshots
👩‍✈️ Automate beta distribution to TestFlight
🚀 Automate App Store distribution
🛠 Manual setup - manually setup your project to automate your tasks

Next, enter your Apple Developer account credentials.

Please enter your Apple ID developer credentials
Apple ID Username:
<YOUR_APPLE_ID_USERNAME>

The process will take a while to setup.
After that in your project you now have a fastlane folder and a generated Gemfile file needed to use Fastlane, we are going to modify them.

Here is some information you will be need in Gemfile:

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 “fastlane/Certificates” for example on your Github personal account or organization.

Initialize Fastlane match for your iOS app.

fastlane match init

Then select option #1 as git (Git Storage).

fastlane match supports multiple storage modes, please select the one you want to use:
1. git
2. google_cloud
3. s3
?

Assign the newly created repository URL.

Please create a new, private git repository to store the certificates and profiles there
URL of the Git Repo: <YOUR_NEWLY_CREATED_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.

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 or store it somewhere because it will be used later by Github Actions to decrypt your certificates repository.

fastlane match developement

Also, you can generate distribution certificates with:

fastlane match appstore

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

All required keys, certificates and provisioning profiles are installed 🙌

⚠️ You have to generate authentication tokens for git.

Just, login to your Git-hub account and follow the steps:

  1. Select Settings on your profile
  2. Select Developer settings
  3. Select Personal access tokens
  4. Generate new token that has the scope to access or read private repositories.

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

You can see the generated certificates and provisioning profiles are uploaded to the “fastlane/Certificates” repository.

GitHub named the main branch as a `main`, and Fastlane works with ‘master’ name, just change the default branch with master.

Lastly, open your_projectin Xcode, and update the provisioning profile of your app.

If you have come up this far, have patient we are almost near to setup every thing in place…

Check the Fastfile file and make sure you put your data. Inside your_project_path/fastlane/Fastfile, replace everything with the following.

# Uncomment the line if you want fastlane to automatically update itself
#update_fastlane
#opt_out_usage

default_platform(:ios)


DEVELOPER_APP_ID = ENV["DEVELOPER_APP_ID"]
DEVELOPER_APP_IDENTIFIER = ENV["DEVELOPER_APP_IDENTIFIER"]
MATCH_PASSWORD = ENV["MATCH_PASSWORD"]
PROVISIONING_PROFILE_SPECIFIER = ENV["PROVISIONING_PROFILE_SPECIFIER"]
TEMP_KEYCHAIN_USER = ENV["TEMP_KEYCHAIN_USER"]
TEMP_KEYCHAIN_PASSWORD = ENV["TEMP_KEYCHAIN_PASSWORD"]
APPLE_ISSUER_ID = ENV["ASC_ISSUER_ID"]
APPLE_KEY_ID = ENV["ASC_KEY_ID"]
APPLE_KEY_CONTENT = ENV["ASC_KEY"]
GIT_AUTHORIZATION = ENV["GIT_AUTHORIZATION"]
PROVISIONING_PROFILE_ID = ENV["PROVISIONING_PROFILE_ID"]
FIREBASE_GROUP = ENV["FIREBASE_GROUP"]
FIREBASE_APP_ID = ENV["FIREBASE_APP_ID"]
FIREBASE_TOKEN_CLI = ENV["FIREBASE_TOKEN_CLI"]

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: 0
)
end

def ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
end

platform :ios do
lane : upload_ios_beta_Testflight do
keychain_name = TEMP_KEYCHAIN_USER
keychain_password = TEMP_KEYCHAIN_PASSWORD
output_name = "example"
workspace = "example.xcworkspace",
ensure_temp_keychain(keychain_name, keychain_password)

api_key = app_store_connect_api_key(
key_id: APPLE_KEY_ID,
issuer_id: APPLE_ISSUER_ID,
key_content: APPLE_KEY_CONTENT,
is_key_content_base64: true,
duration: 1200,
in_house: false
)

increment_build_number(xcodeproj: "example.xcodeproj")

cocoapods(
clean_install: true
)

match(
type: 'development', # can use type like app-store, ad-hoc according to the requirement.
app_identifier: "#{DEVELOPER_APP_IDENTIFIER}",
git_basic_authorization: Base64.strict_encode64(GIT_AUTHORIZATION),
readonly: true,
keychain_name: keychain_name,
keychain_password: keychain_password,
api_key: api_key
)

gym(
configuration: "Debug", # can use config type for Release
workspace: workspace,
scheme: "YOUR_SCHEME_NAME",
export_method: "development", # can use type like app-store, ad-hoc according to the requirement.
export_options: {
provisioningProfiles: {
DEVELOPER_APP_ID => PROVISIONING_PROFILE_SPECIFIER,
DEVELOPER_APP_IDENTIFIER => PROVISIONING_PROFILE_ID
}
}
)

delete_temp_keychain(keychain_name)
end
end

And, Just run the following command:

fastlane upload_ios_beta_Testflight

You will get the artifacts inside your project directory.

To upload the build to testflight , just add the code inside your Fastfile.

pilot(
apple_id: "#{DEVELOPER_APP_ID}",
app_identifier: "#{DEVELOPER_APP_IDENTIFIER}",
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false,
notify_external_testers: false,
ipa: "./example.ipa"
)

MATCH

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 (As already mentioned in the steps) you will see on your fastfile after setting up everything.

match(
...
git_basic_authorization: Base64.strict_encode64(ENV["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")
enddef create_temp_keychain(name, password)
create_keychain(
name: name,
password: password,
unlock: false,
timeout: false
)
enddef 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, 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 concerns for the pricing, 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"])
team_id(ENV["DEVELOPER_PORTAL_TEAM_ID"])

4. Configure Github workflow file

Create a Github workflow directory.

md .github/workflows

Inside the workflow folder, create a file named build_upload_ios.ymland add the following to your github account.

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 dispatches when push or pull request to the main branch, but the normal case should be protecting the main branch and only use for pull_requests.

To set up the workflow file add the following to automate your CD process:

name: iOS binary build & upload
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-ios:
runs-on: macos-13
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Set up ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.2.2
bundler-cache: true
- name: Decode signing certificate into a file
env:
CERTIFICATE_BASE64: ${{ secrets.IOS_DIST_SIGNING_KEY }}
run: |
echo $CERTIFICATE_BASE64 | base64 --decode > signing-cert.p12
- name: pod install
run: pod install
- name: pod update
run: pod update
- name: Select Xcode
run: sudo Xcode-select -switch /Applications/Xcode_14.2.app/Contents/Developer
- name: Build
run: xcodebuild clean build -workspace example.xcworkspace -scheme <Your_proj_scheme> -destination 'platform=iOS Simulator,name=iPhone 14 Pro example,OS=16.2'
- name: upload iOS binary
uses: maierj/fastlane-action@v1.4.0
with:
lane: bundle exec fastlane ios upload_ios_beta_Testflight
env:
DEVELOPER_APP_ID: '${{ secrets.DEVELOPER_APP_ID }}'
DEVELOPER_APP_IDENTIFIER: '${{ secrets.DEVELOPER_APP_IDENTIFIER }}'
DEVELOPER_PORTAL_TEAM_ID: '${{ secrets.DEVELOPER_PORTAL_TEAM_ID }}'
FASTLANE_APPLE_ID: '${{ secrets.FASTLANE_APPLE_ID }}'
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: '${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}'
MATCH_PASSWORD: '${{ secrets.MATCH_PASSWORD }}'
GIT_AUTHORIZATION: '${{ secrets.GIT_AUTHORIZATION }}'
PROVISIONING_PROFILE_SPECIFIER: '${{ secrets.PROVISIONING_PROFILE_SPECIFIER }}'
PROVISIONING_PROFILE_ID: '${{ secrets.PROVISIONING_PROFILE_ID }}'
TEMP_KEYCHAIN_PASSWORD: '${{ secrets.TEMP_KEYCHAIN_PASSWORD }}'
TEMP_KEYCHAIN_USER: '${{ secrets.TEMP_KEYCHAIN_USER }}'
ASC_KEY_ID: '${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}'
ASC_ISSUER_ID: '${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}'
ASC_KEY: '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}'
SIGNING_KEY_PASSWORD: '${{ secrets.IOS_DIST_SIGNING_KEY_PASSWORD }}'
SIGNING_KEY_FILE_PATH: 'signing-cert.p12'
FIREBASE_GROUP: '${{ secrets.FIREBASE_GROUP }}'
FIREBASE_APP_ID: '${{ secrets.FIREBASE_APP_ID }}'
FIREBASE_TOKEN_CLI: '${{ secrets.FIREBASE_TOKEN_CLI }}'

# For uploading the output of the build and archiving...
# you can use github artifacts to store the output of the workflow.

- name: Upload app-store ipa and dsyms to artifacts
uses: actions/upload-artifact@v2
with:
name: app-store ipa & dsyms
path: |
${{ github.workspace }}/example.ipa
${{ github.workspace }}/*example.app.dSYM.zip

5. Trigger workflow

Create a branch

Make a commit and create a push 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.

Now the question arise… Can we deploy from our 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

Okay, so first we need to create a file named .env inside my_project_path/fastlane , just in the same path as Fastfile, add need to add same secret properties found in our Github ENV variables as following:

ASC_ISSUER_ID="******"
ASC_KEY="*****"
ASC_KEY_ID="******"
DEVELOPER_APP_ID="******"
DEVELOPER_APP_IDENTIFIER="******"
DEVELOPER_PORTAL_TEAM_ID="******"
FASTLANE_APPLE_ID="******"
MATCH_PASSWORD="******"
PROVISIONING_PROFILE_SPECIFIER="******"
TEMP_KEYCHAIN_PASSWORD="******"
TEMP_KEYCHAIN_USER="******"
GIT_AUTHORIZATION="******"
PROVISIONING_PROFILE_ID="******"

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

fastlane upload_ios_beta_Testflight

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

fastlane/*.env

It should work the same as it happens from Github Actions on the remote machine.

6. Firebase App Distribution

App distribution on firebase is quite simple as you can just drag the your .ipa and its done.

But But But …

If you want to set the .ipa / created build to the App distribution center while working with GitHub Actions, you can add the following to your fastfile :

...
firebase_app_distribution(
app: FIREBASE_APP_ID,
groups: FIREBASE_GROUP,
firebase_cli_token: FIREBASE_TOKEN_CLI,
debug: true
)
...

Few points to note >>>

Firstly keep in mind that you should have an owner access type of your project.

Now, Before triggering the flow you need to install the firebase plugin curl -sL https://firebase.tools | upgrade=true bash

Get your FIREBASE_APP_IDand FIREBASE_GROUP_ID from your firebase account and then run firebase login:ci , you will get the refresh token as firebase_auth_token.

For more Information to how to create the tokens for Firebase CLI.

And thats it. Congratulations!!

If you have come this far, now you have a fully automated process for your iOS apps with Fastlane, Github Actions and App Distribution via fastlane .

Happy coding!

📣 Thank you for reading. Feel free to comment on any issues you encountered while setting up the CI/CD workflow for your project.

Resources

--

--