Automating Semantic Versioning with Maven

Are you using a semantic versioning approach? Are you using gitflow? Chances are you know the process of adjusting versions, creating branches, merging to master/dev, adjusting versions again, fighting with merge conflicts,…

In this blog post I’ll shortly explain the release process we, as viesure, use for our libraries, and how we automated it. We’re generally using a CI/CD approach, utilizing build numbers, but for our libraries we decided to go with semantic versioning. I’ve encountered the tedious release process that comes with this in several companies, and now I’ve finally found a solution.
This article focuses on Maven, but there are many Gradle alternatives too.

An example project can be found on our GitHub page.

Semantic Versioning and Git

Semantic versioning is a system to classify your releases. I’m sure you’ve seen version numbers like 1.6.4, 1.7.10, 1.12.2 and the like. These figures stand for MAJOR.MINOR.PATCH.
Additionally, there are SNAPSHOT versions, which look similar but with a “-SNAPSHOT” added at the end, e.g. 1.14.4-SNAPSHOT.

A typical release process consists of the following steps:

  1. Create a release branch from the development branch
  2. Change the version inside all pom.xml files from SNAPSHOT (1.2.3-SNAPSHOT) to non-SNAPSHOT (1.2.3) on the release branch
  3. Increase the SNAPSHOT version on the development branch (1.2.4-SNAPSHOT)
  4. When everything is done for the release, merge the release branch into the master branch. This is the actual release.
  5. Merge the master branch or the release branch back into the development branch.

There are a few variations of this process, with different merge timings/strategies, but it boils down to this: The version has to be changed to two different values on two different branches, but both branches shall share the same history to prevent merge conflicts.

While these steps aren’t complicated per se, they’re tedious and error prone when you have to execute them often. At the same time, they require a lot of thought to prevent conflicts.

What were my goals?

  • I want to automate the whole process, so I don’t have to worry about mistakes
  • The current master branch state shall never contain a SNAPSHOT version
  • I want to hook it into the CI Pipelines, but with a manual trigger
  • I never want to deal with merge conflicts
  • I also want to use it for hotfixes (which originate from the master branch)

The gitflow-maven-plugin

You may know the maven versions plugin, which easily allows for setting the versions inside your pom.xml files. However, this requires me to manually enter the new version. I want my solution to derive the next version automatically based on the current version.

Luckily, there already exists some tooling to help achieve my goals. Since the problem is a relatively common one, several people have tried tackling it. However, of all the plugins and tools I’ve found, only one is still maintained, while also providing enough configuration options: the gitflow-maven-plugin

It features exactly what I need:

  • Increasing version numbers
  • Creating a release branch
  • Creating a hotfix branch
  • Merging branches

Additionally, releases are possible directly from the development branch without an extra release branch. Very convenient since our company is running CI/CD anyway, meaning the development branch is always in a releasable state.

All of this can be done by executing maven goals. We only need to tell the plugin how to behave and which number to increase.

Some examples:

The following commands show a few examples I used while evaluating the plugin.

$ mvn gitflow:release-start -B

Creates a release branch from the development branch without user input (-B Batch Mode)

$ mvn gitflow:release

Creates a release directly from the development branch. The plugin merges the current development state into the master branch and increases the version number on the development branch.
This example does not have the batch flag, meaning the user is prompted for all missing information.

$ mvn gitflow:hotfix-start -B
$ mvn gitflow:hotfix-finish -B -DhotfixVersion=1.8.9b

Creates a hotfix branch from the master branch in the first step, then release the hotfix with fix version 1.8.9b in the second step. All plugin goals can be executed in any branch. Because of this, I have to tell the plugin which hotfix is the one I want to release.

Configuration

Adding the plugin dependencies is simple enough, just add the following to the poms build plugins:

<build>
    <plugins>
        <plugin>
            <groupId>com.amashchenko.maven.plugin</groupId>
            <artifactId>gitflow-maven-plugin</artifactId>
            <version>1.13.0</version>
            <configuration>
                <!-- optional configuration -->
            </configuration>
        </plugin>
    </plugins>
</build>

The latest version can be found on GitHub or on maven central.
Additional configuration is well documented on the plugins GitHub page. Here is an example configuration, highlighting some configuration options:

<configuration>
    <!-- We use maven wrapper in all our projects instead of a local maven installation -->
    <mvnExecutable>./mvnw</mvnExecutable>

    <!-- Don’t push to the git remote. Very useful for testing locally -->
    <pushRemote>true</pushRemote>

    <!-- Set to true to immediately bump the development version when creating a release branch -->
    <commitDevelopmentVersionAtStart>false</commitDevelopmentVersionAtStart>

    <!-- Which digit to increas in major.minor.patch versioning, the values being 0.1.2 respectively.
         By default the rightmost number is increased.
         Pass in the number via parameter or profile to allow configuration,
         since everything set in the file can't be overwritten via command line -->
    <versionDigitToIncrement>${gitflowDigitToIncrement}</versionDigitToIncrement>

    <!-- Execute mvn verify before release -->
    <preReleaseGoals>verify</preReleaseGoals>
    <preHotfixGoals>verify</preHotfixGoals>

    <!-- Configure branches -->
    <gitFlowConfig>
        <productionBranch>master</productionBranch>
        <!-- default is develop, but we use development -->
        <developmentBranch>development</developmentBranch>
    </gitFlowConfig>
