So you’re developing an app with Flutter, Google’s new UI toolkit. In this post, we’ll be going over how you can automate the build and distribution of your Flutter app to Android and iOS devices using GitHub Actions, fastlane, and Firebase App Distribution.
Create a GitHub Actions Workflow
In the root of your Flutter app GitHub repo, create a file with the following path:
.github/workflows/pipeline.yml
At the top of the file, give it a name:
name: Pipeline
Next, specify when this workflow should be triggered. Here’s how you would do it on pushes to the master branch:
on:
push:
branches:
- master
Finally, specify the jobs to run. We’ll only have one job called build
, we’ll have it run on macOS
, and we’ll add steps
the job should execute. The first step will be to checkout the code:
jobs:
build:
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v2
After checking out the code, we have to install the following dependencies:
- Ruby (for fastlane)
- Java (for building Android)
- Flutter (for running
flutter build apk
andflutter build ipa
) - Firebase CLI (for distributing the apps via Firebase App Distribution)
To install these, add this to your build
job:
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '2.7'
- name: Install Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Install Flutter
uses: subosito/flutter-action@v1
with:
flutter-version: '1.24.0-6.0.pre'
channel: 'dev'
- name: Install Firebase CLI
uses: w9jds/firebase-action@master
Add fastlane to your project
Before we continue setting up the pipeline, let’s add the necessary fastlane files to our repo. We’ll use fastlane to distribute the .apk
and the .ipa
via Firebase App Distribution and we’ll also use fastlane match to help with iOS code signing.
If you don’t have fastlane installed, you can install it using Homebrew:
brew install fastlane
This will install fastlane and the most adequate Ruby version to go along with it.
Next we’ll initialize fastlane in both the android
and ios
folders of our Flutter project:
cd root/android && bundle exec fastlane init
cd root/ios && bundle exec fastlane init
For Android, enter the package name (com.example.flutter_project) when prompted.
Then we’ll add the fastlane firebase_app_distribution
plugin for both Android and iOS:
cd root/android && bundle exec fastlane add_plugin firebase_app_distribution
cd root/ios && bundle exec fastlane add_plugin firebase_app_distribution
Select (y) when prompted in both cases.
Create a private fastlane match repository
Before we can finish setting up fastlane, we’ll need to create a separate private repo where our iOS code signing certificate and provisioning profile will be stored.
Once you’ve created that repo, run the following command:
cd root/ios && bundle exec fastlane match init
It will ask you which storage mode you’re using. If you’re private repo is on GitHub, choose 1. git.
Next it will ask you for the url of the GitHub repo you just created. In my case, I had to edit the url in the Matchfile
that is generated in this process by adding a GitHub personal access token for authorization in this format:
git_url(https://{token}:{token}@github.com/{username}/certificates)
Finally, we can actually populate the repository with a development certificate and profile by running the following command:
cd root/ios && bundle exec fastlane match development
This step will require you to authenticate with your Apple Developer account and provide a passphrase (referred to as MATCH_PASSWORD later) that will be used to encrypt the repository. Store this passphrase somewhere because it will be required when we’re running our GitHub Actions workflow.
Setup Firebase App Distribution
The next step would be to configure the fastlane Fastfiles for both Android and iOS, but we should create the app in the Firebase Console first, because our firebase_app_distribution fastlane action will require a Firebase app id.
- Navigate to the Firebase Console and create an account if you don’t already have one: https://console.firebase.google.com
- Select Add project.
- Enter your project name.
- Enable Google Analytics if you’d like to (and select the Default Account for Firebase).
- Now you’ll see icons for iOS, Android, and Web.
Setup Firebase for iOS
- Select the iOS icon.
- Enter the bundle id found in your Xcode project (com.example.flutterApp).
- Give your app a nickname (Flutter App iOS).
- You can skip the App Store ID for now.
- Select Register app.
- Download the GoogleService-Info.plist and follow the directions on where to add it to your Xcode project.
- You can skip adding the Firebase SDK and initialization code because Flutter takes care of this for you.
- Select Continue to console.
- Now if you select your new iOS app from the top, and select the settings gear icon, you can scroll down to find your iOS App ID which we’ll need later.
Setup Firebase for Android
- Go back to Project Overview.
- Select Add app.
- Select the Android icon.
- Enter the package name which is the applicationId found in your app level build.gradle file.
- Give your app a nickname (Flutter App Android).
- You can skip the Debug signing certificate SHA-1 for now.
- Select Register app.
- Download the google-services.json file and follow the directions on where to add it to your Android project.
- Add the Firebase SDK to your Android project by following the instructions.
- Select Next.
- Select Continue to console.
- Now if you select your new Android app from the top, and select the settings gear icon, you can scroll down to find your Android App ID which we’ll need later.
Setup Fastfiles
Now we have everything we need to set up our Fastfiles. We’ll start with Android because it is comparatively simpler.
Android Fastfile
Open the Fastfile located at ./android/fastlane/Fastfile
and enter the following under the auto-generated comments:
default_platform(:android)
platform :android do
desc "Distribute Android App for Beta Testing"
lane :distribute_android_app do
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID_ANDROID"],
testers: ENV["ANDROID_TESTERS"],
release_notes: "Test",
firebase_cli_token: ENV["FIREBASE_CLI_TOKEN"],
apk_path: "../build/app/outputs/apk/release/app-release.apk"
)
end
end
The distribute_android_app
lane will use your Android app id, a list of testers’ emails, and a firebase cli token (which we’ll talk about how to generate later) to upload an .apk
to Firebase App Distribution.
iOS Fastfile
Open the Fastfile located at ./ios/fastlane/Fastfile
and enter the following under the auto-generated comments:
default_platform(:ios)
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: false
)
end
def ensure_temp_keychain(name, password)
delete_temp_keychain(name)
create_temp_keychain(name, password)
end
platform :ios do
desc "iOS Lanes"
lane :install_certificate_and_profile do
api_key = app_store_connect_api_key(
key_id: ENV["APP_STORE_CONNECT_KEY_ID"],
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
key_content: ENV["APP_STORE_CONNECT_KEY_CONTENT"],
in_house: false,
)
keychain_name = ENV["TEMP_KEYCHAIN_USER"]
keychain_password = ENV["TEMP_KEYCHAIN_PASSWORD"]
ensure_temp_keychain(keychain_name, keychain_password)
match(
type: 'development',
git_basic_authorization: Base64.strict_encode64(ENV["GIT_AUTHORIZATION"]),
keychain_name: keychain_name,
keychain_password: keychain_password,
api_key: api_key
)
end
lane :set_xcode_version do
xcversion(version: "{xcode_version}")
end
lane :distribute_ios_app do
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID_IOS"],
testers: ENV["IOS_TESTERS"],
release_notes: "Test",
firebase_cli_token: ENV["FIREBASE_CLI_TOKEN"],
ipa_path: "../build/ios/ipa/pos_poc.ipa"
)
end
end
You can see there’s a lot more going on here. This Fastfile has 3 lanes:
- install_certificate_and_profile – uses fastlane match to install a certificate and profile to a temporary keychain on the build machine.
- set_xcode_version – sets the appropriate xcode version for your build. For this step to work, you’ll need to add
gem "xcode-install"
to your ios Gemfile. - distribute_ios_app – distributes the .ipa using the iOS app id, a list of testers’ emails, and the aforementioned firebase cli token.
App Store Connect API key
You’ll notice that the install_certificate_and_profile
lane uses an api key created using the app_store_connect_api_key
fastlane action.
To create an api key for your Apple Developer account, follow these steps:
- Go to App Store Connect: https://appstoreconnect.apple.com
- Select Users and Access.
- Select Keys.
- Select the plus button to add a key.
- Give it a name and set the Access to Developer.
- Copy the Issuer ID.
- Copy the the Key ID.
- Download the .p8 file and generate a Key Content value by opening it and replacing all of the new lines with
\\n
and also escape spaces by adding a backslash before them like so:
-----BEGIN PRIVATE KEY-----
xxx
xxx
xxx
xxx
-----END PRIVATE KEY-----
becomes
-----BEGIN\ PRIVATE\ KEY-----\\nxxx\\nxxx\\nxxx\\nxxx\\n-----END\ PRIVATE\ KEY-----
Finish the GitHub Actions Workflow
We’re finally ready to finish the workflow by adding the steps to build and deploy Android and iOS.
Android Build and Deploy
- name: Install Android Gems
working-directory: ${{ github.workspace }}/android
run: bundle install
- name: Build APK
run: flutter build apk
- name: Distribute Android Beta App
working-directory: ${{ github.workspace }}/android
run: bundle exec fastlane distribute_android_app
env:
FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }}
FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
ANDROID_TESTERS: ${{ secrets.ANDROID_TESTERS }}
iOS Build and Deploy
- name: Install iOS Gems
working-directory: ${{ github.workspace }}/ios
run: bundle install
- name: Install iOS Certificate and Profile
working-directory: ${{ github.workspace }}/ios
run: bundle exec fastlane install_certificate_and_profile
env:
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
TEMP_KEYCHAIN_USER: ${{ secrets.TEMP_KEYCHAIN_USER }}
TEMP_KEYCHAIN_PASSWORD: ${{ secrets.TEMP_KEYCHAIN_PASSWORD }}
GIT_AUTHORIZATION: ${{ secrets.GIT_AUTHORIZATION }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
- name: Set Xcode version
working-directory: ${{ github.workspace }}/ios
run: bundle exec fastlane set_xcode_version
- name: Build IPA
working-directory: ${{ github.workspace }}
run: flutter build ipa --export-options-plist=$GITHUB_WORKSPACE/ios/exportOptions.plist
- name: Distribute iOS Beta App
working-directory: ${{ github.workspace }}/ios
run: bundle exec fastlane distribute_ios_app
env:
FIREBASE_CLI_TOKEN: ${{ secrets.FIREBASE_CLI_TOKEN }}
FIREBASE_APP_ID_IOS: ${{ secrets.FIREBASE_APP_ID_IOS }}
IOS_TESTERS: ${{ secrets.IOS_TESTERS }}
One more thing that needs to be done for iOS is to create an exportOptions.plist
file in your ios directory for the Build IPA step. Here’s a template for that file:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>development</string>
<key>teamID</key>
<string>{{ teamID }}</string>
<key>uploadBitcode</key>
<true/>
<key>compileBitcode</key>
<true/>
<key>uploadSymbols</key>
<true/>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Development: {{ Your Name }}</string>
<key>provisioningProfiles</key>
<dict>
<key>{{ bundleID }}</key>
<string>{{ Provisioning Profile Name }}</string>
</dict>
</dict>
</plist>
GitHub Actions Secrets
You may have noticed that our workflow file references secrets
and our Fastfiles reference ENV
. On our build machine, certain steps will set environment variables that are stored in Actions secrets. To create these secrets, follow these steps:
- Go to your GitHub repository.
- Select Settings.
- Select Secrets at the bottom on the left hand side.
- From this Actions secrets page, you can select New repository secret to add a secret.
Here’s all of the secrets you should have for what we’ve done so far:
- ANDROID_TESTERS
- Comma separated list of emails (no spaces)
- APP_STORE_CONNECT_ISSUER_ID
- Value from step 6 of the App Store Connect API Key section
- APP_STORE_CONNECT_KEY_CONTENT
- Value from step 8 of the App Store Connect API Key section
- APP_STORE_CONNECT_KEY_ID
- Value from step 7 of the App Store Connect API Key section
- FIREBASE_APP_ID_ANDROID
- Value from step 12 of the Setup Firebase for Android section
- FIREBASE_APP_ID_IOS
- Value from step 9 of the Setup iOS for Android section
- FIREBASE_CLI_TOKEN
- Install the Firebase CLI:
curl -sL https://firebase.tools | bash
- Login to get the token:
firebase login:ci
- Look for Success! Use this token to login on a CI server:
- Install the Firebase CLI:
- GIT_AUTHORIZATION
- {{ GitHub personal access token }}:{{ GitHub personal access token }}
- IOS_TESTERS
- Comma separated list of emails (no spaces)
- MATCH_PASSWORD
- Value set at the end of the Create a private fastlane match repository section
- TEMP_KEYCHAIN_PASSWORD
- Any value you choose
- TEMP_KEYCHAIN_USER
- Any value you choose
To run this workflow locally, you can add the following scripts to the root of your flutter project (assuming you have already downloaded Ruby, Java, Flutter, and the Firebase CLI):
distribute_android_app
#!/bin/bash
root=$(dirname $0)
cd "$root"/android
flutter build apk
bundle exec fastlane distribute_android_app
distribute_ios_app
#!/bin/bash
root=$(dirname $0)
cd "$root"/ios
bundle exec fastlane install_certificate_and_profile
bundle exec fastlane set_xcode_version
flutter build ipa --export-options-plist="$root"/ios/exportOptions.plist
bundle exec fastlane distribute_ios_app
distribute_apps
#!/bin/bash
root=$(dirname $0)
"$root"/distribute_android_app
"$root"/distribute_ios_app
Of course, if you’re running it locally, you’ll have to set the environment variables somehow. You can do that by adding the following to your shell profile at ~/.bashrc
, ~/.bash_profile
, ~/.profile
, or ~/.zshrc
depending on your system:
export ANDROID_TESTERS=
export APP_STORE_CONNECT_ISSUER_ID=
export APP_STORE_CONNECT_KEY_CONTENT=
export APP_STORE_CONNECT_KEY_ID=
export FIREBASE_APP_ID_ANDROID=
export FIREBASE_APP_ID_IOS=
export FIREBASE_CLI_TOKEN=
export GIT_AUTHORIZATION=
export IOS_TESTERS=
export MATCH_PASSWORD=
export TEMP_KEYCHAIN_PASSWORD=
export TEMP_KEYCHAIN_USER=
Conclusion
If you’ve made it this far, you’ve now set up a GitHub Actions workflow that will build and deploy an Android and iOS Flutter app for you automatically! Congratulations! Getting all of the details right took me a long time, so I hope this guide can help save you a bit of time and sanity.
Thanks for reading!