Enforcing No Maven SNAPSHOTs (Or No Means No)

Continuous Mindset

When I first set out on my quest to integrate Maven into a Continuous Delivery deployment pipeline, I was pretty used to working with Maven SNAPSHOTs. Back then I wouldn’t have dreamed of getting rid of them because I couldn’t see how or why I should do so. That’s all changed for me now though because as I read and re-read the excellent Continuous Delivery book, it finally sunk in that you can live without SNAPSHOTs – if you think a bit differently.

One of the overarching goals of the Continuous Delivery deployment pipeline is to make releases into production so common and so frequent that we can live up to the first principle of the Agile Manifesto:

Our highest priority is to satisfy the customer through early and continuous delivery of valuable software.

After working with Maven for years, all the time using SNAPSHOTs, how could I possibly come to believe that they aren’t needed anymore? Isn’t that Maven heresy (or some techno-crime variant)?

There was a change in my thinking along the way. Here’s how I used to think about it:

It sure does seem like SNAPSHOTs make it easy to develop because I can pick up other developer’s changes via their SNAPSHOT versioned artifacts just by updating my dependencies. We’ll get around to releasing when we’re ready, but hey right now I just need to get some coding done! Come back later when I have that done and then we’ll talk about the release!

Did that last bit sound like procrastination to you? Upon reflection I realized that while us developers really like the SNAPSHOT, it does little to move our software into production. In fact, the SNAPSHOT is a bit of a crutch because it has temporary written all over it. While we can’t release a temporary artifact, we sure can develop around it with the latest changes magically arriving via SNAPSHOT updates. But how are we going to reconcile that with Continuous Delivery?

Here’s how I think about SNAPSHOTs now with a Continuous Delivery mindset:

I’ll work in one source codebase that has no SNAPSHOT dependencies and every time I commit to Subversion, the Jenkins continuous integration server deploys a releasable artifact into the Nexus Maven repository. If I need the latest changes from other developers, I will update my local source codebase from the Subversion repository. This means that I’ll need to build the whole project, but it also means that when I test my code I’ll be in sync with the other developers.

However I won’t need to build all of the modules every time I make a coding change, but if I really expect this business application to ship with everything working (say tomorrow, or perhaps next week) it would be wise for me to stay on top of those other developer’s changes as I work.

Maven includes a command option that will let you perform a build on a subset of the modules in a multimodule project. You just need to let it know you want to do so. For instance, the following Maven command builds just the demo-domain module in our sample multimodule project:

mvn --projects demo-domain clean install

The Enforcer

The Maven enforcer plugin comes with rules that can enforce no SNAPSHOT versions. When a rule is violated the enforcer plugin will fail the build. That way we’ll know immediately when either the project version or a dependency is using a SNAPSHOT version.

The LOCAL suffix on the version supports builds on a developer workstation. The developer can build and install artifacts into the local Maven repository (~/.m2/repository) that include the LOCAL suffix. However, when Jenkins builds the project, the version will include the Subversion revision number instead of LOCAL as was described in a previous article.

The following plugin configuration illustrates how to enforce a no SNAPSHOT versions project:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <version>1.0.1</version>
  <executions>
    <execution>
      <id>enforce-no-snapshots-and-version-format</id>
      <goals>
        <goal>enforce</goal>
      </goals>
      <configuration>
      <rules>
        <requireProperty>
          <property>project.version</property>
          <message>"Project version must comply with standard."</message>
          <regex>^\d+\.\d+\.\d+-(LOCAL|\d+)$</regex>
          <regexMessage>"Project version must include major.minor.patch
 number parts and end in svn revision number or LOCAL (e.g.,
 1.2.0-LOCAL)."
          </regexMessage>
        </requireProperty>
        <requireReleaseVersion>
          <message>current project must not be a snapshot</message>
        </requireReleaseVersion>
        <requireReleaseDeps>
          <message>no snapshot dependencies allowed</message>
        </requireReleaseDeps>
      </rules>
      <fail>true</fail>
    </configuration>
    </execution>
  </executions>
</plugin>

Living without SNAPSHOTs

So now that we’ve rigged up a POM that enforces no SNAPSHOTs, can we live with it? Does it still let developers develop and also work with Continuous Delivery? Well my short answer is that I think it can work. The longer answer is that this needs to be battle-tested on real projects for a while before making any grand claims for success. As with all technical endeavors, this sort of thing is always a moving target. For example, the recently released Thoughtworks Technology Radar report has put Maven on hold and recommends considering a move to Gradle.

For those of us that are going to stick with Maven for some time, there’s more to this story regarding the deployment pipeline. And I’ll be writing about some of these other topics in future articles. So for now, let’s just say that while SNAPSHOTs are a thing of the past in some circles, there are plenty of other challenges when it comes to fulfilling the goals around delivering software that we can ponder.

