Auto publish React Native app to Android play store using GitLab

In this article, I will show you how to automate the publication of your AAB/APK to the Google Play Console. We will be using the Gradle Play Publisher (GPP) plugin to automate this process. With this plugin, we cannot only automate the publication and release of our app, we can also update the release notes and store listing (including photos) all from GitLab CI.

Note: In this article I will assume that you are using Linux and a React Native version >= 0.60.


Prerequisites


Google Play Console

First, we need to create a service account – this account will be used to automatically make changes to our app, such as publishing it or changing the store listing.

  • Go to the Google Play Console
  • Go to “Settings”
  • Go to “Developer Account”
  • Select “API access”
  • Click the “Create Service Account” button
  • Click the “Google API Console” button, this will take you to the Google Cloud Platform
  • Click “Create Service Account”, and give your service account a name and a description
  • Select the Role as Owner, select the “Continue” button, and then select “Done”
  • After creating your service account, you should have a file that is automatically downloading api<...>.json, if not, click on “Actions (3 Dots) > Create Key” as shown in Image 1
  • Now, go back to “Play Console”
  • Go to “Service Accounts” (on the current page) and select “Grant Access”
  • Next, set the permission as shown in Image 2

If you would like, you can set the permissions globally (so you can use this account for all your apps) or you can specify an app (so that you will create a new account each time you want to auto-publish a new app). Each approach has its own advantages – the first approach is more convenient. However, the second approach is safer because, if your credentials are leaked, only one of your apps will be affected.

widget
widget

GitLab

Now, we need the JSON file we just downloaded to be accessible for our GitLab jobs. The JSON file should look something like this:

{
  "type": "service_account",
  "project_id": "xxx",
  "private_key_id": "xxx",
  "private_key": "xxx",
  "client_email": "xxx",
  "client_id": "xxx",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "xxx"
}

Now, let’s move the relevant key-store information to GitLab CI variables so we can access them during our CI jobs. First, go to your GitLab project:

  • Settings > CI/CD > Variables
  • Add Type: File, Key: PLAYSTOREJSON, Value: (the contents of your JSON file)

Note: When I’m testing locally I sometimes store this JSON file locally. To make sure that the file doesn’t accidentally get published online, I include it in my .gitignore (I call mine play-store.json) file.

build.gradle

We first need to edit the android/build.gradle file. The Maven URL is where we can download the GPP plugin. The dependencies list shows the plugins we need to download and their versions.

buildscript {
    ...
    repositories {
        ...
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:3.5.0"
        classpath "com.github.triplet.gradle:play-publisher:2.4.1"
    }
    ...
}

gradle-wrapper.properties

To use GPP version 2.4.1, we need to use gradle version >= 5.6.1. To do this, open android/gradle/wrapper/gradle-wrapper.properties and edit the line distributionUrl so that the gradle version matches the required version (e.g., distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.1-all.zip).

app/build.gradle

We also need to edit our android/app/build.gradle. Add the apply plugin, "com.github.triplet.play", to the top of your file after you apply the "com.android.application" plugin. Then, at the bottom of the file, add the following code. This is what the GPP plugin uses to determine how to publish your app. In this case, it will:

  • defaultToAppBundles: will generate an AAB (Android App Bundle) instead of APK
  • track: the track we should deploy the new AAB to – you can chose for example production or beta – in this case, it’s the internal track
  • serviceAccountCredentials: location of the Play Store JSON we stored in our CI/CD variables – in this example, it will be kept in android/app/play-store.json
  • resolutionStrategy: automatically updates the app’s version code to match the next available number required, i.e., it’s published
play {
    defaultToAppBundles = true
    track = "internal" // beta, alpha, production.
    serviceAccountCredentials = file("./play-store.json")
    resolutionStrategy = "auto"
    outputProcessor {
        versionNameOverride = "$versionNameOverride.$versionCode"
    }
}

There are many more options you can enable/use like generating a draft that you can manually publish from the Play store console. You can find more details here, in the GPPs very well documented README.

Meta Data

You can also use the GPP to publish meta-data about our app such as the listing, photos, etc. To do this, you can run cd android && ./gradlew bootstrap to initialize the directory layout you need and also pull the information from the play store if you already have it. If you do not want to run this command, you can create a structure similar to the one shown below in your android/app/src/main/play/ folder. You can find more information about meta-data here.

├── contact-email.txt
├── contact-website.txt
├── default-language.txt
├── listings
│   └── en-GB
│       ├── full-description.txt
│       ├── graphics
│       │   ├── feature-graphic
│       │   │   └── 1.png
│       │   ├── icon
│       │   │   └── 1.png
│       │   └── phone-screenshots
│       │       ├── 1.png
│       │       ├── 2.png
│       │       ├── 3.png
│       │       ├── 4.png
│       │       ├── 5.png
│       │       └── 6.png
│       ├── short-description.txt
│       ├── title.txt
│       └── video-url.txt
├── release-names
│   └── default.txt
└── release-notes
    └── en-GB
        └── default.txt

package.json

Add the following script to your package.json – it will be used within our .gitlab-ci.yml. This is so we can simply use, for example, yarn run bundle instead of having to write the whole command in our GitLab CI. The other advantage is that the command is used multiple times in our GitLab CI jobs, but we only have to edit in a single location. It also saves us having to type out the same (very long) command again.

  • bundle: bundles all of our react native code into a single file
  • publish-package: will build our AAB and also publish it along with the meta-data we include
{
  ...
  "scripts": {
    "bundle": "react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.bundle --sourcemap-output android/app/src/main/assets/index.map --assets-dest android/app/src/main/res",
    "publish-package": "cd android && ./gradlew publishRelease",
    ...
  }
  ...
}

.gitlab-ci.yml

Now, we can finally add our job to our GitLab CI file:

stages:
  - publish

publish:android:package:
  stage: publish
  image: reactnativecommunity/react-native-android
  script:
    - echo fs.inotify.max_user_watches=524288 | tee -a /etc/sysctl.conf && sysctl -p
    - mv $PLAY_STORE_JSON android/app/play-store.json
    - yarn bundle
    - yarn publish-package --no-daemon
  artifacts:
    paths:
      - ./android/app/build/outputs/

Let’s break this file down line by line. We need a Docker image that contains all the prerequisites for building our APK. A good one to use is reactnativecommunity/react-native-android. It has everything we need for React Native/Android (Java, Android SDK etc).

Depending on your exact project, you may need to increase the inoitfy file watcher limit – you can do this by using echo fs.inotify.max_user_watches=524288 | tee -a /etc/sysctl.conf && sysctl -p. Essentially, file watchers are used to monitor changes in the file system. You can find more information about Linux’s inotify here.

yarn bundle: creates a bundle this where all of our (JavaScript) React Native file are bundled into a single Javascript file.

yarn android-publish --no-daemon: builds our AAB locally and then publishes it to the Android Play Store or, in this case, to the internal testing track. It will also publish all of our meta-data for us (e.g., photos, store listing, contact information, release notes, etc.) Since this is a CI job, we don’t need to start a daemon to speed up future builds; hence, the --no-daemon argument.

Finally, we create artifacts, ./android/app/build/outputs/, so that we can download the AAB after the job has been completed. I also like to include my artifacts (AAB) with releases of my apps within GitLab. It makes it easier to track what version of the app was released when. But, if you don’t want the AAB/APK, you can simply remove the artifacts part from the job.

That’s it, we’re done :).

Free Resources

Attributions:
  1. undefined by undefined