Building Android with CircleCI
Table of Contents
There are many great CI/CD tools out there: Jenkins, BitRise, GitLab CI, Travis, GitHub Actions, etc. But my current company, Fleetio, uses CircleCI. The above repo has served as a playground for learning yet-another-CI-tool. This post is not intended to be prescriptive nor definitive but to hopefully help someone in the same boat.
🔗: GitHub Repo | GitHub Project | CircleCI Config | build.gradle
🥅 Goals
- Build review, snapshot, and release apps
- Use different version name + app IDs to permit parallel install
- 🚧 Deploy based on lane
🏗 Gradle: Build Variants
// TODO
🛠 CircleCI: Setting up the config
The CircleCI config should be in {your repo}/.circleci/config.yml
. At the top we set the version for the config and will be using the CircleCI Orb for Android, which comes with some useful commands and setup.
🔮 CircleCI’s Orb for Android
At the top we set the version for the config and will be using the CircleCI Orb for Android, which comes with some useful commands and setup.
version: 2.1
orbs:
android: circleci/android@1.0.3
📝 Commands
Commands let us combine reusable steps. This is useful for things like caching to save time between jobs. We can combine the gradle and build cache commands from the Orb into one, allowing us to pop
(restore the cache) and push
(save the cache).
commands:
pop:
description: "Restore build and gradle cache"
steps:
- android/restore-gradle-cache
- android/restore-build-cache
push:
description: "Create build and gradle cache"
steps:
- android/save-gradle-cache
- android/save-build-cache
👷 Jobs
Unit Test
jobs:
unit-test:
executor:
name: android/android-machine
resource-class: large
Our first job will be unit-test
. For the executor (the image doing the building) we can use the one defined from the Orb (android/android-machine
) and set out resource class (large
).
Next we will define the steps for this job:
steps:
- checkout
- pop
- android/run-tests:
test-command: ./gradlew test
- push
We start with checkout
to get the source code and pop
any cache. We then use the android/run-tests
command from the Orb to setup our test and provide the task to run, ./gradlew test
. This runs our unit tests! Finally, we will push any caches for other jobs.
- run:
name: Save test results
command: |
mkdir -p ~/test-results/junit/
find . -type f -regex ".*/build/test-results/.*/.*xml" -exec cp {} ~/test-results/junit/ \;
when: always
- store_test_results:
path: ~/test-results
- store_artifacts:
path: ~/test-results/junit
Next, we add the run
command to create our test reults output directory ~/test-results/junit/
, find any .xml
results and copy them to the directory. Finally, we store the test results and artifacts from this job.
UI Test
android-test:
executor:
name: android/android-machine
resource-class: large
steps:
- checkout
- pop
- android/start-emulator-and-run-tests:
test-command: ./gradlew connectedAndroidTest
system-image: system-images;android-30;google_apis;x86
- push
- run:
name: Save test results
command: |
mkdir -p ~/test-results/junit/
find . -type f -regex ".*/build/outputs/androidTest-results/.*xml" -exec cp {} ~/test-results/junit/ \;
when: always
- store_test_results:
path: ~/test-results
- store_artifacts:
path: ~/test-results/junit
Much like the unit test we start by naming the job, android-test
, and defining its excecutor and steps. The step android/start-emulator-and-run-tests
is another command from the Orb that simplifies our setup. We define which task to run, ./gradlew connectedAndroidTest
, and tell it which image to use; in this case API 30, with Google APIs, and x86. We then follow up with again push
-ing the caches and saving the results.
Build
build:
executor:
name: android/android-machine
resource-class: large
parameters:
task:
type: string
apkDir:
type: string
verCode:
default: $CIRCLE_BUILD_NUM
type: string
verName:
default: ""
type: string
verSuffix:
default: ""
type: string
For build
, we start by setting the executor details. Next, we are going to specify a list of parameters:
task
the gradle task to runapkDir
the path to the APK outputverCode
the version code, defaulting to the Circle CI Build numberverName
the version name, defaulting as emptyverSuffix
the version suffix to append, default as empty
The first two are required while the bottom three are optional. These will allow us to reuse the job for creating various build types (release, review, and snapshot).
steps:
- checkout
- pop
- run:
name: Assemble release build
command: |
name=<< parameters.verName >>
suffix=<< parameters.verSuffix >>
./gradlew << parameters.task >> -PversionCode=<< parameters.verCode >> \
${name:+-PversionName=$name} \
${suffix:+-PversionSuffix=$(echo $suffix | grep -Pio '^(\d*?)(?=\-|_)')}
- store_artifacts:
path: app/build/outputs/apk/<< parameters.apkDir >>
Finally, we set the steps to checkout, pop the cache, and run the gradle command. In Circle CI we can use the parameters for the job via << paramters.{parameter name} >>
. In the command block were setting the version name and suffix as local values, running the gradle task via gradle wrapper, and passing in the gradle paramter -PversionCode={version code}
. Since the name and suffix are optional, we are telling the shell to selectively include the additional arguments when they contain a value. ${name:+-PversionName}
is saying: if name is not empty, include -PversionName=$name
. The same is done for the suffix, along with regex to take the GitHub style issue number (123-my-issue-branch
). We close this out by storing the resulting artifact.
🔀 Workflows
Workflows lets us combine our jobs to run based on criteria we set.
Build review app
For branching workflows that involve testing / QA from a branch before merging into the mainline development (main
|master
|develop
), review apps can be useful for quickly getting them into the hands of reviewers.
workflows:
buildReview:
when:
not:
equal: [ main, << pipeline.git.branch >> ]
jobs:
- unit-test
- android-test
- build:
task: assembleReview
apkDir: review
verSuffix: << pipeline.git.branch >>
requires:
- unit-test
- android-test
We start by defining the workflow buildReview
under workflows
. The when
section lets us decide when CircleCI will run this workflow. For this example, we want to run for any branch besides main
. The not
negates the following section equal: [ main, << pipeline.git.branch >> ]
. equal
is comparing the members of the array, main
and the paramter << pipeline.git.branch >>
which CircleCI provides for us.
For example: if the branch we push to was fix/123-some-bug
, the << pipeline.git.branch >>
would return that and check for equality with main
; since these do not equal, it would be false
. However, the not
negates it and thus would be true
and run!
The jobs
section lets us list our jobs to run (unit-test
, android-test
, and build
with parameters). For the build
paramters, we specify assembleReview
to create our review build variant. We specifying the APK directory as review
and pass the branch as the version suffix. requires
lets use specify that the build step relies on unit-test
and android-test
to complete succesffuly first.
Build snapshot
buildSnapshot:
when:
equal: [ main, << pipeline.git.branch >> ]
jobs:
- unit-test
- android-test
- build:
task: assembleSnapshot
apkDir: snapshot
requires:
- unit-test
- android-test
Build release
buildRelease:
when:
and:
# Main branch + tag
- << pipeline.git.tag >>
- equal: [ main, << pipeline.git.branch >> ]
jobs:
- unit-test
- android-test
- build:
task: assembleRelease
apkDir: release
verName: << pipeline.git.tag >>
requires:
- unit-test
- android-test