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.

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.
- 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_JKS }}
KEY_PATH: ${{ github.workspace }}/key.jks
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
ALIAS_PASSWORD: ${{ secrets.ALIAS_PASSWORD }}
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.GITHUB_TOKEN }} # this is automatically provided by github
commit: ${{ github.sha }}
- 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.
-
Go to https://play.google.com/console/api-access or On playstore Console
Settings > Developer account > API access
-
Accept the Terms of Service
-
Click Create new project. An API project will be automatically generated and linked to your Google Play Console.
-
Under Service Accounts, click Create Service Account
-
Follow the instructions on the page to create your service account.
-
Now go to
Users & Permission
and invite the service account using the invite user form and set release permission -
Add the json key generated as github repo secrets
-
Add
project_root/distribution/whatsnew
directory in project root and addwhatsnew-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.PLAYSTORE_ACCOUNT_KEY }}
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_JKS }}
KEY_PATH: ${{ github.workspace }}/key.jks
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
ALIAS_PASSWORD: ${{ secrets.ALIAS_PASSWORD }}
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.GITHUB_TOKEN }} # this is automatically provided by github
commit: ${{ github.sha }}
- 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.PLAYSTORE_ACCOUNT_KEY }}
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.