CI/CD for Android Devs— Introduction to Github Actions.
By: Victor Elezua
Tags: Continuous Integration, Android, Ci Cd Pipeline, Agile, Github Actions

There are many tools that enable use to create a CI/CD pipeline for our projects; Jenkins, TravisCI, CircleCI, GitLabCI and my favourite GitHub Actions. In this article we’ll be covering continuous integration and continuous deployment with GitHub actions, but before we start, I’ll drop an excerpt from InfoWorld.

Continuous integration (CI) and continuous delivery (CD) embody a culture, set of operating principles, and collection of practices that enable application development teams to deliver code changes more frequently and reliably. The implementation is also known as the CI/CD pipeline. CI/CD is one of the best practices for devops teams to implement. It is also an agile methodology best practice, as it enables software development teams to focus on meeting business requirements, code quality, and security because deployment steps are automated.

First of all, we are going to create an extremely simple Android studio project-A simple interest calculator. We’ll keep adding micro features little by little in order to understand CI/CD.

Before continuing with this article, you should be familiar with the following;

* Basic of Android development

* Basics of Git and GitHub

* Knowledge of unit testing is a plus

* Knowledge of JSON will facilitate better understanding

Setting up project

Create a new Android studio project

Select empty activity

Select empty activity and give your app any name that you want. In my case it’s SimpleCalculator. I’ll also be using kotlin as the programming langusge.

Initial App

This is the initial version of the app. The codes for the app is below.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

xmlns:app="http://schemas.android.com/apk/res-auto"

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity"> <TextView

android:id="@+id/simple_interest_result_view"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginStart="16dp"

android:layout_marginTop="32dp"

android:layout_marginEnd="16dp"

android:padding="16dp"

android:text="0"

android:gravity="center"

android:textSize="25sp"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toStartOf="parent"

app:layout_constraintTop_toTopOf="parent" /> <EditText

android:id="@+id/principal_amt"

android:layout_width="0dp"

android:layout_height="wrap_content"

android:layout_marginTop="32dp"

android:hint="Principal"

android:ems="10"

android:inputType="number"

app:layout_constraintEnd_toEndOf="@+id/simple_interest_result_view"

app:layout_constraintStart_toStartOf="@+id/simple_interest_result_view"

app:layout_constraintTop_toBottomOf="@+id/simple_interest_result_view" /> <Button

android:id="@+id/calculate_btn"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_marginStart="16dp"

android:layout_marginEnd="16dp"

android:layout_marginBottom="32dp"

android:text="Calculate Interest"

android:onClick="calculate"

app:layout_constraintBottom_toBottomOf="parent"

app:layout_constraintEnd_toEndOf="parent"

app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.kt

package me.codeenzyme.simplecalcultor import androidx.appcompat.app.AppCompatActivity

import android.os.Bundle

import android.view.View

import android.widget.EditText

import android.widget.TextView class MainActivity : AppCompatActivity() { lateinit var principalEditText: EditText

lateinit var resultView: TextView override fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState)

setContentView(R.layout.activity_main) principalEditText = findViewById(R.id.principal_amt)

resultView = findViewById(R.id.simple_interest_result_view) } fun calculate(view: View) {

val principal = principalEditText.text.toString().toDouble()

resultView.text = SimpleInterestCalculator().calculateInterest(principal).toString()

}

}

SimpleInterestCalculator.kt

package me.codeenzyme.simplecalcultor class SimpleInterestCalculator {

private val time = 5

private val rate = 15 fun calculateInterest(principal: Double): Double {

return principal * rate * time

} }

At this point our simple interest calculator can only calculate simple interest with static time and rate. Let’s write some unit test to ensure that our app really give the correct result.

SimpleInterestCalculatorTest.kt

package me.codeenzyme.simplecalcultor import org.junit.Assert

import org.junit.Test class SimpleInterestCalculatorTest { @Test

fun calculate_isCorrect() {

val simpleInterestCalculator = SimpleInterestCalculator()

val principal = 2000.0

val interest = principal * 15 * 5

Assert.assertEquals(interest, simpleInterestCalculator.calculateInterest(principal), 0.001)

}

}

Right now, if we run our test it should pass and our app is also working correctly.

Setting CI/CD pipeline

In order to use GitHub actions, we’ll need to use GitHub as the central repository for Git(version control system). So, let’s share our project on GitHub.

Share project on GitHub

We currently have our repository on GitHub. Click on the repo link and then switch to the Actions tab, scroll down to the continuous integration workflows. If you don’t see Android CI, expand the section, when you see Android CI click on setup this workflow button. GitHub should take you to an editor which contains a workflow. The workflow will be created in .github/workflows folder when you commit the file.

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: ./gradlew build

The above code contains the generated template we could use to start our CI pipeline for Android. Let’s understand what this file is. This file is a YAML file — YAML has similar use cases as JSON but it is mainly used for configuration files because of it’s clean syntax. Learn more about YAML here

Every YAML file in .github/workflows folder represents a workflow that can be run by GitHub Actions, but we’ll focus on just one workflow.

A typical workflow contains;

* name: Determines the name of the workflow.

* on: This lets us define the events and branches that can trigger workflows.

