08 March 2015
Introducing the Multi-module Maven Release Plugin for Git
It’s 2015 and I’m writing a Maven plugin. Most Java developers that I respect have a rather strong dislike of Maven, so what am I doing here? Well I don’t think Maven is fundamentally too bad as it is pretty stable and does most of what I want in a build tool. I basically want:
- A simple way to define my dependencies
- A test runner
- A JAR file (or tgz) containing my classes and dependencies created with version numbers
- A simple release process that uploads versioned packages to something like Nexus
Maven mostly fails cosmetically on the first 3 - the XML is in the most verbose format possible, and the XML is, well, XML. But aside from that it is quite straightforward. Alternatives like Gradle are probably better (I much prefer the ability to script over coding in XML), but it’s still not as mature and it just hasn’t been quite strong enough to break through my intertial co-efficent of laziness for properly learning a new build system.
Aside from the copious XML, another place where Maven really doesn’t do well is in releases - particularly when you have
multiple modules within
a single repository. While it’s possible to release single modules within a Maven project with the default
maven-release-plugin
, this will not build and release dependent modules properly. It is common that for each
release, every module is released, and they all share the same version. They don’t have to share the same version,
but if they don’t then the tag on your repo will be of the aggregator name and version, so it is hard to locate
specific versions of a module in your VCS history. Finally, I have found that when the release process makes
commits to your repo (as the maven-release-plugin does to update the versions in the pom) then it results in a
messy VCS history, lots of little merges, and it makes continuous deployment a bit more difficult, as if you
really release to some environment on a commit, then the release will go into an infinite release loop. Also
the fact that tests run twice by default is a waste of time.
After years of working on many different projects of various sizes and complexity, myself and some others identified what we really wanted from a release process:
- Each module in a multi-module project should be versioned independently.
- A module should not be re-released if it hasn’t changed since the last release
- Semantic versioning is important, but we don’t need the full release version in source control. Given a “development version” such as “1.0-SNAPSHOT”, the first release version should be “1.0.0”, the second “1.0.1”, etc. Humans control the the first part of the version; computers automatically control the last part.
- There should be a tag for each module released
- Errors should be detected early and reported clearly
By supporting only Git, these requirements become relatively simple to implement because using jgit, questions such as “has this module changed since the last release” are relatively simple to answer.
The way the plugin works is rather simple:
- For each module in the repo, figure out an appropriate release version
- Rewrite each pom to its release version, and update dependencies between them
- Run
mvn deploy
(or whatever is requested by the user) - Tag the repo and revert the pom changes
A simple example
Given a simple project where there is a parent, a library, and an app that uses the library, you may start with the following versions during development:
parent 1.0-SNAPSHOT
|--lib 2.0-SNAPSHOT
|--app 1.5-SNAPSHOT (depends on lib 2.0-SNAPSHOT)
Then running mvn releaser:release
will result in the following being deployed into Nexus:
parent 1.0.0
|--lib 2.0.0
|--app 1.5.0 (depends on lib 2.0.0)
The git repo will have the following tags added: parent-1.0.0
, lib-2.0.0
, and app-1.5.0
. Note that the tags point
to the SNAPSHOT versions, as the concrete versions are never actually committed to the repo. So development will continue
with the app having verison 1.5-SNAPSHOT
, for example.
Now imagine a change is made only to app
and the project is released again. The fact that only the app has changed will be
detected by the plugin, and so the following will be released:
app 1.5.1 (depends on lib 2.0.0)
The only new tag will be app-1.5.1
. Note how the version of lib
used by app is the version made in a previous release.
If there is now a change made to lib
, then the following will be released:
lib 2.0.1
app 1.5.2 (depends on lib 2.0.1)
And the tags will be lib-2.0.1
and app-1.5.2
. In this case, although app
hasn’t been changed directly, it depends on
lib
, and lib
has changed, and so app
is released with the new version of lib
.
All this time, parent
has not been re-released because it hasn’t changed.
Note that it also works with projects that have just a single module, projects where the parent is separate from the aggregator, nested modules within modules, and more.
Migrating from the maven-release-plugin
It is generally enough to just delete any maven-release-plugin
references you have (and you can delete any scm plugins too),
and then change your release build to just call releaser:release
.
Installing
For information on the current version and other configuration, see the official documentation at the plugin page.
I hope you will give it a try and let me know if you have any feature requests, bugs, or other feedback on the issue tracker at GitHub.