Implementing an intuitive versioning and release strategy

build gradle release versioning git ci travis


May 17, 2018


Implementing a versioning and release strategy for a project can be daunting task. An effective strategy should be elegant and require little manual intervention. When forming a strategy the following factors need to be taken under consideration.

  • What kind of artifact(s) does your project produce? Maybe you are writing a library or a full-fledged application comprised of multiple components.

  • How often do you usually release your project? Do you trigger a release only once every six months or multiple times daily?

  • Are the produced artifact(s) available as preview versions during development for early adopters? How does the version of the release indicate this status?

  • Is the version based on semantic versioning or a custom versioning scheme?

  • When starting a new release cycle does a developer or release engineer have to "bump up" a version in a file manually and commit the change to version control?

  • How well does the worflow tie into the mechanics of your Continuous Delivery pipeline?

In this blog, I would like to present you with a practical versioning and release strategy that works well for small, self-contained projects. You will learn how to use Git tags to derive semantic versioning for the project, how to trigger a new release cycle with a single execution of a task and conditionally release the outgoing artifact to Bintray JCenter from a multi-stage job on Travis CI.

This post uses Gradle as the underlying build tool to drive the release process. With little effort, you should be able to apply the same principles to other build tools. The full source code is available on GitHub.

Assembling the artifact

The versioning and release strategy is best demonstrated by starting with a small and self-contained project. You’ll set up a Java-based project pointing to source code in the directory src/main/java. The Gradle build script applies the Java library plugin to produce a JAR file under build/libs whenever the task assemble is executed.

build.gradle

plugins {
    id 'java-library'
}

group = 'com.bmuschko'

Listing 1. Building a Java library project

By convention Gradle takes into account the project name and version to form the JAR file name. The project name is either derived of the root directory name or specified in the settings.gradle file. The project version needs to be assigned explicitly by providing a value to the property Project.version. Listing 1 does not assign a project version. Therefore, the produced artifact will be named gradle-release-strategy.jar.

Let’s change the naming convention and reflect the project version in the artifact name without having to explicitly hard-code a value.

Defining a versioning strategy

Hard-coded project versions have a major drawback. Starting a new release cycle by modifying the version number requires manual intervention. To make matters worse, the change has to be persisted (most likely by committing and pushing to version control) so that other team members use the same, shared version. Furthermore, the team has to establish a common understanding of a version under development vs. a final version to indicate production-readiness.

The Gradle Git plugins implemented by Andrew Oberstar simplify the process by forming a opinionated versioning strategy. Listing 2 applies the relevant plugins and configures them to produce a desired versioning scheme. Under the covers, the plugins derive the project version based on the latest Git tag of the project and the provided strategy configured through the plugin extension with the namespace release.

build.gradle

plugins {
    id 'org.ajoberstar.grgit' version '1.7.2'
    id 'org.ajoberstar.release-opinion' version '1.7.2'
}

import org.ajoberstar.gradle.git.release.opinion.Strategies

release {
    versionStrategy Strategies.FINAL
    defaultVersionStrategy Strategies.SNAPSHOT
    tagStrategy {
        generateMessage = { version -> "Version $project.version" }
    }
}

Listing 2. Declaring a versioning and release strategy

Inferring the current version

How does the plugin determine the project version at runtime? The plugin looks at the latest tag for the repository containing the source code and uses it as the basis for determining the project version. If you are just starting out with the project and no tag has been set then the version 0.0.1 is used. The default strategy appends the SNAPSHOT suffix whenever an uncommitted change is detected in your local repository.

The following console output shows that the plugin picks the version 1.0.4-SNAPSHOT for a project that has been tagged with v1.0.3 as its latest release.

$ git tag
v1.0.0
v1.0.1
v1.0.2
v1.0.3

$ ./gradlew assemble

> Configure project :
Inferred project: gradle-release-strategy, version: 1.0.4-SNAPSHOT

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 up-to-date

$ ls build/libs
gradle-release-strategy-1.0.4-SNAPSHOT.jar

Releasing a version

The plugin exposes a task named release to trigger a release. Upon execution, the task creates a new, final tag and pushes it to the remote repository. Optionally, the user can decide on the subsequent project version by incrementing major, minor or patch attribute. By default the plugin increments the patch version.

Let’s say you’d want to cut a production-ready release with the version 1.1.0.

./gradlew release -Prelease.stage=final -Prelease.scope=minor

> Configure project :
Inferred project: gradle-release-strategy, version: 1.1.0

> Task :release
Tagging repository as v1.1.0
Pushing changes in [refs/heads/master, v1.1.0] to origin

BUILD SUCCESSFUL in 6s
2 actionable tasks: 2 executed

$ git tag
v1.0.0
v1.0.1
v1.0.2
v1.0.3
v1.1.0

