Many CI services use an incrementing build number to identify builds. GCB, however, uses a randomly assigned unique identifier, as is common in large scale systems. While this is common and helps make GCB infinitely scalable, it makes it difficult to reason about the relationship between builds.

Since I like to use the CI build number in my Android app versioning, I decided to reimplement this functionality using a simple script and text file stored in a shared location. The idea is simple, each build mounts a shared config folder, reads the version number from a text file, and overwrites that text file with a new version number.

In a highly concurrent build environment there is a chance of collisions, resulting in multiple builds with the same build number, but for my small team and limited builds, this isn’t likely.

This post is part 2 in a series on setting up Google Cloud Build (GCB) for Android developers.

  1. Introducing Google Cloud Build
  2. Incrementing Build Numbers
  3. Saving the Build Cache
  4. Filtering Build Steps by Branch
  5. Storing Secrets

The .buildenv Script

The script that I use to increment the build number, in addition to overwriting the original file with the new build number, actually creates a very simple bash script locally that can be used to set a BUILD_NUM environment variable in future build steps.

#!/bin/bash

export CI=true
export BUILD_NUM=43

The reason that I create a bash script for this is in GCB, all build steps are isolated from each other, only sharing the file system. This means that environment variables set in one step won’t be carried to the following steps.

A simple solution is to wrote a script to the file system and let future steps source the script when they start up. In my environment, I’ve taken to calling this script .buildenv.

source .buildenv

Deploying the Docker Container

To save you from having to create your own docker container and script to manage your build numbers, I’ve created a buildnum container in the Android Cloud Build repository.

There are instructions for deploying the container to your own Google Cloud Registry (GCR) for use with GCB in the readme, but as a quick summary, the container can be built using the following GCB command, which will automatically upload it to GCR for you.

gcloud builds submit --config=cloudbuild.yaml

Configuring the Build

The three steps mentioned above, mounting the config folder, getting and incrementing the build number, and saving the updated config file, need to be run sequentially at the beginning of our build process.

The first step, copy_config, uses the gsutil command to copy the contents of the Google Cloud Storage (GCS) bucket to a local shared volume, called config.

- name: 'gcr.io/cloud-builders/gsutil'
  id: copy_config
  waitFor: ['-']
  args: ['rsync', 'gs://${_CONFIG_BUCKET}/', '/config']
  volumes:
  - name: 'config'
    path: '/config'

It’s important to use rsync instead of cp here, as cp will fail if no files exist, which will be the case the first time the build is run.

Also note the waitFor option in this build step. Setting the value to - means that this build step will be run when the build starts, without waiting for any other build steps.

The next step is to run the buildnum script to actually generate and update the build number. This step uses the buildnum container that you previously deployed to your Google Cloud Registry.

- name: 'gcr.io/$PROJECT_ID/buildnum'
  id: setup_env
  args: ['/config/buildnum', '.buildenv']
  waitFor: ['copy_config']
  volumes:
  - name: 'config'
    path: '/config'

The buildnum script takes two arguments, the source version file (/config/buildnum), and the destination script (.buildenv). This adds flexibility, allowing you to use different version number tracking for different builds.

In this step we also have to set the waitFor property so that this step is run only after the copy_config step completes.

For the final step, we copy the updated /config/buildnum file back to our config GCS bucket for future builds.

- name: 'gcr.io/cloud-builders/gsutil'
  id: save_config
  args: ['cp', '/config/buildnum', 'gs://${_CONFIG_BUCKET}/buildnum']
  waitFor: ['setup_env']
  volumes:
  - name: 'config'
    path: '/config'

This step waits for the setup_config step to complete so that we have access to the updates buildnum file.

The Graph

To help visualize how these steps fit together in our build, here’s a graph of the build steps. While this is super simple, notice how our general build steps only wait for the setup_env step to complete.

This allows the save_config step, which simply copies the updated config file back to GCB for future builds, to run in parallel with the rest of the build steps and makes the entire build faster.

That’s All

Stay tuned for more recipes to help make Google Cloud Build a really great continuous integration service for your Android apps.