Deploying Flutter applications to Google Play using Github actions
Tags: flutter, devops, cicd, github
Published: 2022-09-15 09:38:33

## Introduction

After or during the development of a Flutter application, usually you'd want to see how it works on real devices and distribute it to other people.
The de facto distribution service for Android apps is Google Play and deploying to it will be covered in this post using GitHub Actions which are very flexible and customizable.

## Preparations

Before even considering to deploy an app to Google Play, you need to generate an upload keystore; a repository of certificates and private keys used to verify your application.

To do this, run the following command:

- On Windows: `keytool -genkey -v -keystore %userprofile%\upload-keystore.jks -storetype JKS -keyalg RSA -keysize 2048 -validity 10000 -alias upload
`

- On Mac/Linux: `keytool -genkey -v -keystore ~/upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
`

If you want to change the destination of the generated keystore, just change the `keystore` parameter. Remember the password used for generating the keystore as it will be needed later.

**However, keep the file private and do not check it into source control.**

Afterwards, create a file called `key.properties` in your `android` folder with the following contents:

```
storePassword=#{STORE_PASSWORD}#
keyPassword=#{KEY_PASSWORD}#
keyAlias=uploadkey
storeFile=./upload-keystore.jks
```

**Also keep the file private and do not check it into source control.**

Subsequently, the `STORE_PASSWORD` and `KEY_PASSWORD` text will be replaced by secrets located in your Github secrets tab.

