Automate Your Flutter Release with Github Action While Keeping Your Secret Safe

What will be covered

Flutter App Android CICD for personal project. Improvement is needed for professional usage.

Notes

Follow the steps on the branch you are planning to use the action. I used this action on a separate branch. If you are planning to use the action on the same branch as your working branch, make sure to follow this section.

Flutter, CICD, & Github Action

Releasing your first flutter app is exciting. Watching all the unit test and wigdet test pass, building the appbundle or apk and then uploading it to playstore or appstore. But continously doing this manually is tiresome. So in this journey, i wanted to automate the task, with the goal to publish to appstore with just a push or tag added.

As a rule of thumb, before doing any release or publishing, always run unit test, widget test and test it on actual device (integration test).

However in this post, i'm not going to cover on including integration test because for my personal project, i always test it on my own device for a couple of days before release. IMO for personal project, unit and widget test with self test on actual device is enough. But for professional project or a company, it is best to include integration test in the step.

Also i commented flutter analyze and flutter format --set-exit-if-changed because for my workflow, i already tested the code locally and my intention is to publish to playstore on git tag. flutter format is recommended as you don't have to manually format your code and it will automatically format your code to a standard flutter code. This is really helpful when you are working in a team.

Github Action

As you know, github action is one of the tools that can be use to automate software development workflow. Whenever an events (github event) such as pull request or push occur we can use it to trigger an actions set. These actions are run inside a container. We can set the action by creating file inside creating these folder inside project folder .github/workflows and push it to github. Github will automatically recognize these folder and file. Free github account are given 2,000 actions minutes , which means that cumulative of each of your github actions for all repo are 2,000 minutes. For example a simple flutter project might take about 2 minutes from test to build, and as the project go the action might take about 10 minutes.

Github Action Quota can be access at github Settings > Billing & Plans.

Github action can be accessed from the repo page as below.

github_action

Understand Github Action Billings

  • Free action: 2,000 minutes (free for public repo)
  • Free Storage for Action and Packages: 0.5GB (free for public repo)

Action Minutes

Is pretty generous and hard to reach if you set proper trigger and action for project might take about from less than a minutes to 10 minutes.

Storage for Action and Packages

Include action logs and artifacts, which can be easily exceed if not monitor ed properly. What you can do is to set lower retention for action log and artifacts. Or do not upload artifact at all to github.

Consider comment out or remove the create github artifact release block to mitigate exceeding the limit unecessarily as you can always git checkout and create the release locally if needed.

Project Repo > Settings > Artifact and log retention
(*new log retention duration does not apply to existing log and artifact)

Requirement

  • Github Account
  • Setup Android Keystore (to generate signed apk and appbundle)

Continuous Integration