Using Maven versions plugin in the pipeline

Maven versions plugin

The Maven versions plugin includes a goal to set the version for a Maven project. It’s a best practice to always use the versions plugin to set the version of a Maven project because this can prevent mistakes. This is especially handy when the project includes multiple modules, because the versions plugin can set the version in each of the pom.xml files in one command.

For this article, we’ll be using a simple multimodule Maven project to illustrate the use of the versions plugin. The project is named demo-parent and includes multiple Maven modules as illustrated below.

The following example illustrates use of the versions plugin to set the project version. This command is run from the root directory of the Maven project (where the parent pom.xml lives):

mvn versions:set -DnewVersion=1.2.3-LOCAL

After running the above command, the version will have been set in each module of the demo-parent Maven project. It turns out that in my projects, the only version entries that require setting are the /project/version tag in the parent module and the /project/parent/version in each of the modules that inherit from the parent (i.e., sub modules). I don’t include the /project/version tag in the sub modules since they can inherit the version from the parent module. However, Maven will throw a fit if you don’t specify the /project/parent/version in each sub module, so we must set it.

The following is an excerpt from the demo-webapp pom.xml that illustrates what this looks like after setting the version:

<parent>
 <artifactId>demo-parent</artifactId>
 <groupId>com.acme.system</groupId>
 <version>1.2.3-LOCAL</version>
 </parent>

 <artifactId>demo-webapp</artifactId>
 <packaging>war</packaging>
 <name>demo-webapp</name>
 . . .

There is one other minor detail when using the versions plugin that needs our attention. When the command is run as illustrated earlier, a backup copy of the pom.xml file will be written into the same directory (i.e., pom.xml.versionsBackup). Depending on your scenario, you may want this backup pom.xml file, or not. This will come into play later on when I demonstrate how to avoid performing a clean Subversion checkout for each Jenkins build  job execution. To control whether the backup pom.xml file is written, we need to add one more parameter to our Maven command:

mvn versions:set -DnewVersion=1.2.3-LOCAL -DgenerateBackupPoms=false

Setting the version in Jenkins

In order to eliminate Maven SNAPSHOTs in our Jenkins builds, we’ll need to set the Maven version before the Maven build job runs. A script that executes as a Jenkins Pre Step will perform the work. This script could be written in any number of scripting languages. I happen to like Ruby so that’s what we’ll use to set the Maven version.

The script shown below performs the following steps:

  1. Obtain the Subversion revision number from Jenkins environment
  2. Look up the current Maven project version from the parent pom.xml
  3. Extract the main Maven version portion (e.g., 1.2.3)
  4. Combine the main version with the Subversion revision number (e.g., 1.2.3-45678)
  5. Sets the Maven version using the versions plugin.

This all happens in a Jenkins Pre Step before the standard Maven build runs, so the result is indistinguishable from a normal build as far as Maven can tell. The Jenkins job is set up to deploy the resultant build artifacts to a Nexus artifact repository.

#!/usr/bin/env ruby
require 'rexml/document'
include REXML
# [1] obtain Subversion revision number from Jenkins environment:
svn_revision = ENV['SVN_REVISION']
# [2] obtain project version from pom.xml file:
pom_doc = Document.new(File.new("pom.xml") )
pom_project_version = XPath.first(pom_doc, "/project/version").text
# [3] extract the three-part Maven version number:
main_version = "INVALID_VERSION: #{pom_project_version}"
if pom_project_version =~ /(\d+\.\d+\.\d+)(.*)/
 if "-SNAPSHOT".eql?($2)
  puts "main_version: #{main_version}"
  exit(1)
 end
 main_version = $1
else
 puts "main_version: #{main_version}"
 exit(1)
end
# [4] compose the actual Maven version number:
maven_version = "#{main_version}-#{svn_revision}"
# [5] set the Maven version using versions plugin:
mvn_command = "mvn versions:set -DnewVersion=#{maven_version} -DgenerateBackupPoms=true"
system(mvn_command)
puts "\n\nSet version for build #{ENV['BUILD_NUMBER']} to #{maven_version} \n\n"

So is that gonna do it?

Now we have build jobs running in Jenkins that deploy “released” Maven artifacts to a Nexus release artifact repository. The version includes the main Maven three-part numerical version plus the Subversion revision number. And each artifact has the correct version baked into all of its Maven metadata. There aren’t any Maven warnings because we are no longer using version expressions. Furthermore, artifacts are only deployed once into Nexus, so we don’t need to turn off the Nexus feature that prevents overwriting of artifacts with the same Maven coordinates.

While this is much better than my first attempt, there are still more things we can do to tune this solution. Next time I’ll go over a technique to ensure that there are no SNAPSHOTs in the Maven build. After all, what would be the point of all of this if we didn’t fail the build when SNAPSHOTs sneak into the Maven project?

Design a site like this with WordPress.com
Get started