Deploying Flutter iOS apps with fastlane and GitHub Actions

Hiroshi Kato
Flutter Community
Published in
8 min readAug 7, 2020
Photo by Anas Alshanti on Unsplash

Flutter is a framework designed by Google for building cross-platform applications, including iOS, Android and web apps. It enables us to create beautiful UIs or to make an MVP to validate the value of apps quickly.

The more apps we make with Flutter, the more important the methods we use to build and deliver their codebase to our users are. With GitHub Actions and fastlane, I automated this delivery process, including archiving, code signing, and uploading it. In this article, I’m going to introduce how I did it.

Apple Store Connect

First of all, please ensure that you set up to release your app on App Store Connect. In this article, we’re going to upload the app to TestFlight, and you need to create the following items.

We need certificates and provisioning profiles for code signing, but we will issue both of them in the following step with fastlane match. In this step, we don’t need to issue it manually.

fastlane

fastlane is a tool to automate building and releasing apps, providing command-line tools that wrap APIs such as App Store Connect API, Xcode command-line tools, etc.

Getting Started with fastlane

As mentioned in the fastlane document, When you’ll install fastlane, it is recommended that you use a Gemfile to define your dependency and the fastlane version.

Then, as written in the flutter document, you need to initialize fastlane in your project. You can do that by using fastlane init in the ios folder ${your-project}/ios and set up a fastlane configuration in an interactive way.

In the step to enter your Apple ID, if your Apple account has Two-factor Authentication enabled, you’ll automatically be asked to verify your identity. If you don’t have any trusted devices configured, but have trusted a phone number, input sms to escape the prompt and you can select a trusted phone number to send a six-digit code.

