Flutter with GitHub Actions, fastlane, and Firebase App Distribution

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 and flutter 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.

  1. Navigate to the Firebase Console and create an account if you don’t already have one: https://console.firebase.google.com
  2. Select Add project.
  3. Enter your project name.
  4. Enable Google Analytics if you’d like to (and select the Default Account for Firebase).
  5. Now you’ll see icons for iOS, Android, and Web.

Setup Firebase for iOS

  1. Select the iOS icon.
  2. Enter the bundle id found in your Xcode project (com.example.flutterApp).
  3. Give your app a nickname (Flutter App iOS).
  4. You can skip the App Store ID for now.
  5. Select Register app.
  6. Download the GoogleService-Info.plist and follow the directions on where to add it to your Xcode project.
  7. You can skip adding the Firebase SDK and initialization code because Flutter takes care of this for you.
  8. Select Continue to console.
  9. 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

  1. Go back to Project Overview.
  2. Select Add app.
  3. Select the Android icon.
  4. Enter the package name which is the applicationId found in your app level build.gradle file.
  5. Give your app a nickname (Flutter App Android).
  6. You can skip the Debug signing certificate SHA-1 for now.
  7. Select Register app.
  8. Download the google-services.json file and follow the directions on where to add it to your Android project.
  9. Add the Firebase SDK to your Android project by following the instructions.
  10. Select Next.
  11. Select Continue to console.
  12. 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:

  1. install_certificate_and_profile – uses fastlane match to install a certificate and profile to a temporary keychain on the build machine.
  2. 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.
  3. 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:

  1. Go to App Store Connect: https://appstoreconnect.apple.com
  2. Select Users and Access.
  3. Select Keys.
  4. Select the plus button to add a key.
  5. Give it a name and set the Access to Developer.
  6. Copy the Issuer ID.
  7. Copy the the Key ID.
  8. 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:

  1. Go to your GitHub repository.
  2. Select Settings.
  3. Select Secrets at the bottom on the left hand side.
  4. 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:
  • 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!