Conversation
Adds com.jardoapps:pao-maven-plugin:0.1.0 with a single "apply" goal that
manages branch-specific SNAPSHOT versions: on a feature branch it appends the
branch name to the project version, and on a core branch it strips that suffix
back off, from the project version and from dependency versions alike.
The goal runs as its own invocation rather than binding into the build, because
Maven reads and interpolates every POM before any mojo executes, so a version
written during a build would not change what that build deploys.
ApplyMojo is parameter plumbing only; the behaviour lives in
PreventOverwritesRunner, which works on plain types so it can be driven directly
from tests. Every environment variable of the shell version maps to a pao.*
property, and the .prevent-overwrites.conf format is supported unchanged.
Rather than depending on versions-maven-plugin internals, PomDocument scans a
pom.xml into elements carrying their source offsets and splices edits into the
original text, leaving formatting, comments and attribute quoting untouched.
Comments, CDATA and quoted attributes are skipped, so a <version> inside a
comment is never rewritten.
Working from Maven's model rather than grep and sed fixes three defects in the
shell implementation:
- Multi-module reactors broke, because only a <version> outside <parent> was
rewritten and module <parent><version> references were left pointing at a
version that no longer existed. Parent references across the reactor now move
with the project version.
- A two-digit patch number was truncated: the prefix pattern matched a single
patch digit, so 1.2.10-feature-x-SNAPSHOT stripped to 1.2.1-SNAPSHOT.
- Pinned values needed a restricted character set purely because they flowed
into a sed replacement; with structured editing the check is now that a pin is
revertible, which is the actual requirement.
Two behaviours differ from the shell version on purpose, both documented:
versions written as ${property} are followed to the property that defines them
rather than having the reference overwritten, and every configuration row is
validated even when it targets another branch, so typos surface on the first run.
Covered by 71 unit tests, including the twelve cases ported from the shell test
suite, which compare output to the original fixtures byte for byte. Mojo wiring,
reactor collection and the git operations are covered by it/run-integration-tests.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jardo-51
left a comment
There was a problem hiding this comment.
Code review summary
| Severity | Count |
|---|---|
| CRITICAL | 1 |
| HIGH | 1 |
| MEDIUM | 5 |
| LOW | 6 |
Overall assessment: needs rework
The design is clean and the splice-based POM editing is a genuinely good call - formatting, comments and ${property} indirection all survive, and the test suite (71 tests, green locally) is thorough for the paths it covers. The blocker is a gap in the enforce path: when the project version changes, <dependency> entries pointing at other reactor modules are not moved with it, so a multi-module project ends up building against the shared x.y.z-SNAPSHOT from the repository - precisely the artifact this plugin exists to protect. Below that, the 120s git timeout in CommandLineGitClient can never fire, and git commit -a commits more than the plugin changed.
General notes
- No CI workflow. The repository has no
.github/directory, so neithermvn testnorit/run-integration-tests.shruns on a PR, and the GitHub Actions snippet the README recommends to users is itself never exercised. Worth adding before this merges, given the plugin's whole job is to be correct inside someone else's pipeline. - Integration coverage is thin on the paths most likely to break.
it/run-integration-tests.shcovers the round trip, idempotence, env detection, an invalid pin andskip. Nothing exercises core-branch dependency stripping,${revision}versions, or per-branch pinning against a real Maven invocation, and no IT covers the multi-module dependency case described in the CRITICAL finding. - Documentation is accurate for what the code does. The one gap: the README's "Multi-module projects" section states the whole reactor is handled in one run and mentions only
<parent><version>; whichever way the CRITICAL finding is resolved, that paragraph needs to say what happens to inter-module<dependency>versions.
Verification notes: findings 1, 5 and 8 were reproduced by driving PreventOverwritesRunner against scratch reactors; the rest are from reading. mvn -o test passes at b56dd9f.
changeProjectVersion rewrote only <project><version> and <parent><version>, so a module depending on a sibling by literal version kept pointing at the old one and resolved it from the repository - the branch-agnostic snapshot the plugin exists to avoid. The reactor fixture gains an "app" module depending on "core", which reproduces it. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/PreventOverwritesRunner.java:236.
…ntials readAllBytes() blocked until git exited, so waitFor always ran against a finished process and the 120s guard could never fire. The output is now drained on a separate thread so waitFor is the thing that bounds the call. Reading after waitFor would have deadlocked on output larger than the pipe buffer. Closing the child's stdin and setting GIT_TERMINAL_PROMPT=0 makes a push to an https remote with no usable credential helper fail immediately instead of blocking on a username prompt. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/git/CommandLineGitClient.java:101.
git commit -a staged every modified tracked file, so anything an earlier pipeline step or a developer had touched was swept into a commit labelled "Switched to branch-specific version." and pushed. PomEditSession.save() already returns the exact files it wrote, so those paths are now passed down to git add/commit as an explicit pathspec. That also removes the hasUncommittedChanges/commit -a mismatch: git status --porcelain reported untracked files that commit -a would then not stage, so a tree whose only change was untracked passed the guard and failed with exit 1. The guard is gone; an empty file list is the condition that matters. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/git/CommandLineGitClient.java:54.
configureUser ran unconditionally at the top of every run, so a core-branch run that changed nothing still left user.name=ci-bot in the repository's .git/config - permanently, in a developer's own clone - and a run outside a working copy died on `git config --local` before it had decided whether it needed to commit at all. The identity now travels with the commit as `git -c user.name=... -c user.email=... commit`, so it applies to that commit only and no git command runs until there is something to commit. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/PreventOverwritesRunner.java:80.
For <version>1.2.3</version> on a feature branch the fallback produced 1.2.3-feature-x-SNAPSHOT, and the trip back on a core branch produced 1.2.3-SNAPSHOT - a lossy round trip that converted a released project into a snapshot one without asking. The existing "does not match <base>-<suffix>-SNAPSHOT" warning did not fire, because the derived version was itself valid. withBranch now returns Optional and is empty for a non-snapshot version; enforceBranchVersion logs why it is leaving the version alone and returns. Also documents the behaviour in the README's version format section. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/BranchVersions.java:75.
On GitHub Actions pull_request / pull_request_target events GITHUB_REF is refs/pull/<n>/merge, so GITHUB_REF_NAME is the synthetic '<n>/merge'. That derived a version nobody can merge back and then pushed to a ref the forge rejects. GITHUB_HEAD_REF carries the real source branch and is set only for pull-request events, so it is checked first; on push events it is present but empty and the existing blank check skips it. CI_MERGE_REQUEST_SOURCE_BRANCH_NAME gets the same treatment, since GitLab merged results pipelines set CI_COMMIT_REF_NAME to refs/merge-requests/<n>/merge. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/BranchDetector.java:18.
A pom declaring ISO-8859-1 and containing any non-ASCII byte - an accented name in <developers>, a word in <description> - made readString throw MalformedInputException, failing the goal on a file Maven parses without complaint. save() had the mirror problem: it always wrote UTF-8, so the declared encoding and the actual bytes could drift apart for content the plugin never touched. The prolog is now read out of the raw bytes as ISO-8859-1, which maps every byte to a character and cannot itself fail, and the charset it names is used for both the read and the write. Absent or unparseable prolog means UTF-8, as before. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/pom/PomDocument.java:51.
RunResult.projectVersion was filled from root.version(), read before any rewrite, so a field whose name reads as "the version this run produced" reported the stale value. enforceBranchVersion and removeBranchVersion now return the version they leave the project at, and the pre-run value is kept as previousProjectVersion. Also exposes it as a project-version output alongside changes-made, so CI can pick up the version the build will publish under. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/PreventOverwritesRunner.java:104.
A null file logged "No config file at 'null'". The two cases now get their own message, so someone who expected their pins to apply sees that the file the plugin looked for is not there, rather than a line about null. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/config/PinConfigParser.java:47.
Only too-few columns were rejected; extra ones were silently dropped. A stray space inside a value - `1.2.3 -f1-SNAPSHOT` - parsed as four columns and pinned the project to `1.2.3`, failing later with a confusing "invalid pinned version", or succeeding with the wrong pin when the truncated value happened to be valid on its own. stripComment already removes trailing comments, so a legitimate fourth column cannot exist. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/config/PinConfigParser.java:71.
PASSED was incremented at the end of every test function regardless of whether fail had been called inside it, so a run with broken assertions reported the same test in both columns. Each function now brackets itself with begin_test/end_test, and end_test only counts a pass when the failure count has not moved. The exit status was already correct; this is about the summary being readable. Addresses review comment from @Jardo-51 on it/run-integration-tests.sh:117.
The class body went into the Java regex verbatim, so bash-valid classes broke or changed meaning: []] - bash's literal ']' - became the empty class [] and threw PatternSyntaxException, [a&&b] silently became a regex intersection matching nothing, and a backslash meant something else entirely. The body is now copied with '\', '[', ']', '&' and '^' escaped, a ']' in the first position is recognised as a literal, and an unmatched '[' stays literal as in bash. Since these patterns come from pao.coreBranches and the config file's branch column, a pattern that still cannot compile is reported as a PaoException naming the pattern rather than an unadorned PatternSyntaxException. Addresses review comment from @Jardo-51 on src/main/java/com/jardoapps/pao/GlobMatcher.java:36.
FakeGitClient recorded the identity but nothing read it back, so the one path where a wrong value produces commits attributed to the wrong author went unverified. Three tests now cover it: the default identity, an overridden one, and a run that commits nothing setting no identity at all. Addresses review comment from @Jardo-51 on src/test/java/com/jardoapps/pao/git/FakeGitClient.java:53.
|
Verification pass over all 13 unresolved review threads, at
Each fix was traced to the code rather than taken from the reply, and the four with the most substance were checked by mutation — reverting the fix in a scratch worktree and confirming the named tests fail: the sibling-dependency loop (2 failures in Tests at Two General notes from the review summary have no thread and are still open:
One thing to note rather than act on: |
Initial implementation of
com.jardoapps:pao-maven-plugin:0.1.0— a Maven-native port of the prevent-artifact-overwrites shell script.Approach
A single
applygoal, run as its own invocation before the publishing build:Two invocations are a constraint, not a preference: Maven reads and interpolates every POM before any mojo executes, so a version written during a build does not change what that same build deploys. Running as a lifecycle extension would allow a single invocation, but it would then execute on every developer
mvncall and every IDE import — the compile-time version rewriting the original project deliberately avoids.No POM changes are required. Every environment variable of the shell version maps to a
pao.*property, and.prevent-overwrites.confis supported unchanged, so the existing CI interface carries over by swapping one line.ApplyMojois parameter plumbing only; the behaviour lives inPreventOverwritesRunner, which works on plain types and can be driven directly from tests without the Maven testing harness.Rather than depending on
versions-maven-plugininternals,PomDocumentscans a pom.xml into elements carrying their source offsets and splices edits into the original text. Formatting, comments and attribute quoting survive untouched, and comments/CDATA/quoted attributes are skipped so a<version>inside a comment is never rewritten.Bugs in the shell version this fixes
Each was reproduced against
prevent-overwrites.shbefore being fixed here.enforce_branch_versiononly rewrites a<version>outside<parent>, never a module's<parent><version>. On a two-module project the child kept1.2.3-SNAPSHOTwhile the aggregator moved, andmvn validatethen failed withNon-resolvable parent POM. The plugin moves parent references across the reactor in step with the project version.1.2.10-feature-x-SNAPSHOTstrips to1.2.1-SNAPSHOT— a silently wrong version.sed. Pinned values were restricted to a safe character set only because they flowed into asedreplacement. With structured editing the check is now the actual requirement: that a pin is revertible.A fourth was caught by these tests during development — an ambiguous
-rcpattern that read1.2.3-rc.4-SNAPSHOTas base1.2.3plus suffixrc.4, which would have dropped the qualifier on a core branch. Fixed with possessive quantifiers.Intentional behaviour changes
Both documented in the README:
<version>${revision}</version>updates the<revision>property rather than replacing the reference. Undefined properties are left alone with a warning.Testing
bash it/run-integration-tests.sh) cover what only a real Maven invocation exercises: parameter binding, reactor collection from the session, and git operations against real repositories. The round-trip test assertsmvn validatestill resolves after the version change — the check that fails on the shell version.Not included
Maven Central distribution (signing, staging, a
releaseprofile) is not set up, so the plugin currently resolves only from a localmvn installor an internal repository.🤖 Generated with Claude Code