Image for post
Image for post
Thanks for the photo Michał!

Automating publishing your Flutter apps to Google Play using GitHub Actions

Albert Wolszon
Sep 13 · 6 min read

Automating can help you save some time. It’s also very convenient. It also often helps your environment stay consistent: e.g. you tag a commit in your GitHub repository and a new store release is published.

Image for post
Image for post
xkcd: Automation

Lastly, I spent some time on automating publishing my Flutter app to Google Play, which I usually did from my Windows desktop, but it was annoying to type this long fastlane supply command with my Google Service Account JSON key path, etc. It took me a good day to assemble everything together and I finally managed to succeed. In this post, I wanted to share this knowledge (and workflow source code) to the community so that you won’t have to waste that much time on it.

Intro

I’m going to do a walkthrough through my GitHub Actions workflow. It has the following features:

  • flutter analyze‘ying,
Image for post
Image for post
All workflow steps

Flutter Continuous Integration

Let’s start by creating a new workflow file. I’m calling this workflow Flutter release. We’re going to put it in .github/workflows/flutter-release.yml.

name: Flutter releaseon:
release:
types: [published]

Here, we are declaring the name of the workflow and the events that will trigger it.

jobs:
release:
name: Test, build and release
runs-on: ubuntu-latest steps:
- name: Checkout
uses: actions/checkout@v1

- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: '12.x'
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: beta
- name: Flutter version
run: flutter --version

Let’s now create our first (and only) job.

BTW, why did I set all of this up in only single job? Sharing files between jobs requires publishing artifacts, and those are files that you have a free usage limit on. I wanted to avoid that.

We’re giving it a name and specifying the platform it will run on. Not only Ubuntu is enough for us, but it’s also the cheapest and you probably won’t have to worry about exceeding the free GitHub Actions usage limits anyway.

We’ll start by checking out the code, setting up the Java and Flutter, and printing the Flutter version that the Actions is using to the output. It can be helpful in case of debugging.

      - name: Cache pub dependencies
uses: actions/cache@v2
with:
path: ${{ env.FLUTTER_HOME }}/.pub-cache
key: ${{ runner.os }}-pub-${{ hashFiles('**/pubspec.lock') }}
restore-keys: ${{ runner.os }}-pub-
- name: Download pub dependencies
run: flutter pub get

- name: Run build_runner
run: flutter pub run build_runner build --delete-conflicting-outputs

Here, we are telling Actions on what to cache, or rather — in that point of time — on what to retrieve. Specifically the pub cache. We’re using the FLUTTER_HOME environment variable from the previous subosito/flutter-action step.

Next, we’re downloading the pub dependencies using nothing else but flutter pub get.

If you’re using build_runner in your project and you decided to not push the generated files to the repository, that’s the place where you’d want to have your build_runner build step, which I do want and do have.

      - name: Run analyzer
run: flutter analyze
- name: Run tests
run: flutter test

Let’s make sure our code passes analyzer check and our tests.

Compiling and signing Android App Bundle

If you were following the Signing the app section of Build and release an Android app, you use android/key.properties file for giving keystore path and passwords to the Gradle. I’m using that too.

To use the keystore for signing we firstly need to have it available in the Actions filesystem.

      - name: Download Android keystore
id: android_keystore
uses: timheuer/base64-to-file@v1.0.3
with:
fileName: key.jks
encodedString: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}

I’m retrieving the base64 encoded JKS file from secrets and saving it in the workflow.

But if we want to use that secret, we firstly have to declare it. Head to your GitHub repository Settings > Secrets and create a new one. Name it ANDROID_KEYSTORE_BASE64.

You need to base64 encode your keystore file. You should either have bash or Git Bash present on your operating system. Run the following command:

base64 <your-keystore-file.jks>

and copy the output. Paste it as a secret’s value and save it.

It’s safe there.

      - name: Create key.properties
run: |
echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > android/key.properties
echo "storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> android/key.properties
echo "keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}" >> android/key.properties
echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" >> android/key.properties

We still need the key.properties file. Let’s create it with the step above. For the storeFile value we’ll be using the path from the previous step. You still need to set ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_PASSWORD and ANDROID_KEY_ALIAS secrets.

      - name: Build Android App Bundle
run: flutter build appbundle

Now, we can finally run our flutter build. Nothing fancy to see here.

Publishing the app to Google Play

This is it. We have our app compiled and we can proceed to publish it to Google Play.

We will use fastlane’s supply for this. It’s nice because it works on all platforms, not only on macOS. Apart from the application binary itself, we’ll be also making sure the store listing information are up to date with the metadata we have in our repository. Because we have it in the first place, right?

fastlane
└───metadata
└───android
└───en-US
│ full_description.txt
│ short_description.txt
│ title.txt

├───changelogs
│ 10.txt
│ 11.txt
│ 9.txt

└───images
│ featureGraphic.jpg
│ icon.png

└───phoneScreenshots
1.jpg
2.jpg
3.jpg

Above is an example files structure for fastlane’s metadata. They’re basically the strings and images that you’d put in your Google Play Console.

You can see how it looks like in practice by looking at my repository.

      - name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: '2.6'
- name: Cache bundle dependencies
uses: actions/cache@v2
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: ${{ runner.os }}-gems-
- name: Download bundle dependencies
run: |
gem install bundler:2.0.2
bundle config path vendor/bundle
bundle install

Because fastlane is a ruby tool, we need to setup Ruby first. We’re also caching the gems (like pub packages but for Ruby).

This last step installs the dependencies from the Gemfile (or its lock file), but we currently don’t have any. Let’s create it in the root of our project:

source "https://rubygems.org"gem "fastlane"

You can but don’t have to install it locally with bundle install. That way you’ll get Gemfile.lock created which you can track in the repository to always be sure of the fastlane (and other dependencies) version you’re using.

Snare drums…

      - name: Release to Google Play (beta)
env:
SUPPLY_PACKAGE_NAME: ${{ secrets.ANDROID_PACKAGE_NAME }}
SUPPLY_JSON_KEY_DATA: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
run: |
bundle exec fastlane supply \
--aab build/app/outputs/bundle/release/app-release.aab \
--track beta

Yes! That’s the very last step. We’re publishing it to the Google Play using supply. You need to set ANDROID_PACKAGE_NAME and GOOGLE_SERVICE_ACCOUNT_KEY secrets. The first one is just your app package name. You can find it in your build.gradle file under the applicationId key. The latter one is more tricky to obtain. Just follow supply’s setup section.

You may want to change the track to which the app is published from beta to production, alpha or internal.

Whole workflow file

Secrets you need:

  • ANDROID_KEYSTORE_BASE64

Bonus hint for your test workflow

If you had (or want to have) a test workflow, where you basically do everything like above, but stopping on flutter test, you’ll probably set the push (and maybe pull_request) events in the workflow on clause. But this will result in running both the test and release workflows at the same time when you tag a commit.

To prevent this, change your test workflow on clause as follows:

on:
push:
branches:
- '**'
tags-ignore:
- 'v*'
pull_request:

The tags-ignore v** will tell the GitHub to not run this workflow when the commit’s tag begins with a v (so v1.0.0, v2.1.4 etc.).

The branches is needed, because otherwise, the push event won’t listen for anything. This is the part that made me debug the workflows for a few hours. Solution source.


If you liked this article don’t forget to clap and share it!

Read more

Flutter Community

Articles and Stories from the Flutter Community

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store