* jobs: This consists of of one or more jobs that run in parallel.

* env: This is where environment variables that should be available throughout the runtime of the workflow should be defined.

name as highlighted above is only used to name a workflow.

on is used to define repository events, and those events specify branches they listen to. There are a lot of events that can trigger a workflow, the common ones are; push, pull_request, scheduled, page_build, release and also through webhooks. Learn more about events here.

jobs is where much focus is usually on. Jobs is an objects that can contain one or many jobs to run. Each job run in parallel, so the steps in each in each job should be independent of other jobs. You shouldn’t run jobs that modifies a shared resource or run build and tests on different jobs if the test depends on the build result.

We now know that the jobs object can contain one or more jobs that run in parallel, but what does a single job consist of?

A job consists of the following:

* name

* runs_on

* strategy

* steps

Jobs both have names and IDs. The ID is the key of the job object while the name is a property. The name is what GitHub shows you when running that particular job while the ID is used to reference a particular job within the workflow. A use case of ID is when a job depends on the work of another job, we can specify the dependency by giving needs property of job the value of another job ID. Read more about jobs here

runs_on specifies the OS(operating system) that the workflow runs on. In our case it is ubuntu_latest . You can also run on either Mac or Windows.

strategy can be used to add extra configurations to how your jobs runs. This property is generally used to configure a run matrix for a particular job. strategy can take three properties — matrix fail-fast max-parallel . A matrix is usually used to run a job against many configurations, a common use-case is to build a project with different versions of a language. Jobs that run in a matrix run in parallel. Read more about this in the official docs.

steps is a list of commands to be executed sequentially within a particular job. We can write command that works on that particular operating system. Within these steps we can either run our own commands or we can leverage actions to do some common tasks; in our template we use an action to do the initial setup of a java project. Each object in the steps list can contain uses (for actions) or run for running our commands on the command line. Steps can optionally be named.

In our own case we don’t need environment variables, env is just an object of all environment variables. We can set environment variable for a specific job or for the whole workflow.

The current state of our workflow file should build our project, and if there are no errors at build time our workflow will run to completion successfully. If an error occurs during build time we could read the logs to know what caused the failure. Previously, when I tried running this workflow on a project, it failed because gradlew didn’t have an execute permission, so I’ll add that to the workflow and then commit the file.

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: add execute permission to gradlew

run: chmod u+x ./gradlew

- name: Build with Gradle

run: ./gradlew build

In the updated workflow we can see that we added execute permission using the chmod command available on linux. If we click on the build job at the left hand side of the screen we’ll see all the steps that ran within that job. Let’s go back to our SimpleInterestCalculator class and add a bug that should make our tests fail, then we’ll add a test step to our job.

Now our test fails locally, let’s add the test step to our workflow and commit/push our recent changes to GitHub.

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: add execute permission to gradlew

run: chmod u+x ./gradlew

- name: Build with Gradle

run: ./gradlew build

- name: run tests

run: ./gradlew test

You can now see that we’ve added a test step. Now on every push or pull request on this branch of the repo, the build job will run all the steps within it. Let’s see whether the test is actually working by pushing the new buggy changes we’ve made.

As expected our build job failed since we know our unit test will fail, but something is wrong. The job failed in our build step and not in our test step. Why? That is because our gradle build task also runs unit tests. We could replace the build gradle task with assembleDebug so that we can run our test in a separate step.

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: add execute permission to gradlew

run: chmod u+x ./gradlew

- name: Assemble debug apk

run: ./gradlew assembleDebug

- name: run tests

run: ./gradlew test

This above workflow still fails. but now it fails at the test step. Anytime any job fails GitHub notifies us via email, then we can go through the logs to know exactly what made our job to fail. There are actions in GitHub Marketplace that can enable us configure different mode of notifications. In a team that has adopted Agile methodologies, only changes that doesn’t cause any workflow to fail will be integrated into the the master branch.

GitHub actions is great because it has a ton of actions that can help us carry out some task without thinking about implementation details and writing a lot of steps. We are going to use an action that will enable us upload our generated artifact(APK in our case). There are many ways to deploy a build but let’s just upload it to GitHub.

Firstly, we’ll fix our buggy code then we’ll add the action to upload our artifact. I’m only going to show the updated workflow file and not the fix to the bug we introduced earlier, fix the bug and push changes before you update your workflow file.

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: add execute permission to gradlew

run: chmod u+x ./gradlew

- name: Assemble debug apk

run: ./gradlew assembleDebug

- name: run tests

run: ./gradlew test

- name: Upload a Build Artifact

uses: actions/upload-artifact@v2.2.1

with:

name: debug-apk

path: app/build/outputs/apk/debug/app-debug.apk

We are using version 2.2.1 of upload-artifact action to upload our apk. The action takes arguments using with property. We passed in name and path. Name determines the upload name of our artifact while path is the location where the artifact is generated. There is a bit more to this action so you can look up the documentation.

We can now see that our workflow completed successfully and also generates our debug APK. With this workflow we are sure that when we go to our repo, we can always get/download the latest version of our app up to that last commit that passed all checks.

Thanks for reading this article. It would be nice to leave some feedback and suggests improvements. The repository for this article is on GitHub. You can also reach out to me on Twitter.