Here East, London, September 2025

Introduction

This post looks at automating the deployment of an Android app to Google Play with GitHub Actions. The app itself is covered in this post here. An earlier post on GitHub Actions is here. The notes are based on the ongoing work on the app. To deploy the app to Google Play involved the following steps:

1. Building an Android App Bundle (An .aab file) from the app’s Kotlin scripts on GitHub
2. As the app is being built inject the API keys it uses rather than have them in the code on GitHub
3. Sign the app with my private key to authenticate it
4. Log into Google Play and upload the app

These steps can be done using Android Studio, but this involves manually signing and uploading the app to Google Play whenever a change is made to it. Automating this process with a GitHub Action makes deploying updates quicker and integrates them more directly with version control.

This post covers implementing the following steps via a GitHub Action:

1. Specifying the event that triggers the Action
2. Specifying where the Action is run and the secrets that are used to build and sign the app
3. Setting up the actions that are needed to clone the repo and build the app
4. Publishing the app on Google Play

The script of the GitHub Action which does this is below. It is saved in the app repo as a yaml file at the location /.github/workflows/app_launch_to_play.yml. This post covers the steps of the action in turn.

name: Deploy to Google Play 

on:
  push:
    branches:
      - main 
   
jobs:
  deploy:
    runs-on: ubuntu-latest
      
    permissions:
      contents: read

    env:
      MODULE: app
      MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }}
      LISTED_API_KEY: ${{ secrets.LISTED_API_KEY }}
      ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
      ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
      ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4
  
      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Set up Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Give permissions to the gradle wrapper (gradlew)
        run: chmod +x ./gradlew

      - name: Restore keystore 
        run: |
         ANDROID_KEYSTORE="$RUNNER_TEMP/upload.jks"
         echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$ANDROID_KEYSTORE"
         echo "ANDROID_KEYSTORE=$ANDROID_KEYSTORE" >> "$GITHUB_ENV"

      - name: Create placeholder local.properties (optional)
        run: '[ -f local.properties ] || echo "# generated by CI" > local.properties'

      - name: Set version code
        run: echo "VERSION_CODE=${{ github.run_number }}" >> $GITHUB_ENV

      - name: Build a Release Bundle and publish to Internal testing
        env:
         ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}  # raw JSON
        run: |
         ./gradlew :${{ env.MODULE }}:publishReleaseBundle \
         -PversionCode=${{ env.VERSION_CODE }} \
         -Pplay.track=internal \
         -Pplay.releaseStatus=draft \
         -Pplay.changesNotSentForReview=true \
         --info --no-daemon --rerun-tasks --no-configuration-cache

1. Specifying the event that triggers the Action

In this case the action is triggered by a push to the main branch of the app repo on GitHub. For example if we push a branch to the app’s GitHub repo and then merge it into main then this starts the action to build the app from the Kotlin scripts, signing it and publishing it to Google Play.

name: Deploy to Google Play 

on:
  push:
    branches:
      - main 

2. Specifying where the Action is run and the secrets that are used to build and sign the app

The action is run on an ubuntu-latest instance with read permissions. The name of the module that will be built and published is specified by the MODULE environment variable. The default module name for most Kotlin apps is ‘app’. The module name can be found from the settings.gradle.kts file in the app code and is specified as include(":app").


jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      contents: read

    env:
      # App module path (change if your module isn’t named `app`)
      MODULE: app
      MAPS_API_KEY: ${{ secrets.MAPS_API_KEY }}
      LISTED_API_KEY: ${{ secrets.LISTED_API_KEY }}
      ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
      ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
      ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}

The app requires two API keys and the private key information to sign the app. We don’t want to add these in the repo code so they are added as GitHub secrets. These can be added by going to the Settings tab followed by Secrets and variables and Actions. These are then accessed by the action using the ${{secrets.xxxxxx}} format. Here the information is read into the environment of the GitHub Action so that the API keys and signing key are available to Gradle. This information is then extracted from the environment when the Kotlin app is built by the app’s Build.gradle.kts script to insert the API keys into the app and then sign it with the private key. The Kotlin code in the app used to extract the action’s environment variables (shown here for the example of ANDROID_KEYSTORE) is:

