Releasing
|
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 with the Apache Maven Release Plugin 3.3.1, the Deploy Plugin 3.2.0 and the Versions
Maven Plugin 2.23.0 (a MojoHaus plugin), and Git 2.54. Every example marked as run was executed on JDK 26 on 2026-10-16. Maven 3.9.16
and Maven 4.0.0-rc-7 (a release candidate) were run where the page says so. A release writes to a source repository, so every
run was made against a scratch Git repository that has a local bare repository as its "remote" (scm:git:file://<lab>/remote.git,
where <lab> is the scratch directory), a throwaway Git identity set through a private GIT_CONFIG_GLOBAL file (so the user’s own
Git configuration was not touched), and a file: repository for the deploy. Nothing was pushed to a real remote and nothing was
published anywhere. In the output, <lab> stands for the scratch directory, commit and tag object ids are the ones of the run, the
nested build that the plugin forks is shown only where said, and lines were filtered with grep (said each time). Publishing to
Maven Central is on
Publishing to Maven Central and is "not run here".
A release is the moment a version stops changing. The 1.0.0-SNAPSHOT that everybody has been building becomes 1.0.0, which is
tagged in the source repository, built once from that tag, and deployed to a repository where it must never change again; the
development line moves on to 1.1.0-SNAPSHOT. There are two common ways to do it with Maven. The Release Plugin automates the
sequence by editing the version in the POMs and committing; the tag-driven way keeps the version out of the commits and lets a
pipeline build whatever a tag names. This page runs both. The deploy that ends a release is on
Install, Deploy & Distribution Management;
the version syntax is on
Versions, Snapshots & Ranges; the scenario is on the
section landing page.
The Release Plugin in two steps
The plugin splits a release into two goals so that a failure in the second does not undo the first.
-
release:preparemakes the source ready: it checks the working copy, changes the version, builds and tests, commits, tags, moves to the next snapshot and commits again. -
release:performmakes the binaries: it checks out the tag thatpreparecreated into a clean directory and runs a deploy there, so what is published was built from the tagged source and not from whatever was lying in the developer’s working copy.
The diagram shows what each goal does and where it talks to the outside: only prepare pushes to the source repository, only
perform uploads to the artifact repository.
The lab
The lab is the tidepool parent with three modules, the distributionManagement of the deploy page (the two repositories are
directories below -Drepo.url), and these additions to the parent POM. The plugin is pinned in pluginManagement, because the 3.10 super POM
no longer manages maven-release-plugin (the 3.9.x super POM pinned it to 3.0.1):
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<scm>
<connection>scm:git:file://<lab>/remote.git</connection>
<developerConnection>scm:git:file://<lab>/remote.git</developerConnection>
<tag>HEAD</tag>
</scm>
<build>
<pluginManagement>
<plugins>
<plugin><artifactId>maven-release-plugin</artifactId><version>3.3.1</version></plugin>
<!-- clean, resources, compiler, surefire, jar, install, deploy as before -->
</plugins>
</pluginManagement>
</build>
and a .gitignore with the files the plugin creates: target/, build/, release.properties, pom.xml.releaseBackup and pom.xml.tag.
The project was committed to the working repository (git init), a bare repository was created beside it (git init --bare), and the
working copy pushed main to it. The URL is a file: URL because it is the form that works for a local bare repository; real
projects use https: or ssh: URLs (not run here). This POM is this page’s own lab setup, not an example from the Maven documentation.
The scm section
The <scm> section tells the plugin, and humans, where the source is:
| Element | Meaning |
|---|---|
|
The URL for reading the source, in the Maven SCM form |
|
The URL for writing: by convention the one with credentials. In the lab both elements held the same |
|
A web URL for browsing the source. |
|
The tag or branch the POM was built from. The convention is |
The plugin works through Apache Maven SCM, which has providers for Git, Subversion, Mercurial and others; the Git provider shells out to the
git command, which must therefore be installed and configured (user, signing).
A trap with multi-module projects is that the children inherit the scm URLs with their artifact id appended to the path, which suits
a repository with one directory per module. For a Git repository, where the whole multi-module project lives in one repository, the
inherited URL is wrong. help:evaluate on the child:
scm:git:file://<lab>/remote.git/tidepool-model
The attributes child.scm.connection.inherit.append.path="false" and child.scm.developerConnection.inherit.append.path="false" on the
<scm> element turn it off, and the child got the right URL:
<scm child.scm.connection.inherit.append.path="false" child.scm.developerConnection.inherit.append.path="false">
scm:git:file://<lab>/remote.git
Source: Maven: POM Reference, SCM
Source: Apache Maven SCM: Git
release:prepare
A dry run first
mvn -B release:prepare -DdryRun=true runs every phase without committing or tagging: it modifies nothing in the source
repository and writes the modified POMs beside the originals. The plugin composes prepare of 17 phases, in this order. The output
(grep for the numbered phase lines, the "Full run would" lines and the build result; the build the plugin forks for
clean verify is not shown) was:
[INFO] starting prepare goal in dry-run mode, composed of 17 phases: [...]
[INFO] 1/17 prepare:check-poms dry-run
[INFO] 2/17 prepare:scm-check-modifications dry-run
[INFO] 3/17 prepare:check-dependency-snapshots dry-run
[INFO] 4/17 prepare:create-backup-poms dry-run
[INFO] 5/17 prepare:map-release-versions dry-run
[INFO] 6/17 prepare:input-variables dry-run
[INFO] 7/17 prepare:map-development-versions dry-run
[INFO] 8/17 prepare:rewrite-poms-for-release dry-run
[INFO] 9/17 prepare:generate-release-poms dry-run
[INFO] 10/17 prepare:run-preparation-goals dry-run
[INFO] Executing preparation goals - since this is simulation mode it is running against the original project, not the rewritten ones
[INFO] Executing goals 'clean verify'...
[INFO] 11/17 prepare:scm-commit-release dry-run
[INFO] Full run would be commit 4 files with message: '[maven-release-plugin] prepare release tidepool-parent-1.0.0'
[INFO] 12/17 prepare:scm-tag dry-run
[INFO] Full run would tag working copy '<lab>/rl'
[INFO] 13/17 prepare:rewrite-poms-for-development dry-run
[INFO] 14/17 prepare:remove-release-poms dry-run
[INFO] 15/17 prepare:run-completion-goals dry-run
[INFO] Executing completion goals - since this is simulation mode it is running against the original project, not the rewritten ones
[INFO] 16/17 prepare:scm-commit-development dry-run
[INFO] Full run would commit 4 files with message: '[maven-release-plugin] prepare for next development iteration'
[INFO] 17/17 prepare:end-release dry-run
[INFO] Release preparation simulation complete.
[INFO] BUILD SUCCESS
What each phase does, from the numbered lines above, the plugin’s usage page and the runs on this page:
| # | Phase | What happens |
|---|---|---|
1 |
|
The POMs are checked for what a release needs, such as the |
2 |
|
The working copy must have no local changes. The plugin ignores its own files: the log says |
3 |
|
No dependency, plugin, report or extension may be a snapshot. |
4 |
|
Each |
5-7 |
|
The release version, the tag and the next development version are decided, from the command line, |
8 |
|
The POMs are rewritten with the release version (in a dry run, to |
9 |
|
Only for |
10 |
|
The |
11 |
|
The release POMs are committed and, by default, pushed. |
12 |
|
The tag is created (and pushed). |
13 |
|
The POMs are rewritten with the next snapshot version. |
14 |
|
Removes the release POMs that |
15 |
|
The |
16 |
|
The development POMs are committed and pushed. |
17 |
|
|
After the dry run the working directory had these files, and Git saw only the .next copies (the others are in the .gitignore):
data
pom.xml
pom.xml.next
pom.xml.releaseBackup
pom.xml.tag
release.properties
tidepool-cli
tidepool-engine
tidepool-model
?? pom.xml.next
?? tidepool-cli/pom.xml.next
?? tidepool-engine/pom.xml.next
?? tidepool-model/pom.xml.next
pom.xml.tag is the POM that would be tagged. Its differences from pom.xml (git diff --no-index --unified=1, header removed):
@@ -1,5 +1,3 @@
<?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">
+<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>
@@ -8,3 +6,3 @@
<artifactId>tidepool-parent</artifactId>
- <version>1.0.0-SNAPSHOT</version>
+ <version>1.0.0</version>
<packaging>pom</packaging>
@@ -20,3 +18,3 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.build.outputTimestamp>2026-10-16T00:00:00Z</project.build.outputTimestamp>
+ <project.build.outputTimestamp>2026-10-16T14:33:21Z</project.build.outputTimestamp>
<junit.version>6.1.3</junit.version>
@@ -59,3 +57,3 @@
<developerConnection>scm:git:file://<lab>/remote.git</developerConnection>
- <tag>HEAD</tag>
+ <tag>tidepool-parent-1.0.0</tag>
</scm>
Four things are in it. The version is the release version. The <tag> of the scm section holds the name of the tag. The
property project.build.outputTimestamp (the reproducible-build timestamp that the archive plugins write into entries, see
Security & Reproducible Builds and the 3.10 notes on
Versions & Release History) was updated to the time of the
release run (here 2026-10-16T14:33:21Z), where it had been fixed at midnight in the POM. And the
root <project> element, which was on four lines, was written on one: the plugin re-serializes the POM, so a release commit contains
formatting noise as well as the version change (shown below).
release.properties is the memory of the run. The lines that matter (grep):
completedPhase=end-release
preparationGoals=clean verify
project.dev.io.tidepool\:tidepool-parent=1.0.1-SNAPSHOT
project.rel.io.tidepool\:tidepool-parent=1.0.0
projectVersionPolicyId=default
pushChanges=true
remoteTagging=true
scm.developmentCommitComment=@{prefix} prepare for next development iteration
scm.releaseCommitComment=@{prefix} prepare release @{releaseLabel}
scm.tag=tidepool-parent-1.0.0
scm.tagNameFormat=@{project.artifactId}-@{project.version}
It records the versions decided so far, the tag, the commit messages, and the phase that was last completed (completedPhase). The
file is what lets an interrupted release resume, and what release:perform reads. mvn release:clean removes the dry run’s files:
[INFO] --- release:3.3.1:clean (default-cli) @ tidepool-parent ---
[INFO] BUILD SUCCESS
Choosing the versions
Without options the plugin asks three questions, with defaults from the version policy (it drops -SNAPSHOT and then increments the last
number: 1.0.0 and 1.0.1-SNAPSHOT for the lab). Run without -B and with its standard input fed from a pipe, the plugin printed these
questions and took the defaults (the answers I piped in were not used, so no interactive answer is shown on this page):
What is the release version for "tidepool-parent"? (tidepool-parent) 1.0.0:
What is the SCM release tag or label for "tidepool-parent"? (tidepool-parent) tidepool-parent-1.0.0:
What is the new development version for "tidepool-parent"? (tidepool-parent) 1.0.1-SNAPSHOT:
In a pipeline, nobody can answer, so use batch mode (-B) and give the values on the command line: -DreleaseVersion, -DdevelopmentVersion,
-Dtag for the exact tag, or -DtagNameFormat for the pattern. The pattern uses @{...} and not ${...}, so that Maven does not
interpolate it before the plugin has the release version. The default is @{project.artifactId}-@{project.version}, which gave
tidepool-parent-1.0.0; most teams prefer v@{project.version}. For a multi-module project each module is asked its own version unless
-DautoVersionSubmodules=true. The same options can live in the POM:
<plugin>
<artifactId>maven-release-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<tagNameFormat>v@{project.version}</tagNameFormat>
<autoVersionSubmodules>true</autoVersionSubmodules>
<scmCommentPrefix>[release] </scmCommentPrefix>
<preparationGoals>clean verify</preparationGoals>
<goals>deploy</goals>
<arguments>-DskipTests</arguments>
</configuration>
</plugin>
A dry run with that configuration (grep for the lines that show the effect) printed:
[INFO] Executing goals 'clean verify'...
[INFO] with additional arguments: -DskipTests
[INFO] Full run would be commit 4 files with message: '[release] prepare release v1.0.0'
[INFO] Full run would tag working copy '<lab>/t_cfg'
[INFO] with label 'v1.0.0'
[INFO] Full run would commit 4 files with message: '[release] prepare for next development iteration'
[INFO] BUILD SUCCESS
The commit messages carry the scmCommentPrefix, the tag follows the pattern, and the extra arguments reach the forked build.
The real run
The release of the lab (mvn -B release:prepare -DreleaseVersion=1.0.0 -DdevelopmentVersion=1.1.0-SNAPSHOT -DtagNameFormat=v@{project.version}) printed, after grep for the phase lines and results:
[INFO] 1/17 prepare:check-poms
[INFO] 2/17 prepare:scm-check-modifications
[INFO] 3/17 prepare:check-dependency-snapshots
[INFO] 4/17 prepare:create-backup-poms
[INFO] 5/17 prepare:map-release-versions
[INFO] 6/17 prepare:input-variables
[INFO] 7/17 prepare:map-development-versions
[INFO] 8/17 prepare:rewrite-poms-for-release
[INFO] 9/17 prepare:generate-release-poms
[INFO] 10/17 prepare:run-preparation-goals
[INFO] Executing goals 'clean verify'...
[INFO] 11/17 prepare:scm-commit-release
[INFO] Checking in modified POMs...
[INFO] 12/17 prepare:scm-tag
[INFO] Tagging release with the label v1.0.0...
[INFO] 13/17 prepare:rewrite-poms-for-development
[INFO] 14/17 prepare:remove-release-poms
[INFO] 15/17 prepare:run-completion-goals
[INFO] 16/17 prepare:scm-commit-development
[INFO] Checking in modified POMs...
[INFO] 17/17 prepare:end-release
[INFO] Release preparation complete.
[INFO] BUILD SUCCESS
The working copy had two new commits and a tag (git log --oneline --decorate), and the remote had the same (git ls-remote origin,
tag objects included):
3dc3f5b (HEAD -> main) [maven-release-plugin] prepare for next development iteration
a3c3e3d (tag: v1.0.0) [maven-release-plugin] prepare release v1.0.0
5631a64 (origin/main) tidepool 1.0.0-SNAPSHOT
3dc3f5b75b57e64cbd95a1cfda65002d76f71e11 HEAD
3dc3f5b75b57e64cbd95a1cfda65002d76f71e11 refs/heads/main
7dbc61a156781de9deab4aba7ab5b8ef37cdc9b5 refs/tags/v1.0.0
a3c3e3db345af7e0b9d20693ac7e4731e5b4fead refs/tags/v1.0.0^{}
main of the remote moved to the second commit, and the tag exists as an annotated tag (refs/tags/v1.0.0, with ^{} the commit it points to).
The tag object:
tag v1.0.0
tagger <name> <email> <time>
[maven-release-plugin] copy for tag v1.0.0
The two commits (git show --stat) touched all four POMs; the release commit is bigger than the development one because it re-serializes the
POMs:
a3c3e3d [maven-release-plugin] prepare release v1.0.0
pom.xml | 10 ++++------
tidepool-cli/pom.xml | 6 ++----
tidepool-engine/pom.xml | 6 ++----
tidepool-model/pom.xml | 6 ++----
4 files changed, 10 insertions(+), 18 deletions(-)
3dc3f5b [maven-release-plugin] prepare for next development iteration
pom.xml | 6 +++---
tidepool-cli/pom.xml | 2 +-
tidepool-engine/pom.xml | 2 +-
tidepool-model/pom.xml | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
The POMs at the tagged commit (git show v1.0.0^{commit}:pom.xml, parent abridged) have the release version, a fixed reproducible timestamp,
and a <tag> that names the tag:
<?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>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.0.0</version>
<scm>
<connection>scm:git:file://<lab>/remote.git</connection>
<developerConnection>scm:git:file://<lab>/remote.git</developerConnection>
<tag>v1.0.0</tag>
</scm>
and a module at the tag points at the released parent:
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>tidepool-engine</artifactId>
The development commit moved on to the next snapshot and put HEAD back (git diff between the two commits, changed lines only):
- <version>1.0.0</version>
+ <version>1.1.0-SNAPSHOT</version>
- <project.build.outputTimestamp>2026-10-16T14:33:26Z</project.build.outputTimestamp>
+ <project.build.outputTimestamp>2026-10-16T14:33:28Z</project.build.outputTimestamp>
- <tag>v1.0.0</tag>
+ <tag>HEAD</tag>
After prepare the tree is clean for Git, and release.properties and pom.xml.releaseBackup are left for the next goal:
data
pom.xml
pom.xml.releaseBackup
release.properties
tidepool-cli
tidepool-engine
tidepool-model
Because pushChanges defaults to true and remoteTagging to true, the commits and the tag were pushed by prepare itself; with -DpushChanges=false
they stay local until somebody pushes (not run here).
The parameters that matter
These come from help:describe -Dplugin=org.apache.maven.plugins:maven-release-plugin:3.3.1 -Ddetail.
| Parameter | User property | Meaning |
|---|---|---|
|
the same |
The versions to use; otherwise the version policy proposes them. |
|
|
The exact tag, or the pattern (default |
|
|
Give every module the version of the parent without asking (default false). |
|
|
Do not commit or tag; write the modified POMs beside the originals. |
|
|
Resume the previous attempt from |
|
the same |
Goals (default |
|
|
Arguments for the forked Maven builds (see |
|
the same |
Push the commits (default true); create the tag in the remote (default true). |
|
the same |
The commit messages ( |
|
|
Sign the tag. Not run here. |
|
the same |
A version policy (default |
|
|
Resolve snapshot dependencies automatically (all, dependencies, plugins, reports, extensions). |
|
|
Only the goal |
|
|
More files to ignore in the modification check. |
|
|
Allow timestamped snapshot dependencies (default false). |
release:perform
perform reads release.properties, checks out the tag into target/checkout and runs deploy there in a forked Maven. In a real release
the deploy repository comes from the distributionManagement in the tagged POM and the credentials from settings.xml. The lab has to
tell the forked build where its repository is, and this is the classic stumbling block: a -D option on the outer command line does not reach the
forked build. They go in the arguments parameter. To see it, the same release was performed twice more, once with no option, once with
-Drepo.url=file://<lab>/repo_pa_b on the outer command line only. Both uploaded into the checkout, where the POM’s own default points
(file://${maven.multiModuleProjectDirectory}/build/repo in the checkout), and where the next mvn clean or a new checkout deletes them:
[INFO] Uploaded to tidepool-releases: file://<lab>/t_pa_a/target/checkout/build/repo/releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
[INFO] BUILD SUCCESS
[INFO] Uploaded to tidepool-releases: file://<lab>/t_pa_b/target/checkout/build/repo/releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
[INFO] BUILD SUCCESS
The right form passes the option to the fork:
mvn -B release:perform -Darguments="-Drepo.url=file://<lab>/repo"
The output of that run (filtered with grep for the plugin’s lines):
[INFO] starting perform goal, composed of 3 phases: [...]
[INFO] 1/3 perform:verify-completed-prepare-phases
[INFO] 2/3 perform:checkout-project-from-scm
[INFO] Checking out the project to perform the release ...
[INFO] 3/3 perform:run-perform-goals
[INFO] Invoking perform goals in directory <lab>/rl/target/checkout
[INFO] Executing goals 'deploy'...
[INFO] BUILD SUCCESS
and the nested build’s deploy lines (grep -E "deploy:|Uploaded to .*(jar|pom) "):
[INFO] --- deploy:3.2.0:deploy
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom
[INFO] --- deploy:3.2.0:deploy
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
[INFO] --- deploy:3.2.0:deploy
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
[INFO] --- deploy:3.2.0:deploy
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-cli/1.0.0/tidepool-cli-1.0.0.pom
[INFO] Uploaded to tidepool-releases: file://<lab>/repo/releases/io/tidepool/tidepool-cli/1.0.0/tidepool-cli-1.0.0.jar
The repository contained every module at 1.0.0 and nothing at 1.0.0-SNAPSHOT (find . -type f without the checksum files):
./releases/io/tidepool/tidepool-cli/1.0.0/tidepool-cli-1.0.0.jar
./releases/io/tidepool/tidepool-cli/1.0.0/tidepool-cli-1.0.0.pom
./releases/io/tidepool/tidepool-cli/maven-metadata.xml
./releases/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
./releases/io/tidepool/tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
./releases/io/tidepool/tidepool-engine/maven-metadata.xml
./releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
./releases/io/tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.pom
./releases/io/tidepool/tidepool-model/maven-metadata.xml
./releases/io/tidepool/tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom
./releases/io/tidepool/tidepool-parent/maven-metadata.xml
The working directory of the plugin, target/checkout, is a full checkout of the tag. After a successful perform the files of prepare are
removed, so a second perform has nothing to read (the working copy listing, ls and git status --short, which printed nothing):
data
pom.xml
target
tidepool-cli
tidepool-engine
tidepool-model
The release is complete when the artifacts are in the repository: the tag is in the source repository and the version is final. The deploy repository decides whether the same version can be uploaded again, and normally it refuses (see the deploy page).
The parameters of perform
| Parameter | User property | Meaning |
|---|---|---|
|
|
The goals to run in the checkout. The default is |
|
|
Arguments for the forked build, separated by spaces. |
|
|
Profiles to enable in the forked build, in addition to the active ones. |
|
|
Default false and deprecated; sets |
|
|
Where to check out, if there is no |
|
|
Use a local checkout instead of cloning; works with distributed SCMs such as Git. Not run here. |
|
|
The checkout directory (default |
|
|
Do not check out or run the goals. |
The release profile that Maven 3.10 removed
Older documentation says that a release adds source and Javadoc JARs "automatically". That worked through a profile called
release-profile in the super POM that the plugin activated by setting the property performRelease (with useReleaseProfile).
Maven 3.10 removed it. With -DperformRelease=true set, help:active-profiles on the lab (the profile list only):
--- Maven 3.9.16
The following profiles are active:
- release-profile (source: )
--- Maven 3.10.0
The following profiles are active:
--- Maven 4.0.0-rc-7
The following profiles are active:
and mvn -B -DperformRelease=true -pl tidepool-model clean package on a POM that has the Source and Javadoc plugins pinned in
pluginManagement but not bound (executions only, filtered with grep -E "^\[INFO\] --- |Building jar|BUILD" and the plugin id and execution id kept):
--- Maven 3.9.16
[INFO] --- clean (default-clean) ...
[INFO] --- resources (default-resources) ...
[INFO] --- compiler (default-compile) ...
[INFO] --- resources (default-testResources) ...
[INFO] --- compiler (default-testCompile) ...
[INFO] --- surefire (default-test) ...
[INFO] --- jar (default-jar) ...
[INFO] Building jar: tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT.jar
[INFO] --- source (attach-sources) ...
[INFO] Building jar: tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT-sources.jar
[INFO] --- javadoc (attach-javadocs) ...
[INFO] Building jar: tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT-javadoc.jar
[INFO] BUILD SUCCESS
--- Maven 3.10.0
[INFO] --- clean (default-clean) ...
[INFO] --- resources (default-resources) ...
[INFO] --- compiler (default-compile) ...
[INFO] --- resources (default-testResources) ...
[INFO] --- compiler (default-testCompile) ...
[INFO] --- surefire (default-test) ...
[INFO] --- jar (default-jar) ...
[INFO] Building jar: tidepool-model/target/tidepool-model-1.0.0-SNAPSHOT.jar
[INFO] BUILD SUCCESS
On 3.9.16 the profile ran the Source and Javadoc plugins (attach-sources, attach-javadocs, the execution ids of the profile) and built
-sources.jar and -javadoc.jar; on 3.10.0 nothing did. A release that relied on the old profile silently loses its sources and
Javadoc JARs after an upgrade to 3.10. The fix is to bind the plugins in your own POM, as on the deploy page, so that every build, not only
the releases, makes them. (Central requires both, see
Publishing to Maven Central.)
The other goals
| Goal | What it does |
|---|---|
|
Cleans up the working copy after a failed or abandoned release or a dry run (it happens by itself after a successful |
|
Restores the POMs from the backups, commits that, and removes the tag. Needs the files that |
|
Only changes the versions in the POMs (prompting or from |
|
Creates a branch from the current state, optionally with its own versions. |
|
Like |
|
Like |
rollback
After a clean prepare of 1.0.0 (-DtagNameFormat=v@{project.version}) the lab had the tag in the remote and two release commits:
<id> refs/tags/v1.0.0
6337c66 (HEAD -> main) [maven-release-plugin] prepare for next development iteration
e172fa3 (tag: v1.0.0) [maven-release-plugin] prepare release v1.0.0
5631a64 (origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
mvn -B release:rollback printed (grep):
[INFO] 1/3 rollback:restore-backup-poms
[INFO] 2/3 rollback:scm-commit-rollback
[INFO] 3/3 rollback:remove-scm-tag
[INFO] BUILD SUCCESS
and left the repository in this state:
2f09d6d (HEAD -> main) [maven-release-plugin] rollback the release of v1.0.0
6337c66 [maven-release-plugin] prepare for next development iteration
e172fa3 [maven-release-plugin] prepare release v1.0.0
5631a64 (origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
(The remote tag list is empty: the tag was removed from the remote as well. The version in the working copy was 1.0.0-SNAPSHOT again, and
the extra files were gone.)
<version>1.0.0-SNAPSHOT</version>
data
pom.xml
tidepool-cli
tidepool-engine
tidepool-model
Note what rollback does not do: it does not rewrite history. The release commit and the development commit stay, and a third commit ("rollback the release of v1.0.0") restores the old POMs. The commits were already pushed, so they stay in the remote too. Plan for that: a rolled-back release leaves three commits of noise. The usage page says that tag removal is available since 3.0.0-M1.
update-versions and branch
release:update-versions is for bumping the version without a release, for example after cutting a maintenance branch. Run in the
tag-driven clone below:
$ mvn release:update-versions -DdevelopmentVersion=1.4.0-SNAPSHOT
[INFO] 1/4 updateVersions:check-poms-updateversions
[INFO] 2/4 updateVersions:create-backup-poms
[INFO] 3/4 updateVersions:map-development-versions
[INFO] 4/4 updateVersions:rewrite-pom-versions
[INFO] BUILD SUCCESS
$ git diff --stat
pom.xml | 8 +++-----
tidepool-cli/pom.xml | 6 ++----
tidepool-engine/pom.xml | 6 ++----
tidepool-model/pom.xml | 6 ++----
4 files changed, 9 insertions(+), 17 deletions(-)
The 9 insertions and 17 deletions come from the same re-serialization of the root element as in the release commit. release:branch creates a
branch in the remote with the versions you choose, commits there, and (here with -DupdateWorkingCopyVersions=false) leaves the working copy on
the branch it started from:
$ mvn release:branch -DbranchName=release-1.3.x -DupdateBranchVersions=true -DreleaseVersion=1.3.1-SNAPSHOT -DupdateWorkingCopyVersions=false
[INFO] 1/12 branch:check-poms
[INFO] 2/12 branch:scm-check-modifications
[INFO] 3/12 branch:create-backup-poms
[INFO] 4/12 branch:map-branch-versions
[INFO] 5/12 branch:branch-input-variables
[INFO] 6/12 branch:map-development-versions
[INFO] 7/12 branch:rewrite-poms-for-branch
[INFO] 8/12 branch:scm-commit-branch
[INFO] 9/12 branch:scm-branch
[INFO] 10/12 branch:rewrite-poms-for-development
[INFO] 11/12 branch:scm-commit-development
[INFO] 12/12 branch:end-release
[INFO] BUILD SUCCESS
$ git ls-remote --heads origin
<id> refs/heads/main
<id> refs/heads/release-1.3.x
$ git show release-1.3.x:pom.xml | grep -m1 "<version>"
<version>1.3.1-SNAPSHOT</version>
When it goes wrong
A release has many parts, and the failures are usually at the seams. The messages below are those of real runs.
Local modifications
An uncommitted edit stops prepare in scm-check-modifications:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.3.1:prepare (default-cli) on project tidepool-parent: Cannot prepare the release because you have local modifications :
[ERROR] [tidepool-model/src/main/java/io/tidepool/model/Height.java:modified]
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
The release descriptor is left behind, and remembers where it stopped; after the file was reverted, git status printed nothing (empty
listing) and release.properties held:
completedPhase=check-poms
The stale release.properties and resume
Because of that file, running the goal again resumes: the next run (a dry run) printed that it resumed and skipped the phase that had completed:
[INFO] Resuming release from phase 'scm-check-modifications'
[INFO] 2/17 prepare:scm-check-modifications dry-run
[INFO] 3/17 prepare:check-dependency-snapshots dry-run
-Dresume=false (or release:clean first) starts again from phase 1:
[INFO] 1/17 prepare:check-poms dry-run
[INFO] 2/17 prepare:scm-check-modifications dry-run
The classic problem is a release.properties from an earlier, abandoned attempt: it remembers the old release version, tag name and
phase, and the new attempt silently reuses them. The rule is to run release:clean (or rollback) before starting over, and to look at
release.properties when a release does something unexpected.
Snapshot dependencies
A release must be reproducible, so every dependency must be a release too. A dependency on io.tidepool:snap-lib:2.0-SNAPSHOT (installed
into the scratch local repository with install:install-file) in tidepool-cli failed check-dependency-snapshots:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.3.1:prepare (default-cli) on project tidepool-parent: Can't release project due to non released dependencies :
[ERROR] io.tidepool:snap-lib:jar:2.0-SNAPSHOT:compile
[ERROR] in project 'tidepool-cli' (io.tidepool:tidepool-cli:jar:1.0.0-SNAPSHOT)
With -DautoResolveSnapshots=dependencies the plugin offers to replace them with the latest release instead of failing. The proper fix is
to release the dependency first.
No scm section
A project without <scm> fails in the first phase, check-poms, before anything is written (no release.properties was created):
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.3.1:prepare (default-cli) on project tidepool-parent: Missing required setting: scm connection or developerConnection must be specified. -> [Help 1]
A tag that already exists
The most dangerous failure. A tag v1.0.0 was left in the repository by an earlier attempt (git tag -a v1.0.0 and git push origin v1.0.0,
simulating an abandoned release), and the new prepare -DreleaseVersion=1.0.0 -DtagNameFormat=v@{project.version} was run:
<id> refs/tags/v1.0.0
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.3.1:prepare (default-cli) on project tidepool-parent: Unable to tag SCM
[ERROR] Provider message:
[ERROR] The git-tag command failed.
[ERROR] Command output:
[ERROR] fatal: tag 'v1.0.0' already exists
The commit step happens before the tag step, and it pushes. So the run failed after it had already committed and pushed the release commit:
8012334 (HEAD -> main) [maven-release-plugin] prepare release v1.0.0
5631a64 (tag: v1.0.0, origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
8012334 [maven-release-plugin] prepare release v1.0.0
5631a64 tidepool 1.0.0-SNAPSHOT
The working directory was left with the descriptor and the backups:
data
pom.xml
pom.xml.releaseBackup
release.properties
tidepool-cli
tidepool-engine
tidepool-model
The tempting way out is release:rollback. It restored the POMs in a new commit (pushed) and then did the third thing it does, it removed
the tag named v1.0.0, which was the stale tag that was not made by this run (grep of the output, and the tags in the remote and the log of the remote
afterwards):
[INFO] 1/3 rollback:restore-backup-poms
[INFO] 2/3 rollback:scm-commit-rollback
[INFO] 3/3 rollback:remove-scm-tag
[INFO] BUILD SUCCESS
a215985 [maven-release-plugin] rollback the release of v1.0.0
8012334 [maven-release-plugin] prepare release v1.0.0
5631a64 tidepool 1.0.0-SNAPSHOT
The remote tag list is empty: the tag that existed before is gone (and the local list too). If that tag had pointed at a real, published release,
it would have been deleted by the rollback of an attempt to repeat it. So: pick a version that has no tag (git tag -l and
git ls-remote --tags origin first), and read what a rollback is going to remove.
A push that the remote rejects
If somebody else pushed to main after the working copy was cloned, the push in the commit phase is rejected. In a stale clone of the lab,
after another clone had pushed a commit, prepare failed (the grep of the error lines, and the two logs):
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-release-plugin:3.3.1:prepare (default-cli) on project tidepool-parent: Unable to commit files
[ERROR] Provider message:
[ERROR] The git-push command failed.
[ERROR] Command output:
[ERROR] To file://<lab>/remote.git
[ERROR] ! [rejected] main -> main (fetch first)
[ERROR] hint: Updates were rejected because the remote contains work that you do not
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
The local release commit already exists and the remote has the other commit:
d130c37 (HEAD -> main) [maven-release-plugin] prepare release tidepool-parent-1.0.0
5631a64 (origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
d06543d someone else pushes
5631a64 tidepool 1.0.0-SNAPSHOT
Pull (or rebase) the other work and then release:rollback, or reset the local release commit by hand and release:clean, before trying again; the
descriptor remembers the half-finished attempt.
Failures that were not reproduced here
-
The authenticity of host ... can't be establishedand a build that hangs on an SSH remote, and asite-deploythat hangs: both are in the troubleshooting section of the official guide. An SSH remote was not run here. -
Signing of commits or tags (
signTag) failing in a CI job that has no key.
Source: Maven Release Plugin: FAQ
The alternative: tag-driven releases
The Release Plugin edits the POM to say 1.0.0, commits, tags and edits the POM again. Many teams stopped doing that. The alternative is that the
tag is the release: a developer (or a pipeline) creates a tag; a CI job builds the tag and publishes the version the tag names, and nothing else is
committed. Two ways to put the version into the build:
-
CI-friendly versions (
${revision},${sha1},${changelist}): the POM keeps a default and the job passes-Drevision=1.2.0. This is all on CI-Friendly Versions, including the flatten plugin that Maven 3 needs to publish such a POM, and the shell script that lets a tag decide. -
versions:setof the Versions Maven Plugin: a commit that sets the version in every POM, a tag on it, a deploy, and a commit that moves to the next snapshot. It is the manual version ofrelease:preparewith the build in the middle. Run on a clone of the lab (grepfor the plugin’s lines):
$ mvn versions:set -DnewVersion=1.2.0 -DgenerateBackupPoms=false
[INFO] Processing change of io.tidepool:tidepool-parent:1.0.0-SNAPSHOT -> 1.2.0
[INFO] Processing io.tidepool:tidepool-parent
[INFO] Updating project io.tidepool:tidepool-parent
[INFO] Processing io.tidepool:tidepool-cli
[INFO] Updating parent io.tidepool:tidepool-parent
[INFO] Processing io.tidepool:tidepool-engine
[INFO] Updating parent io.tidepool:tidepool-parent
[INFO] Processing io.tidepool:tidepool-model
[INFO] Updating parent io.tidepool:tidepool-parent
[INFO] BUILD SUCCESS
$ git diff --stat
pom.xml | 4 ++--
tidepool-cli/pom.xml | 2 +-
tidepool-engine/pom.xml | 2 +-
tidepool-model/pom.xml | 2 +-
4 files changed, 5 insertions(+), 5 deletions(-)
$ mvn deploy
[INFO] Uploaded to tidepool-releases: file://<lab>/repo_tag/releases/io/tidepool/tidepool-model/1.2.0/tidepool-model-1.2.0.jar
[INFO] Uploaded to tidepool-releases: file://<lab>/repo_tag/releases/io/tidepool/tidepool-engine/1.2.0/tidepool-engine-1.2.0.jar
[INFO] Uploaded to tidepool-releases: file://<lab>/repo_tag/releases/io/tidepool/tidepool-cli/1.2.0/tidepool-cli-1.2.0.jar
[INFO] BUILD SUCCESS
$ mvn versions:set -DnewVersion=1.3.0-SNAPSHOT -DgenerateBackupPoms=false
[INFO] BUILD SUCCESS
$ git log --oneline --decorate
42b4184 (HEAD -> main, origin/main, origin/HEAD) Start 1.3.0-SNAPSHOT
67bd957 (tag: v1.2.0) Release 1.2.0
5631a64 tidepool 1.0.0-SNAPSHOT
$ git ls-remote --tags origin
<id> refs/tags/v1.2.0
The release commit and the tag exist, the deploy has built from the tagged commit, and the repository received 1.2.0. The commit
history is cleaner than the Release Plugin’s (no [maven-release-plugin] commits, no reformatted POM headers beyond what versions:set changes), at the
cost of doing the sequence yourself; a script or a CI pipeline does that, and it also lets the build run before the tag is created. The trade-offs:
| Release Plugin | Tag-driven | |
|---|---|---|
Version in the POM |
Edited and committed twice |
Default in the POM, or set in one commit |
Source of the release |
|
The CI job builds the tag, once |
Reproducibility |
|
|
Rollback |
|
Delete the tag and revert the commit by hand; there is no plugin state to repair |
Learning cost |
Many phases, files and failure modes (above) |
A script |
A published release should be built once and promoted, not rebuilt, so that the bytes tested are the bytes released (see Publishing to Maven Central and Security & Reproducible Builds).
Maven 3.9, 3.10 and 4
The Release Plugin 3.3.1 was run on all three. A complete prepare and perform on Maven 3.9.16 (grep, in a fresh clone, defaults and the tag
tidepool-parent-1.0.0):
--- Maven 3.9.16: release:prepare
[INFO] Tagging release with the label tidepool-parent-1.0.0...
[INFO] Release preparation complete.
[INFO] BUILD SUCCESS
--- Maven 3.9.16: release:perform
[INFO] [inner] BUILD SUCCESS
[INFO] BUILD SUCCESS
--- git log
312eaa1 (HEAD -> main) [maven-release-plugin] prepare for next development iteration
8b09dc6 (tag: tidepool-parent-1.0.0) [maven-release-plugin] prepare release tidepool-parent-1.0.0
5631a64 (origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
--- repository
tidepool-cli/1.0.0/tidepool-cli-1.0.0.jar
tidepool-cli/1.0.0/tidepool-cli-1.0.0.pom
tidepool-cli/maven-metadata.xml
tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
tidepool-engine/maven-metadata.xml
tidepool-model/1.0.0/tidepool-model-1.0.0.jar
tidepool-model/1.0.0/tidepool-model-1.0.0.pom
tidepool-model/maven-metadata.xml
tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom
tidepool-parent/maven-metadata.xml
and on Maven 4.0.0-rc-7 (a release candidate):
--- Maven 4.0.0-rc-7: release:prepare
[WARNING] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.
[INFO] Tagging release with the label tidepool-parent-1.0.0...
[INFO] Release preparation complete.
[INFO] BUILD SUCCESS
--- Maven 4.0.0-rc-7: release:perform
[INFO] [WARNING] The top project (<lab>/t_v4/target/checkout/pom.xml) cannot be found in the reactor from root project (<lab>/t_v4/pom.xml). Make sure the root directory is correct (a missing '.mvn' directory in the root project is the most common cause) and the project is correctly included in the
[INFO] [inner] BUILD SUCCESS
[INFO] BUILD SUCCESS
--- git log
72b27b5 (HEAD -> main) [maven-release-plugin] prepare for next development iteration
7866fbb (tag: tidepool-parent-1.0.0) [maven-release-plugin] prepare release tidepool-parent-1.0.0
5631a64 (origin/main, origin/HEAD) tidepool 1.0.0-SNAPSHOT
--- repository
tidepool-cli/1.0.0/tidepool-cli-1.0.0-build.pom
tidepool-cli/1.0.0/tidepool-cli-1.0.0.jar
tidepool-cli/1.0.0/tidepool-cli-1.0.0.pom
tidepool-cli/maven-metadata.xml
tidepool-engine/1.0.0/tidepool-engine-1.0.0-build.pom
tidepool-engine/1.0.0/tidepool-engine-1.0.0.jar
tidepool-engine/1.0.0/tidepool-engine-1.0.0.pom
tidepool-engine/maven-metadata.xml
tidepool-model/1.0.0/tidepool-model-1.0.0-build.pom
tidepool-model/1.0.0/tidepool-model-1.0.0.jar
tidepool-model/1.0.0/tidepool-model-1.0.0.pom
tidepool-model/maven-metadata.xml
tidepool-parent/1.0.0/tidepool-parent-1.0.0-build.pom
tidepool-parent/1.0.0/tidepool-parent-1.0.0.pom
tidepool-parent/maven-metadata.xml
What differs:
-
On Maven 4 the plugin worked, with a warning about the missing root directory in both goals (the clone has no
.mvndirectory because Git does not track an empty directory, and the checkout undertarget/checkoutis a project of its own). Add a.mvndirectory with a file in it, or the attributeroot="true"on the root project, to Maven 4 builds (see Migrating to Maven 4). -
On Maven 4 the deploy also published the build POM (
-build.pom) beside the consumer POM of every module (the listing above); see the deploy page. -
The
release-profileof the super POM exists on 3.9.16 and not on 3.10.0 or 4.0.0-rc-7 (above). -
The Release Plugin rewrites the
project.build.outputTimestampproperty at release time when the POM defines it, as the lab’s parent POM does. -
The release plugin version is not managed by Maven 3.10 any more; pin it.
The same job in Ant and Gradle
Ant has no release concept: a release is a target that sets a property, tags with exec or the Git tasks and publishes with the Resolver Ant
Tasks or Ivy (see Ivy: Publishing & Multi-Project Builds).
Gradle reads the version from the build script or gradle.properties, and releasing is either a convention (a tag, the version passed with -P)
or a third-party plugin; the publishing side is on
Publishing. The comparison is on
Repositories & Publishing Across Tools.
Where the books and old pages are outdated
-
Release guides written for Maven 2 and Subversion (the official "Releasing" mini guide still shows
scm:svn:URLs,tagBaseand plugin 2.5.2 in its examples) apply to Git with different properties:tagNameFormatand adeveloperConnectionwithscm:git:. Arelease.propertieswithcheckpoint.*entries is from the old plugin; the 3.x file hascompletedPhase. -
The
release-profileanduseReleaseProfileare gone or deprecated (3.10, above). -
release:prepareis no longer the only way to release: tag-driven pipelines with CI-friendly versions are common. -
Plugin versions: the Release Plugin 2.x is obsolete; use 3.x, and pin it.
What was not run here
-
A real remote (
https:orssh:) with credentials, signing of commits and tags, and CI jobs for Git hosts. -
Publishing to Maven Central,
release:stage,release:prepare-with-pom,-DlocalCheckout=true,-DconnectionUrlwithout arelease.properties,-DpushChanges=false, and Subversion. -
The interactive questions with answers (the questions were printed, the answers taken were the defaults).