Nerd For Tech
Published in

Nerd For Tech

CI/CD for Android using GitHub Actions and Gradle Play Publisher

Last year I published an article about CI/CD using BitBucket Pipelines and Gradle Play Publisher. This article explains how to do the same using GitHub Actions with two main differences (apart from the fact it’s using GitHub instead of BitBucket):

  • I’ll use gradle files written in Kotlin (not Groovy)
  • I’ll also explain how to enroll the app in Play App Signing

A complete code sample can be found as part of one of my demo apps: https://github.com/1gravity/Android-ColorPicker.

Overview

There are five steps to setup the pipeline:

  1. We need programmatic access to Google Play to publish and promote apps (plus manage the meta data) -> we need an API key for the Google Play Developer API.
  2. The app needs to be enrolled in Play Signing
  3. We need to configure the Gradle build to use a signing configuration that reads the signing information from environment variables.
  4. We need to configure the Gradle Play Publisher plugin to interact with Google Play (upload, publish apps and manage meta data).
  5. We need to configure a GitHub Action to tie everything together.

Step 1: Google Play Developer API

The official documentation explains all steps in detail: https://developers.google.com/android-publisher/getting_started. Here’s a summary:

  • Accept the Terms of Service (if not done yet)
  • Create a new Google Cloud project if you haven’t created one already (otherwise link an existing one).
  • Under “Service Accounts” click on “Create new service account” and open the link that leads to the Google Cloud Platform:
  • In Google Cloud Platform click on “CREATE SERVICE ACCOUNT”:
  • Pick a meaningful name and description before hitting the “CREATE” button:
  • The account needs the role “Service Account User”:
  • You don’t need to grant user access to the new service account, Google Cloud adds the required users automatically with the correct permissions (a Google Play service and your own user) so just hit “DONE”:
  • Next you need to create an API key for the account.
    Open the actions menu (the three dots) and select “Manage keys”:
  • Under “ADD Key” select “Create new key”:
  • Create a JSON key:
  • After hitting the “CREATE” button, the key file will be downloaded to your computer. I recommend to rename the file to make its purpose more obvious:
  • Now you’re done in Google Cloud Platform and you can go back to the Google Play Console (to the API access screen). The newly created account should appear under “Service accounts” (hit the “Refresh service account” button). Click on “Grant access”:
  • Click on “Add app” and select all apps you want to manage with this service account:
  • The Account permissions are already set correctly so that the service can manage all release related activities (create releases including publication to production, management of meta data etc.).
  • Click on “Invite user” and you’re done.
    We will use the Gradle Play Publisher plugin to validate the API key setup later on.

Step 2: Enable Play App Signing

If you have published an app just recently it’s likely already enrolled in Play App Signing (in which case you can skip this chapter and go to step 3), otherwise you need to follow the steps explained here: https://developer.android.com/studio/publish/app-signing#enroll.

I will explain the process for one of my apps already published on Google Play. First you open the “App integrity” page in the Google Play Console (under “Setup”):

Select the option “Use existing app signing key from Android Studio”. Before continuing open Android Studio and export the key by going to the “Build/Generate Signed Bundle/APK” and pick the “Android App Bundle” option:

Click “Next” and make sure “Export encrypted key for enrolling published apps in Google Play App Signing” is checked:

On the next screen select “release” as build variant (so it’s signed with the release key) and click “Finish”:

Now go back to the Google Play Console and click on the “Upload private key” link and upload the key you exported in the previous step:

Accept” the terms:

You should now see a page like this:

Step 3: Gradle Build

The Gradle build needs to be configured to include a signing configuration that reads the secrets from environment variables (or the gradle.properties file in your ~/.gradle folder). If you already have one then you can skip this chapter.

The assumption is that you have a published app in Google Play and that you have access to the keystore and a signing key (or upload key) including the passwords (otherwise you would not have been able to complete the previous step).

For a local build the location of the keystore, the keystore password, the key alias and the key password will be configured in your ~/.gradle/gradle.properties file.

If you don’t have a ~/.gradle/gradle.properties file, please create one and add these four parameters (the bold part needs to be configured to fit your setup):

KEYSTORE_FILE=/path to the keystore file/playstore.keystore
KEYSTORE_PASSWORD=keystore password
KEY_ALIAS=key alias
KEY_PASSWORD=key password

Note: don’t use ~ for your home directory but use absolute paths. ~ works in a shell context but not with Gradle and Gradle Play Publisher.

Create a signing config in your app’s gradle.build.kts file:

create("release") {
storeFile = file(project.property("KEYSTORE_FILE").toString())
storePassword = project.property("KEYSTORE_PASSWORD").toString()
keyAlias = project.property("KEY_ALIAS").toString()
keyPassword = project.property("KEY_PASSWORD").toString()
}

Add the signing config to the build type:

buildTypes {
getByName("debug") {
// more stuff here
signingConfig = signingConfigs.getByName(name)
}

getByName("release") {
// more stuff here
signingConfig = signingConfigs.getByName(name)
}
}

If the signing configuration is correct then the following command should run and create one or more aab files in your build/outputs/bundle folder

