Creating a CI/CD Pipeline For Android SDK Using GitHub Actions
We recently incorporated a CI/CD pipeline in our core Android SDK, and it has given us significant results. The following article aims to go through various concepts and challenges involved in the process.
CI/CD stands for continuous integration (CI) and continuous delivery (or deployment) and is a term given to a set of strategies and processes for a fast, reliable, and an effective development process of a software product. Together, these two make the backbone of the modern DevOps Environment.
What are GitHub Actions?
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform provided by GitHub that allows us to automate our build, test, and deployment pipeline.
We can create workflows that build and test every pull request to our repository or deploy merged pull requests to production. There are many use cases and events which we can combine to get the outputs we desire.
How are GitHub Actions Used by CleverTap’s Android SDK?
We have defined a set of actions (lint checks, code analysis, unit tests, JaCoCo code coverage, publishing to maven central) inside our SDK code that can be run via Android Studio’s Gradle or Gradle installed on any computer via command line.
GitHub actions can now run these commands on our code in the cloud, on certain triggers such as when someone pushes to a branch, or when a PR is raised, or when a PR is merged, etc.
For this, we have defined separate
.yml files in the
.github/workflows folder in our code. These run for specific triggers, namely:
- When a PR is raised from task/<something> branch to develop branch
- When a PR is raised from develop branch to master branch
- When a code is merged into the master branch (via direct push to master, or when PR is merged to master)
We have also used composite actions in our workflows, so check out our SDK’s mini flows folder for detailed implementation of various actions. Composite actions are separate workflow files that can have one or multiple steps defined independently. These steps are accessible from a parent workflow’s job via
uses syntax. This helps in reducing duplication across multiple jobs and workflows.
Here is one such YML file for a workflow defined in our GitHub Actions:
name: validate PR raised from task/** branched to develop branch on: pull_request: branches: [ develop ] jobs: lint-staticChecks-test-build: if: startsWith(github.head_ref, 'task/') runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Checkout the code from Repo uses: actions/checkout@v2 - name: Setup JDK 11. uses: ./.github/mini_flows/s2_setup_jdk - name: Mandatory File Changes uses: ./.github/mini_flows/s1_mandatory_filechanges - name: Run lint tests and Upload results uses: ./.github/mini_flows/s3_lint - name: Static Code Check Via detekt uses: ./.github/mini_flows/s4_detekt - name: Static Code Check Via checkstyle uses: ./.github/mini_flows/s5_checkstyle - name: Unit Tests (DEBUG AND RELEASE) uses: ./.github/mini_flows/s6_test - name: Build Code uses: ./.github/mini_flows/s7_build
And here’s what each step does:
1. Checkout Code and Setup JDK
This is a mandatory step in all our actions because it’s going to check out our code into the host machine. The setup JDK is going to install Java version 11 on the host machine.
2. Mandatory File Changes
This step is used to ensure that certain files are always changed as a part of PR, or else the workflow will fail. As of now, this filter is only set for
CHANGELOG.MD file: i.e, if any PR is raised, then it must ensure changes in this file.
We use dorny/paths-filter, a popular 3rd party action, and GitHub’s conditional expressions to execute this action.
3. Lint Analysis (Lint Check and Upload Results )
Android Studio provides a code scanning tool called Lint that can help you to identify and correct problems with the structural quality of your code without having to execute the app or write test cases.
Lint checks are useful because they help see warnings that are raised by Android Studio while building the project. These are not of immediate concern, but could possibly become errors in the future based on the severity.
In our CI/CD actions, we are using it via the built-in Gradle command
./gradlew lint and uploading its results via the upload marketplace action.
4. Code Analysis ( Static Checks via Detekt and Checkstyle Plugin)
Static code analysis is a method of debugging by examining source code before a program is run.
In our CI/CD actions, we have configured Detekt and Checkstyle: the two most commonly used static code analysers for Kotlin and Java code respectively. This is because the CleverTap Android SDKs use both these languages.
We can further configure their Gradle tasks to fail if the errors reported by them are not resolved completely
5. Unit Tests and Code Coverage
Unit tests are an integral part of any CI/CD workflow as they ensure the proper working of code.
In our code, we use frameworks like Robolectric, JUnit 5, and Mockito for writing unit tests that could be executed via command line or on the cloud, without any need for an external device/ emulator.
Code coverage refers to the percentage of code which is covered by automated tests. Having a large codebase covered by automated tests gives a sense of robustness and quality of the codebase.An ideal project should have > 90% of code coverage. In our project CI/CD workflows, we use the JaCoCo library via a 3rd party Gradle plugin. Currently, our code coverage is 28.75% and we are planning to increase this to greater than 60% by the end of this year.
6. Building the code
./gradlew assembleDebug is the command that Android Studio uses every time we press the green ‘run’ button. Therefore we use the same command in our CI/CD workflow and upload the build artifacts to the runner.
So far we’ve automated the above-mentioned steps in our CI/CD pipeline using GitHub Actions. But we have enhancements lined up to be added to our workflows:
1. Slack Integration
Slack provides resources to create web hooks, which can be used to send informational messages to various channels.
These web hooks can be integrated into a GitHub workflow via 3rd party actions, or even an officially supported GitHub action from Slack.
Thus, we can send a Slack message at the end of the GitHub workflow, listing which steps ran successfully and which steps failed to execute.
2. Publish to Github Releases
Currently, whenever a new version of an SDK is released, we manually create tags on the code via the command line, and then the release code alongside the AARs and push it into the ‘Releases’ section on GitHub manually.GitHub actions provide an ability to automate these steps: i.e if tag name and release version is available as input, then marketplace actions like GitHub Automatic Releases action can automatically create draft releases with the required artifacts.
3. Publish to Maven Central
Maven Central is the official marketplace that hosts CleverTap Android SDKs.
For every new release, we have a Gradle task that generates release builds of the libraries and uploads them to the staging version of the Nexus Repository Manager.A third-party Gradle publish plugin exists that can not only publish to staging but can also directly publish the release version on Nexus Repository Manager, making an immediate release possible with zero manual intervention.
Adding a CI/CD pipeline in the CleverTap Android SDK has been a major task that required lots of research, testing and behavioural observation of the codebase. However the end results have been very satisfying and will continue to benefit us in the future.
1. Improvement in Code Quality
Using code style tools like Lint helped in defining a common style guide and coding formats across our team of five Android developers. This helped in improving consistency, and reducing common stylistic errors and suspicious constructs.
Using static code analysis tools like Detekt and Checkstyle provide a consolidated list of potential errors and System API warnings that could impact the performance and working of the Android SDK.
Using code coverage tools like JaCoCo helped us in creating a test-driven environment for our codebase and ensuring the code quality and robustness.
Overall, the various tools and frameworks incorporated in our pipelines help us in getting various statistics around the codebase and help in taking informed decisions forward.
2. Reduced Review Time and Accelerated Release Cycles
A change in codebase that passes through a certain workflow implies that it has been written in accordance with our standards and style guides, is not causing any breaking changes, has passed through all the regression tests and is ready to be deployed to the public. This has resulted in reduction in action review time, as the reviewers can be more sure of the impact of the change.
3. Spending More Time on Product Than Maintenance
Since all the checks are going to run on cloud, developers can spend more time adding new features and improvements rather than performing regression tests and repetitive tasks.
There are many more advantages that we have yet to quantify but adding GitHub Actions to our CleverTap Android SDK repo has been a great success.