Automating Building Android APKs with GitHub Actions
Tags: android, githubactions, automation
Published: 2023-10-01 20:39:47

As part of my goal to decrease my smartphone addiction, I recently bought a Unihertz Jelly phone, which could be a good compromise between a smartphone and a dumbphone. The phone has a multi-purpose function button. You can assign it to specific pre-defined actions or launch any application. As I regularly use the phone screen in grayscale mode, I decided to assign the button to an application that would toggle this mode. I searched and couldn't find a simple app to toggle it when launching. So I decided to write my own. However, this previous background could be a post on its own.


As I've been trying to leverage GitHub actions for manual tasks, I [searched GitHub](https://github.com/search?q=release+apk+action&type=repositories) and found many actions that could do this for me. I tried with a couple of them, and they didn't work. One of the issues is that the actions are using obsolete dependencies, and I spent way more time trying to make them run than I wanted.


I decided to make it work without a pre-built action and write all of the required steps in a GitHub workflow, which, in the end, was very simple. Here's the action. I will do a walk-through so you can use it in your projects.


```yaml
name: Release APK

on:
  push:
    tags:
      - '*'
```


This workflow will run when pushing a tag to a repository.


```yaml
jobs:
  Gradle:
    runs-on: ubuntu-latest
    steps:
    - name: checkout code
      uses: actions/checkout@v3
```


This is the standard step to checkout the code. It can run with the latest `checkout` action (v3).


```yaml
    - name: setup jdk
      uses: actions/setup-java@v3
      with:
        java-version: 17
        distribution: 'temurin'
```


Then, we need to set up the JDK. Again, using the latest `setup-java` action (v3). I set the Java version to 17, which is the one that my Android Project uses. You can adjust the distribution, too. Refer to [`setup-java`s documentation](https://github.com/actions/setup-java#usage).


Now, to sign the released APK, we'll need to load the Keystore (you can read how to generate it on [Android's official documentation](https://developer.android.com/studio/publish/app-signing)).


```yaml
   - name: Load keystore
      run: |
        echo "${{ secrets.KEYSTORE_FILE_CONTENTS }}" | base64 -d > keystore.jks && \
```

The secret `KEYSTORE_FILE_CONTENTS`, contains the base64 encoded content of the keystore.jks file you generated. You can get the text for the secret by running `cat keystore.jks  | base64 -w0`.

```yaml
	cat <<EOL > local.properties
        storeFilePath=$PWD/keystore.jks
        storePassword=${{ secrets.RELEASE_STORE_PASSWORD }}
        keyAlias=${{ secrets.RELEASE_KEY_ALIAS }}
        keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}
        EOL
```


The rest of the secrets are the ones you entered when configuring your keystore.


Next, we'll build the APK:


```yaml
    - name: Build Release APK
      run: ./gradlew assembleRelease
      env:
        GRADLE_USER_HOME: ./gradle-config
```

And set the current directory as a safe repository, as this is a known issue with `actions/checkout@v3`. And because we'll later use `github-cli` to do the release, I didn't want it to complain that it couldn't read the repository.

```yaml
    - name: Set current directory as a safe repository
      run: git config --global --add safe.directory /github/workspace
    - uses: ButterCam/setup-github-cli@master
```


Finally, release the APK and upload it to the repository's releases page.


```yaml
    - name: Release using github cli
      run: gh release create ${GITHUB_REF##*/} ./app/build/outputs/apk/release/**.apk
      env:
       GITHUB_TOKEN: ${{ secrets.TOKEN }}
```


You'll need to generate a token with `repo` access for this to work. For some reason, using the default repository's `GITHUB_TOKEN` won't work. 


And here's the full YAML in case you want to copy-paste it. [You can also check it in my repository](https://github.com/ivanvc/android-grayscale-toggle/blob/main/.github/workflows/release.yml).


```yaml
name: Release APK

on:
  push:
    tags:
      - '*'
jobs:
  Release:
    runs-on: ubuntu-latest
    steps:
    - name: checkout code
      uses: actions/checkout@v3
    - name: setup jdk
      uses: actions/setup-java@v3
      with:
        java-version: 17
        distribution: 'temurin'
    - name: Load keystore
      run: |
        echo "${{ secrets.KEYSTORE_FILE_CONTENTS }}" | base64 -d > keystore.jks && \
        cat <<EOL > local.properties
        storeFilePath=$PWD/keystore.jks
        storePassword=${{ secrets.RELEASE_STORE_PASSWORD }}
        keyAlias=${{ secrets.RELEASE_KEY_ALIAS }}
        keyPassword=${{ secrets.RELEASE_KEY_PASSWORD }}
        EOL
    - name: Build Release APK
      run: ./gradlew assembleRelease
      env:
        GRADLE_USER_HOME: ./gradle-config
    - name: Set current directory as a safe repository
      run: git config --global --add safe.directory /github/workspace
    - uses: ButterCam/setup-github-cli@master
    - name: Release using github cli
      run: gh release create ${GITHUB_REF##*/} ./app/build/outputs/apk/release/**.apk
      env:
       GITHUB_TOKEN: ${{ secrets.TOKEN }}
```


There's still one thing that needs configuration to sign the APK properly. You'll need to edit your `app/build.gradle.kts` file so we can feed the properties from GitHub secrets to the `assmbleRelease` task. Kudos to [Willi Metzel's Stack Overflow answer](https://stackoverflow.com/questions/18328730/how-to-create-a-release-signed-apk-file-using-gradle/61188101#61188101). Inside `android` in `app/build.gradle.kts` add:


```kotlin
    signingConfigs {
        create("release") {
            val properties = Properties().apply {
                load(File("local.properties").reader())
            }
            storeFile = File(properties.getProperty("storeFilePath"))
            storePassword = properties.getProperty("storePassword")
            keyPassword = properties.getProperty("keyPassword")
            keyAlias = properties.getProperty("keyAlias")
        }
    }

```


Then, add `signingConfig = signingConfigs.getByName("release")` to `release` inside `buildTypes`. It should look something like:


```kotlin
   buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
            signingConfig = signingConfigs.getByName("release")
        }
    }
```

[See here for a complete reference to my `app/build.gradle.kts`](https://github.com/ivanvc/android-grayscale-toggle/blob/main/app/build.gradle.kts).

Credits to [r0adkll](https://github.com/r0adkll) for his previous work; without it, building this would have been more difficult.


You can follow me at [GitHub (@ivanvc)](https://github.com/ivanvc). And check the repository (and source code for my grayscale switching app) if you're interested.


[![GitHub Repo](https://opengraph.githubassets.com/bdbcb8d1aae43dbf9d83e917a53a1ee933092df99d6efeab41ba5df3a02c82d8/ivanvc/android-grayscale-toggle)](https://github.com/ivanvc/android-grayscale-toggle)