Flutter with GitHub Actions, Fastlane, and Firebase App Distribution (May 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.
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 :
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.
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
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.
Now that we have all this info, it is time to save on Github secrets.
Create a new secret with the button.
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.
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…
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:
- Go to App Store Connect: https://appstoreconnect.apple.com
- Select Users and Access.
- Select Keys. (This is only visible if you are ADMIN, so could need help for some else here)
- Select the plus button to add a key.
- Give it a name and set the Access to Developer.
- Copy the Issuer ID. This is what we are going to use as APP_STORE_CONNECT_ISSUER_ID secret.
- Copy the the Key ID. This is what we are going to use as APP_STORE_CONNECT_KEY_ID secret.
- 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 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
Paste the url for the repo for the certs that we just created
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.