Maven CI-Friendly Versions
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Apache Maven 3.10.0. Every example was run on JDK 26 on 2026-10-16 in a scratch workspace that mimics the
tidepool build: a parent POM with the modules tidepool-model and tidepool-engine. Artifacts were deployed only to file://
directories, with a scratch local repository so that nothing touched ~/.m2, and the scratch git repository used for the tag example has
no remote. Where Maven 3.9.16 or Maven 4.0.0-rc-7 (a release candidate) was run too, the text says so. Paths were shortened to $W,
and the lines Loaded N auto-discovered prefixes ... that Maven 3.10 prints were left out.
A normal Maven build has a version written in the POM, and every release means editing it, committing, tagging and editing it again, which is what the release plugin automates (see Releasing). CI-friendly versions take another route: the version in the POM is a placeholder, and the pipeline that builds the project supplies the real value on the command line, for example from a git tag. Since the placeholder is resolved at build time, no commit is needed to change the version. The three placeholders are only names of properties, so they use the property mechanics described on POM Fundamentals, and the version rules they feed are on Versions, SNAPSHOTs & Ranges.
The three properties
Since Maven 3.5.0 (see Versions & Release History)
the properties ${revision}, ${sha1} and ${changelist} may be used in the version element of a POM, including the version of
the parent reference and the version of the project itself. They are special only because Maven allows them there: a version otherwise
has to be a constant, and a POM that puts any other expression in version gets a warning, shown below. The names suggest a division
of the version into parts that a pipeline can set separately:
| Property | Intended use | Typical value |
|---|---|---|
|
the release number, such as the version of the next release |
|
|
the commit (the short hash), when every build must be traceable |
|
|
the qualifier that tells a release from a development build |
|
The three can be used alone or concatenated in the version, as ${revision}${sha1}${changelist}. The properties of the version that is
not needed are left empty, with an empty element such as <sha1/>. The value is built from them as plain text, so the leading dash
of -SNAPSHOT or of -6275f0f belongs to the property value, not to the version template.
A single project
The version of a project is ${revision}, and the default value is a property of the POM:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-single</artifactId>
<version>${revision}</version>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<maven.compiler.release>21</maven.compiler.release>
</properties>
</project>
The property can be overridden with -D, because a user property takes precedence over a property of the POM:
$ mvn package
[INFO] Building tidepool-single 1.0.0-SNAPSHOT
[INFO] Building jar: $W/s/target/tidepool-single-1.0.0-SNAPSHOT.jar
$ mvn -Drevision=2.0.0-SNAPSHOT package
[INFO] Building tidepool-single 2.0.0-SNAPSHOT
[INFO] Building jar: $W/s/target/tidepool-single-2.0.0-SNAPSHOT.jar
(only the lines that name the version are shown.) The value 1.0.0-SNAPSHOT in the POM is a convenience for developers who just run
mvn package. The pipeline always passes its own. For a project that needs a finer division, the three properties are concatenated:
<version>${revision}${sha1}${changelist}</version>
...
<properties>
<revision>1.0.0</revision>
<changelist>-SNAPSHOT</changelist>
<sha1/>
</properties>
and the command line chooses what is a release and what is a snapshot. These are the file names of the jars built in the multi-module tidepool project below, with these five commands:
$ mvn package
tidepool-model-1.0.0-SNAPSHOT.jar
$ mvn -Drevision=1.2.0 package
tidepool-model-1.2.0-SNAPSHOT.jar
$ mvn -Drevision=1.2.0 -Dchangelist= package
tidepool-model-1.2.0.jar
$ mvn -Dsha1=-abc1234 -Dchangelist= package
tidepool-model-1.0.0-abc1234.jar
$ mvn -Drevision=2.7.8 -Dsha1=-abc1234 package
tidepool-model-2.7.8-abc1234-SNAPSHOT.jar
-Dchangelist= sets the property to the empty string, which is how a release build removes the -SNAPSHOT qualifier. The command-line
arguments can also be put in a file, .mvn/maven.config in the root of the project, one argument per line:
-Drevision=1.3.0
-Dchangelist=-SNAPSHOT
(see Configuring Maven). With that file, mvn clean package built
tidepool-model-1.3.0-SNAPSHOT.jar, and the extra argument -Dchangelist= on the command line, which comes after the file’s content,
built tidepool-model-1.3.0.jar.
Only these names work
The guide states that only the properties revision, sha1 and changelist can be used, and not other names. The check was to use
${tide.version} as the version of the parent and of both modules in the multi-module POM. Maven 3.10.0 still built it, but each
POM was reported with a warning:
[WARNING] Some problems were encountered while building the effective model for io.tidepool:tidepool-model:jar:1.0.0
[WARNING] 'version' contains an expression but should be a constant. @ io.tidepool:tidepool-parent:${tide.version}, $W/bad2/pom.xml, line 5, column 12
The warning means "this is not a supported setup": the build may work, and the published POM is not usable (see
Install and deploy: why Maven 3 needs the flatten plugin). Use the three names. The guide gives a second example of what not to do: a
property that is built from another one, such as <revision>1.0.0-${buildNumber}-SNAPSHOT</revision>, does not work as expected, so
combine the three standard properties instead.
A multi-module build
In a multi-module build (see Multi-Module Builds & the Reactor
and Inheritance, Aggregation & the Effective POM)
the parent declares the property, and every module repeats the placeholder in its parent reference:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}${sha1}${changelist}</version>
<packaging>pom</packaging>
<properties>
<revision>1.0.0</revision>
<changelist>-SNAPSHOT</changelist>
<sha1/>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<modules>
<module>tidepool-model</module>
<module>tidepool-engine</module>
</modules>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}${sha1}${changelist}</version>
</parent>
<artifactId>tidepool-model</artifactId>
</project>
A module does not declare its own version; it inherits the parent’s. The module tidepool-engine depends on tidepool-model, and the
version there must be ${project.version}, the usual way of keeping modules in step:
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
The guide warns that ${revision} instead of ${project.version} fails the build. That is true when the three properties are
combined in the version, as here, because ${revision} alone is 1.0.0, not 1.0.0-SNAPSHOT: the module tidepool-model is then looked
for in the repositories, with this real result on Maven 3.10.0:
[WARNING] The POM for io.tidepool:tidepool-model:jar:1.0.0 is missing, no dependency information available
[ERROR] Failed to execute goal on project tidepool-engine: Could not resolve dependencies for project io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT
[ERROR] dependency: io.tidepool:tidepool-model:jar:1.0.0 (compile)
[ERROR] prefixes: Path io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar NOT allowed from central
With ${project.version}, the reactor sorted the modules and built them in order, for every value of the properties above. The
prefixes line comes from the repository prefix filtering that Maven 3.10 does for Maven Central (see
Maven Resolver).
Install and deploy: why Maven 3 needs the flatten plugin
Building and testing with a placeholder version is enough for a pipeline that never publishes. As soon as the artifacts are installed or
deployed, the problem appears: Maven copies the file pom.xml as it is, so the repository holds a POM whose parent version is still the
text ${revision}${sha1}${changelist}, and nobody can resolve it. The multi-module project was deployed with
mvn -Drevision=1.2.0 -Dchangelist= deploy -DaltDeploymentRepository=local::file://$W/repo-noflat, and the deployed POM of
tidepool-engine read:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}${sha1}${changelist}</version>
</parent>
<artifactId>tidepool-engine</artifactId>
...
A consumer that depends on io.tidepool:tidepool-engine:1.2.0 from that repository failed on Maven 3.9.16, 3.10.0 and 4.0.0-rc-7 alike:
[ERROR] Failed to read artifact descriptor for io.tidepool:tidepool-engine:jar:1.2.0
[ERROR] Caused by: The following artifacts could not be resolved: io.tidepool:tidepool-parent:pom:${revision}${sha1}${changelist} (absent): Could not find artifact io.tidepool:tidepool-parent:pom:${revision}${sha1}${changelist} in tide (file://$W/repo-noflat)
(on Maven 4.0.0-rc-7 the same cause is followed by the list of repositories that were asked; Maven 3.9.16 failed at the same step with
Failed to read artifact descriptor.) The jars are correct, and the metadata is wrong.
The cure on Maven 3 is the flatten-maven-plugin of MojoHaus. It writes a copy of the POM in which the placeholders are replaced by their values, and makes that copy the POM that is installed and deployed. The plugin is declared once, in the parent. This is the configuration of the guide with the current plugin version (1.8.0 here):
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.8.0</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten.clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
With updatePomFile, the generated flattened POM is set as the POM file of the project, which is what install and deploy then publish.
flattenMode says how much to flatten. resolveCiFriendliesOnly only resolves the variables revision, sha1 and changelist and keeps
everything else, which is what the CI-friendly setup needs; the other modes (oss, ossrh, bom, defaults, clean and fatjar) also
remove optional elements of the POM, and are meant for publishing a trimmed POM. By default the plugin replaces the POM only for
projects whose packaging is not pom, so updatePomFile is set to true here to flatten the pom packaging parent as well. Since plugin
version 1.8.0 the parameter revisionVariableName (user property flatten.revision.variable.name) names the variable that is used as
the version in this mode; it defaults to revision. The flatten.clean execution removes the generated file, .flattened-pom.xml,
on mvn clean. That file is created in every module, including the root, so add it to .gitignore.
The log of mvn -Drevision=1.2.0 -Dchangelist= clean deploy ... with the plugin (a selection of its lines):
[INFO] --- flatten:1.8.0:clean (flatten.clean) @ tidepool-parent ---
[INFO] --- flatten:1.8.0:flatten (flatten) @ tidepool-parent ---
[INFO] Generating flattened POM of project io.tidepool:tidepool-parent:pom:1.2.0...
[INFO] Installing $W/mmf/.flattened-pom.xml to $W/m2-flat/io/tidepool/tidepool-parent/1.2.0/tidepool-parent-1.2.0.pom
[INFO] --- flatten:1.8.0:flatten (flatten) @ tidepool-model ---
[INFO] Generating flattened POM of project io.tidepool:tidepool-model:jar:1.2.0...
[INFO] Installing $W/mmf/tidepool-model/.flattened-pom.xml to $W/m2-flat/io/tidepool/tidepool-model/1.2.0/tidepool-model-1.2.0.pom
[INFO] --- flatten:1.8.0:flatten (flatten) @ tidepool-engine ---
[INFO] Generating flattened POM of project io.tidepool:tidepool-engine:jar:1.2.0...
[INFO] Installing $W/mmf/tidepool-engine/.flattened-pom.xml to $W/m2-flat/io/tidepool/tidepool-engine/1.2.0/tidepool-engine-1.2.0.pom
and the deployed POM of tidepool-engine became:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.2.0</version>
</parent>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>
The parent version is now the constant 1.2.0, and ${project.version} in the dependency is still an expression, which is correct:
project.version is a property that every Maven resolves from the POM it reads, not a placeholder of the build. A consumer on Maven 3.9.16,
3.10.0 and 4.0.0-rc-7 resolved tidepool-engine and tidepool-model at 1.2.0 from this repository. The deployed parent POM kept the
property <revision>1.0.0</revision> from the source, which is harmless because nothing uses it any more, but it is misleading to a reader.
The deployed POM of tidepool-engine from Maven 3.9.16 was identical.
Maven 4
Maven 4 (release candidate) takes the flattening of CI-friendly versions into the build itself, together with two other changes. With model
version 4.1.0 a module does not need to say the version of its parent, nor the version of a dependency on another module of the same build
(automatic version resolution), and the modules are called subprojects (see
Maven 4: POM Model & Consumer POM). The same tidepool
project, without the flatten plugin:
<project xmlns="http://maven.apache.org/POM/4.1.0" root="true">
<modelVersion>4.1.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}${changelist}</version>
<packaging>pom</packaging>
<properties>
<revision>1.0.0</revision>
<changelist>-SNAPSHOT</changelist>
<maven.compiler.release>21</maven.compiler.release>
</properties>
<subprojects>
<subproject>tidepool-model</subproject>
<subproject>tidepool-engine</subproject>
</subprojects>
</project>
<project xmlns="http://maven.apache.org/POM/4.1.0">
<modelVersion>4.1.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
</parent>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
</dependency>
</dependencies>
</project>
mvn -Drevision=1.2.0 -Dchangelist= deploy on Maven 4.0.0-rc-7 deployed, for each module, the jar and two POMs: tidepool-engine-1.2.0.pom
(the consumer POM, which is what other builds read) and tidepool-engine-1.2.0-build.pom (the build POM, a copy of the POM as
written, with the model version 4.1.0 and without the versions). The consumer POM had no placeholder left, with a model version that a Maven 3 client understands:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.2.0</version>
</parent>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
</project>
The deployed parent POM also had its properties set to the values of the build (<revision>1.2.0</revision> and an empty <changelist>).
A consumer on Maven 3.10.0 and on 4.0.0-rc-7 resolved tidepool-engine:1.2.0 and tidepool-model:1.2.0 from this repository.
Maven 4.0.0-rc-7 did the same for the Maven 3 style POMs of the previous section (model version 4.0.0, ${project.version} in the
dependency, no flatten plugin): mvn -Drevision=1.2.0 -Dchangelist= deploy deployed a tidepool-engine-1.2.0.pom whose parent version was
1.2.0, and the dependency kept the expression ${project.version}. So the flatten plugin can be dropped as soon as the build runs
on Maven 4, even before the POMs move to model version 4.1.0. The details of the consumer POM and the
other model changes are on
Maven 4: POM Model & Consumer POM, and a path from
Maven 3 to 4 is on Migrating to Maven 4.
Pairing with tag-driven releases
The reason for the three properties is a pipeline that builds a release from a tag, with no release commit. Here is a small script (it is the tidepool one, run in a scratch git repository without a remote): the tag decides what is built.
#!/bin/sh
# Tag-driven build: a tag v1.4.0 builds release 1.4.0, anything else builds a snapshot.
set -e
if TAG=$(git describe --tags --exact-match 2>/dev/null); then
ARGS="-Drevision=${TAG#v} -Dchangelist="
else
ARGS="-Dsha1=-$(git rev-parse --short HEAD)"
fi
echo "arguments: $ARGS"
mvn -B $ARGS "$@"
With the commit tagged v1.4.0, ./release.sh clean deploy ... printed arguments: -Drevision=1.4.0 -Dchangelist=, built
tidepool-model-1.4.0.jar and tidepool-engine-1.4.0.jar, and deployed them to a local file:// repository whose directory was
io/tidepool/tidepool-model/1.4.0. After an untagged commit, ./release.sh clean package printed arguments: -Dsha1=-6275f0f and built
tidepool-model-1.0.0-6275f0f-SNAPSHOT.jar, a snapshot that names the commit it came from. (The hash in your run is different.) The script is this page’s own lab setup, not an example from the Maven documentation.
The consequences of this style:
-
The version of a released artifact is the tag, and nothing else needs to be committed; the POM keeps showing the development default.
-
The default in the POM is not a released version. Developers see
1.0.0-SNAPSHOTin their IDE, which can differ from the last tag, and they have to know that the pipeline passes the actual value. Without thesha1, two snapshot builds of different commits have the same name. -
Everything that publishes needs the flatten plugin on Maven 3 or the consumer POM of Maven 4, as shown above.
-
It is an alternative to the release plugin, which works by editing the version in the POM and committing; a project normally chooses one of the two approaches (see Releasing).
-
A published release should be built once and promoted, not rebuilt, so that the deployed bytes are the tested bytes (see Publishing to Maven Central and Security & Reproducible Builds).
The same idea in Ant and Gradle
Ant has no versions of artifacts in its build file; a version is a property such as version that can be set on the command line with
-Dversion=1.4.0, and there is no consumer POM to repair (see Ant Properties).
Gradle reads the version from the build script or from gradle.properties, and a project property such as -Pversion=1.4.0 can supply
it when the script does not set it, with the published metadata always written from the resolved value
(see Publishing). The mapping is on
Repositories & Publishing across Tools.
Summary
-
${revision},${sha1}and${changelist}may be used in a POM version since Maven 3.5.0; no other property name is supported, and other expressions inversionare warned about. -
Give them defaults in the parent’s
propertiesand override them with-Dor.mvn/maven.config;-Dchangelist=drops-SNAPSHOT. -
Between modules, use
${project.version}, never${revision}when the properties are combined. -
On Maven 3,
installanddeploypublish an unresolvable POM unless the flatten-maven-plugin runs withresolveCiFriendliesOnly; the failing consumer message isCould not find artifact io.tidepool:tidepool-parent:pom:${revision}${sha1}${changelist}. -
On Maven 4 (release candidate), the consumer POM has the values filled in, and with model 4.1.0 parent and module versions can be omitted.
-
The pattern fits tag-driven pipelines, and is an alternative to the release plugin.