Maven, Jazz, Releases and Versions. Part 1.

I've been playing around with the Maven Release Plugin [123] and Versions Maven Plugin [123] in the context of IBM Jazz [jazz.net].

A couple of observations:

  1. If you want to use mvn release:prepare, you need to be in full control of all your dependencies; all need to be release versions (i.e. not SNAPSHOTs)
  2. The Maven Release Plugin does not really handle multi-module projects where module versions disagree with the parent — at least not in any way I'd want to maintain.

mvn versions:set updates the POM file of the current project, and appropriate parent entries in modules.

Our project setup used to look more or less like this:

parent A (version 1.0)
  |- module A-A (version inherited from parent)
  |- module A-B "Common" (version inherited from parent)
  |- module B-B (version 1.5)
  |- module A-C (version inherited from parent)
        |- module A-C1 (version inherited from parent)
        |- module A-C2 (version inherited from parent)

parent B (version 1.5)
  |- module A-B "Common" (version 1.0)
  |- module B-A (version inherited from parent)
  |- module B-B (version inherited from parent)

It is kind of a mess, to say the least. I wanted to clean it up. I wanted to make sure that our releases would be as simple as possible.

What I've learnt was that the parent-child and modul-submodule relationships in Maven can be used in a number of ways, and, Maven being flexible as it is, it'll allow you to do almost anything. Including shooting yourself in the foot.

First thing first, I wanted to set the Common module apart, giving it its own, separate version. At this stage, the Common module was a child of Parent A, whilst being a managed dependency of it; this meant that making a release version of either was literally impossible. Since the Common module didn't really have any dependencies, I've decided to move it completely outside of any parent POM.

Having done so, I've moved on to invoke mvn release:prepare on the Common module. Things were going fine, until, at the very end, Maven returned a build failure Error code for Jazz SCM deliver command - 53. After some quick internet search (DuckDuckGo, not Google 😁), it turns out that error code 53 "Indicates that a deliver succeeded, but there were no changes to deliver to the repository".

Well, ok...

Luckily, the Jazz SCM command is nothing more than a shell script! So it was very easy to change that 'error code that means success' into 0:

if [ "$USE_NATIVE" = "1" ]; then
    "${PRGPATH}/fec" "$@"
else
    java -classpath "${PRGPATH}/plugins/com.ibm.team.filesystem.cli.minimal_3.1.600.v20140108_0240.jar:${PRGPATH}/plugins/com.ibm.team.rtc.cli.infrastructure_3.1.800.v20140619_0246.jar:${PRGPATH}/plugins/com.ibm.team.filesystem.cli.core_3.2.400.v20141011_0139.jar:${PRGPATH}/plugins/com.ibm.team.filesystem.client.daemon_3.1.500.v20130930_0113.jar:${PRGPATH}/plugins/org.eclipse.equinox.common_3.6.0.v20100503.jar:${PRGPATH}/plugins/com.ibm.team.filesystem.client_3.2.400.v20141015_1622.jar:${PRGPATH}/plugins/org.eclipse.osgi_3.6.50.R36x_v20120315-1500.jar:${PRGPATH}/plugins/com.ibm.team.repository.common_1.4.200.v20141015_2351.jar" com.ibm.team.filesystem.cli.minimal.client.FrontEndClient "$0" "$@"
fi

required addition of just those couple of lines right below:

RESULT=$?

if [[ $RESULT -eq 53 || $RESULT -eq 52 ]]; then
  exit 0
else
  exit $RESULT
fi

