This article is the second part of our series on automating the deployment process for Flutter apps. In the previous article, we explored how to automate the deployment of a Flutter iOS app.
Part 1:
Now, we’ll set up a Github action for an Android application using fastlane.
By the end of this article, you’ll have an efficient distribution pipeline for your Android application.
Why fastlane?
fastlane is a straightforward and efficient tool for integrating CI/CD pipelines into Android app development. It not only simplifies deployment but also handles tasks like generating screenshots, managing code signing, and automating releasing processes.
Before diving in, ensure you have a Flutter application and a GitHub repository ready for your project.
A basic workflow setup is also essential to proceed, including checking out the repository and setting up the Flutter environment. Since this setup is covered in our previous article, we won’t repeat it here.
For detailed instructions on setting up the workflow and Flutter environment, refer to our Basic Workflow Setup Guide.
Ready? Let’s go! 🚀
We’ll break down the auto-deployment process into three key parts.
Let’s dive into each of these steps. 👇
To publish your app on the Play Store, it must be signed with a digital certificate. Android uses two signing keys for this process: Upload key and App signing key.
What are these keys, and why are they important?
The Upload Key is used to sign your app when you upload it to the Play Console. After uploading, Google Play will re-sign your app with the App Signing Key before distributing it to users.
To generate the Upload Key, follow the official Android Developer Guide on App Signing.
While creating the Upload Key, make sure to remember the password and key alias that you set for the key. You’ll need these later when configuring Fastlane and setting up the deployment process.
After following these steps, you will have the XXX.jks
file at the selected file path.
The App Signing Key is the primary key used by Google Play to sign your app before delivering it to users. This key ensures that:
To generate the App Signing Key, follow the step-by-step instructions in the Collect your Google credentials section in the Fastlane setup documentation guide.
When creating the upload key, a JSON file is generated and downloaded. This file contains essential credentials that you will need to authenticate and manage your app on the Play Console. It will be required in the later steps.
Now, that we have the Upload Key and App Signing Key ready, it’s time to set up fastlane.
To get started, you’ll need to install Fastlane on your machine.
# Using RubyGems(macOS/Linux/Windows)
sudo gem install fastlane
# Alternatively using Homebrew(macOS)
brew install fastlane
fastlane is installed, let’s configure it within your Flutter project.
Initialize fastlane
Open your terminal and navigate to your project’s root directory and change the directory to android
. Then, run the following command
fastlane init
During the setup process, you’ll be prompted with a series of questions:
android/app/build.gradle
file, under defaultConfig > applicationId
. For example:applicationId "io.example.yourapp"
Now, you can see a newly created ./fastlane
directory in your project. Inside this directory, you’ll find two key files:
Now, open the Appfile and add the following line to specify the path to your JSON key file, which will be used for authenticating with the Google Play API.
Also, ensure that the package_name is set to the correct value for your app and set the JSON file path in the Appfile
as follows
json_key_file("google_play_api_key.json") # Path to the json secret file
package_name("com.exapmle.appname")
🤔 Don’t worry!! We will add the google_play_api_key.json
file in the next steps. Stay tuned!
In this step, we’ll add all the necessary environment variables and secrets that fastlane and the app will use during deployment.
To add new secrets and variables to your repository, go to Settings > Secrets and Variables.
development.jks
file.development.jks
keystore file, which is generated during the creation of the App Signing Key, into Base64 format to store it as a secret. For that, open the terminal and navigate to the directory where the development.jks
file is located.base64 -i <File name>| pbcopy
Now that the Keystore is copied to your clipboard, paste this Base64 content as the value.
We will set up the environment variables for the deployment job. These variables will reference the secrets you added to your GitHub repository.
jobs:
android_deployment:
runs-on: ubuntu-latest
env:
APP_PLAY_SERVICE_JSON: ${{ secrets.APP_PLAY_SERVICE_JSON_BASE64 }}
APKSIGN_KEYSTORE_BASE64: ${{ secrets.APKSIGN_KEYSTORE_BASE64 }}
APKSIGN_KEYSTORE_PASS: ${{ secrets.APKSIGN_KEYSTORE_PASS }}
APKSIGN_KEY_ALIAS: ${{ secrets.APKSIGN_KEY_ALIAS }}
APKSIGN_KEY_PASS: ${{ secrets.APKSIGN_KEY_PASS }}
build.gradle
for SigningTo enable the Android build system to use these environment variables during the build process, add the following configuration to your android/app/build.gradle
file.
signingConfigs {
if (System.getenv("APKSIGN_KEYSTORE") != null) {
release {
storeFile file(System.getenv("APKSIGN_KEYSTORE"))
storePassword System.getenv("APKSIGN_KEYSTORE_PASS")
keyAlias System.getenv("APKSIGN_KEY_ALIAS")
keyPassword System.getenv("APKSIGN_KEY_PASS")
}
} else {
release {
// Signing with the debug keys for now, so `flutter run --release` works.
// Generate Debug keystore and add it here
}
}
}
buildTypes {
release {
minifyEnabled true
debuggable false
shrinkResources true
signingConfig signingConfigs.release
}
debug {
minifyEnabled true
debuggable true
signingConfig signingConfigs.release
}
}
Whenever we initiate a release build, the system will look for the Keystore in the specified environment variables and use them to sign the build with the provided Keystore.
For local testing in release mode, you should generate a debug Keystore. The app will use this debug keystore if the release keystore is not available.
In this step, we will define the jobs in your GitHub Actions workflow to automate the distribution of the Android app.
- name: set up JDK 1.8
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: 1.8
cache: 'gradle'
This job will set up a specific version of Java
in the workflow. Here, 'gradle'
enables the caching of Gradle dependencies, which can speed up subsequent builds by avoiding redundant downloads.
- name: Set up ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
This step sets up the Ruby environment required for running fastlane and other Ruby-based tools. It installs Ruby version 3.3.0 and caches the dependencies managed by Bundler.
- name: Deploy Internally
run: |
# Extract version information from the VERSION file
file='VERSION'
fileData=`cat $file`
IFS='.'
read -a versionValue <<< "$fileData"
# Generate a build number by combining major, minor, and the GitHub run number
buildNumber=$(( ${versionValue[0]} * 1000000 + ${versionValue[1]} * 10000 + ${{ github.run_number }} ))
IFS=''
# Generate version name for the build
buildName="${versionValue[0]}.${versionValue[1]}.${{ github.run_number }}"
echo "Generating android build $buildName $buildNumber"
echo $APKSIGN_KEYSTORE_BASE64 | base64 -di > release.jks
export APKSIGN_KEYSTORE=`pwd`/release.jks
cd android
gem install bundler -v 2.4.22
bundle install
echo $APP_PLAY_SERVICE_JSON | base64 -di > google_play_api_key.json
bundle exec fastlane supply init --track internal
bundle exec fastlane upload_internal versionName:$buildName versionCode:$buildNumber
We are creating a versioning system based on Semantic Versioning. The details of this versioning system were explained in the previous article, so if you’re unfamiliar with it, please refer to it.
echo $APKSIGN_KEYSTORE_BASE64 | base64 -di > release.jks
: We decode the Base64 encoded keystore and save it as release.jks
. This keystore will be used for signing the app during the upload process to Google Play.export APKSIGN_KEYSTORE=`pwd`/release.jks
: This command exports the keystore file and sets its path as an environment variable in the current working directory.gem install bundler -v 2.4.22
: Installs Bundler, a dependency manager for Ruby, which helps manage and install specific versions of Ruby gems.echo $APP_PLAY_SERVICE_JSON | base64 -di > google_play_api_key.json
: This will decode and write the environment variable into a JSON file so, the name should be the same as the name we’ve added in the Fastfile.bundle exec fastlane supply init — track internal
: This command initializes fastlane with the supply
action, which is used to upload your app to Google Play. The --track internal
flag specifies that the app will be uploaded to the internal track in Google Play.bundle exec fastlane upload_internal versionName:$buildName versionCode:$buildNumber
: This command uses fastlane to upload the app to the internal track on Google Play. The upload_internal
lane is responsible for uploading the build to the Google Play Console with the version name and version number.# Specifies that the default platform for this Fastfile is Android.
default_platform(:android)
platform :android do
desc "Submit a new Internal Build to Play Store"
# Defines a lane named upload_internal that takes versionName and versionCode as input options.
# This lane is responsible for building and uploading the app to Google Play's internal track.
lane :upload_internal do |options|
versionName = options[:versionName]
versionCode = options[:versionCode]
# Generate Androidd App Bundle
Dir.chdir "../.." do
sh("flutter", "build", "appbundle", "--release", "--build-number=#{versionCode}" ,"--build-name=#{versionName}")
end
upload_to_play_store(
track: 'internal', # Uploads the build to the internal track on Google Play.
skip_upload_metadata: true, # skip uploading metadata
skip_upload_images: true, # skip uploading images
skip_upload_apk: true, # skip uploading apk
aab: '../build/app/outputs/bundle/release/app-release.aab', # Specifies the path to the generated AAB file for upload.
skip_upload_screenshots: true) # skip uploading screenshots
end
end
Your project is now ready for automated Android app deployment! 🎉
The first build of your app must be published manually on the Play Console. This step includes uploading your app bundle, configuring app details, and completing the release process. After the initial setup, you can automate subsequent deployments.
I haven’t created an app for this sample project on the Google Play Store. However, if you follow the correct configuration, it’s time to push your code! 🚀 Go ahead and push your changes to the main branch, and you’ll see output like this.
It’s uploaded Successfully✨
Here is the full code:
name: Push android build on Play Store
on:
push:
branches:
- main
jobs:
android_deployment:
runs-on: ubuntu-latest
env:
APP_PLAY_SERVICE_JSON: ${{ secrets.APP_PLAY_SERVICE_JSON_BASE64 }}
APKSIGN_KEYSTORE_BASE64: ${{ secrets.APKSIGN_KEYSTORE_BASE64 }}
APKSIGN_KEYSTORE_PASS: ${{ secrets.APKSIGN_KEYSTORE_PASS }}
APKSIGN_KEY_ALIAS: ${{ secrets.APKSIGN_KEY_ALIAS }}
APKSIGN_KEY_PASS: ${{ secrets.APKSIGN_KEY_PASS }}
steps:
- name: Checkout Repo
uses: actions/checkout@v4
- name: Set up JDK 1.8
uses: actions/setup-java@v4
with:
distribution: 'oracle'
java-version: 1.8
cache: 'gradle'
- name: Set up Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'
version: 3.24.2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
cache: 'gradle'
- name: Set up ruby env
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.3.0
bundler-cache: true
- name: Deploy Internally
run: |
# Extract version information from the VERSION file
file='VERSION'
fileData=`cat $file`
IFS='.'
read -a versionValue <<< "$fileData"
# Generate a build number by combining major, minor, and the GitHub run number
buildNumber=$(( ${versionValue[0]} * 1000000 + ${versionValue[1]} * 10000 + ${{ github.run_number }} ))
IFS=''
# Generate version name for the build
buildName="${versionValue[0]}.${versionValue[1]}.${{ github.run_number }}"
echo "Generating android build $buildName $buildNumber"
echo $APKSIGN_KEYSTORE_BASE64 | base64 -di > release.jks
export APKSIGN_KEYSTORE=`pwd`/release.jks
cd android
gem install bundler -v 2.4.22
bundle install
echo $APP_PLAY_SERVICE_JSON | base64 -di > google_play_api_key.json
bundle exec fastlane supply init --track internal
bundle exec fastlane upload_internal versionName:$buildName versionCode:$buildNumber:$buildNumber
You can check out this open-source project where we’ve used the same workflow.
In this guide, we covered the process of setting up automated deployment for a Flutter Android app using GitHub Actions and Fastlane. If you’re interested in further customization or adding other deployment tracks (e.g., production, beta), you can modify the Fastfile and GitHub Actions workflow accordingly.
If you've come that far, I hope you've learned something and can improve your CI/CD process. 😄