plugins {
id 'java-library'
}
group = 'com.bmuschko'
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.
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.
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
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
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
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
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 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.
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.
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.
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
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.