Build and Deploy your iOS app with Github Actions and Fastlane
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.
- Create a folder called
fastlane
(or runfastlane init
) - 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.toolsdefault_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.
tests
for running testsbeta
for building, signing and uploading the build to testflightset_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.
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
FASTLANE_USER
FASTLANE_PASSWORD
FASTLANE_SESSION
MATCH_PASSWORD
KNOWN_HOSTS
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.
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. pbcopy
is 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.
- Create new key (I named it
id_rsa_github_actions
) - Upload the public key to the match repository as a deploy key (Read only key)
- 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
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.
- Open your github repository
- Select releases on the right side of the screen
- Press Draft a new release
- 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
- Fill in a release title (I typically use the same as the tag version)
- Fill in a description (This will also be used as “What to Test” in Testflight)
- 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.