CI/CD for better team collaboration and integration, Photo by Marvin Meyer

CI/CD pipelines

How to setup Github Actions for Android projects

Complete guide to get your Android project running with Github actions

Mostafa El-Abady
Published in
8 min readMay 7, 2021

--

CI/CD are core practices in modern software development and there are many platforms/tools that can be used for Android projects. Github Actions is one of them. It’s a platform that enables you to automate development workflows directly in your Github repository, and they can run automatically, triggered by certain events.

In this article I walk through a few examples of how you can automate common Android tasks using Github Actions. We will see a complete workflow for PR checks that you can apply to your Repo, and a workflow for publishing internal builds and create Github releases. Beforehand, let’s raise a question to ourselves:

Why should we use Github Actions?

In my opinion, the top 3 reasons are:

  • Seamless integration 🚀: Github Actions is fully integrated into Github. You have both, code and CI/CD pipelines, at the same place. Actually, you can get it started by copying the .yaml file from this article to your repo.
  • Pricing 💰: It’s completely free for open source projects and self-hosted runners. For private repos, though, a few different pricing models are offered, depending of the Github account preferences. The account has to subscribe to a paid product as soon as the usage exceeds 2000 minutes per month. The pricing breakdown is given therein the page on GitHub here.
  • Easy to customize 🛠: Every workflow is consisted of jobs that can be run in parallel or sequentially; Each job is capable of having multiple steps/actions; There’s a vast of ready-to-use actions built by the community and you can build and share custom actions easily

So, let’s get started!

Create your first workflow:

You need a separate .yaml file for each workflow. Additionally, add this path .github/workflows/ to your repo. For example, let’s create a .github/workflows/pr-check.yaml file to build the app and run both, unit and instrumentation tests, before merging each PR.

name: Check PR

on:
pull_request:
branches:
- main
workflow_dispatch:jobs:
build:
runs-on: ubuntu-latest

steps:

name: The name of workflow that will be displayed in actions page

on: Stands for specifying the events which are about to trigger the workflow. In this case it will be running for each pull request in main branch. You can also have scheduled events for nightly checks. A complete list of triggers can be found here.

workflow_dispatch: Allows you to run the workflow manually.

jobs: Each workflow can have one or more jobs and, if the latter, they run in parallel by default. Our first job is build and it runs on ubuntu-latest runner. You can choose between mac, ubuntu and windows runners, Nonetheless, you can come up with a self-hosted runner by your own too, More info about hosted runners can be found here.

Each job contains a sequence of steps and each step runs a shell command, or uses a published action from Github marketplace. As it’s been previously mentioned, there’s an ability to build a custom action by yourself and have it shared with the community. More about creating actions can be found here.

Check out the code

The first action to do for a PR workflow is checking out the code, using the checkout action from Github, I use the major version number @v2 to make sure that my workflow won’t break when a new version of action is published with a change that could break things.

- name: Checkout the code
uses: actions/checkout@v2

Setting up JDK

Set up the Java environment using setup-java action, so we can then build the app.

- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8

Generate and upload an apk

You can simply run an assemble gradle task to build the apk, then use upload-artifact action to upload it, using the apk’s path.

- name: Build the app
run: ./gradlew assembleDebug

- name: Upload apk
uses: actions/upload-artifact@v2
with:
name: debug apk
path: app/build/outputs/apk/debug/app-debug.apk

Caching dependencies

We all know that downloading dependencies for Android projects is usually a time consuming task. For that reason, we can cache them using the cache action and update it only when either dependencies class or a gradle file has been changed.

- uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }}

Keeping secrets safe 🔒

Using sensitive information like API keys in your app is a very common case. This information shouldn’t get stored in your repo and pushed, especially for open source projects. Github Secrets is a safe place to secretly store this information. You can save your secrets there and retrieve them into your workflow.

In this example we are loading a key from secrets and adding it to a local.properties file so it can be used later on in the app. For more about secrets check this article and detailed steps for Android here.

- name: Load API Token from secrets
env:
API_TOKEN: ${{ secrets.API_KEY }}
run: echo API_KEY=\"$API_KEY\" > ./local.properties

Custom gradle properties

As an example to have custom properties for builds running with Github Actions, I have added a new file .github/ci-gradle.properties that contains some configurations, and then I’m adding it to gradle.properties .

- name: Copy gradle properties file
run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties

Running tests

Unit tests can be done using a gradle task, and also detekt checks; detekt is a nice static code analysis tool for Kotlin, You can find code smells and potential bugs while using it. Altogether, it is highly configurable too, and some hints are available here.

- name: Run unit tests
run: ./gradlew test --stacktrace
- name: Run detekt
run: ./gradlew detektCheck