![Github secrets tab](https://jizipjmjieshqxsqkvgw.supabase.co/storage/v1/object/public/bucket/gh-secrets?t=2022-09-09T11%3A11%3A29.799Z)

To make Gradle automatically use your upload key while building in release mode, a few changes need to be made in the `android/app/build.gradle` file.

Place the following before the `android` block:

```gradle
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
   keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
	 ...
}
```

This'll be used to load in the keystore information later.

Additionally, signing configuration info must be added before the `buildTypes` block:

```gradle
signingConfigs {
   release {
	   keyAlias keystoreProperties['keyAlias']
	   keyPassword keystoreProperties['keyPassword']
	   storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
	   storePassword keystoreProperties['storePassword']
   }
}

buildTypes {
	 ...
}
```

The `buildTypes` block has to be changed as well, however:

```gradle
buildTypes {
   release {
	   signingConfig signingConfigs.release
   }
}
```

The app will now be automatically signed each time it is assembled into an APK or AAB.

Flutter uses auto versioning and updates the Android and iOS settings based on the version entry in the `pubspec.yaml` file.

In order to create a dynamic version number, we'll have to edit the version number to something we can change later.

```yaml
name: app
description: Description.
publish_to: "none"
version: 99.99.99+99
```

## Setting up the workflow

To create a workflow using GitHub actions, create a file with the name of your choosing in your root directory: `.github/workflows/<fileName>.yaml`

Give the workflow a name and define which branch you want to use to trigger the action:

```yaml
name: Flutter CI

on:
  push:
    branches: [master]
```

## Creating a version number

The first step of the whole process is creating a version number using generated git tags.

Since we'll need to access some info on the repository, add your personal GitHub token to the repository secrets.

You can generate your GitHub token at [this link](https://github.com/settings/tokens).

The steps are defined as such:

```yaml
jobs:
  version:
    name: Create version number
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Fetch all history for all tags and branches
        run: |
          git config remote.origin.url https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
          git fetch --prune --depth=10000
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: "5.x"
      - name: Use GitVersion
        id: gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7
      - name: Create version.txt with nuGetVersion
        run: echo ${{ steps.gitversion.outputs.nuGetVersion  }} > version.txt
      - name: Upload version.txt
        uses: actions/upload-artifact@v2
        with:
          name: gitversion
          path: version.txt
```

To get any info about the repository we need to use the `actions/checkout` action and furthermore the additional `GitVersion` actions.

`GitVersion` is not available by default and needs to be installed beforehand if we want to use the features it offers. The action is available [here](https://github.com/GitTools/GitVersion).

We create the version.txt file with the content being the output of the GitVersion `execute` command and then upload it to the artifacts section of the repository using the `upload-artifact` action.

This file will be used later in order to replace the version number inside the `pubspec.yaml` file.

## Building the app

Moving on from the previous step, now we attempt to change the version number and build the application.

```yaml
jobs:
  build:
    name: Build APK and Create release
    needs: [version]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Get version.txt
        uses: actions/download-artifact@v2
        with:
          name: gitversion
      - name: Create new file without newline char from version.txt
        run: tr -d '\n' < version.txt > version1.txt
      - name: Read version
        id: version
        uses: juliangruber/read-file-action@v1
        with:
          path: version1.txt
      - name: Update version in YAML
        run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml
      - name: Update KeyStore password in gradle properties
        run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties
      - name: Update KeyStore key password in gradle properties
        run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
      - uses: actions/setup-java@v1
        with:
          java-version: "12.x"
      - uses: subosito/flutter-action@v1
        with:
          channel: "beta"
      - run: flutter clean
      - run: flutter pub get
      - run: flutter build apk --release --split-per-abi
      - run: flutter build appbundle --release
      - name: Create a Release in GitHub
        uses: ncipollo/release-action@v1
        with:
          artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
          token: ${{ secrets.GH_TOKEN }}
          tag: ${{ steps.version.outputs.content }}
          commit: ${{ github.sha }}
      - name: Upload app bundle
        uses: actions/upload-artifact@v2
        with:
          name: appbundle
          path: build/app/outputs/bundle/release/app-release.aab
```

Using `read-file-action` we read the contents of the previously generated `version.txt` file and, using the `sed` command, we replace the contents of the `pubspec.yaml` file (the app version) and the contents of the `key.properties` file with the secrets we entered before.

To build flutter applications though, we need an additional action called `flutter-action` - available [here](https://github.com/subosito/flutter-action).

Specify the version of Flutter you want to use; in this case I use the `beta` channel because of the experimental features that are needed in my application.

Firstly we clean the project using the `flutter clean` command, then get the required packages with `flutter pub get`, build the apk using `flutter build apk --release --split-per-abi` and finally finish it off with a bundle generation: `flutter build appbundle --release`.

After all of that is done we create a release using the `release-action` action, located [here](https://github.com/ncipollo/release-action) and upload the app bundle we just generated as it will be needed in the next workflow step.

## Google Play deploy preparations

Before reviewing the final step, a Google service account on the [Google Cloud platform](https://console.cloud.google.com) needs to be set up in order to deploy to the Google Play Console.

Create a new project and create a service account:

![Google service account](https://jizipjmjieshqxsqkvgw.supabase.co/storage/v1/object/public/bucket/google-service-account.png?t=2022-09-09T12%3A12%3A14.542Z)

After making an account, click on the three dots on the right side and select `Manage keys` to generate a new access key which will be used to deploy the app.

When making the key, the JSON option is encouraged:

![Service account key selection](https://jizipjmjieshqxsqkvgw.supabase.co/storage/v1/object/public/bucket/service-account-key.png)

Copy the contents of the JSON file to your GitHub secrets tab under the name `PLAYSTORE_ACCOUNT_KEY`.

After creating the service account, enable API access on your Google Play Console project and link it with the Google Cloud Platform project.

![Google play console API](https://jizipjmjieshqxsqkvgw.supabase.co/storage/v1/object/public/bucket/google-play-console-api.png?t=2022-09-09T12%3A22%3A08.600Z)

Service accounts should automatically be displayed under the **Service accounts** section.

## Deploying to Google Play and creating a release

**⚠️ Important note**: You may need to create an initial release by manually uploading an Android bundle and filling out the rest of the required info

The final step in the workflow is deploying the generated AAB file to the Play Store onto a track of your choosing and then creating a release on GitHub and Google Play.

```yaml
jobs:
  release:
    name: Release app to internal track
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - 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.app.package_name
          releaseFiles: app-release.aab
          track: alpha
          status: completed
		  
```

Using the `checkout` action we get the AAB from the artifacts section and afterwards use the `upload-google-play` action to upload the bundle to the Google Play Console.

The `upload-google-play` action is located [here](https://github.com/r0adkll/upload-google-play). It offers quite a few parameters used to release the app, the most important being:

- serviceAccountJsonPlainText - The JSON record fetched from the Google Cloud Platform service account
- packageName - The package name generated from your `pubspec.yaml` file, you can change this at any point before an initial release, but it needs to stay the **same** on every subsequent release
- releaseFiles - Generated files from the previous step, located in the Artifacts section
- track - The track you want to release the file to, in this instance the **Closed testing** track
- status - An additional parameter indicating the state of the application

## The whole workflow file

```yaml
name: Flutter CI

on:
  push:
    branches: [master]
jobs:
  version:
    name: Create version number
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Fetch all history for all tags and branches
        run: |
          git config remote.origin.url https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
          git fetch --prune --depth=10000
      - name: Install GitVersion
        uses: gittools/actions/gitversion/setup@v0.9.7
        with:
          versionSpec: "5.x"
      - name: Use GitVersion
        id: gitversion
        uses: gittools/actions/gitversion/execute@v0.9.7
      - name: Create version.txt with nuGetVersion
        run: echo ${{ steps.gitversion.outputs.nuGetVersion  }} > version.txt
      - name: Upload version.txt
        uses: actions/upload-artifact@v2
        with:
          name: gitversion
          path: version.txt
  build:
    name: Build APK and Create release
    needs: [version]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Get version.txt
        uses: actions/download-artifact@v2
        with:
          name: gitversion
      - name: Create new file without newline char from version.txt
        run: tr -d '\n' < version.txt > version1.txt
      - name: Read version
        id: version
        uses: juliangruber/read-file-action@v1
        with:
          path: version1.txt
      - name: Update version in YAML
        run: sed -i 's/99.99.99+99/${{ steps.version.outputs.content }}+${{ github.run_number }}/g' pubspec.yaml
      - name: Update KeyStore password in gradle properties
        run: sed -i 's/#{KEYSTORE_PASS}#/${{ secrets.KEYSTORE_PASS }}/g' android/key.properties
      - name: Update KeyStore key password in gradle properties
        run: sed -i 's/#{KEYSTORE_KEY_PASS}#/${{ secrets.KEYSTORE_KEY_PASS }}/g' android/key.properties
      - uses: actions/setup-java@v1
        with:
          java-version: "12.x"
      - uses: subosito/flutter-action@v1
        with:
          channel: "beta"
      - run: flutter clean
      - run: flutter pub get
      - run: flutter build apk --release --split-per-abi
      - run: flutter build appbundle --release
      - name: Create a Release in GitHub
        uses: ncipollo/release-action@v1
        with:
          artifacts: "build/app/outputs/apk/release/*.apk,build/app/outputs/bundle/release/app-release.aab"
          token: ${{ secrets.GH_TOKEN }}
          tag: ${{ steps.version.outputs.content }}
          commit: ${{ github.sha }}
      - name: Upload app bundle
        uses: actions/upload-artifact@v2
        with:
          name: appbundle
          path: build/app/outputs/bundle/release/app-release.aab
  release:
    name: Release app to internal track
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - 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.app.package_name
          releaseFiles: app-release.aab
          track: alpha
          status: completed
```

## Conclusion

Even though setting up automatic deployments is a chore, it is extremely useful in the long run because of the lack of additional work needed to upload apps to external sources.