</configuration>

It’s a great plugin, everything works flawlessly when I run it locally. There are a few select things that take a little persuasion to work with Gitlab CI.

Automating it with Gitlab CI

We’re running a CI/CD pipeline using Gitlab CI for our projects, so every new commit on our development branch results in a snapshot build, and a merge to master results in a release.

As described above, my goal is to automate the development → master merge and the version updates that go along with that, as well as hotfixes.

I’m using Gitlab CI pipelines to simplify the process and make it available to my colleagues at the same time. Here is an example:

This is what my example’s development branch pipeline, with optional release steps, looks like. So if I want to create a release, I just trigger the release-step and it automatically sets the version to non-snapshot, merges it to master and bumps the version on the development branch to the next snapshot version. Without any further actions required on my side.

Making git work in Gitlab CI

If you’ve tried using git commands inside Gitlab CI then you know: it’s not exactly straightforward, since the CI runners don’t have git credentials.
For simplicity’s sake, I’m using access tokens with write_repository rights. I set up a dedicated user for this, so I can see which commits were created automatically.
I’m injecting the token via the environment variable GITLAB_TOKEN, which I set to protected, and mark the development, release/* and hotfix/* branches as protected. This way only builds on those branches have access to the token.

I’m setting the git remote on the runner manually to enable the CI environment to push to the repository. I’m utilizing the Gitlab provided variables so I don’t have to hardcode anything:

$ git remote set-url --push origin "https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"

With this git commands can be executed using the credentials of the injected access token. Git also requires some information about the “user” who is performing the git actions. It’s a good idea to have the git user match the user whose access token we use. In my example:

$ git config user.name "Gitlab CI"
$ git config user.email gitlab-ci@viesure.io

With this, I can easily find all git commits done by the CI. The whole code snippet is available in the example repository.

Bringing it all together

Now that I can access git from the CI, I need to set up the gitflow plugin. There are a few peculiarities that I encountered.

For simplicity’s sake I’m using predefined variables to decide which version digit I want to increment:

  • MINOR for releases
  • PATCH for hotfixes

One could also pass the variable in when executing the pipeline, or derive it by another way.
All goals are passed the -B parameter, which stands for batch mode and means there will be no prompt for user input.

Automatic Release

$ ./mvnw gitflow:release -B -DgitflowDigitToIncrement=$RELEASE_DIGIT

The full release process is relatively straightforward. The plugin merges the development branch to master without SNAPSHOT inside the version and then increases the development version. I’m just calling the maven goal and tell it which digit to increment.

Manual Release

$ ./mvnw gitflow:release-start -B -DgitflowDigitToIncrement=$RELEASE_DIGIT
$ git push origin HEAD

The manual release starts by creating a release branch. Technically the digit is not needed since the increase is done on merge, but if the option to increase development-version on branch-out is active it’ll work with it out of the box (for example if it later changes, or different projects use different settings). Note that I have to push the branch manually since the plugin is designed for local usage.

$ git symbolic-ref refs/heads/$CI_COMMIT_REF_NAME refs/remotes/origin/$CI_COMMIT_REF_NAME
$ ./mvnw gitflow:release-finish -B -DgitflowDigitToIncrement=$RELEASE_DIGIT

Finishing the release brings the first workaround with it. Git keeps a reference (ref for short) to the HEAD of all your branches. However, Gitlab CI does not set up all of these references when checking out the repository. The plugin uses these refs to HEAD to check the branches. It does have the correct references locally though, so I’m simply creating the missing HEAD ref.
After that the release-finish goal merges the release branch into master and development branch, increasing the development version.

Hotfix

A Hotfix follows the same process as a manual release, except it starts on master instead of development.

$ ./mvnw gitflow:hotfix-start -B -DgitflowDigitToIncrement=$HOTFIX_DIGIT
$ git push origin HEAD

Hotfix-start creates the hotfix branch which already contains the increased version.

$ export CURRENT_VERSION=${CI_COMMIT_REF_NAME/hotfix\/}
$ git symbolic-ref refs/heads/$CI_COMMIT_REF_NAME refs/remotes/origin/$CI_COMMIT_REF_NAME
$ ./mvnw gitflow:hotfix-finish -B -DgitflowDigitToIncrement=$HOTFIX_DIGIT -DhotfixVersion=$CURRENT_VERSION

Hotfix-finish merges it into the master and development branch. However, hotfix requires one more parameter: The hotfix version. The reason for this is, again, that the plugin is designed for local use, where you might have multiple branches. It does not expect goal execution inside the hotfix branch.
As said above, hotfix-start already increases the version number to signify the hotfix’s version. The branch name therefore already signifies the hotfix version.
I’m simply parsing the version out of the current branch name. Since the hotfix-finish pipeline job is defined for hotfix branches it will only ever be executed on said branch.

Summary

That’s it. With that, you’re only one click away from all the version bumping and branching you’ll ever need!

It took a bit of effort to get git interactions working inside the CI runners, and to work around the missing references. But now that it works, I’m very happy with the result.
Never again will I have to think about the order in which I need to branch and increase versions. The CI will take care of it for me. 🙂

You can find the full Gitlab CI script and an example project with all the code snippets from above on our GitHub repository.

Links

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Close Menu