Deploying Flutter iOS apps with fastlane and GitHub Actions
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.
- Bundle Identifier
- App in App Store Connect (connected to the above Bundle Identifier)
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/HT204915If 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:
smsPlease 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-lateststeps:
- 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
anddeliver
) or TestFlight (actionsupload_to_testflight
,pilot
ortestflight
) 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-lateststeps:
- 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!