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: |
mkdir android/app/src/productionRelease
echo '${{ secrets.GOOGLE_SERVICES_JSON }}' > android/app/src/productionRelease/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.
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.