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

${revision}

the release number, such as the version of the next release

1.3.1

${sha1}

the commit (the short hash), when every build must be traceable

-6275f0f or empty

${changelist}

the qualifier that tells a release from a development build

-SNAPSHOT or empty

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.

flowchart LR A["-Drevision=1.4.0"] --> V["version = revision + sha1 + changelist"] B["-Dsha1=-6275f0f"] --> V C["-Dchangelist="] --> V V --> R["release: 1.4.0"] V --> S["snapshot: 1.0.0-6275f0f-SNAPSHOT"]

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-SNAPSHOT in their IDE, which can differ from the last tag, and they have to know that the pipeline passes the actual value. Without the sha1, 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 in version are warned about.

  • Give them defaults in the parent’s properties and override them with -D or .mvn/maven.config; -Dchangelist= drops -SNAPSHOT.

  • Between modules, use ${project.version}, never ${revision} when the properties are combined.

  • On Maven 3, install and deploy publish an unresolvable POM unless the flatten-maven-plugin runs with resolveCiFriendliesOnly; the failing consumer message is Could 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.