Build and Deploy your iOS app with Github Actions and Fastlane

Rasmus Styrk
7 min readJan 17, 2021

I wanted to use Github Actions to run my test and to deploy a new version to testflight. After 3 days of debuggning and using all my build minutes i got it to work. With this article i want to show you have it is done and hopefully save you a lot of trouble.

Update 26/04/2021: I have written a new article on how to migrate Fastlane to use Auth keys instead. This is much easier for deploying to TestFlight.

Setting up Fastlane

Gemfile

To get up and running you need to install some depencencies. I use bundler as it its the recommended way

# frozen_string_literal: truesource "https://rubygems.org"git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }gem "cocoapods"
gem "fastlane"

Then install them using

$ bundle install

Fastlane

Next thing to get done is setting up your fastlane to work on your local machine with match and upload_to_testflight commands. When that works we can better get it to work on Github Actions.

  1. Create a folder called fastlane (or run fastlane init)
  2. Create 3 files inside this folder
  • Appfile
  • Fastfile
  • Matchfile

Appfile

app_identifier("fill in") # The bundle identifier of your app
apple_id("fill in") # Your Apple email address
itc_team_id("fill in") # App Store Connect Team ID
team_id("fill in") # Developer Portal Team ID

Fastfile

# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
default_platform(:ios)
platform :ios do
desc "Run tests"
lane :tests do
run_tests(scheme: "iOS")
end
desc "Push a new beta build to TestFlight"
lane :beta do
setup_ci
match(type: "appstore", readonly: is_ci)
build_app(workspace: "App.xcworkspace", scheme: "iOS") # Change name of workspace
upload_to_testflight(changelog: ENV["CHANGELOG"] || "No changelog provided")
end
desc "Sets the version of the bundle to a RELEASE_VERSION passed in as an environment variable"
lane :set_release_version do
version = ENV["RELEASE_VERSION"]
if version
UI.message("Setting version to #{version}")
increment_version_number(version_number: version)
increment_build_number(build_number: Time.now.to_i)
else
UI.user_error!("Environment variable RELEASE_VERSION not set")
end
end
end

Matchfile

If you dont already have match setup then start by creating a new repository and paste in the URL below. Match stores your apple certificates so they can be installed on the build server later (or by one of your teammates on their machine)

git_url "fill in"type "development" # The default type, can be: appstore, adhoc or development

Now we should verify that you can generate certificates and have them installed and saved in the repository that you created

$ bundle exec fastlane match

If it is first time, then match will ask you to login to your apple account and then select a new password that is used to encrypt the certificates. Store this password as we need it later on the build server. If your apple account is messy and needs to be cleaned you can do that with bundle exec fastlane match nuke. For me it was great to get clean sheet to work from.

Next Step

In the Fastfile above we have 3 lanes and we use these lanes via GitHub Actions to make everything work.

  1. tests for running tests
  2. beta for building, signing and uploading the build to testflight
  3. set_release_version for increasing the verison number of the app

With that in mind we should see if the beta lane works and fix any issues that might arise.

$ bundle exec fastlane beta

Have it build, sign and upload the app to testflight before you continue on.

Setting up GitHub Actions

To have it all run on GitHub Actions you first need to set all the required environment variables. GitHub uses a concept called Action Secrets that basically is a way of storing credentials and other secret stuff without exposing them to anyone.

Here is the full list.

  1. FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
  2. FASTLANE_USER
  3. FASTLANE_PASSWORD
  4. FASTLANE_SESSION
  5. MATCH_PASSWORD
  6. KNOWN_HOSTS
  7. SSH_KEY

FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD

For fastlane to be able to upload your app it needs a password that can be used together with iTMSTransporter that is used internally by fastlane. This password can be generated for your apple account at https://appleid.apple.com.

Read more here

FASTLANE_USER

The email used to login to your Apple Developer Portal with

FASTLANE_PASSWORD

The password used to login to your Apple Developer Portal with.

FASTLANE_SESSION

If you have two factor authentication enabled you need to provide this variable to fastlane so that it can login on your behalf. It is also possible to to use an apple account that does not use 2FA and then save you the trouble.

To get the session you can simply run the command below and it will be copied to your pasteboard for easy pasting on github.

$ bundle exec fastlane spaceauth -u user@email.com

The problem with this is that the session expires and you will have to update it in the action secrets when it is expired. I dont know for how long the sessions is active so you have to experiment with this.

Read more here (scroll a bit to the spaceauth section) and here

MATCH_PASSWORD

