Flutter with GitHub Actions, Fastlane, and Firebase App Distribution (May 2021)

Oscar Tigreros
12 min readJun 4, 2021

You are developing with Flutter and Firebase, and you want to avoid the hassle of publishing by hand on distribution? Here is my solution for you. I base this guide on the Adam Zarn(A.Z. from here on) (Guide here), but I found some parts that were unclear or from my side wrong. In any case thanks to A.Z. for the starting point.

Create the first Action

You can create the actions by hand like the A.Z. guide but I will explain the Github action way.

In your repo go to actions, here you can copy some other actions from other users or press in the set-up link to create a new one
In your repo go to actions, here you can copy some other actions from other users or press in the set-up link to create a new one.
set up name and branch target
Add your pipeline name and your trigger

I name this test since we are going to distribute into Firebase Distribution and this code is the one for the testers. You can use other triggers but in our case the branch development is locked and will only accept changes via PR, when a PR is closed this creates a push into the branch target.

Jobs

In the A.Z guide, there is only one job but we are going to separate the job in two. One for the Android, and one for iOS. The main reason for this is to improve costs, since each minute on MacOS cost 10 minutes in Github actions and each minute on Linux cost only 1 Github action minutes. Other benefits for this separation is that the deployment will be parallel so if Android fails for some reason but the iOS part is good, Android will not deploy but iOS will do (same for the opposite case). To be clear :

Github actions time for small project

These are the times for the actions on a small project. With one execution we waste 10 Mac minutes or 100 Github actions minutes plus 6 minutes for the android part.

If this process was on the same job, the android part would be reduced by half because on each job we set up some similar dependencies. In any case the time will be around 13 to 15 Mac minutes equivalent to 130 to 150 Github action minutes, just by separating the jobs we save around 25 minutes. Sounds a small amount but when you have multiple projects each minute saved counts.

Android Job

Let’s start with our first job and it will be Android as it is the easiest of the two.

android job

Let’s see the parts for the job:

This determines that we are going to work with Ubuntu.

runs-on: ubuntu-latest

This checkout the code.

- name: Checkout
uses: actions/checkout@v2

Install Ruby, Java and Flutter

Ruby for Fastlane, and the other are needed for Flutter projects

- 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:
channel: 'stable'

This is the installation for the gems that are needed the Fastlane we need. We use the github.workspace to get the path of the project.

- name: Install Android Gems
working-directory: ${{ github.workspace }}/android
run: bundle install

This is to build the apk.

- name: Build APK
run: flutter build apk

And the last one for the distribution step, we use a fastlane gem for this.

- 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 }}

As you can see above we use some secrets that are going to be set-up into the Github page. To complete this in parts you can save this by committing, which will create a commit in master and since the pipeline is for development this job will not be triggered.

Android Secrets

If you don’t know what a secret is, a simple way to think about it is where you put any info that you need to use but need to be sure that no one can see it like passwords or tokens

So we need 3 secrets for the android job, let’s see how to get those and how to store it in Github. Let’s assume that you already have a Firebase project and you already added the 2 apps.

FIREBASE_APP_ID_ANDROID: Is the id for your project in Firebase

firebase android id

ANDROID_TESTERS: This is a list of the emails that are going to receive the app. This is not the best way however, in a future edit of this article I will post a better way to do it. For now, we can do like this:

email1@email.com,email2@email.com

FIREBASE_CLI_TOKEN: For this you need to install the firebase cli in you pc. You can find the documentation here https://firebase.google.com/docs/cli With the cli installed you just need just to type in your command line:

firebase login:ci

This will launch an auth page, select your account and then will print on the terminal the token that we are going to use.

Firebase cli token
All is need from the 1 to the end.

Now that we have all this info, it is time to save on Github secrets.

github secrets

Create a new secret with the button.

new secret

Add the other secrets and we are done here.

Extra Step on Firebase

Make sure you start the distribution on Firebase selection the app and press the Get started Button (“Comenzar”). This is needed for each app.

start distribution app
Sorry for the Spanish Image

If you miss this part, the job will FAIL!!.

This is all you need for the android Test distribution part, you can test this by pushing something into development branch.

iOS Job

iOS is quite a bit more complex because even for test distribution we are going to need Certificates, Profiles and an API key to complete this deployment. But we will see that later.

Now and let’s see the job code

This code is code is similar to the Android section outlined at the beginning of the article, and until the Install iOS Gems, it is the same.

So let’s focus on the differences…

ios job

This installs all the certificates that we are going to use in the Github machine. All the secrets will be explained in detail later in the Certification and Profile dependencies section.

- 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 }}

This sets up the xcode version for the build.

- name: Set Xcode version
working-directory: ${{ github.workspace }}/ios
run: bundle exec fastlane set_xcode_version

This builds the ipa that is the file we need to send to Firebase.

- name: Build IPA
working-directory: ${{ github.workspace }}
run: flutter build ipa --export-options-plist=$GITHUB_WORKSPACE/ios/exportOptions.plist

And last but not least the distribution step, the secrets here work in the same way as the android ones. The only new one is the ios id but you can get in the same way inside the ios app in the firebase project.

- 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 }}

You can commit here to save the iOS jobs.

Certification and Profile dependencies

App Store Connect API key

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. (This is only visible if you are ADMIN, so could need help for some else here)
  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. This is what we are going to use as APP_STORE_CONNECT_ISSUER_ID secret.
  7. Copy the the Key ID. This is what we are going to use as APP_STORE_CONNECT_KEY_ID secret.
  8. Download the .p8 file and generate a Key Content value by opening it and parse all to base64 with https://www.base64encode.org/:

