| name | verify-pom-refactor |
| description | Verify that a change to any pom.xml did not silently alter build behaviour. Use whenever a POM is restructured, a plugin or dependency is moved between sections, or a pluginManagement/dependencyManagement entry is pruned — Maven's model merging can change what executes with no visible symptom. |
Verifying a POM refactor
Maven's model merging is implicit enough that a POM change can alter behaviour with no visible symptom — that is the
common thread through the 4.0.1 obfuscation regression, the silent zero-test Failsafe run, and the parent POM
inlining. For any structural POM change, diff the effective POM rather than reading the source diff:
for m in . api build-tasks core jax-rs jsf maven-plugin obfuscate spi swing tests ui v1 v2-core v2-json v2-xml v4; do
n=$(echo "$m" | sed 's|^\.$|ROOT|')
for p in plain release; do
[ "$p" = release ] && PF="-P sonatype-oss-release" || PF=""
./mvnw -o -q help:effective-pom -pl "$m" $PF -Doutput="$DIR/$n.$p.xml"
done
done
Strip the Generated by Maven Help Plugin on line (it carries a timestamp), capture before and after, and diff. Two
things make the result readable:
- A pure reordering is not a behaviour change unless two plugins share a phase. Check with
diff <(sort before) <(sort after): if that is empty, only the order moved.
- Inherited-but-inactive profiles are invisible.
help:effective-pom lists a project's own profiles and
injects the active ones into <build>; profiles inherited from an ancestor are not listed at all, which is why
module effective POMs have no <profiles> section. So a profile moving between POMs shows up as a large
<profiles> addition on the declaring project and nothing anywhere else. Diff the <build> section separately —
that is what actually executes.
The inlining commit was signed off on exactly this: all 32 module effective POMs content-identical, one adjacent
transposition (truelicense-maven-plugin ↔ maven-antrun-plugin, phases process-classes vs prepare-package) in
the release-profile ones, and the root's <build> unchanged. Profile declaration order in the root POM is arranged
to reproduce the old parent-then-child injection order for that reason, and for no other.
Then confirm against artifacts, not the log — ./mvnw -o clean install -P sonatype-oss-release -Dgpg.skip=true -Dmaven.javadoc.skip=true, followed by the grep -rlaE '_clinit@|_string#' check under Build-time verification
in CLAUDE.md.
Pruning
For a prune, where the effective POM is supposed to change, invert the check: assert that the things which
must not move, didn't. Two comparisons cover it, and both caught nothing only because the prune was scoped from
evidence rather than from reading:
- Resolved version of every plugin that executes a goal. Extract the
<artifactId>/<version> pairs from the
<build><plugins> section of all 34 effective POMs and diff the sorted set. Removing a pluginManagement entry
for a plugin that turns out to be bound somewhere silently downgrades it to a lifecycle default.
- Full GAV and scope of every resolved dependency.
./mvnw -o dependency:list over the reactor, before and
after, diffed. Removing a dependencyManagement entry can shift a transitive version even when no module
declares that artifact directly. On the prune commit the only delta was ScalaCheck and its test-interface
leaving the test classpath, which was the intent.