Automatically Deploying Android Apps Built with Flutter to Google Play Using GitHub Actions

Introduction

Recently, I released an Android app called subskun.

For the initial release, I built the app locally and manually uploaded it to the store. However, doing this manually every time became tedious, so I decided to automate the process.

I set up a workflow using GitHub Actions.

Workflow

Here’s the overall workflow I created. It is triggered by a push to the main branch, which is used for releases.

name: Deploy to Google Play

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Flutter
        uses: subosito/flutter-action@v2
        with:
          flutter-version: '3.0.1'
          channel: 'stable'
          cache: true
      - name: Run gen-l10n
        run: flutter gen-l10n
      - name: Build aab
        run: |
          echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/google-services.json
          echo '${{ secrets.ANDROID_JKS_BASE64 }}' | base64 -d > android/app/release.keystore
          export KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
          export KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
          export KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
          flutter build appbundle --release --flavor production --dart-define=FLAVOR=production --build-number='${{ secrets.ANDROID_BUILD_NUMBER }}' --obfuscate --split-debug-info=obfuscate/android
      - name: Create service_account.json
        run: echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json
      - name: Upload artifact to Google Play
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJson: service_account.json
          packageName: ${{ secrets.PACKAGE_NAME }}
          releaseFiles: build/app/outputs/bundle/productionRelease/app-production-release.aab
          track: production
          status: completed

Step-by-Step Explanation

The workflow up to the Build aab step is the same as the one I introduced in a previous article, so if you’re already familiar with that, feel free to skip to the Create service_account.json step.

Automatically Deploying Android Apps Built with Flutter to Firebase App Distribution Using GitHub Actions

actions/checkout@v3

This step checks out the repository, so there’s not much to explain.

Set up Flutter

This sets up the Flutter environment. The subosito/flutter-action action is commonly used for this purpose.

For reference, subskun uses Flutter 3.

Run gen-l10n

Since subskun supports multiple languages, I need to run the following command to generate localization code before building:

flutter gen-l10n

Build aab

Now it’s time to start the build, but first, I need to do some preparation.

Since subskun uses Firebase, I generate configuration files from GitHub Actions secrets:

mkdir android/app/src/productionRelease
echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/src/productionRelease/google-services.json

The following code prepares the passwords and other details required to sign the Android app. Since the jks file is binary, I store its base64-encoded version in GitHub Actions secrets and decode it here:

echo '${{ secrets.ANDROID_JKS_BASE64 }}' | base64 -d > android/app/release.keystore
export KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
export KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
export KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'

As you can see above, passwords and other information are read from environment variables, so the signingConfigs section of android/app/build.gradle looks like this:

signingConfigs {
    release {
        keyAlias System.getenv('KEY_ALIAS')
        keyPassword System.getenv('KEY_PASSWORD')
        storeFile file('release.keystore')
        storePassword System.getenv('KEYSTORE_PASSWORD')
    }
}

Finally, the build process begins.

I use dart-define to handle environment settings and --obfuscate to obfuscate the code.

I’m currently providing the build number from secrets, but I’m considering automatically setting the build number based on the app’s version number. Personally, I prefer to provide the build number externally for both Android and iOS, but I might switch to a more convenient method if I find one.

flutter build appbundle --release --flavor production --dart-define=FLAVOR=production --build-number='${{ secrets.ANDROID_BUILD_NUMBER }}' --obfuscate --split-debug-info=obfuscate/android

Note (2022-10-05 update)

I’ve been informed of a way to set the build number based on the number of commits. If you’re interested, check out the following article:

Setting the Build Number Based on Commit Count When Building a Flutter App on GitHub Actions

End of note

Create service_account.json

To upload to the store via API, you need a service account, and this step creates the necessary file.

echo '${{ secrets.ANDROID_SERVICE_ACCOUNT_JSON }}' > service_account.json

I referred to the following article for instructions on creating the service account:

How to Create a Service Account for Google Play Store

Upload artifact to Google Play

Now that everything is ready, I finally upload the build to Google Play.

I used the r0adkll/upload-google-play@v1 action for this.

- name: Upload artifact to Google Play
  uses: r0adkll/upload-google-play@v1
  with:
    serviceAccountJson: service_account.json
    packageName: ${{ secrets.PACKAGE_NAME }}
    releaseFiles: build/app/outputs/bundle/productionRelease/app-production-release.aab
    track: production
    status: completed

You can also set the update priority, user rollout percentage, release notes, and more when uploading the app. For more information, refer to the README.

Conclusion

With this setup, I was able to automate the deployment to the Play Store triggered by a merge to the main branch.

A word of caution from my own experience: while debugging the workflow, it succeeded and unintentionally submitted my app for review on the Play Store. Since there was an actual update and I was planning to submit it soon anyway, I left it for review. However, if an app is accidentally submitted for review, you cannot cancel it, which was a bit problematic. So, be careful with your workflow.

Previous

Switching to Warp from iTerm2

Next

Automatically Deploying Android Apps Built with Flutter to Firebase App Distribution Using GitHub Actions

PR

Related Posts