$ ./gradlew assemble

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 up-to-date

$ ls build/libs
gradle-release-strategy-1.1.0.jar

When does the plugin pick the next version?

The project will continue to use the final version 1.1.0 for as long as there have been no local changes to the source code. The inferred project version switches to the next reasonable SNAPSHOT version immediately after at least one of the source files have been changed. Refer to the plugin documentation to learn more about the algorithm used to determine the next version.

$ echo "description = 'hello'" >> build.gradle

$ ./gradlew assemble

> Configure project :
Inferred project: gradle-release-strategy, version: 1.1.1-SNAPSHOT

The meaning of a "release"

Formally, this process releases a new version of the artifact. In practice we haven’t told Gradle what to do when the release process has been triggered apart from tagging the source code with a specific version. As the desired state the JAR file should be published to the binary repository JCenter. Next, you’ll learn how to create the necessary setup.

Publishing the artifact

Publishing one or many artifacts with Gradle is relatively straightforward. The project needs to apply the necessary publishing plugins and provides additional metadata.

You can set up Maven repositories on JCenter to host your released artifacts. Listing 3 shows how to publish the JAR file produced by the project to the repository named "experimental". Credentials can be provided through environment variables or project properties.

build.gradle

plugins {
    id 'maven-publish'
    id 'com.jfrog.bintray' version '1.8.0'
}

publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
}

bintray {
    user = resolveProperty('BINTRAY_USER', 'bintrayUser')
    key = resolveProperty('BINTRAY_KEY', 'bintrayKey')
    publications = ['mavenJava']
    publish = true

    pkg {
        repo = 'experimental'
        name = 'com.bmuschko:gradle-release-strategy'
        desc = 'Release strategy interfering project version from latest Git tag.'
        websiteUrl = "https://github.com/bmuschko/${project.name}"
        issueTrackerUrl = "https://github.com/bmuschko/${project.name}/issues"
        vcsUrl = "https://github.com/bmuschko/${project.name}.git"
        licenses = ['Apache-2.0']
        labels = ['gradle', 'release']
        publicDownloadNumbers = true
        githubRepo = "bmuschko/${project.name}"

        version {
            released = new Date()
            vcsTag = "v$project.version"
        }
    }
}

String resolveProperty(String envVarKey, String projectPropKey) {
    String propValue = System.getenv()[envVarKey]
    propValue ?: findProperty(projectPropKey)
}

Listing 3. Publishing to Bintray JCenter

In the next section, you will learn how to publish the artifact whenever the release process tags a commit on CI.

Automating the release process on CI

Optimally, you do not want to require your developers to deeply think about the release process. In the best case scenario, a developer triggers the release process and CI takes care of the rest. This section focuses on the use of Travis CI. You can implement a similar workflow with other CI products.

Travis CI is helpful for formalizing deployment pipelines for open source projects. In the past, I have used the product extensively for building and publishing Gradle plugins andlibraries written in Java or Go.

The CI job shown in listing 4 consists of two build stages: "build" and "release". The stage "build" simply compiles the code, runs the tests and assembles the artifact. "release" takes care of publishing the artifact to JCenter. The "release" stage only executes the assigned script if the commit was tagged beforehand. In all other cases, Travis will simply skip the deploy instructions.

.travis.yml

language: java
install: true

jdk: oraclejdk8

jobs:
  include:
    - stage: build
      script: ./gradlew build -s
    - stage: release
      script: skip
      deploy:
        provider: script
        script: ./gradlew bintrayUpload -s
        on:
          tags: true

Listing 4. Build stages defined in Travis CI

In this workflow, the tagging would be performed on the developer’s machine but could also be implemented as a push button-release as part of another CI job. The next section shoulds you how the job looks like upon execution.

A successul release on Travis

Figure 1 shows the job execution on Travis CI for a release. The tag v1.1.3 has been previously created by executing the release task from the developer’s machine.

travis build stages

Figure 1. A tagged commit on CI triggering the release stage

If you dig deeper into the log files of the "release" stage, you’d find a section called "Deploying application". It contains detailed information about the deployment process, more specifically the executed Gradle publishing tasks.

Deploying application
> Task :generatePomFileForMavenJavaPublication
> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes
> Task :jar
> Task :publishMavenJavaPublicationToMavenLocal
> Task :bintrayUpload

BUILD SUCCESSFUL in 10s
5 actionable tasks: 5 executed

Summary

The use of Git tags to derive the project version is a clever way to avoid manual intervention. Gone are the days of having to "bump up" the version number in a file to initiate a new release cycle. With the help of relevant Gradle plugins, the build can infer a sensible project version by convention. It is fairly easy to make the process part of a Continuous Delivery pipeline to automate releases end-to-end.



comments powered by Disqus