Chapter 14Lesson 02~205 minutes

Maven 4 Preview, Build Consumer POM, Compatibility, Migration Planning, and Maven 3 Production Baselines: Guided Hands-On Workflow and Core Operations

Run one Maven 3 project through isolated Maven 3.9.16 and Maven 4.0.0-rc-6 lanes, compare evidence, inspect mvnup findings, and observe consumer POM behavior safely.

Maven 3.9.16Maven 4 RCmvnupChecksumsIsolated Test

A compatibility test is useful only when you know exactly what changed. This workflow starts from one Maven 3-compatible project, creates separate Maven 3 and Maven 4 lanes, pins both runtimes, isolates their local repositories, and preserves every diff and checksum before touching model 4.1.0.

Current status — verified 2026-08-24. Apache Maven 3.9.16 is the current GA Maven 3 baseline. Maven 4.0.0-rc-6, released 2026-08-04, is still not GA and is used here only for compatibility testing. Maven 4 requires Java 17+ to run; the lab uses JDK 21 for both Maven 3 and Maven 4 so the Maven version—not the launcher JDK—is the deliberate variable. Maven Wrapper 3.3.4 remains the stable wrapper baseline. The example pins Clean 3.5.0, Resources 3.5.0, Compiler 3.15.0, Surefire 3.5.6, JAR 3.5.1, and JUnit 6.1.3 so lifecycle/plugin drift does not contaminate the core-version comparison. Network access is needed only to acquire the Maven distributions/plugins; no hosted CI or artifact repository is required.

Learning objectives

  • Create identical Maven 3 and Maven 4 test lanes without editing the production baseline in place.
  • Verify and pin the Maven 4 RC distribution before executing it.
  • Compare effective model, build/test result, JAR contents, and SHA-256 across both lanes.
  • Run mvnup in check mode before applying any Maven 4-specific POM edit.
  • Create an optional model 4.1.0 copy and inspect installed consumer metadata.
  • Leave the original Maven 3 project immediately recoverable.

1. Start from a trusted Maven 3 wrapper and create disposable lanes

Use the wrapper established earlier in the course. Do not fetch wrapper scripts from a gist or paste unverified launchers into a repository. The baseline copy remains Maven 3.9.16; the candidate copy receives a separately verified Maven 4 distribution.

set -euo pipefail
ROOT="$PWD/../maven4-migration-lab"
rm -rf "$ROOT"
mkdir -p "$ROOT"/{baseline-m3,candidate-m4,evidence,downloads,.lab}

# Run from a trusted project already containing mvnw, mvnw.cmd, and .mvn/
cp mvnw mvnw.cmd "$ROOT/baseline-m3/"
cp -R .mvn "$ROOT/baseline-m3/"
cp mvnw mvnw.cmd "$ROOT/candidate-m4/"
cp -R .mvn "$ROOT/candidate-m4/"

cd "$ROOT/baseline-m3"
export MAVEN_USER_HOME="$ROOT/.lab/home-m3"
./mvnw -v | tee ../evidence/maven3-version.txt
java -version 2> ../evidence/java-baseline.txt
Preflight: the baseline wrapper must report Maven 3.9.16 before you continue. If it does not, stop and correct the wrapper identity; do not compare an unknown Maven 3 runtime with Maven 4.

2. Put the exact same Maven 3-compatible project in both lanes

<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>dev.academy</groupId>
  <artifactId>maven4-migration-lab</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration><release>17</release></configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.6</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.5.1</version>
      </plugin>
    </plugins>
  </build>
</project>
package dev.academy;

public final class App {
    private App() {}
    public static String identity() {
        return "maven4-migration-lab";
    }
}
package dev.academy;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class AppTest {
    @Test
    void identityIsStable() {
        assertEquals("maven4-migration-lab", App.identity());
    }
}

Create pom.xml, src/main/java/dev/academy/App.java, and src/test/java/dev/academy/AppTest.java in baseline-m3, then copy those three repository-owned inputs byte-for-byte into candidate-m4. The test gives both lanes an independently visible Surefire result instead of treating a zero-test build as verification.

cd "$ROOT"
mkdir -p baseline-m3/src/{main,test}/java/dev/academy
# Save the POM and the two Java files shown above at their stated paths.
cp baseline-m3/pom.xml candidate-m4/pom.xml
mkdir -p candidate-m4/src/{main,test}/java/dev/academy
cp baseline-m3/src/main/java/dev/academy/App.java candidate-m4/src/main/java/dev/academy/App.java
cp baseline-m3/src/test/java/dev/academy/AppTest.java candidate-m4/src/test/java/dev/academy/AppTest.java
sha256sum baseline-m3/pom.xml candidate-m4/pom.xml > evidence/source-pom.sha256

The two POM hashes must match before changing the Maven 4 wrapper. Model version 4.0.0 deliberately keeps Maven 3 compatibility while Maven 4 is being evaluated.

3. Verify Maven 4 RC before changing the candidate wrapper

Download the RC archive and its Apache-published SHA-512 sidecar. Verify SHA-512 first, then calculate the SHA-256 value required by the Maven Wrapper integrity property. This makes the candidate runtime both version-pinned and integrity-pinned.