This is how should look the p8 when you open with TextEdit.

-----BEGIN PRIVATE KEY----- 
xxx
xxx
xxx
xxx
-----END PRIVATE KEY-----

This is different in A.Z.’s guide but should be easy and trouble free. This base64 is what we are going to use as APP_STORE_CONNECT_KEY_CONTENT secret.

It should be easy to save all those 3 secrets now. The remaining 2 TEMP_KEYCHAIN_PASSWORD and TEMP_KEYCHAIN_USER can be any value.

With this, all the Github work is done!!

Now we need to work on the local project.

For convenience, do all this on the target branch, first pull from master to pull the github actions that we made.

Fastlane

We need this to make the process of create .apk and .ipa. Also with Match we create/use the certificates more smoothly.

Install Fastlane

brew install fastlane

Easy as that now we have Fastlane to be setup in our project. Now we need add Fastlane into Android and iOS. This will create some folders and Files that we are going to edit to complete the process.

Fastlane in Android

cd android && fastlane init

This will ask you for the package id com.something.app you can find this on the android manifest.

Now we install the firebase plugin

fastlane add_plugin firebase_app_distribution

Select (y) when the terminal ask you. Now you should have something like:

fastlane folders

Fastlane in iOS

This at the beginning is the same as Android

cd android && fastlane init
fastlane add_plugin firebase_app_distribution

Now we need to work on the certifications. First create a private empty Github repo. This repo will be used to store the certifications for Fastlane and Match and will populate everything. Once this is setup you can forget about all the certifications.

Now we need to run Match

fastlane match init

Select 1 for git

git for match repo

Paste the url for the repo for the certs that we just created

url for certs repos

Now we run this:

fastlane match development

This will populate the private repo with the certs. If later on the deployment have the error No profiles for ‘com.xxx.app’ were found, try to put the signing in manual and select the profile created for Match.

Add a pass for the Match storage, and save this pass in the secrets as MATCH_PASSWORD

Now will ask you for you Apple Developer Program account credentials, which is just your email that you have in the developer.apple. This will check your 2FA.

Now will ask you for your Apple ID Username a couple of times.

Inside the log that you saw in the terminal you should take look on one that looks like

You are going to need the Profile Name later.

And now your cert repo should have something like:

So we just created the certifications and profiles for the Firebase distribution, but this step is quite similar when you need certs to deploy in the stores (this will be covered in a future post).

Now we need to edit some of the Fastlane files. Like always let’s start with Android

Fastlane Android Files

Fastfile (./android/fastlane/Fastfile):

This will upload our .apk to the Firebase

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

And thats all for android (simple and practical)

Fastlane iOS Files

Fastfile (./ios/fastlane/Fastfile):

This has a lot of code so let’s explain all of this:

This has methods, the first delete the temporal keychain, the second create a new temporal keychain and the last one uses the previous ones.

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

This part installs the credentials that Match brings from the repo into the keychain. If the job fails because cant find the app_store_connect_api_key, update the fastlane.

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"],
is_key_content_base64: true,
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

This is the lane that sets the version for the xcode, this is called from the Github action.

lane :set_xcode_version do

xcversion(version: '12.4')

end

I put that '12.4' by hand because for some reason the macOs version in Github don't have the lastest xcode.

And at the end we have the distribution lane

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/APP-NAME.ipa"
)

end

If you don't know the APP-NAME run:

flutter build ipa --export-options plist=ios/exportOptions.plist

This will create the .ipa on your local machine and you can use the same name.
if you don't have a MacOs to run it add this step into the job:

- name: printipa dfolder
working-directory: ${{ github.workspace }}/build/ios/ipa
run: ls *.ipa

Just before the step named Distribute iOS Beta App. This will print on the gitHub actions steps the name of the .ipa.

Now on your ios Gemfile we need to add, this will install xcode on the machine.

gem "xcode-install"

In this same folder with Xcode we need to add a .plist (For the android developers open Xcode and from there add this file. If you add it elsewhere, it won’t work!!!) This is how you should do it.

The open the file as source code

and add this:

<?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>TEAM-ID</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: CERTIFICATION-NAME</string>
<key>provisioningProfiles</key>
<dict>
<key>
com.something.app</key>
<string>PROFILE-NAME-SAVED-EARLIER</string>
</dict>
</dict>
</plist>

TEAM-ID: this you can find in developer.apple and is the number in the right

CERTIFICATION-NAME: this is the name which matches the created certification on developer.apple, in my case was this:

PROFILE-NAME-SAVED-EARLIER: you remember the profile name saved on the terminal log?… Well now is the time to use it.

Now lets add one last secret named GIT_AUTHORIZATION should be :

githubUser:Password

A.Z. use a token configuration but when I try that with a repo that I'm not the owner of it fails.

Now we are ready to commit the local changes.

Conclusion

Here we add to a project all that is needed to be distributed into Firebase Distribution using Github actions and Fastlane. This will help you to code more and worry less about how or when to deploy to the testers. And in the process we improve the A.Z. guide to be more cost efficient so your wallet will be happier now.

Improvements

  • Release notes, here all the deployment have just ‘Test’ as release notes, but you can take the last commits for example to create a better release note.
  • Xcode version, this version should be seeded with a dynamic version not the version burned in the lane.
  • Github Auth, should be possible to use a token instead of the user/password as secret.
  • Testers, instead of a secret should take a group in the firebase.

--

--