Automate Flutter Android App Deployment with GitHub Actions and fastlane

Learn how to auto-deploy the Flutter Android app with Github actions and fastlane.
Nov 20 2024 · 9 min read

Background

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.

Prerequisite

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! 🚀

Let’s Get Started

We’ll break down the auto-deployment process into three key parts.

  1. Configure Android Code Signing
  2. Set Up fastlane
  3. Add Jobs for Publishing the App

Let’s dive into each of these steps. 👇

1. Configure Android Code Signing

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?

Upload Key

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.

  • The Upload Key is essential for authenticating your app during uploads.
  • It needs to be kept secure to prevent unauthorized access and to protect the integrity of your app’s updates.

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.

App Signing Key

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:

  • Your app updates are trusted by users.
  • Only apps signed with the same key can be installed or updated.
  • This key remains consistent throughout the lifetime of your app, providing security for future updates.

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.

2. Set Up Fastlane

Now, that we have the Upload Key and App Signing Key ready, it’s time to set up fastlane.

Install 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

Set Up fastlane in Your Project

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:

  • Package Name: Provide the package name of your application. You can find it in your android/app/build.gradle file, under defaultConfig > applicationId. For example:
applicationId "io.example.yourapp"
  • Path to JSON Secret File: Press Enter when asked for the path to your JSON secret file. We'll set up it later.
  • Download existing metadata and set up metadata management: Choose the option based on how you plan to manage app metadata and screenshots during deployment.
  • Google Play Upload: When asked if you plan on uploading information to Google Play via fastlane, answer ’n’ (No). We will configure this in a later step.

Now, you can see a newly created ./fastlane directory in your project. Inside this directory, you’ll find two key files:

  • Appfile: This file contains global configuration information for your app, such as the app’s package name and JSON key file.
  • Fastfile: This file defines the “lanes,” which are sets of actions that drive the behavior of fastlane for various tasks.

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!

Add Secrets to the Repository

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.

Github Action Secrets
  • APKSIGN_KEYSTORE_PASS: The password for the keystore that is set when creating the development.jks file.
  • APKSIGN_KEY_ALIAS: The alias created for the key inside the keystore .
  • APKSIGN_KEY_PASS: The password associated with the alias created for app’s signing key.
  • APKSIGN_KEYSTORE_BASE64: We need to convert the 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.

  • APP_PLAY_SERVICE_JSON_BASE64: Convert JSON file downloaded while creating Upload key to Base64 format in a similar way as the keystore file and add it as secret.

Set up environment variables

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

Configure build.gradle for Signing

To 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.

3. Add Jobs For Distribution

In this step, we will define the jobs in your GitHub Actions workflow to automate the distribution of the Android app.

Set up JDK
 

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

Set up the Ruby environment

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

Deploy internally

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

Fastfile configurations

# 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.

Github Action Workflow

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

Source code

You can check out this open-source project where we’ve used the same workflow. 


Conclusion

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. 😄


Related Articles

 


Code, Build, Repeat.
Stay updated with the latest trends and tutorials in Android, iOS, and web development.
sneha-s image
Sneha Sanghani
Flutter developer | Writing a Blog on Flutter development
sneha-s image
Sneha Sanghani
Flutter developer | Writing a Blog on Flutter development
canopas-logo
We build products that customers can't help but love!
Get in touch
footer
Follow us on
2024 Canopas Software LLP. All rights reserved.