Upload the password/passphrase that was used to encrypt your match repository.

KNOWN_HOSTS

When Github Action is running it needs to clone the match repository in order to install signing certificates and provisioning profiles on to the build machines.

If your match repository is hosted on GitHub you can use this command.

ssh-keyscan github.com | pbcopy -

Then paste the result into the KNOWN_HOSTS secret. pbcopyis just a mac command that saves whatever is piped to into your pasteboard.

SSH_KEY

GitHub actions will not be able to clone the match repository before you supply it with your ssh-key. What I did was to create a new key so that I did not use my normal keys.

  1. Create new key (I named it id_rsa_github_actions)
  2. Upload the public key to the match repository as a deploy key (Read only key)
  3. Upload the private key to GitHub Actions in the SSH_KEY secret. (Btw, it must be in PEM format)
$ ssh-keygen
$ ssh-keygen -p -m PEM -f id_rsa_github_actions
$ pbcopy < id_rsa_github_actions # Upload it to SSH_KEY secret
$ pbcopy < id_rsa_github_actions.pub # Upload it as deploy key in the match repository

Read more here

GitHub Workflows (Actions)

GitHub Actions is made up of a workflow. I have two workflows, one for tests and one for uploading to testflight. We will first get the tests to work se we know that GitHub can build your app.

.github/workflows/tests.yml

name: Tests on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
tests:
name: Tests
runs-on: macos-latest

steps:
- name: Checkout
uses: actions/checkout@v2
- name: Select Xcode Version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Setup ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.2
bundler-cache: true

- name: Install Pods
run: |
pod install

- name: Run tests
run: |
bundle exec fastlane tests

Save, commit and push it to github. Then open the Actions tab in your github repository and it should already have started. Hopefully everything should work and you can move on the next workflow.

.github/workflows/testflight.yml

name: Deploy to Testflight on:
release:
types: [created]
jobs:
deploy:
name: Deploy
runs-on: macos-latest

steps:
- name: Checkout
uses: actions/checkout@v2
- name: Select Xcode Version
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: latest-stable

- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: ${{ secrets.KNOWN_HOSTS }}

- name: Setup ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7.2
bundler-cache: true

- name: Install Pods
run: pod install

- name: Build & Distribute to Testflight
run: |
export LC_ALL=en_US.UTF-8
export LANG=en_US.UTF-8
bundle exec fastlane set_release_version
bundle exec fastlane beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
RELEASE_VERSION: ${{ github.event.release.tag_name }}
CHANGELOG: ${{ github.event.release.body }}

Save, commit and push it to github but this time it will not run automatically. This is because we do not want to upload a new build everyime someone changes a tiny thing. Uploads to testflight should be a manual thing and only done when the code is ready to be tested by your testers.

To have it run, you simply create a new release on your github repository.

  1. Open your github repository
  2. Select releases on the right side of the screen
  3. Press Draft a new release
  4. Fill in tag version, for example 1.0.5 (This will also be used as the appversion when uploading). It should follow the CFBundVersion format. Read more
  5. Fill in a release title (I typically use the same as the tag version)
  6. Fill in a description (This will also be used as “What to Test” in Testflight)
  7. Press publish release

Now the release is created and the action is triggered and if everything worked as expected then you should at some point see the build in testflight. It will be distributed to internal tester automatically.

Build Minutes

GitHub charges build minutes by 10x for MaC OS meaning that 1 minute of mac build time costs you 10 minutes of your credit.

Gemfile and Match

Do not put match in the Gemfile as i did at first, then you will get a nasty error that no one really knows anything about. I wasted like a full day of work with this. The error I pasted below, so that if you do the mistake you can move on quickly.

[16:41:32]: Could not find option 'api_key' in the list of available options: git_url, git_branch, type, app_identifier, username, keychain_name, keychain_password, readonly, team_id, team_name, verbose, force, skip_confirmation, shallow_clone, workspace, force_for_new_devices, skip_docs

Final Words

Okey, so this was quite a bit to setup and things might have gone wrong or you might have missed a step. Just take your time and read the configuration files and you should be able to understand the most parts.

If you have 2FA enabled i tend to simply upload the new FASTLANE_SESSION each time so that i know it will work since it sucks to use 10 build minutes to just have it fail at the final step (and remember: github charges you 100 minutes for 10 minutes of mac build time).

I hope it works out for you and that it saves you days of pulling your hair :-) Feel free to comment or contact me if you need any help.

--

--

Rasmus Styrk

I work as a software developer with years of experience within the field of web, apps and server architecture.