Build Number Plug-in
Automate the generation of build numbers on the CI/CD with a custom Gradle Plug-in
Introduction
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 we 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 fromthe 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 on setup 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 the test job to run in parallel with the setup 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.