cd "$ROOT/downloads"
M4_VERSION='4.0.0-rc-6'
M4_URL="https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${M4_VERSION}/apache-maven-${M4_VERSION}-bin.zip"

curl -fL -o "apache-maven-${M4_VERSION}-bin.zip" "$M4_URL"
curl -fL -o "apache-maven-${M4_VERSION}-bin.zip.sha512" "$M4_URL.sha512"

EXPECTED_SHA512="$(tr -d '[:space:]' < "apache-maven-${M4_VERSION}-bin.zip.sha512")"
ACTUAL_SHA512="$(sha512sum "apache-maven-${M4_VERSION}-bin.zip" | awk '{print $1}')"
test "$EXPECTED_SHA512" = "$ACTUAL_SHA512"

M4_SHA256="$(sha256sum "apache-maven-${M4_VERSION}-bin.zip" | awk '{print $1}')"
printf '%s
' "$M4_SHA256" > ../evidence/maven4-distribution.sha256
Do not bypass a checksum mismatch. A mismatched distribution is a supply-chain or corruption event to investigate, not a migration inconvenience.
Windows PowerShell: use Invoke-WebRequest for the ZIP and .sha512 sidecar, Get-FileHash -Algorithm SHA512 for Apache integrity verification, and Get-FileHash -Algorithm SHA256 for the wrapper pin. Use .\mvnw.cmd and the extracted bin\mvnup.cmd in the equivalent Windows lane.

4. Pin only the candidate wrapper to Maven 4.0.0-rc-6

from pathlib import Path
p = Path("candidate-m4/.mvn/wrapper/maven-wrapper.properties")
text = p.read_text(encoding="utf-8")
lines = []
for line in text.splitlines():
    if line.startswith("distributionUrl="):
        lines.append("distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/4.0.0-rc-6/apache-maven-4.0.0-rc-6-bin.zip")
    elif line.startswith("distributionSha256Sum="):
        continue
    else:
        lines.append(line)
sha = Path("evidence/maven4-distribution.sha256").read_text().strip()
lines.append(f"distributionSha256Sum={sha}")
p.write_text("\n".join(lines) + "\n", encoding="utf-8")

Run that script from $ROOT. The Maven 3 baseline wrapper stays untouched. The candidate wrapper now names exactly one Maven 4 RC distribution and its SHA-256 digest.

cd "$ROOT"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./candidate-m4/mvnw -v | tee evidence/maven4-version.txt

Expected identity: Maven 4.0.0-rc-6 running on Java 21. If Java is below 17, stop before interpreting any project result.

5. Build the same model in isolated repositories

cd "$ROOT/baseline-m3"
MAVEN_USER_HOME="$ROOT/.lab/home-m3" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-m3" clean verify   | tee "$ROOT/evidence/maven3-verify.log"
MAVEN_USER_HOME="$ROOT/.lab/home-m3" ./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -Doutput="$ROOT/evidence/effective-m3.xml"
sha256sum target/maven4-migration-lab-1.0.0.jar   > "$ROOT/evidence/artifact-m3.sha256"
jar tf target/maven4-migration-lab-1.0.0.jar   > "$ROOT/evidence/jar-m3.txt"
grep -R 'tests="1"' target/surefire-reports/TEST-*.xml > "$ROOT/evidence/tests-m3.txt"

cd "$ROOT/candidate-m4"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-m4" clean verify   | tee "$ROOT/evidence/maven4-verify.log"
MAVEN_USER_HOME="$ROOT/.lab/home-m4" ./mvnw org.apache.maven.plugins:maven-help-plugin:3.5.2:effective-pom -Doutput="$ROOT/evidence/effective-m4.xml"
sha256sum target/maven4-migration-lab-1.0.0.jar   > "$ROOT/evidence/artifact-m4.sha256"
jar tf target/maven4-migration-lab-1.0.0.jar   > "$ROOT/evidence/jar-m4.txt"
grep -R 'tests="1"' target/surefire-reports/TEST-*.xml > "$ROOT/evidence/tests-m4.txt"

Compare the digest values and JAR member lists. Matching checksums are strong evidence for this simple controlled artifact, but still do not prove every plugin, profile, release path, or extension in a real repository is compatible.

6. Run the Maven Upgrade Tool read-only first

The wrapper launches Maven itself, but mvnup is a separate executable shipped inside the Maven 4 distribution. Reuse the archive you already verified, extract it only under the disposable lab, and run its bin/mvnup.

cd "$ROOT"
mkdir -p .lab/tools
unzip -q downloads/apache-maven-4.0.0-rc-6-bin.zip -d .lab/tools
M4_HOME="$ROOT/.lab/tools/apache-maven-4.0.0-rc-6"

"$M4_HOME/bin/mvnup" check --directory candidate-m4   | tee evidence/mvnup-check-400.txt
"$M4_HOME/bin/mvnup" check --model-version 4.1.0 --all --directory candidate-m4   | tee evidence/mvnup-check-410.txt

