This article will guide you on how you can automate the CI/CD workflow of your Flutter’s Android app.

If you are not familiar with the concept behind CI/CD, Github Actions, or Fastlane, you can read this article I wrote to help you get started.

Prerequisites

Before continue reading this,

  • You should have at least tried building your Flutter app locally, and released it manually to Google Play Console.
  • Make sure you have Fastlane installed on your development machine.

Workflow Setup

ci/cd

Setup Fastlane

a. Initialize Fastlane for Android

cd android
fastlane init

This will ask for the details of your app.

[01:00:00]: Package Name (com.krausefx.app): io.github.joshuadeguzman.fastlane_flutter_actions

Leave the Path to the json secret file blank for now.

...
Feel free to press Enter at any time in order to skip providing pieces of information when asked
[01:00:00]: Path to the json secret file:

b. Install fastlane plugin to retrieve version code from Flutter

In android/Gemfile, add the following plugin.

...
gem "fastlane"

# Add this plugin
gem "fastlane-plugin-flutter_version", git: "https://github.com/tianhaoz95/fastlane-plugin-flutter-version"

Install the plugin.

cd android/fastlane
fastlane install_plugins

c. Configure Fastfile

In android/fastlane/Fastfile, replace everything with the following

default_platform(:android)

platform :android do
  desc "Deploy to closed beta track"
  lane :closed_beta do
    begin
      gradle(task: "clean")
      gradle(
        task: "bundle",
        build_type: 'Release'
      )
      upload_to_play_store(
        track: 'Closed beta',
        aab: '../build/app/outputs/bundle/release/app-release.aab',
        skip_upload_metadata: true,
        skip_upload_images: true,
        skip_upload_screenshots: true,
        release_status: "draft",
        version_code: flutter_version()["version_code"],
      )
    end
  end
end

NOTE: By default, when you set the track to beta, fastlane uploads your build to the Open beta testing track in Google Play Console. To create a custom and closed beta track named Closed beta, please follow the instructions here.

d. Configure Appfile

In android/fastlane/Appfile, add the path for the service_account_key.json

json_key_file("service_account_key.json") # Add this
package_name("io.github.joshuadeguzman.demo_flutter_actions")

Setup Android App

a. Generate a Service Account Key

Google Developers Service Account Key (a.k.a. Service Account Key) will be used for authenticating requests sent to the Google Play Developer API. Fastlane then establishes a connection to this API to publish your app to Google Play.

1 . Open the Google Play Console

2 . In the Settings menu, select API access, then click CREATE SERVICE ACCOUNT

ci/cd

3 . Navigate to the provided Google Developers Console link in the dialog

4 . Click CREATE SERVICE ACCOUNT at the top of the Google Developers Console

5 . Provide the details required, then click CREATE

ci/cd

6 . Click Select a role, select Service Accounts, then click Service Account User

ci/cd

7 . In the Service Accounts dashboard, navigate to the Actions column, tap the menu for the service account that you created, then click Create Key

ci/cd

8 . Select JSON as the key type, then click SAVE

9 . Back on the Google Play Console, click DONE to close the dialog

10 . Click on Grant Access for the newly added service account

11 . Make sure that the role of this service account will have the permission upload builds

12 . Click ADD USER to close the dialog

b. Encrypt sensitive files

Encrypting files such as *.jks, key.properties, or the service account key adds additional layer of security to prevent any unauthorized access to view sensitive credentials or regain access to the services you use in your app.

First, add these files to your root .gitignore

...

# Ignore Android keys
key.jks
key.properties
service_account_key.json
android_keys.zip

In key.properties, you should set the correct path of the storeFile relative to the app directory

storePassword=<YOUR_STORE_PASSWORD>
keyPassword=<YOUR_PASSWORD>
keyAlias=<YOUR_STORE_KEY>
storeFile=../key.jks

Next, zip the following files, then name the zip file as android_keys.zip

ci/cd

android
- app/
- fastlane/
- ...
- android_keys.zip 👈

Lastly, encrypt the files

Encrypt files using GPG.

You will be prompted to enter a passphrase. Remember it correctly because it will be used later by Github Actions to decrypt the files.

cd android
gpg --symmetric --cipher-algo AES256 android_keys.zip

Test if you can decrypt and unzip the file locally

