Daniel Alvarez

Automate AppGallery releases with GitHub Actions (1/2)

hmsandroidgithubgithub actions

If you keep your Android project on GitHub, you already know how tedious a manual release is: run the tests, generate the APK or AAB, sign it, open the AppGallery Console, fill in the information, upload the file, and so on.

In this two-part guide we will automate that release as simply as possible, staying close to the metal so every step is understandable, and publishing the APK or AAB to AppGallery through the Publishing API. First, though, we need to handle the secret files: the keystore that signs the application and the agconnect-services.json file. The project uses Huawei Mobile Services, so we will also cover the extra considerations that come with it.

GitHub Actions

GitHub Actions lets us automate our development workflow and build the CI/CD pipeline for an Android project.

The first step is to have the Android project on GitHub. Then, in the repository, open the Actions tab.

GitHub Actions page

GitHub offers quick-start templates for many technologies. To get a starting point, pick the Android CI template.

GitHub Actions Android

That generates a YAML file where we describe everything that should happen when the master branch changes, whether from a commit or an approved pull request.

GitHub Actions Android

The template runs as it is, with one addition: we need to make gradlew executable to avoid permission errors.

chmod +x gradlew

Our starting YAML looks like this:

name: Android CI
on:
 push:
   branches: [ master ]
 pull_request:
   branches: [ master ]
jobs:
 build:
   runs-on: ubuntu-latest
   steps:
   - uses: actions/checkout@v2
   - name: set up JDK 1.8
     uses: actions/setup-java@v1
     with:
       java-version: 1.8
   - name: Build with Gradle
     run: |
       chmod +x gradlew
       ./gradlew build

Any change to the YAML file triggers the workflow. You can also run it manually and read the full log of the process.

GitHub Actions Android

Securing sensitive data and files

For the workflow to produce an APK or AAB signed and ready to upload to AppGallery, it needs a few files, and those files must stay out of the repository. Add them to .gitignore so they are never committed by mistake.

In an Android project that uses Huawei Mobile Services, these are the pieces to keep private:

The workflow needs this information, but it cannot live in the YAML file, where anyone with access to the repository can read it. GitHub has a dedicated place for it: Secrets.

GitHub Actions Android

The catch is that Secrets stores key-value pairs, not files.

So to store the keystore, which we need in order to sign the application, we turn it into a string, and do the same with the agconnect-services.json configuration file.

We do that from the command line with gpg.

gpg -c --armor mykeystore.jks

It asks for a passphrase and produces a mykeystore.jks.asc file. That file holds a Base64 string, and that string is what we save in Secrets.

GitHub Actions Android

Repeat this for every sensitive file the workflow needs, and store each passphrase as well: we will use it to rebuild the file from its Base64 value when the pipeline runs.

The result looks like this, with one passphrase per file, though a single shared one would work too:

GitHub Actions Android

Note the keystore.properties file, which holds the keystore passwords and the alias.

The idea is to use these values in the workflow to produce the signed APK.

We add a new step to the YAML that reverses the process: write the .asc file, then use the passphrase to restore the original file in its directory.

- name: Prepare Secret Files
  run: |
     echo "${{ secrets.KEYSTORE }}" > alvareztech.jks.asc
     gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch alvareztech.jks.asc > app/alvareztech.jks
     echo "${{ secrets.KEYSTORE_PROPERTIES }}" > keystore.properties.asc
     gpg -d --passphrase "${{ secrets.KEYSTORE_PROPERTIES_PASSPHRASE }}" --batch keystore.properties.asc > keystore.properties
     echo "${{ secrets.AGCONNECT_SERVICES }}" > agconnect-services.json.asc
     gpg -d --passphrase "${{ secrets.AGCONNECT_SERVICES_PASSPHRASE }}" --batch agconnect-services.json.asc > app/agconnect-services.json

Generating the signed application

Once every file is in place and the signing configuration lives in the app module’s build.gradle, generating the APK is a single Gradle command.

signingConfigs {
    release {
        storeFile file(keystoreProperties['storeFile'])
        storePassword keystoreProperties['storePassword']
        keyAlias keystoreProperties['keyAlias']
        keyPassword keystoreProperties['keyPassword']
        v2SigningEnabled true
    }
}

Generating the signed APK is then just:

./gradlew assembleRelease

Getting the signed APK

Eventually we want to upload this file to AppGallery through the Publishing API that AppGallery Connect provides. For now, we settle for downloading the artifact the workflow produces.

- name: Publish APK
  uses: actions/upload-artifact@v2
  with:
    name: app-release
    path: app/build/outputs/apk/release/

What we have is a workflow that automatically generates a signed APK for every change we push to the master branch. The artifact is downloadable, though for now GitHub only serves it as a ZIP.

GitHub Actions Android

Conclusion

Everything is ready to start using the Publishing API, and the sensitive files of a Huawei Mobile Services project are handled properly. In the second part we keep assembling the pipeline at a low level, so that no step stays a black box.

Here is the complete file so far:

name: Android CI
on:
 push:
   branches: [ master ]
 pull_request:
   branches: [ master ]
jobs:
 build:
   runs-on: ubuntu-latest
   steps:
   - uses: actions/checkout@v2
   - name: set up JDK 1.8
     uses: actions/setup-java@v1
     with:
       java-version: 1.8
   - name: Prepare Secret Files
     run: |
        echo "${{ secrets.KEYSTORE }}" > alvareztech.jks.asc
        gpg -d --passphrase "${{ secrets.KEYSTORE_PASSPHRASE }}" --batch alvareztech.jks.asc > app/alvareztech.jks
        echo "${{ secrets.KEYSTORE_PROPERTIES }}" > keystore.properties.asc
        gpg -d --passphrase "${{ secrets.KEYSTORE_PROPERTIES_PASSPHRASE }}" --batch keystore.properties.asc > keystore.properties
        echo "${{ secrets.AGCONNECT_SERVICES }}" > agconnect-services.json.asc
        gpg -d --passphrase "${{ secrets.AGCONNECT_SERVICES_PASSPHRASE }}" --batch agconnect-services.json.asc > app/agconnect-services.json
   - name: Build with Gradle
     run: |
       chmod +x gradlew
       ./gradlew assembleRelease
   - name: Publish APK
     uses: actions/upload-artifact@v2
     with:
       name: app-release
       path: app/build/outputs/apk/release/

You can see all of this implemented in a real project:

facts-android

Resources

Publising API


You can follow the second part on: Automate your releases to AppGallery with Github Actions (Part 2)