Semantic versioning with Gradle

There are lot of different approaches when it comes to application versioning. Absolute minimum for modern application is to have immutable version for each release. I would suggest to go a step further and use semantic versioning and Git tags as a source of version information.
In this post I will show you how easy it is to use proper semantic versioning for your application with Gradle and axion-release-plugin.

What is semantic versioning

Semantic versioning is a simple set of rules and requirements that dictate how version numbers are assigned and incremented. In basic case release version format is major.minor.patch (e.g. 2.15.3) where:

That is basically it.

When your application starts to have public API - it is time to start with version 1.0.0. If you change your application and break public API - increment major number. Adding some functionality which doesn’t break API - increment minor number. In case of quick non-breaking hotfix - increment patch number.
It is clean and simple.

There are some special cases like having pre-release versions with alpha/beta or being before public API release. Semantic versioning handles this cases as well. You can find more information about it and additional FAQ section on the official semantic versioning web page.

Configuring axion-release-plugin

Semantic versioning idea is really nice. Now we need to somehow apply it to the project. Manually changing version in build.gradle file does not sound appealing. Implementing Gradle task to do that should be pretty simple, but committing to Git repository during build on CI is always a pain, especially with modern CI which often pulls repository in detached state.
Fortunately there is a simpler solution and ready to use Gradle plugin for it.

Axion-release-plugin solves the problem of committing by keeping the version set on Git tag instead of storing it inside a file. That means - there is no version in any file, but as long as you are in Git repository, version can be easily read or calculated from tag.

Basic usage

Add to your build.gradle:

plugins {
    id 'pl.allegro.tech.build.axion-release' version '1.11.0'
}

version = scmVersion.version

As you can see above, version is not set statically. Instead, it is read from scmVersion.version which is set by axion-release-plugin.
This is how you can use it with Gradle now:

./gradlew currentVersion #checks current version
Project version: 0.1.0-SNAPSHOT # it is SNAPSHOT as you haven't run release yet

./gradlew release # release new version incrementing minor by default (it's configurable)

> Task :verifyRelease
Looking for uncommitted changes..
Checking if branch is ahead of remote..
Checking for snapshot versions..

> Task :release
Creating tag: release-0.1.0
Pushing all to remote: origin

./gradlew currentVersion
Project version: 0.1.0 # it is released version now

echo "Ready to release 1.0.0" > src/main/resources/info
git add .; git commit -m "Adding info file"
git push

./gradlew release -Prelease.versionIncrementer=incrementMajor # it is time to release 1.0.0

> Task :verifyRelease
Looking for uncommitted changes..
Checking if branch is ahead of remote..
Checking for snapshot versions..

> Task :release
Creating tag: release-1.0.0
Pushing all to remote: origin

./gradlew currentVersion
Project version: 1.0.0

Axion-release-plugin has sane defaults for semantic versioning. However, you can adjust it with configuration in Gradle. This is useful if you work with feature or hotfix branches. You can learn more about it from documentation.

Releasing with continuous integration

I am guessing you don’t want to release your application from your PC. Axion-release-plugin is prepared to run on various continuous integration systems like Jenkins, Bamboo, Travis and Azure DevOps.

A lot of modern continuous integration systems do not work on the branch directly, but instead pull specific commit and run the build in so called detached state. This is done to ensure reproducibility of the builds. Because axion-release-plugin uses Git tags to set version, instead of committing it to file, detached state is not a big problem. Still, there are cases when this is a problem. First one is how axion-release-plugin validates state of the Git repository make it impossible with detached state. Second problem is when release process needs to know branch name to increment specific version (e.g. branch with “hotfix” prefix should only increment “patch” version).

To overcome these problems you need to:

Example for Azure DevOps will look like this:

./gradlew release \
    -Prelease.disableChecks \
    -Prelease.pushTagsOnly \
    -Prelease.overriddenBranchName=$(Build.SourceBranch)

You can find extended example on GitHub.

Comments

comments powered by Disqus