The first check targets model 4.0.0 by default and focuses on Maven 4 compatibility improvements that can remain Maven 3-buildable. The second asks what would change if the project deliberately crosses the Maven 4-only model boundary.

7. Inspect model 4.1.0 and consumer POM behavior only in a second disposable copy

Do not overwrite the baseline or your first candidate. Create candidate-41, then either let mvnup apply --model-version 4.1.0 --all edit it or use the compact 4.1.0 POM below for a focused consumer-POM demonstration.

<project xmlns="http://maven.apache.org/POM/4.1.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd"
         root="true">
  <modelVersion>4.1.0</modelVersion>
  <groupId>dev.academy</groupId>
  <artifactId>maven4-consumer-demo</artifactId>
  <version>1.0.0</version>
  <packaging>jar</packaging>

  <properties>
    <maven.compiler.release>17</maven.compiler.release>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.outputTimestamp>2026-08-24T00:00:00Z</project.build.outputTimestamp>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>6.1.3</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>
        <version>3.5.0</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.15.0</version>
        <configuration><release>17</release></configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>3.5.6</version>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-jar-plugin</artifactId>
        <version>3.5.1</version>
      </plugin>
    </plugins>
  </build>
</project>
cd "$ROOT"
cp -R candidate-m4 candidate-41
cp candidate-41/pom.xml evidence/pom-before-41.xml

# Option A: inspect what mvnup would apply, then apply in this disposable copy.
"$M4_HOME/bin/mvnup" apply --model-version 4.1.0 --all --directory candidate-41   | tee evidence/mvnup-apply-410.txt

# If you prefer the focused example, replace candidate-41/pom.xml with the
# model 4.1.0 POM shown above after saving the mvnup diff.

diff -u evidence/pom-before-41.xml candidate-41/pom.xml   > evidence/pom-41.diff || true

cd candidate-41
MAVEN_USER_HOME="$ROOT/.lab/home-41" ./mvnw -Dmaven.repo.local="$ROOT/.lab/repo-41" clean install   | tee "$ROOT/evidence/maven4-41-install.log"

INSTALLED_POM="$ROOT/.lab/repo-41/dev/academy/maven4-migration-lab/1.0.0/maven4-migration-lab-1.0.0.pom"
# If the focused POM uses artifactId maven4-consumer-demo, adjust this path accordingly.
test -f "$INSTALLED_POM" && cp "$INSTALLED_POM" "$ROOT/evidence/consumer-installed.pom" || true

For an ordinary Maven 4 JAR project using model 4.1.0, expect downstream-oriented installed/deployed metadata to differ from the source build POM. Inspect the actual file; do not hard-code an assumption about every element. Confirm coordinates and dependencies, note whether build-only plugin/properties content was removed, and note the model version Maven emitted for consumers.

Current RC-6 caveat: the rc-6 release notes document a known issue involving unresolved property references in BOM consumer POMs. The simple JAR lab does not exercise that BOM bug, but a real migration dossier must record it when the repository publishes BOMs.

8. Compare warnings and model differences, not only exit codes

cd "$ROOT"
grep -E '\[WARNING\]|\[ERROR\]' evidence/maven3-verify.log   > evidence/warnings-m3.txt || true
grep -E '\[WARNING\]|\[ERROR\]' evidence/maven4-verify.log   > evidence/warnings-m4.txt || true

diff -u evidence/warnings-m3.txt evidence/warnings-m4.txt   > evidence/warning-diff.txt || true
diff -u evidence/effective-m3.xml evidence/effective-m4.xml   > evidence/effective-diff.txt || true

A Maven 4 warning may represent a future build failure or a semantic difference worth fixing even when the build succeeds today. Preserve the exact warning and map it to a declared model or plugin before editing.

9. Challenge: choose the smallest safe next action

Your Maven 3 and Maven 4 builds both pass, but mvnup check --model-version 4.1.0 proposes many inference edits and your production release policy still requires Maven 3 rollback. What should you do?

Answer before revealing: keep the source POM at model 4.0.0, continue the dual-baseline Maven 4 test lane, fix only Maven 4 compatibility issues that remain Maven 3-compatible, and defer 4.1.0-only edits until the rollback requirement is intentionally removed.

10. Cleanup and rollback

The Maven 3 baseline directory was never modified to Maven 4, so rollback is structural: delete the candidate directories and their isolated repositories. Do not delete your normal Maven local repository or wrapper distribution cache.

cd "$ROOT/.."
rm -rf maven4-migration-lab

Knowledge check

Why manually verify the Maven 4 ZIP if the wrapper also supports distributionSha256Sum?

Why use separate repo-m3 and repo-m4 directories?

What does mvnup check change?

If the source 4.1.0 POM and installed consumer POM differ, is that automatically a bug?

Why is the first Maven 4 test performed with model 4.0.0?

11. Bridge to migration decisions

You now have the raw evidence needed for a policy decision. Lesson 3 converts that evidence into choices about Maven 3 support windows, dual-baseline CI, model 4.1.0 adoption, plugin/extension risk, and the cost of carrying two build-tool baselines.

Official references and version notes

Version-sensitive statements in this lesson were checked against current Apache Maven primary documentation on 2026-08-24.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.