Two-factor Authentication (6 digits code) is enabled for account 'your-account@example.com'
More information about Two-factor Authentication: https://support.apple.com/en-us/HT204915
If you're running this in a non-interactive session (e.g. server or CI)
check out https://github.com/fastlane/fastlane/tree/master/spaceship#2-step-verification
(Input `sms` to escape this prompt and select a trusted phone number to send the code as a text message)(You can also set the environment variable `SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER` to automate this)
(Read more at: https://github.com/fastlane/fastlane/blob/master/spaceship/docs/Authentication.md#auto-select-sms-via-spaceship_2fa_sms_default_phone_number)
Please enter the 6 digit code:
sms
Please select a trusted phone number to send code to:
1. +XX •••-••••-••XX
2. +XX •••-••••-••XX
3. +XX •••-••••-••XX
4. +XX •••-••••-••XX
5. +XX •••-••••-••XX
6. +XX •••-••••-••XX
? 4 # select a phone number

After the login is succeeded the following fastlane configuration files will be generated in ${your-project}/ios.

  • fastlane/Appfile
  • fastlane/Fastfile

fastlane/Appfile

Appfile is a configuration file that is written by ruby, and stores the information used across all fastlane tools. (ex. Apple ID, Bundle Identifier…etc.) You can also use environment variables to replace the value of arguments, as shown below.

# The bundle identifier of your app
app_identifier("your.bundle.identifier")
# Your Apple email address
apple_id(ENV['FASTLANE_USER'])
# App Store Connect Team ID
itc_team_id(ENV['ITC_TEAM_ID'])
# Developer Portal Team ID
team_id(ENV['TEAM_ID'])

fastlane/Fastfile

Fastfile stores the automation configuration that can be run with fastlane command. You can use action methods provided by fastlane to build, upload or code signing…etc

To build your app, you can use build_app action. In this article, to upload your app to TestFlight set the value app-store to the field export_method .

lane :beta do
build_app(
scheme: "MyApp",
workspace: "Runner.xcworkspace",
export_method: "app-store"
)
end

Code signing

To code signing your app, you need to prepare certificates and provisioning profiles. You can issue them manually from the Apple Developer site but when you develop with your team members and have to share those files with them fastlane match is useful. It makes it easy to onboard and to set up new Mac machines.

Let’s set up fastlane match according to its guide.

$ match init
...
URL of the Git Repo: https://github.com/your-private-git-repo

You will be asked Passphrase for Git repo in this prompt. This passphrase is used for encrypting and decrypting certificates and provisioning profile, that will be required for the following step please remember it.

Then this will create a Matchfile in your project’s ./fastlane folder.

git_url("git@github.com:your-private-git-repo.git")storage_mode("git")app_identifier(["your.bundle.identifier"])
username(ENV['FASTLANE_USER'])

Then you can issue and install certificates and provisioning profile by match command. To upload to TestFlight set appstore to the argument. Of course, you can create them for development by match development .

$ match appstore

The certificates and private key are installed in your Keychain, and the provisioning profile is installed in ~/Library/MobileDevice/Provisioning Profiles.

Then you will be able to code signing with the certificates on your local machine. Next for doing that on CI machine add match action in your Fastfile to automatically fetch the latest certificates.

lane :beta do
if is_ci
create_keychain(
name: ENV['MATCH_KEYCHAIN_NAME'],
password: ENV["MATCH_KEYCHAIN_PASSWORD"],
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end
match(
type: "appstore",
readonly: is_ci,
keychain_name: ENV['MATCH_KEYCHAIN_NAME'],
keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"],
)
end

Since the certificates will be installed to a keychain store, you need to create it on your CI machine by create_keychain. On your local machine keychain already exists, so you don’t need to create one.

MATCH_KEYCHAIN_NAME , MATCH_KEYCHAIN_PASSWORD are environment variables for your keychain name and password. These values are set in the following step.

Change Signing Setting in Xcode

To codesign with the certificates and provisioning files installed through fastlane match, you have to change the certificates setting in Xcode. Go into Xcode and go to Signing & Capabilities manually change the signing profile to the match xxx one in the list.

If not you will receive the error, Xcode couldn’t find any iOS App Development provisioning profiles.

Upload to TestFlight

You can upload your app archive to TestFlight with upload_to_testflight action.

lane :beta do
upload_to_testflight
end

Then your Fastfile will be like this.

default_platform(:ios)platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
if is_ci
create_keychain(
name: ENV['MATCH_KEYCHAIN_NAME'],
password: ENV["MATCH_KEYCHAIN_PASSWORD"],
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end
match(
type: "appstore",
readonly: is_ci,
keychain_name: ENV['MATCH_KEYCHAIN_NAME'],
keychain_password: ENV["MATCH_KEYCHAIN_PASSWORD"],
)
build_app(
workspace: "Runner.xcworkspace",
scheme: "MyApp",
export_method: "app-store"
)
upload_to_testflight
end
end

GitHub Actions workflow

Next, we’ll define the GitHub Actions workflow that runs :beta lane on CI machine when the new code is pushed to the develop branch. Let’s create ${your-project}/.github/workflows/deploy.yml.

name: deployon:
push:
branches: [develop]

Define jobs

Then, define each job. First, select OS where fastlane command runs on and install necessary tools into it.

jobs:
deploy:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_11.3.app/Contents/Developer'
- name: Bundle install
run: cd ./ios && bundle install

macos-latest

With setting macos-latest you can use mac OS on GitHub Actions workflow. It allows us to use Xcode command-line tools. And you can also specify Xcode version in it. I recommend setting the version that is same with your local Xcode version.

bundle install

We’ll install fastlane and its dependency by running bundle install .

And then we’ll also need to install Flutter and necessary packages of your projects. You can use Flutter action to set up flutter environment.

- name: Setup JDK
uses: actions/setup-java@v1
with:
java-version: "12.x"
- name: Setup flutter
uses: subosito/flutter-action@v1
with:
flutter-version: "1.17.0"
channel: "stable"
- name: Install tools
run: |
flutter pub get
cd ./ios && pod install

Don’t forget pod install otherwise, you would receive the error Pods/Target Support Files/Pods-Runner when you create your archive.

And then for getting your certificates and provisioning files from your certificate’s Git repo, we’ll set ssh configuration.

We’ll generate public/private rsa key pair by ssh-keygen according to GitHub document. After generating them, upload the public key to the Deploy key of your repo and add the private key to id_rsa on CI machine as shown below.

- name: Setup SSH Keys and known_hosts for fastlane match
run: |
SSH_PATH="$HOME/.ssh"
mkdir -p "$SSH_PATH"
touch "$SSH_PATH/known_hosts"
echo "$PRIVATE_KEY" > "$SSH_PATH/id_rsa" chmod 700 "$SSH_PATH"
ssh-keyscan github.com >> ~/.ssh/known_hosts
chmod 600 "$SSH_PATH/known_hosts"
chmod 600 "$SSH_PATH/id_rsa"
eval $(ssh-agent)
ssh-add "$SSH_PATH/id_rsa"
env:
PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}

You can get the value of your private key safely through GitHub secrets. Set the environment variable from Settings > Secrets on your GitHub repo and fetch the value from GitHub Actions workflow with env declaration.

Run fastlane command

Finally, we’ll define the job to run fastlane command. You can run it by bundle exec fastlane beta like this.

- name: Deploy to TestFlight
run: |
cd ./ios && bundle exec fastlane beta
env:
TEAM_ID: ${{ secrets.TEAM_ID }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
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 }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_KEYCHAIN_NAME: ${{ secrets.MATCH_KEYCHAIN_NAME }}
MATCH_KEYCHAIN_PASSWORD: ${{ secrets.MATCH_KEYCHAIN_PASSWORD }}
DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS: ${{ secrets.DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS }}

A lot of environment variables are needed when running our Fastfile. You have to set the value of them to GitHub secrets. The definitions and the reasons why we need them are below.

  • TEAM_ID , ITC_TEAM_ID

These variables are referred to in Appfile that was created in a previous step. Set your team id of Apple Developer to TEAM_ID while set your team id of App Store Connect to ITC_TEAM_ID .

  • FASTLANE_USER

This will be referred to in Appfile and Fastfile . Set your Apple ID to this variable.

  • FASTLANE_PASSWORD

This variable will be referred to internally in fastlane. Set the password of your Apple ID to this variable.

  • FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD

As mentioned in fastlane document, you need to generate an application specific password to upload your app to TestFlight and set the value to this environment variable.

If you want to upload builds to App Store Connect (actions upload_to_app_store and deliver) or TestFlight (actions upload_to_testflight, pilot or testflight) from your CI machine, you need to generate an application specific password:

1. Visit appleid.apple.com/account/manage

2. Generate a new application specific password

3. Provide the application specific password using the environment variable FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD

This will supply the application specific password to iTMSTransporter, the tool used by those actions to perform the upload.

  • FASTLANE_SESSION

When your Apple ID has Two-Factor Authentication or Two-Step verification information, you can’t validate your Apple ID with a prompt on your CI machine. You need to generate a login session for Apple ID in advance with spaceauth action.

fastlane spaceauth -u ${your-apple-id}

This will generate a new session and then set the value to FASTLANE_SESSION variable.

  • MATCH_PASSWORD

This will be referred to internally in fastlane match to decrypt your profiles. Set the value that you entered when you ran match init.

  • MATCH_KEYCHAIN_NAME , MATCH_KEYCHAIN_PASSWORD

These will be referred to when calling create_keychain and match in Fastfile.

  • DELIVER_ITMSTRANSPORTER_ADDITIONAL_UPLOAD_PARAMETERS

If you receive the error Broken pipe when uploading binaries, you should be able to use the Signiant delivery protocol by overriding this environment variable with -t Signiant.

In the end, your workflow will be like this.

jobs:
deploy:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Select Xcode version
run: sudo xcode-select -s '/Applications/Xcode_11.3.app/Contents/Developer'
- name: Bundle install
run: cd ./ios && bundle install
- name: Setup JDK
uses: actions/setup-java@v1
with:
java-version: "12.x"
- name: Setup flutter
uses: subosito/flutter-action@v1
with:
flutter-version: "1.17.0"
channel: "stable"
- name: Install tools
run: |
flutter pub get
cd ./ios && pod install
- name: Setup SSH Keys and known_hosts for fastlane match
run: |
...
env:
PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy to TestFlight
run: |
cd ./ios && bundle exec fastlane beta
env:
...

Push the workflow file to GitHub repo, add new code, and the deploy flow will start to run, building your app and deploying it to TestFlight!

https://www.twitter.com/FlutterComm

--

--