(I've split that fix off into its own short post, because it's the kind of thing that's nice to find on its own when you hit the exact same error.)

Common, released

With the Jazz SCM wrapper now telling the truth about success, mvn release:prepare ran clean on Common. Then mvn release:perform did its job, an A-B-1.0.0 artifact ended up in our internal Nexus, and the SCM had a Common-1.0.0 tag for it. So far so good.

The next step is the one that's easy to underestimate: now that Common has a release version of its own, every other module that depended on it has to point at that version explicitly. Inside the old Parent A tree, Common was inherited; nobody had to say which version they wanted because there was only ever one. Now that Common lives outside the parent POM, the dependency has to be declared like any other library.

The right place to do that is <dependencyManagement> in Parent A:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.example.acme</groupId>
      <artifactId>common</artifactId>
      <version>1.0.0</version>
    </dependency>
  </dependencies>
</dependencyManagement>

The child modules still reference common without specifying a version. They inherit the version from the parent's <dependencyManagement> section. The important difference is that the version is now explicit in exactly one place in that tree.

If we want to bump Common, we bump it there — and only there.

The same pattern applies to Parent B. Previously, its modules pulled A-B "Common" (version 1.0) as an explicit oddity in the tree diagram. That is now just a normal managed dependency: Parent B's <dependencyManagement> pins Common at 1.0.0, while the child modules declare the dependency without repeating the version.

The B-B problem

Releasing Parent A is where the second problem starts to bite.

Recall that module B-B lives inside Parent A's directory tree, while conceptually belonging to Parent B. Parent A is versioned as 1.0.x; B-B is versioned as 1.5.x.

That does not fit well with the Maven Release Plugin's model of the world.

The Release Plugin treats a multi-module release as one coherent release unit. In practice, it expects the modules in the reactor to follow the parent's release versioning scheme. A module with its own independent version inside that reactor creates exactly the kind of friction we were trying to remove.

A common workaround is to mark the offending module's version explicitly. That may get you past the immediate release, but it also reintroduces manual version management. Every release becomes a small ritual of remembering what Maven should not touch. That is not a fix; it is operational debt wearing a fake moustache.

In our case, B-B was not really part of Parent A. It was only sitting inside Parent A's directory tree for historical reasons.

The cleanest move was to remove B-B from Parent A's <modules> list, while leaving the directory physically where it was on disk. Parent B then became the source of truth for B-B by listing it explicitly as one of its modules.

After that:

  • Parent A can release as 1.0.x without B-B in its reactor.
  • Parent B can release as 1.5.x.
  • B-B inherits Parent B's version like any other child module.
  • The physical directory layout no longer defines the release topology.

This matters especially in environments like Jazz, where moving directories is not always a casual refactor. If workspaces, components, and build paths already point at specific locations, moving files may require coordination you do not want to sneak into a Maven cleanup.

If you cannot move the directory, there is a boring but effective escape hatch: create a separate aggregator POM.

That POM has packaging=pom and lists the modules you want to build together, but it is not used as their <parent>. It gives you a reactor for build convenience without forcing all modules into the same inheritance and release-versioning model.

In other words:

  • parent POMs define inheritance and managed versions;
  • aggregator POMs define what gets built together;
  • those two jobs do not have to be performed by the same POM.

Once you separate those concerns, the structure becomes much easier to reason about.

Where this leaves us

After the cleanup:

  • Common has its own version and release cadence.
  • Common is referenced explicitly from both parents through <dependencyManagement>.
  • Child modules depend on Common without repeating the version.
  • B-B is no longer part of Parent A's reactor.
  • B-B now belongs to Parent B's release flow.
  • mvn release:prepare on each parent does the obvious thing.

It still is not beautiful.

The two parents now share a managed dependency, which means bumping Common is a two-POM change rather than a free upgrade. Any "release everything at once" workflow also needs to know the correct order:

  1. release Common;
  2. update Parent A and/or Parent B to the new Common version;
  3. release the affected parent projects.

But the important part is that the process is now predictable.

There are no hidden cross-parent version assumptions. There is no module in the wrong reactor. There is no need for someone to remember a private list of release incantations.

That is usually the right trade-off. Explicit and slightly repetitive beats magical and fragile.

Coming up in Part 2

The next question is what happens when Common needs to break compatibility.

Bumping Common from 1.x to 2.0.0 means Parent A and Parent B may need to coordinate. Worse, tools like mvn versions:use-latest-versions can happily do the wrong thing if you let them upgrade everything blindly.

Part 2 will cover BOMs as a way to make that coordination explicit, and the release:branch pattern for cases where you genuinely need to maintain two major versions of a shared library at the same time.