val ksPath: String? = System.getenv("ANDROID_KEYSTORE")

secrets

3. Setting up the actions that are needed to clone the repo and build the app

The action uses a number of other actions. The action checkout@v4 clones the GitHub repo. The two subsequent actions setup-java@v4 and setup-gradle@v4 sets up Java and Gradle to allow the app to be build. Gradle is a tool that compiles projects in Java-related languages like Kotlin. The chmod +x ./gradlew allows the Gradle wrapper (which enables Gradle to be run from the command line) to be run by the action to build the app bundle.

To give access to the signing key in the Android keystore in ANDROID_KEYSTORE="$RUNNER_TEMP/upload.jks" the variable ANDROID_KEYSTORE is defined which will be saved in the temporary location upload.jks by the action. The command echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$ANDROID_KEYSTORE" decodes the keystore secret value from base64 to the variable. The reason for this is that GitHub Actions can’t store binary information in a secret. We therefore store it in base64 (an encoding from binary to 64 characters) and get the action to convert it to a binary and then write it back to the GitHub environment GITHUB_ENV). The GitHub environment is used as, unlike the action environment env where the values are static, it allows us to create new variables while the action runs (here the decoded keystore value).

When testing the app locally we have the API keys in a local properties file that is not committed to Git. The local.properties file is therefore not in the remote and if available the app uses the API and signing key information from the action’s environments in the first instance. However, the local properties file is used in the build process so we get the GitHub Action to create it.

    steps:
      - name: Checkout
        uses: actions/checkout@v4
  
      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '17'

      - name: Set up Gradle
        uses: gradle/actions/setup-gradle@v4

      - name: Give permissions to the Gradle wrapper (gradlew)
        run: chmod +x ./gradlew

      - name: Restore keystore from base64
        run: |
         ANDROID_KEYSTORE="$RUNNER_TEMP/upload.jks"
         echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > "$ANDROID_KEYSTORE"
         echo "ANDROID_KEYSTORE=$ANDROID_KEYSTORE" >> "$GITHUB_ENV"

      - name: Create placeholder local.properties 
        run: '[ -f local.properties ] || echo "# generated by CI" > local.properties'

4. Publishing the app on Google Play

To update the version number each time the app is rebuilt we write the action’s github.run_number to the GITHUB_ENV as the VERSION_CODE. The action then uses the credentials ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON } to login to Google Play.

There are different release tracks on Google Play: Production (The app is fully available), Open testing (The app is publicly available for testing on Google Play) and Closed and Internal testing (the app is released to a larger and smaller group of private testers respectively). There is also a release status.

Having logged into Google Play the action then uses gradlew command publishReleaseBundle to publish a release bundle with the specified version_code. Here the track is the internal one with the draft status. As the app is still being worked on in the internal track the changes are not sent for review.

        run: echo "VERSION_CODE=${{ github.run_number }}" >> $GITHUB_ENV

      - name: Build a Release Bundle and publish to Internal testing
        env:
         ANDROID_PUBLISHER_CREDENTIALS: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}  # raw JSON
        run: |
         ./gradlew :${{ env.MODULE }}:publishReleaseBundle \
         -PversionCode=${{ env.VERSION_CODE }} \
         -Pplay.track=internal \
         -Pplay.releaseStatus=draft \
         -Pplay.changesNotSentForReview=true \
         --info --no-daemon --rerun-tasks --no-configuration-cache

When a change is made to the main branch the action runs through the specified stages. Here a previous run is shown in the app repo’s GitHub Actions tab: action_example The app is then visible on Google Play on the internal testing track. play_example

Previous cloud posts:

Cloud 1: Introduction to launching a Virtual Machine in the Cloud
Cloud 2: Getting started with using a Virtual Machine in the Cloud
Cloud 3: Docker and Jupyter notebooks in the Cloud
Cloud 4: Using Serverless
Cloud 5: Introduction to deploying an app with simple CI/CD
Cloud 6: Introduction to Infrastructure as Code using CloudFormation
Cloud 7: Building an API with Lambda, Docker and CloudFormation

References:

Using secrets in GitHub Actions
The Gradle command line guide
Android developer publishing guidance
Google Play console guidance