Photo by Simon Kadula on Unsplash
Automating Flutter Deployments with Fastlane and GitHub Actions - My Experience
Living life on the Fastlane!!
This is a documentation of my experience setting up a CI/CD flow including deployments to Google Playstore, App Store, and Huawei App Gallery with Fastlane. With every step, I will be discussing the issues I had and how I came about solving them.
Installation
You need Ruby installed on your local machine to get Fastlane to work. If you don't have Ruby installed, you can follow the setup instructions outlined in this link. In my case, it didn't go according to plan as my ruby version was outdated. So I had to use rvm. Rvm, much like Fvm is used to manage multiple versions of the same environment. In this case, it's ruby. Documentation for rvm can be found here.
After deciding to use rvm, I had to find a link to download the GPG file that worked, the links can be found here.
Snippets of the commands to do the above look like this:
//To install rvm, first get GPG keys
gpg2 --keyserver keyserver.ubuntu.com --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
//Then download rvm
\curl -sSL https://get.rvm.io | bash -s stable
Now that you have your rvm installed, you can navigate to your Android or iOS folder in your Flutter project using a new version of Ruby. You can do that using the following commands.
//To list ruby versions that exist
rvm list known
//To download a particular ruby version, replace 3.2.2 with your version
rvm install 3.2.2
//To use that version in your current project, replace 3.2.2 with your version
rvm use 3.2.2
If your ruby version is updated i.e. >= 2.5 or rvm has been installed correctly, you can proceed to install bundler and then install Fastlane. The procedure to do that can be found here.
Setting up Fastlane
Setting up Fastlane was very straightforward, the documentation for both platforms can be found here and here. In essence, you initialize Fastlane using the command:
fastlane init
Afterward, you give all the needed information and then you're done! I didn't have any issues with this section. After setting up Fastlane successfully, you should have both Appfile
and Fastfile
files in a fastlane
folder inside your android
and ios
directories. We will be heavily using .env files in this implementation. You'll need to add the below inside inside each Gemfile
in your android
and ios folder
. We will be using an env
file in our android
and ios
folders.
gem 'dotenv-rails'
gitignore
file. They contain very sensitive information that should not be checked into version control. You can add it to your gitignore
with *.env
in your gitignore
file.Writing Lanes
In this section, we'll be looking at the peculiarities of my deployment to the platforms specified in the title of this article. I will assume you have the credentials listed below:
Your package name (com.company.app)
Your service account JSON file. You can get your service account JSON file by following the steps in the
Collect your Google credentials
section here.Your credentials for deploying to the Huawei App Gallery;
HUAWEI_CLIENT_ID
,HUAWEI_CLIENT_SECRET
,HUAWEI_APP_ID
The path to your release apk or aab
Your medium of delivering the above information. We used a
.env
file in the root directory of ourandroid
andios
folders.
Before we move on, it is important to specify what the AppFile
looks like, we declare the app identifier i.e. package name as shown below.
# android/fastlane/Appfile
package_name("#{ENV['APP_IDENTIFIER']}")
Huawei
We used the huawei_appgallery_connect
plugin to upload our release APK to the Huawei App Gallery. Our lane looked like this:
# android/fastlane/Fastfile
desc "Release for Huawei App Gallery"
lane :huawei_deploy do
is_aab = true # This is hardcoded for now, can change for your case.
if ENV['HUAWEI_CLIENT_ID'].nil? || ENV['HUAWEI_CLIENT_SECRET'].nil? || ENV['HUAWEI_APP_ID'].nil? || (is_aab && ENV['AAB_PATH'].nil?) || (!is_aab && ENV['APK_PATH'].nil?)
UI.user_error!("You need to provide HUAWEI_CLIENT_ID, HUAWEI_CLIENT_SECRET, HUAWEI_APP_ID and #{is_aab ? 'AAB_PATH' : 'APK_PATH'} to upload to Huawei App Gallery Connect.}")
end
root_path = nil
full_path= nil
Dir.chdir("..") do
Dir.chdir("..") do
root_path = Dir.pwd
end
end
full_path = root_path + '/' + (is_aab ? ENV['AAB_PATH'] : ENV['APK_PATH'])
if File.exist?(full_path)
huawei_appgallery_connect(
client_id: ENV['HUAWEI_CLIENT_ID'],
client_secret: ENV['HUAWEI_CLIENT_SECRET'],
app_id: ENV['HUAWEI_APP_ID'],
apk_path: full_path,
# OPTIONAL, Parameter beyond this are optional:-
# if you're uploading aab instead of apk, specify is_aab to true and specify path to aab file on full_path
is_aab: is_aab,
submit_for_review: false,
# If you are facing errors when submitting for review, increase the delay time before submitting the app for review using this option:
# delay_before_submit_for_review: 20,
)
else
UI.user_error!("No file found at path: #{full_path}")
UI.user_error!("Make sure you have provided the correct path to the #{is_aab ? 'AAB' : 'APK'} file.")
end
end
Let's take a look at the code line by line. We declare the description of the lane as Release for Huawei App Gallery
first. This ensures future developers and stakeholders can follow along. We name this lane huawei_deploy
.
Moving on, we check for the existence of needed documents for this lane to run properly including HUAWEI_CLIENT_ID
, HUAWEI_CLIENT_SECRET
, HUAWEI_APP_ID
, AAB_PATH
or APK_PATH
(depending on if you're uploading an APK or App bundle).
# android/fastlane/Fastfile
is_aab = true # This is hardcoded for now, can change for your case.
if ENV['HUAWEI_CLIENT_ID'].nil? || ENV['HUAWEI_CLIENT_SECRET'].nil? || ENV['HUAWEI_APP_ID'].nil? || (is_aab && ENV['AAB_PATH'].nil?) || (!is_aab && ENV['APK_PATH'].nil?)
UI.user_error!("You need to provide HUAWEI_CLIENT_ID, HUAWEI_CLIENT_SECRET, HUAWEI_APP_ID and #{is_aab ? 'AAB_PATH' : 'APK_PATH'} to upload to Huawei App Gallery Connect.}")
end
is_abb
is used to determine the kind of resource we're submitting to the Huawei App gallery; APK, or App bundle. You can get the HUAWEI credentials from your dashboard. Next, we find the full path of the apk. We do that using this snippet:
# android/fastlane/Fastfile
root_path = nil
full_path= nil
Dir.chdir("..") do
Dir.chdir("..") do
root_path = Dir.pwd
end
end
full_path = root_path + '/' + (is_aab ? ENV['AAB_PATH'] : ENV['APK_PATH'])
We use the full_path
as an input to the plugin to upload the apk to the app gallery. We call the plugin in the following snippet:
# android/fastlane/Fastfile
if File.exist?(full_path)
huawei_appgallery_connect(
client_id: ENV['HUAWEI_CLIENT_ID'],
client_secret: ENV['HUAWEI_CLIENT_SECRET'],
app_id: ENV['HUAWEI_APP_ID'],
apk_path: full_path,
# OPTIONAL, Parameter beyond this are optional:-
# if you're uploading aab instead of apk, specify is_aab to true and specify path to aab file on full_path
is_aab: is_aab,
submit_for_review: false,
# If you are facing errors when submitting for review, increase the delay time before submitting the app for review using this option:
# delay_before_submit_for_review: 20,
)
else
UI.user_error!("No file found at path: #{full_path}")
UI.user_error!("Make sure you have provided the correct path to the #{is_aab ? 'AAB' : 'APK'} file.")
end
Here, we check if an apk or app bundle exists at the path we got before, and then we call the huawei_appgallery_connect
plugin. The parameters of this call are especially straightforward. If no files exist for this path, we send error messages to the user to check the path.
To run this lane, the command below was used by me:
# android
bundle exec fastlane huawei_deploy
Playstore
Deploying to Playstore was quite straightforward, and I have a recommendation and advice upon completing the deployment.
The first recommendation would be to store the Base64 encoded version of your service account JSON file instead of the raw file. This improves security by adding a layer of encryption and making it easier to use in multiple CI/CD flows.
My advice is that you'll have to manually update the app version name and number in your
pubspec.yaml
when using this plugin. As of writing this article, the field for version number doesn't work.
Moving on, the lane for deploying to Playstore looks like this.
# android/fastlane/Fastfile
lane :playstore_deploy do
is_aab = true
if ENV['FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY'].nil? || (is_aab && ENV['AAB_PATH'].nil?) || (!is_aab && ENV['APK_PATH'].nil?)
UI.user_error!("You need to provide FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY and #{is_aab ? 'AAB_PATH' : 'APK_PATH'} to upload to Google Playstore.}")
end
root_path = nil
full_path= nil
Dir.chdir("..") do
Dir.chdir("..") do
root_path = Dir.pwd
end
end
full_path = root_path + '/' + (is_aab ? ENV['AAB_PATH'] : ENV['APK_PATH'])
encoded_json_key = ENV['FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY']
decoded_json_key = Base64.decode64(encoded_json_key)
if File.exist?(full_path)
upload_to_play_store(
track: "beta", #should be production, can be alpha, beta
package_name: ENV['APP_IDENTIFIER'],
aab: is_aab ? full_path : nil,
apk: !is_aab ? full_path : nil,
json_key_data: decoded_json_key,
in_app_update_priority: 5, #This is to ensure highest priority update for in app updates
)
else
UI.user_error!("No file found at path: #{full_path}")
UI.user_error!("Make sure you have provided the correct path to the #{is_aab ? 'AAB' : 'APK'} file.")
end
end
We first check for the existence of the essential elements needed for this lane to work properly:FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY
, AAB_PATH
, APK_PATH
. The next few lines of code are familiar as we've covered them before. Next, we decoded our service account file in the snippet below:
# android/fastlane/Fastfile
encoded_json_key = ENV['FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY']
decoded_json_key = Base64.decode64(encoded_json_key)
We then pass decoded_json_key
to the upload_to_play_store
plugin to upload your apk/aab to playstore.
# android/fastlane/Fastfile
if File.exist?(full_path)
upload_to_play_store(
track: "beta", #should be production, can be alpha, beta
package_name: ENV['APP_IDENTIFIER'],
aab: is_aab ? full_path : nil,
apk: !is_aab ? full_path : nil,
json_key_data: decoded_json_key,
in_app_update_priority: 5, #This is to ensure highest priority update for in app updates
)
else
UI.user_error!("No file found at path: #{full_path}")
UI.user_error!("Make sure you have provided the correct path to the #{is_aab ? 'AAB' : 'APK'} file.")
end
If the file exists on the path generated, we call upload_to_play_store
. We pass in the track
, package_name
, json_key_data
, and in my case in_app_update_priority
(to ensure users are always updated at all times).
To run this lane, I used the command here:
# android
bundle exec fastlane playstore_deploy
After getting Huawei and Playstore deployments to work correctly, my env file looked like this:
path: ./android/
APP_IDENTIFIER=co.company.app
HUAWEI_CLIENT_ID=***
HUAWEI_CLIENT_SECRET=***
HUAWEI_APP_ID=****
APK_PATH=build/app/outputs/apk/production/release/app-production-release.apk
AAB_PATH=build/app/outputs/bundle/release/app.aab
FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY=**** //your service account json in base64 format
App store
I had many issues deploying to Testflight with Fastlane as you might imagine. I will show my final lane and the fixes I had to make to get it working. Let's take a look at my Appfile
.
# ios/fastlane/Appfile
app_identifier("#{ENV['APP_IDENTIFIER']}") # The bundle identifier of your app
apple_id("#{ENV['IOS_APPLE_ID']}") # Your Apple Developer Portal username
itc_team_id("#{ENV['IOS_ITC_TEAM_ID']}") # App Store Connect Team ID
team_id("#{ENV['IOS_TEAM_ID']}") # Developer Portal Team ID
My final lane Fastfile
looks like this:
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
desc "Push a new build to TestFlight"
lane :ios_deploy do
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
upload_to_testflight(
notify_external_testers: false
)
end
end
The first two lines of code declare that this lane runs on the ios
platform. The name of the lane is ios_deploy
.
The first fix I had to make was the next line of code:
# ./ios/fastlane/Fastfile
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"
The line of code above was used to address the issue that can be found here. The remaining two lines of code are self-explanatory, they are; build the app and upload it to Testflight. You can choose to set the field notify_external_testers
to true or false if your app-specific password has the authorization for that or not.
I had some other issues while trying to upload to Testflight. They are highlighted below:
An issue with
authorization to get username and password
, I had to create an app-specific password and add it to my env file with the keyFASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD
. More info can be found in this issue hereAdded this update
source="$(readlink -f "${source}")"
to the file /Pods/Target Support Files/Pods-APPNAME/Pods-APPNAME-frameworks.sh as described here.I added these other ENV variables
IOS_APPLE_ID
which is the email to your developer account.FASTLANE_PASSWORD
which is the password to said account
After I got the Testflight upload to work successfully, this is how my env file looked:
path: ./ios/
APP_IDENTIFIER=co.company.app
IOS_APPLE_ID=name@mail.co
IOS_ITC_TEAM_ID=*****
IOS_TEAM_ID=*****
FASTLANE_USER=name@mail.co
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD=appe-spec-passw-orde
To run this lane, I used the command:
# ./ios/
bundle exec fastlane ios_deploy
Working with GitHub Actions
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform that allows you to automate your build, test, and deployment pipeline. You can create workflows that build and test every pull request to your repository, or deploy merged pull requests to production. I will not be getting into the nitty-gritty of setting up GitHub actions; however, I will show how to make it work with the lanes we have written.
My final Github Workflow file looks like this:
# ./.github/workflow.yaml
env:
keyAlias: ${{ secrets.KEYALIAS }}
keyPassword: ${{ secrets.KEYPASSWORD }}
storePassword: ${{ secrets.STOREPASSWORD }}
HUAWEI_CLIENT_ID: ${{ secrets.HUAWEI_CLIENT_ID }}
HUAWEI_CLIENT_SECRET: ${{ secrets.HUAWEI_CLIENT_SECRET }}
HUAWEI_APP_ID: ${{ secrets.HUAWEI_APP_ID }}
FLUTTER_VERSION: ${{secrets.FLUTTER_VERSION }}
AAB_PATH: ${{ secrets.AAB_PATH }}
APK_PATH: ${{ secrets.APK_PATH }}
FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY }}
jobs:
build_and_deploy_for_playstore_huawei:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3.9.0
with:
distribution: "zulu"
java-version: "11"
- uses: subosito/flutter-action@v2.8.0
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build APK
run: |
flutter pub get
flutter packages get
flutter test
flutter build appbundle --flavor production --release
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2.2'
bundler-cache: true
- name: Install dependencies
run: |
cd android
gem install bundler
bundle install
- name: Deploy To Google Playstore
uses: maierj/fastlane-action@v3.0.0
with:
lane: huawei_deploy
subdirectory: android
First, notice how the env variables declared at the top match the keys of the variables in our local env file. This is essential for interoperability. To create secrets (essentially environment variables) for your repository, you can follow the documentation here.
# ./.github/workflow.yaml
HUAWEI_CLIENT_ID: ${{ secrets.HUAWEI_CLIENT_ID }}
HUAWEI_CLIENT_SECRET: ${{ secrets.HUAWEI_CLIENT_SECRET }}
HUAWEI_APP_ID: ${{ secrets.HUAWEI_APP_ID }}
FLUTTER_VERSION: ${{secrets.FLUTTER_VERSION }}
AAB_PATH: ${{ secrets.AAB_PATH }}
APK_PATH: ${{ secrets.APK_PATH }}
FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.FASTLANE_GOOGLE_SERVICE_ACCOUNT_KEY }}
Next, we use the ruby/setup-ruby@v1
action to install Ruby on our virtual machine. We install the dependencies needed to run Fastlane i.e bundler using gem install bundler
.
# ./.github/workflow.yaml
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2.2'
bundler-cache: true
- name: Install dependencies
run: |
cd android
gem install bundler
bundle install
- name: Deploy To Huawei App Gallery
uses: maierj/fastlane-action@v3.0.0
with:
lane: huawei_deploy
subdirectory: android
We then call the lane of choice, this time huawei_deploy
, to deploy to Huawei App Galley, we can follow this format for Playstore too. Next time, we will look at the GitHub Action for iOS.
Conclusion
I hope my experience with automating flutter deployments with Fastlane made for a good read. I certainly enjoyed my time researching and writing up the code, and I hope you do too. If you have any questions, please feel free to reach out to me on Twitter or via email.
Have a great day!