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 run
  • apkDir the path to the APK output
  • verCode the version code, defaulting to the Circle CI Build number
  • verName the version name, defaulting as empty
  • verSuffix 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