Instrumentation tests: can be done using Android emulator runner and can have configurations like api-level for API level of the platform system image — e.g. 29 for Android 10, arch for cpu architecture.

- name: Instrumentation Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 29
target: default
arch: x86
profile: Nexus 6
script: ./gradlew connectedCheck --stacktrace

What if tests are failing? 🤔..… you can simply upload test reports using upload-artifact action and see which tests failed.

- name: Upload Reports
uses: actions/upload-artifact@v2
with:
name: Test-Reports
path: app/build/reports
if: always()

You can notice that I added a new condition expression if: always() which means that reports will be uploaded even though the previous step has failed. More status checks functions can be used, like failure() to run a step only if a previous step in the job failed.

Build Matrix

In instrumentation test, it was running on api-level 29, but what if we want to test it with more than one api-level? You can use an awesome feature called Build Matrix. Just add the build options as an array and Matrix will run the job multiple times, per item. Here’s an example, running an instrumentation test against multiple api-levels.

test:
runs-on: macos-latest
needs: build
strategy:
matrix:
api-level: [21, 23, 29]
target: [default,google_apis]

steps:
- name: Checkout the code
uses: actions/checkout@v2

- name: Instrumentation Tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
target: ${{ matrix.target }}
arch: x86
profile: Nexus 6
script: ./gradlew connectedCheck --stacktrace

Complete PR workflow 😎

Combining all previously mentioned steps together, we can build a workflow for PRs. It contains 2 jobs:

build : which checkouts the code, builds the apk and uploads it.

test: to make this job applicable on running, the previous build job should’ve been run successfully. Finally, it proceeds further by running detekt checks, unit tests, and instrumentation tests. Ultimately, a test report is generated and uploaded.

Workflow result 🎉

Once the pr-check.yaml is pushed to your repo, you can see the workflow running for each PR in the actions tab.

PR workflow run

To check on logs and/or steps for any specific job, it can be managed by clicking at any of the jobs.

After merging the PR

Upon a successful merging of the PR, you might need to generate a signed apk, share it with colleagues using firebase app distribution, and create a Github release for it, let’s do it. 💪

Sign the apk

You can use sign-android-release action to sign the app. It will require 4 inputs signingKeyBase64, alias, keyStorePassword, andkeyPassword. Remember GitHub secrets? It’s a proper place to have these items stored.

After signing it, you can upload the signed apk to github using upload-artifact action.

- name: Sign APK
uses: r0adkll/sign-android-release@v1
id: sign
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- uses: actions/upload-artifact@v2
with:
name: release.apk
path: ${{steps.sign.outputs.signedReleaseFile}}

Share apk to Firebase app distribution

In order to use Firebase app distribution, I use Firebase-Distribution-Github-Action action to share the apk, you need a few inputs

  • appId which can be found in app setting on firebase.
  • token which can be obtained after installing firebase cli from here and then running firebase login:ci command.
  • group the distribution group app will be shared with.
  • file apk file path.
- name: upload artifact to Firebase App Distribution
uses: wzieba/Firebase-Distribution-Github-Action@v1
with:
appId: ${{secrets.FIREBASE_APP_ID}}
token: ${{secrets.FIREBASE_TOKEN}}
groups: Testers
file: app/build/outputs/apk/release/app-release-unsigned.apk

Create a Github release with custom release version

After sending the app, we would like to create a release in Github with a custom release number, changelog and the apk itself. Each version and release should be unique, thereby we will use the commit count as part of the release.

Let’s first get the commit count using a git command, and add it as an output in our current step, so other steps can use this unique number later.

- name: Get App version code
id: version
run: |
echo "::set-output name=commits::$(git rev-list --count HEAD)"

After generating the unique release number, we will use create-release action to create our automated release.

- name: Release
id: create_release
uses: actions/create-release@v1
with:
tag_name: ${{ steps.version.outputs.commits }}
release_name: Release v.1.${{ steps.version.outputs.commits }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Upload release assets

Let’s upload our signed apk as release asset using upload-release-action action. We use the upload url from previous step create_release, and the apk from previous step sign.

- name: Upload Release APK
id: upload_release_asset
uses: actions/upload-release-asset@v1.0.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{steps.sign.outputs.signedReleaseFile}}
asset_name: signed-app.apk
asset_content_type: application/zip

Internal release workflow

A complete workflow for sharing the app internally using firebase app distribution and create a new release after each push to our main branch. You get a new app version shared with firebase, and a new release in Github with changes and the apl file.

Conclusion

Github Actions is a promising tool that can be easily customized. What I really like is how you can share actions between projects and share it with the community, and how easily you can start automating tasks with it. I hope the article is useful for you, have a nice day, and remember:

Automate as much as you can, this will make everyone happier!

References:

--

--