In this part, we want to make sure all test are passed, and build the release. Assuming you have your Android Keystore ready (if not follow: https://flutter.dev/docs/deployment/android#signing-the-app ).

*add explanation for each steps

Step 1: Configure your build.gradle

Go to project-folder/android/app/build.gradle and make sure your signing config are as follow:

signingConfigs {
  release {
      keyAlias System.getenv("ALIAS_PASSWORD")
      keyPassword System.getenv("KEY_PASSWORD")
      storeFile System.getenv("KEY_PATH") ? file(System.getenv("KEY_PATH")) : null
      storePassword System.getenv("KEY_PASSWORD")
  }
}

Step 2: Add the secret as Repo Secret

To keep our keystore and all our secrets safe, make sure to add the keyfile.jks in the gitignore and never upload any secrets to version control system.

  1. First you want to get the base64 blob of your Android Keystore from the .jks file. (on windows you can get openssl for windows by google from here https://code.google.com/archive/p/openssl-for-windows/downloads). Run the command in your terminal as below and this will output base64 encoded blob.
openssl base64 -in key.jks 

Copy this blob and open your project repo on github, go to repo Settings > Secrets > New Repository Secret. Paste the base 64 blob as Value and KEY_JKS as Name.

Create two more new secret as follow and replace the value with your own:

Name: KEY_PASSWORD
Value: YOUR_KEYSTORE-PASSWORD

Name: ALIAS_PASSWORD
Value YOUR-PASSWORD-ALIAS

Step 3: Writing The Action

a. First we set how we are going to trigger the action which in this case whenever we push a tag.

b. Then we write the job, we are going to run this in ubuntu-latest and set our environment variables. (these are temporary and will be deleted during the cleanup at the end of the action)

c. Then we use actions/checkout@v2 to set our project folder as current directory.

d. We are going to use java jdk-8 which is the same version as Android Studio.

e. Then echo $KEY_JKS | base64 -di > key.jks will create the key inside the project folder from our secrets that we store on Step 2. (these are temporary and will be deleted during the cleanup at the end of the action) (Notes: never upload or push any key or secret to github or any version control system)

f. Then we use subosito/flutter-action@v1 to run our flutter command. Change flutter version as you needed for your project.

g. After we ran all flutter command, we have our appbundle and apks so we need to store it temporarily.

h. Next we will uploads the apks and appbundle into github release using the release tag that we pushed earlier. You can omit this part if you don't want to save it as your github release.

For this we are done with this integration section. Next we are going to continue in the deployment section where we add a couple more line to upload to playstore.

name: Flutter CICD # action name

on:
  push:
    tags:
      - "v*"
  # push:
  #   branches: [ android-stable ]

jobs:
  build: # job's name
    runs-on: ubuntu-latest # container os
    env: # ADD environment variables
      KEY_JKS: ${{!secrets!}}
      KEY_PATH: ${{!github!}}/key.jks
      KEY_PASSWORD: ${{!secrets!}}
      ALIAS_PASSWORD: ${{!secrets!}}
    steps:
      - uses: actions/checkout@v2 # cd to current dir
      - uses: actions/setup-java@v2
        with:
          distribution: 'adopt' # See 'Supported distributions' for available options
          java-version: '8'
      - name: Create key file
        run: echo $KEY_JKS | base64 -di > key.jks
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.0.4' # change accordingly
      - run: flutter pub get
      # Statically analyze the Dart code for any errors.
      # - run: flutter analyze
      # Check for any formatting issues in the code.
      # - run: flutter format --set-exit-if-changed .
      - run: flutter test
      - run: flutter build apk --release --split-per-abi
      - run: flutter build appbundle
      - name: Create github artifact release # disable this to save storage
        uses: ncipollo/release-action@v1
        with:
          artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
          token: ${{!secrets!}} # this is automatically provided by github
          commit: ${{!github!}}
      - name: Upload app bundle artifact
        uses: actions/upload-artifact@v2
        with:
          name: appbundle
          path: build/app/outputs/bundle/release/app-release.aab

Deployment

For deployment we want to use the release that we generate from previous step and use it to upload to plasytore console. This is part of the same file to reduce action's time.

  1. Go to https://play.google.com/console/api-access or On playstore Console Settings > Developer account > API access

  2. Accept the Terms of Service

  3. Click Create new project. An API project will be automatically generated and linked to your Google Play Console.

  4. Under Service Accounts, click Create Service Account

  5. Follow the instructions on the page to create your service account.

  6. Now go to Users & Permission and invite the service account using the invite user form and set release permission

  7. Add the json key generated as github repo secrets

  8. Add project_root/distribution/whatsnew directory in project root and add whatsnew-en-US for each language with no exension

  release:
    name: Upload App to Playstore
    needs: [ build ]
    runs-on: ubuntu-latest
    steps:
    - name: cd to current directory
    - uses: actions/checkout@v2
    - name: Get appbundle from artifacts
      uses: actions/download-artifact@v2
      with:
        name: appbundle
    - name: Release app to internal track
      uses: r0adkll/upload-google-play@v1
      with:
        serviceAccountJsonPlainText: ${{!secrets!}}
        packageName: com.noxasch.hari_date_tracker
        releaseFile: app-release.aab
        track: internal
        whatsNewDirectory: distribution/whatsnew # file path from project root

Using the same configuration on local

To make sure things works locally as well, you must place your keystore inside your project folder for example my-project/key.jks.

Add your keystore password and alias in the environment variables as in Step 2.

Things To Consider

For a startup or professionally, you may want to include Integration Test, flutter format --set-exit-if-changed, flutter analyze and Code Coverage in the Github Action. So it is better to run the test on macos-latest where you can easily include Android and iOS emulator for integration test.

Full Action Code

name: Flutter CICD # action name

on:
  push:
    tags:
      - "v*"
  # push:
  #   branches: [ android-stable ]

jobs:
  build: # job's name
    runs-on: ubuntu-latest # container os
    env: # ADD environment variables
      KEY_JKS: ${{!secrets!}}
      KEY_PATH: ${{!github!}}/key.jks
      KEY_PASSWORD: ${{!secrets!}}
      ALIAS_PASSWORD: ${{!secrets!}}
    steps:
      - uses: actions/checkout@v2 # cd to current dir
      - uses: actions/setup-java@v2
        with:
          distribution: 'adopt' # See 'Supported distributions' for available options
          java-version: '8'
      - name: Create key file
        run: echo $KEY_JKS | base64 -di > key.jks
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.0.4' # change accordingly
      - run: flutter pub get
      # Statically analyze the Dart code for any errors.
      # - run: flutter analyze
      # Check for any formatting issues in the code.
      # - run: flutter format --set-exit-if-changed .
      - run: flutter test
      - run: flutter build apk --release --split-per-abi
      - run: flutter build appbundle
      - name: Create github artifact release # disable this to save storage
        uses: ncipollo/release-action@v1
        with:
          artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
          token: ${{!secrets!}} # this is automatically provided by github
          commit: ${{!github!}}
      - name: Upload app bundle artifact
        uses: actions/upload-artifact@v2
        with:
          name: appbundle
          path: build/app/outputs/bundle/release/app-release.aab
    release:
      name: Upload App to Playstore
      needs: [ build ]
      runs-on: ubuntu-latest
      steps:
      - name: cd to current directory
      - uses: actions/checkout@v2
      - name: Get appbundle from artifacts
        uses: actions/download-artifact@v2
        with:
          name: appbundle
      - name: Release app to internal track
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{!secrets!}}
          packageName: com.noxasch.hari_date_tracker
          releaseFile: app-release.aab
          track: internal
          whatsNewDirectory: distribution/whatsnew # file path from project root

Conclusion

Investing time to automate your workflow is worth it, however you have to make sure to never publish secret or any sensitive information to any online repository.

References