./gradlew bundle

Version Code

The version code of an app needs to increase with newer versions (see https://developer.android.com/studio/publish/versioning). Because there’s no easy way to define an auto-incrementing version number, we’ll use a version code derived from a UTC time stamp (see Step 5). In the app's gradle.build.kts replace:

versionCode = 124

with:

versionCode = project.properties["BUILD_NUMBER"]
?.toString()?.toInt()?.minus(1643952714)
?: 124

This will read the version code as a property and subtract 1643952714 (1643952714 = Fri, Feb 4, 2022 5:31:54 AM GMT) in order to make sure the version code doesn’t reach the max value of 2100000000 anytime soon (in around 14 years from now, 2100000000 = Fri, Jul 18, 2036 1:20:00 PM GMT). If no parameter is passed in (local build), it will take the version code after the Elvis operator ?:.

Step 4: Gradle Play Publisher

We are now able to build the app and create a signed bundle (or apk) but we still need to configure Gradle Play Publisher to publish the signed app to Google Play.

We could also use Fastlane for this but I don’t recommend to go down that path (been there, done that). Trust me on this one…

Setting up the Gradle Play Publisher plugin is easy (see also https://github.com/Triple-T/gradle-play-publisher):

  • Add the plugin to the app’s gradle.build.kts file:
plugins {
id("com.android.application")
id("com.github.triplet.play") version "3.7.0"
// other plugins
}
  • Add a configuration block to the app’s gradle.build.kts (after the android block):
play {
val apiKeyFile = project.property("googlePlayApiKey").toString()
serviceAccountCredentials.set(file(apiKeyFile))
track.set("internal")
}

You’ll notice the “googlePlayApiKey” parameter. It’s a reference to the api key file we got when setting up the key for the service account -> google-play-api-key.json. The parameter needs to be defined in ~/.gradle/gradle.properties (analogous the signing config parameters):

googlePlayApiKey=/some-path/google-play-api-key.json

If everything was setup properly, the following command, when run from the root directory of your app, will download the app’s meta data:

./gradlew bootstrapListing

Step 5: GitHub Action

Repository Secrets

First we need to configure the secrets passed to the Gradle build as parameters:

  • GOOGLE_PLAY_API_KEY
  • KEYSTORE_FILE
  • KEYSTORE_PASSWORD
  • KEY_ALIAS
  • KEY_PASSWORD

It’s easy to define the three values for KEYSTORE_PASSWORD, KEY_ALIAS and KEY_PASSWORD since they are just text values. To do so, go to the “Repository settings” and select “Secrets / Actions”. Enter all three variables with the correct values:

To store the KEYSTORE_FILE and the GOOGLE_PLAY_API_KEY as a repository secret variables, we base64 encode the files. The build pipeline will decode it and recreate the original files (see below).

Run the following commands to encode the two files:

base64 google-play-api-key.json > google-play-api-key.json.base64
base64 playstore.keystore > playstore.keystore.base64

Copy the base64 strings and create repository secrets in GitHub. You should have something like this now:

GitHub Action

We have the secrets, now it’s time to create the GitHub Action. First create a directories .github/workflows and and file publish_google_play.yml:

You can find a complete example in one of my demo apps here and since you can read up on the basics about GitHub Actions here, I won’t explain all the details but focus on the specifics of the publishing process.

The build number: we mentioned above that we want to create a timestamp based version code and that’s how we do it:

# Create a build number based on timestamp / UTC time
- name: set release date
run: |
echo "BUILD_NUMBER=$(date +"%s")" >> ${GITHUB_ENV}

This defines an environment variable BUILD_NUMBER and assigns the timestamp (UTC) as a value.

The keystore file needs to be extracted from the secrets and written to a file since our Gradle build takes a file path/name as a parameter. We will use this action for this: https://github.com/timheuer/base64-to-file.

# Decode the keystore file containing the signing key
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1.1
with:
fileDir: './secrets'
fileName: 'my.keystore'
encodedString: ${{ secrets.KEYSTORE_FILE }}

The Google Play API key file is extracted in the same way:

# Decode the Google Play api key file
- name: Decode Google Play API key
id: decode_api_key
uses: timheuer/base64-to-file@v1.1
with:
fileDir: './secrets'
fileName: 'google-play-api-key.json'
encodedString: ${{ secrets.GOOGLE_PLAY_API_KEY }}

Now all that’s left to do is call Gradle with the right parameters:

# Build bundle and publish to Google Play
- name: Build & publish to Google Play
run: ./gradlew
-PBUILD_NUMBER="${{ env.BUILD_NUMBER }}"
-PgooglePlayApiKey="../${{ steps.decode_api_key.outputs.filePath }}"
-PKEYSTORE_FILE="../${{ steps.decode_keystore.outputs.filePath }}"
-PKEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}
-PKEY_ALIAS=${{ secrets.KEY_ALIAS }}
-PKEY_PASSWORD=${{ secrets.KEY_PASSWORD }}
publishBundle --max-workers 1

That’s it. Enjoy and happy coding!

--

--

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