cd android
rm android_keys.zip key.jks key.properties service_account_key.json
gpg --output android_keys.zip --decrypt android_keys.zip.gpg && jar xvf android_keys.zip

Setup Github Actions

a. Create a Github Action script to decrypt the files

Create a scripts directory

md .github/scripts

Inside the scripts folder, create a file named decrypt_android_secrets.sh, and add the following

#!/bin/sh

# --batch to prevent interactive command
# --yes to assume "yes" for questions
gpg --quiet --batch --yes --decrypt --passphrase="$ANDROID_KEYS_SECRET_PASSPHRASE" \
--output android/android_keys.zip android/android_keys.zip.gpg && cd android && jar xvf android_keys.zip && cd -

b. Configure Github secrets

In Github repository’s settings, add the passphrase you assigned earlier as a secret variable named ANDROID_KEYS_SECRET_PASSPHRASE.

ci/cd

c. Configure Github workflow file

Create a Github workflow directory.

md .github/workflow

Inside the workflow folder, create a file named cd.yml, and add the following.

# cd.yml
name: CD

on:
  push:
    branches:
      - "v*"

jobs:
  # CI
  build_android:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Setup Java
        uses: actions/setup-java@v1
        with:
          java-version: 12.x
      - name: Decrypt Android keys
        run: sh ./.github/scripts/decrypt_android_keys.sh
        env:
          ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }}
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
        with:
          flutter-version: 1.17.5
      - name: Install Flutter dependencies
        run: flutter pub get
        # Add build runner commands here if you have any
      - name: Format files
        run: flutter format --set-exit-if-changed .
      - name: Analyze files
        run: flutter analyze .
      - name: Run the tests
        run: flutter test
      - name: Build the APK
        run: flutter build apk --release
      - name: Upload artifact to Github
        uses: actions/upload-artifact@v1
        with:
          name: release-apk
          path: build/app/outputs/apk/release/app-release.apk
  # CD
  deploy_android:
    runs-on: ubuntu-latest
    needs: [build_android]
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Setup Java
        uses: actions/setup-java@v1
        with:
          java-version: 12.x
      - name: Decrypt Android keys
        run: sh ./.github/scripts/decrypt_android_keys.sh
        env:
          ANDROID_KEYS_SECRET_PASSPHRASE: ${{ secrets.ANDROID_KEYS_SECRET_PASSPHRASE }}
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
        with:
          flutter-version: 1.17.5
      - name: Install Flutter dependencies
        run: flutter pub get
        run: flutter build apk --release
      - name: Run Fastlane
        uses: maierj/fastlane-action@v1.4.0
        with:
          lane: closed_beta
          subdirectory: android

That’s it! Now for the moment of truth, test the workflow on Github Actions.

Test Workflow

a. Create a branch

Create a branch, eg. v0.0.3.

git checkout -b v0.0.3

b. Trigger the workflow

Push the new commits to the newly created branch to trigger the workflow.

git push origin v0.0.3

ci/cd

Bonus

You can also create other Github workflows such as running CI jobs whenever there’s a new pull request to the master branch.

Here’s an example of a CI only workflow for a pull request

# ci.yml
name: CI

on:
  pull_request:
    branches:
      - master

jobs:
  build_apk:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Setup Java
        uses: actions/setup-java@v1
        with:
          java-version: 12.x
      - name: Decrypt Android keys
        run: sh ./.github/scripts/decrypt_android_keys.sh
        env:
          ANDROID_KEYS_SECRET_PASSPHRASE: $
      - name: Setup Flutter
        uses: subosito/flutter-action@v1
        with:
          flutter-version: 1.17.5
      - name: Install Flutter dependencies
        run: flutter pub get
      - name: Format files
        run: flutter format --set-exit-if-changed .
      - name: Analyze files
        run: flutter analyze .
      - name: Run the tests
        run: flutter test
      - name: Build the APK
        run: flutter build apk --release
      - name: Upload artifact
        uses: actions/upload-artifact@v1
        with:
          name: release-apk
          path: build/app/outputs/apk/release/app-release.apk

This triggers the workflow to run and display its status on the pull request.

ci/cd

Awesome!

That’s it for your Flutter’s Android app.

Take note, the setup of jobs may vary depending on the tools or dependencies your project requires. For example, if it uses Firebase Cloud services, you might need to encrypt the google_services.json file as well.

What’s next?

Resources