Gradle Plug-in to automate build numbers on the CI/CD
In today’s article we will learn how to create a custom gradle plug-in that automates the build number on our apps on the CI/CD (Continuous Integration / Continuous Deployment), and how to update the GitHub workflows to leverage it.
The GitHub action
We won’t be writing everything from scratch, we will leverage this GitHub action to keep track of the current build number. The docs on the action are pretty good so you can reference them for the specific details on how it works, but the short version is that this action uses a git tag of the form build-number-123
to track the current build number (123 in this example), and when it runs, it reads the number, deletes the tag, and creates a new tag with the new number, build-number-124
in our example. The build number to use is then made available as an environment variable to other workflows on the CI/CD.
Local builds
While we want the CI/CD to use the action to generate new build numbers, on local builds we simply want to be able to set any number as build number, so we will provide a couple of ways to specify the number, either by passing a command line argument to the gradle build command, or by defining a local file in the root of the project that will contain the build number to use. So, the way we want this to work is as follows:
- we check if an argument specifying the build number was provided, and we parse it if so
- if no argument was provided, then we look for a specific file in the root directory of our project and, if it exists, we read it to load the build number to use
- if neither the argument nor the file exist (which will be the case on the CI/CD), then we load the build number from the environment variables
- if we could not load any number from the steps above, then we will throw an error (alternatively we could use a default build number)
Creating the gradle plug-in
Now that we have our requirements set, we can write the plug-in. We will start by defining the plug-in extension.
Build number extension
An extension is just a plain class that provides configuration options for the plug-in and is accessible from within the gradle build scripts. In our case, we only need to expose a single property, the build number, which will initialize from the plug-in block. Our extension will be fairly simple, let’s see it:
- we create an
open
class, as gradle will extend it - we have a single public property, the
versionCode
that we will use for the build — here we throw an error if not set, but you could alternatively default to a specific number - we have an internal setter method to set the build number we want to use, which we will call from the plug-in block
That’s all we need for the extension, we can now move to the plug-in itself.
The build number plug-in
The plug-in needs to do the following tasks:
- register the extension, so that it is available to use in the gradle build scripts
- parse the build number from the command line, the file, or the system environment variables (in that order)
- provide the build number to the extension
Let’s see the code of our plug-in:
- the Plug-in extends the
Plugin
interface - we override the
apply
method that gets called when the plug-in is applied - the first thing we do in our plug-in block is to register the extension associated with our plug-in
- next we create a File object for the file containing the build number to use for our build
- now we load the build number, first checking if we provided a command line argument to the build
- if that argument is not available, but the file is, then we parse the file using a
Properties
object and extract the build number from it - finally, if neither the command line argument nor the file are available, we attempt to load from the system environment variables
- then, if we managed to load the build number, we provide it to our extension
With that our plug-in is now complete, we just need to register it and apply it.
Applying the plug-in
Using the build logic convention, we will register our plug-in by using the includeBuild
directive. All we have to do for this is create a folder inside the build-logic
folder, add our plug-in code and then a build.gradle.kts
file to register it. This is common to all custom plug-ins, so I won’t delve into the details here as it is well covered in the gradle documentation, I’ll just show the build.gradle.kts
file:
- This is the only interesting part, we register our plug-in, specifying an id (how we will apply it in our project), and referencing the class that implements it
Once we have this, we can then apply it to our project and then reference the build number from the extension, as shown here:
- we apply the plug-in to our project, using the id we defined earlier
- once we have the plug-in applied, we can reference the extension and access the property that contains the build number
That’s pretty much it for the plug-in and local builds, we can specify the build number either with an argument in the command line,
./gradlew assembleDebug -PbuildNumber=123
or using a build_number.properties
file with the following content
buildNumber = 123
Next we’ll see how we can leverage this on the CI/CD to increment build numbers for each build.
Updating the GitHub workflows
So we have created and applied the plug-in to our build script, now we need to expose the build number we want to use as a system environment variable on the CI/CD. For this, we need to add a new job that will use the action mentioned earlier on this article to expose the build number, from the tag, as an environment variable. This is already covered in the action’s documentation, but let’s see it here anyways:
- we define the output of this job, the build number we want to use in other jobs
- we run the action to read the tags, parse the build number and generate the new one, and apply a new tag
- we print the build number to the console, so we can see on the CI/CD output what build number is being used
Once we have this, we simply need to update the other jobs to use the build number. Note that, because the Setup
job needs to run first, we will set a dependency on Setup
for the jobs that need the build number:
- we indicate that the
build
job depends onsetup
so that it waits until the build number is available - we read the build number output from the
setup
step and expose it as an environment variable - for jobs that do not need the actual build number because no build artifacts are generated, we can specify a static build number so that we do not depend on the
setup
job — this allows thetest
job to run in parallel with thesetup
and any other jobs that do not need the actual build number
And that’s it, every time we push to the main
branch we will trigger these actions to generate a build number and our plug-in will parse it and apply it to the generated artifact.
A full implementation of this solution is available on this GitHub repo.