Conversation
bae1db9 to
5a5af1e
Compare
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
e64db31 to
de8044a
Compare
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a structured build report that captures per-module and per-mojo execution results, timing, log events, and failures as a JSON file (target/build-reports/) at the end of every build. Part 2 of the #12572 split. Builds on the logging foundation from PR #12694 (LogEvent, LogLevel, LogEventSink). New API interfaces: - BuildReport: root report with metadata, modules, failures, problems - BuildStatus: SUCCESS/FAILURE/SKIPPED enum - ModuleReport: per-module results with mojo list - MojoReport: per-mojo execution with captured log events - FailureReport: exception details and stack traces Implementation: - BuildReportCollector: EventSpy that tracks lifecycle events and captures log output via LogEventSink, routing events to mojo/module/build-level buffers using thread-based tracking - BuildReportJsonWriter: zero-dependency JSON serializer - Atomic file writes with timestamped files and latest symlink - Thread-safe for parallel builds (-T) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
8baa65a to
02ac855
Compare
gnodet
left a comment
There was a problem hiding this comment.
Well-designed foundational logging infrastructure. The structured LogEvent API, JUL handler, and Log API enhancements provide a solid base for the build report and console modes PRs. A few issues noted below.
Also noted:
- Good catch fixing
warn(Supplier<String>, Throwable)callinglogger.info()instead oflogger.warn(). - The logger name change from
getFullGoalName()togetImplementation()(FQCN) enables proper hierarchical SLF4J level configuration but is a behavioral change — worth mentioning in release notes for users who configured logging by short-form names. - No unit tests were added for the new functionality (MavenJulHandler, DefaultLogEvent, StackWalker metadata capture, LogSink contract). Given this is foundational for the entire logging pipeline, targeted tests would increase confidence.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
ascheman
left a comment
There was a problem hiding this comment.
Really solid foundation — the three-path convergence (Log API / SLF4J / JUL) onto one structured LogEvent is clean, and preserving the LogRecord metadata the stock SLF4JBridgeHandler drops is a genuine improvement. Nice catch on the warn(Supplier, Throwable) → logger.info() bug.
A few things worth a look before this becomes the base of the 7-PR chain — one API-compat question, one fork-context correctness question, one perf note, and some small nits. Nothing structural.
On tests (echoing the earlier note): the two I'd most want are a regression test asserting warn(Supplier, Throwable) actually logs at WARN, and a table test for the JUL→SLF4J level mapping (esp. FINEST→TRACE and CONFIG→INFO). Given the ThreadLocal/StackWalker plumbing, those would lock down the easy-to-regress bits.
gnodet
left a comment
There was a problem hiding this comment.
Well-designed logging infrastructure foundation with clean three-path convergence (Log API, JUL, SLF4J). The bug fix for warn(Supplier, Throwable) calling logger.info() is confirmed correct.
Findings:
-
[medium]
sequenceNumber()javadoc/contract mismatch —LogEvent.sequenceNumber()javadoc says@return the sequence number, always non-negativebut the default implementation returns-1. The sibling methodthreadId()correctly documentsor -1 if unavailablein its@returntag. ThesequenceNumber()javadoc should follow the same pattern for consistency. -
[medium] Inconsistent
formattedMessageformat between JUL and SLF4J — When aLogSinkis installed, JUL events'formattedMessageis built byformatForConsole()which produces a minimal[LEVEL] messagestring, while SLF4J events produce a full formatted string with timestamps, thread names, and logger names viaMavenBaseLogger.innerHandleNormalizedLoggingCall(). The practical impact is limited since the cleanmessage()field is available for consumers who need consistent content, but inSimpleBuildEventListener.projectLogMessage()which usesformattedMessage()for console output, JUL events will look noticeably different from SLF4J events. -
[low] Log4j2/Logback backend removal — The removal of
Log4j2Configuration,LogbackConfiguration, and thelogback-classicdependency means Maven no longer supports these as alternative SLF4J backends. This is intentional for the Maven 4.x logging redesign, but warrants mention in release notes for users who embedded Maven with a custom logging backend.
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
gnodet
left a comment
There was a problem hiding this comment.
Well-architected logging foundation PR. Clean design with proper ThreadLocal management, volatile concurrency handling, and good layering (API → impl → collector). A few items to address:
High severity:
-
API contract contradiction (
LogEvent.javaline 188):sequenceNumber()Javadoc says "@return the sequence number, always non-negative" but the default implementation returns-1. Compare withthreadId()which correctly documents "or -1 if unavailable". This is a public API interface marked@Experimental/@since 4.1.0— the Javadoc should match the actual contract. -
No test coverage: 1000+ lines of foundational code across 22 files with zero test files.
LogEvent/DefaultLogEvent,MavenJulHandler(249 lines),DefaultLog.withMetadata/trace/child,LogSinkinterface,ProjectBuildLogAppenderstructured event creation, and the mojo MDC lifecycle are all untested. The PR description mentions "580 tests pass" but these are all pre-existing tests.
Medium severity:
-
StackWalker overhead (
DefaultLog.javaline 649):withMetadata()callsStackWalker.walk()on every log call for enabled levels. While trace/debug are typically disabled and info/warn/error are low-volume, plugins logging many INFO/WARN messages will pay the 1-5μs per-call cost. -
Logger name change (
DefaultBuildPluginManager.javaline 128): Logger name changed fromgetFullGoalName()(e.g., "compiler:compile") togetImplementation()(e.g., "org.apache.maven.plugins.compiler.CompilerMojo"). Intentional for proper hierarchical SLF4J configuration, but a user-visible behavior change that could break existing SLF4J level configurations. -
Dead code for future PR (
ProjectBuildLogAppender.javaline 130):reportCapturevolatile field and setter are infrastructure for PR #12695 (build report). Currently unused in this PR — consider adding a brief comment noting the intent.
Low severity:
-
setMojoId(null)is called beforedelegate.mojoSucceeded/mojoFailedcallbacks, inconsistent with theforkSucceeded/forkFailedpattern where cleanup happens after the delegate. -
The bug fix changing
logger.info()tologger.warn()inwarn(Supplier<String>, Throwable)is correct and important. 👍
The removal of Logback/Log4j2 support is a significant architectural decision — worth explicit mention in release notes since users plugging in alternative SLF4J backends will lose that ability.
This review was generated by an AI agent (Claude Code) and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of Guillaume Nodet
02ac855 to
812a842
Compare
Review feedback addressedAll 8 review comments from @gnodet and @ascheman have been addressed in the latest force-push. Summary of changes: Bug fixes
Design improvements
Tests added
All 6 downstream PRs (#12695, #12697, #12698, #12699, #12702, #12714) have been rebased onto the updated commit. |
812a842 to
84568d2
Compare
Apply review fixes from #12694 to align the backport: - Log.java: make all 6 trace methods default (no-ops) to prevent AbstractMethodError for existing third-party Log implementors. isTraceEnabled() returns false by default. - ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is called, the forking mojo's ID is restored instead of clearing. - LoggingExecutionListener: save current mojoId in forkStarted(), clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup ordering in mojoSucceeded/mojoFailed — delegate runs first, then MDC is cleared. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Backport Log API enhancements and mojo MDC to 4.0.x
Backport four Log-related improvements from master to the 4.0.x branch
for inclusion in rc-7:
1. Log.trace() — new trace level (maps to SLF4J TRACE / JUL FINEST)
to separate Maven core internals from user-facing debug messages.
Currently -X floods debug output with resolver/interpolation details
that drown user-relevant diagnostics.
2. Log.child(name) — creates a sub-logger with an independently
filterable name (e.g. "CompilerMojo.diagnostics"), letting plugin
sub-components log under their own namespace.
3. Logger name alignment — Maven 4 Log now uses the mojo implementation
class name (e.g. "org.apache.maven.plugins.compiler.CompilerMojo")
instead of the goal name ("compiler:compile"). This matches what
Maven 3 mojos already use and enables standard SLF4J hierarchical
level configuration.
4. Mojo MDC propagation — sets "maven.mojo.id" (prefix:goal@executionId)
in the SLF4J MDC during mojo execution. All log messages — including
those arriving through the JUL-to-SLF4J bridge — now carry mojo
context, available to any SLF4J appender via %X{maven.mojo.id}.
Also fixes a pre-existing bug in DefaultLog where warn(Supplier, Throwable)
incorrectly delegated to logger.info() instead of logger.warn().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add isXxxEnabled() guards to Throwable-only log overloads
Align with master by wrapping the five xxx(Throwable) overloads
in level-enabled checks, avoiding unnecessary method calls and
empty string construction when the level is disabled.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address review: default trace methods and fork-aware mojoId
Apply review fixes from #12694 to align the backport:
- Log.java: make all 6 trace methods default (no-ops) to prevent
AbstractMethodError for existing third-party Log implementors.
isTraceEnabled() returns false by default.
- ProjectBuildLogAppender: add FORKING_MOJO_ID ThreadLocal mirroring
the existing FORKING_PROJECT_ID pattern. When setMojoId(null) is
called, the forking mojo's ID is restored instead of clearing.
- LoggingExecutionListener: save current mojoId in forkStarted(),
clear forking mojoId in forkSucceeded/forkFailed. Fix cleanup
ordering in mojoSucceeded/mojoFailed — delegate runs first, then
MDC is cleared.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address review: add DefaultLogTest and clear MDC on mojoSkipped
- Add DefaultLogTest with 5 tests: warn/supplier regression,
trace delegation, trace no-op guard, child() sub-logger,
and default trace methods (AbstractMethodError prevention).
- Clear mojo MDC in mojoSkipped() to prevent stale mojo context
from leaking into subsequent log messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 222071d7 ("Address review: restore @PARAM tags on trace(Supplier) overloads, fix quiet-mode JUL level to SEVERE, fix test comment").
All three findings from the CHANGES_REQUESTED on 6ccbc27a are addressed:
@param contentontrace(Supplier<String>)— restored at lines 91-93 ofLog.java. ✅@param content+@param errorontrace(Supplier<String>, Throwable)— restored at lines 101-104. ✅- Quiet-mode JUL root level
WARNING→SEVERE— fixed inLookupInvoker.java; comment updated to referenceSEVEREand explain why it matches the SLF4J ERROR threshold. ✅
The test comment in publishIsReentrantSafe() is also updated to accurately describe the reentrancy simulation. ✅
The full logging foundation — MavenJulHandler, LogEvent API, LogSink structured sink, DefaultLog.withMetadata() with hasReportCapture() gating, pendingEarlyLogs drain ordering, and ProjectBuildLogAppender — is solid. No new issues introduced by this commit.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet
left a comment
There was a problem hiding this comment.
Re-review of commits bdb3ec29aa ("fix: Javadoc missing tags in maven-cli, fix MavenITmng6065 log replay ordering") and 3ea2643fcc ("feat: carry projectId and mojoId in LogEvent").
bdb3ec29aa — Javadoc + log replay fix
CI failure diagnosed and fixed correctly:
- Javadoc error in
LookupContext.javaline 82 — the broken{@link #createTerminal()}reference is removed along with thependingEarlyLogsfield that was the subject of the link. Clean. - MavenITmng6065 replay ordering — the
pendingEarlyLogsdrain was dead code becausecreateTerminal()ran beforeactivateLogging()populated the field. The fix drains insideactivateLogging()after the logger swap, where the sink is already installed. Correct. - Javadoc for
AbstractMavenTransferListenerandAbstractUpgradeGoal— missing tags added. No concerns.
3ea2643fcc — projectId() / mojoId() on LogEvent
Clean addition: two new default methods on the LogEvent interface backed by new record components in DefaultLogEvent. ProjectBuildLogAppender reads the thread-locals at capture time. Interface-safe (default methods, no breaking change). Good improvement for consumers downstream.
No new issues. All previously reported findings remain addressed.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 3ea2643f ("feat: carry projectId and mojoId in LogEvent").
The prior CHANGES_REQUESTED finding is confirmed addressed: @param content is present on trace(Supplier<String> content) in Log.java (line 91), and sequenceNumber() @return now documents {@code -1} if unavailable. One new finding on the overhead gap.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
|
Fixed the Windows CI failure in Root cause: In quiet mode (
The JLine terminal-init JUL events fired between steps 1 and 3, before Fix: In |
gnodet
left a comment
There was a problem hiding this comment.
Re-review of commits 3a363b335e ("fix: gate LOG_API_METADATA set/remove inside hasReportCapture() in DefaultLog"), ad8a390432 ("Address review: move withMetadata body inside hasReportCapture() for zero overhead in normal builds"), and 7134372471 ("fix: suppress JLine terminal-init DEBUG logs before activateLogging in quiet mode").
3a363b335e + ad8a390432 — ThreadLocal overhead finding
The finding is addressed: the entire withMetadata() body — Thread.currentThread().getId(), new LogApiMetadata(...) record allocation, LOG_API_METADATA.set(), and LOG_API_METADATA.remove() — is now inside the hasReportCapture() branch. The fast path is a direct logAction.run() with zero ThreadLocal overhead. Normal builds pay nothing; capture overhead is strictly opt-in. ✅
The two commits are slightly redundant (both touch the same method), but the final state in ad8a390432 is clean and correct.
7134372471 — Windows MavenITmng4387 fix
The root cause analysis in the commit message is correct: configureLogging() → createTerminal() → activateLogging() ordering meant JLine terminal-init JUL DEBUG events fired in a window where MavenJulHandler was not yet installed. The fix — eagerly calling MavenJulHandler.install() + setting JUL root to SEVERE inside configureLogging() when quiet mode is active — closes that window cleanly.
The isInstalled() guard prevents double-installation. Level.SEVERE is the correct choice (matches SLF4J ERROR threshold exactly). The existing activateLogging() path remains correct and idempotent.
One observation (not blocking):
java.util.logging.LogManager FQCN — the fix uses the fully-qualified name inline; the file already imports Level and Logger from java.util.logging elsewhere. Adding import java.util.logging.LogManager; and using the short form would be consistent, but this is a style nit and does not affect correctness.
All prior findings remain addressed. The logging foundation is solid.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 3ea2643fcc ("feat: carry projectId and mojoId in LogEvent").
Prior findings resolved:
- ✅
@param content/@param errortags ontrace(Supplier<String>)overloads — confirmed present at the current head (restored in222071d770). The priorCHANGES_REQUESTEDis fully addressed.
New content (this commit):
The projectId() / mojoId() additions are correct: default null, @Nullable-annotated, documented with examples, and covered by MachineBuildEventListenerTest (both absent-field and present-field cases). DefaultLogEvent's new convenience constructors correctly pass null, null for the new fields. FORKING_MOJO_ID symmetry with FORKING_PROJECT_ID is intact.
One nit on the new methods:
[low] Redundant @since 4.1.0 on methods of a @since 4.1.0 interface
LogEvent is already tagged @since 4.1.0 at the interface level (line 43). Adding @since 4.1.0 to projectId() (line 172) and mojoId() (line 185) implies these methods were added after the interface was introduced — which is the exact opposite of what you want to communicate. Either drop the method-level tags (they're redundant noise), or if you want to distinguish them from the original set, keep them and add similar tags to the earlier methods for consistency. The mixed state is misleading.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 7134372471 (fix: suppress JLine terminal-init DEBUG logs before activateLogging in quiet mode).
Previous CHANGES_REQUESTED findings — both resolved:
@param contentmissing fromtrace(Supplier<String>)— ✅ Fixed. Present at line 91 in the current head (@param content the message supplier).@param content+@param errormissing fromtrace(Supplier<String>, Throwable)— ✅ Fixed. Both tags present at lines 101–102.
New commit review:
The early MavenJulHandler.install() + Level.SEVERE assignment added to configureLogging() in the quiet-mode branch is correct. The sequence is:
configureLogging()callsLoggerFactory.getILoggerFactory()first (line 289) — SLF4J is already initialized beforeinstall()runs, so thecomputeIfAbsentreentrancy risk doesn't apply here.isInstalled()guard prevents double-installation whenactivateLogging()runs later.- Setting
SEVEREat the JUL root beforecreateTerminal()closes the exact race window: JLine's terminal-init events (JULFINE/FINER/INFO) are blocked at source, not at the SLF4J level filter which could be transiently atINFOfor freshly-created loggers. activateLogging()re-appliesSEVEREidempotently in quiet mode — harmless.- Non-quiet mode is unaffected: JUL root stays at its default
INFOduringcreateTerminal(), which already suppresses JLine'sFINE/FINER/FINESTevents naturally.
The fix is surgical and correctly targeted. The IT MavenITmng4387QuietLoggingTest covers the end-to-end scenario.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet
left a comment
There was a problem hiding this comment.
Re-review of commit 54177fcc4b ("Address review: drop redundant @SInCE 4.1.0 on projectId/mojoId, use short LogManager/Level imports").
Both findings from the prior review are confirmed addressed:
-
Redundant
@since 4.1.0onprojectId()andmojoId()— ✅ Dropped. The methods no longer carry method-level@sincetags; the interface-level@since 4.1.0onLogEventis sufficient and unambiguous. -
java.util.logging.LogManagerFQCN — ✅ Fixed. BothLevelandLogManagerare now imported at the top ofLookupInvoker.javaand used by short name throughout, consistent with the existingLoggerandLevelimports. Import ordering is correct.
No new issues. All prior findings across the full review history are addressed. The logging foundation is solid and ready.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
7134372 to
a004638
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit a004638e84 (force-push rebase, 2026-09-18 15:25).
Both prior findings are resolved:
-
@param content/@param errorrestored —trace(Supplier<String>)andtrace(Supplier<String>, Throwable)inLog.javanow carry the missing@paramtags that were dropped in the6ccbc27asquash. Both are present and consistent with the rest of the interface. -
Early JUL install in quiet mode —
configureLogging()now eagerly installsMavenJulHandlerand clamps the JUL root toSEVEREwhen-qis in effect. The root cause (JLine terminal-init JUL events leaking betweenconfigureLogging()andactivateLogging()) is correctly identified and the fix is safe:SEVEREonly suppresses events so it cannot trigger thecomputeIfAbsentreentrancy flood thatLevel.ALLwould.activateLogging()still sets the level idempotently.
Additionally, the @since 4.1.0 tags were removed from projectId() / mojoId() in LogEvent.java — reasonable de-duplication since the class-level @since 4.1.0 already covers all members.
All previously raised findings are addressed. No new issues.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit a004638e (squash-rebase onto master, 2026-09-18).
The single outstanding finding from the previous CHANGES_REQUESTED review (6ccbc27a) is confirmed addressed:
@param contentontrace(Supplier<String>)— restored. Javadoc now reads@param content the message supplier.@param content+@param errorontrace(Supplier<String>, Throwable)— restored. Both tags present, consistent with every other supplier-based overload in the interface.
All previously approved items (JUL quiet-mode at Level.SEVERE, pendingEarlyLogs drain in activateLogging(), IN_PUBLISH reentrancy guard, MavenStyleResolver catch block) remain intact in this squash. No new issues introduced.
Prior APPROVE stands.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit a004638e84 (squash-rebase onto master, 2026-09-18 15:25).
Prior CHANGES_REQUESTED finding — resolved:
- ✅
@param content/@param errortags ontrace(Supplier<String>)andtrace(Supplier<String>, Throwable)are present in the currentLog.java. The rebase regression from6ccbc27ais fixed.
Two new findings:
-
LogEvent.message()Javadoc@returncontains a copy-paste fromformattedMessage()— says "the formatted message" but should say "the log message". -
DefaultLogTest.logApiMetadataIsClearedAfterCall()does not activate report capture before exercising the path it claims to test.ProjectBuildLogAppender.hasReportCapture()returnsfalse(no capture installed), sowithMetadata()takes theelsebranch and never touches theThreadLocal. TheassertNullis trivially true regardless of thefinally { LOG_API_METADATA.remove() }block — the test proves nothing about the safety guarantee it documents.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| /** | ||
| * The log message, without level prefix or timestamp formatting. | ||
| * | ||
| * @return the formatted message, never {@code null} |
There was a problem hiding this comment.
Javadoc copy-paste: @return says "the formatted message" but this is message(), not formattedMessage(). Should read "the log message, never null".
| * @return the formatted message, never {@code null} | |
| * @return the log message, never {@code null} |
| log.info("test message"); | ||
|
|
||
| // After the call completes, metadata should be cleared | ||
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); |
There was a problem hiding this comment.
This assertion is trivially true and proves nothing about the finally { LOG_API_METADATA.remove() } path.
Before log.info("test message") is called, ProjectBuildLogAppender.hasReportCapture() returns false (no capture installed). withMetadata() therefore takes the else branch:
} else {
logAction.run(); // metadata never touched
}LOG_API_METADATA is never set, so assertNull always passes — even if the finally block were deleted.
To actually test the guarantee, activate capture first:
ProjectBuildLogAppender.setReportCapture(event -> {});
try {
log.info("test message");
assertNull(DefaultLog.getLogApiMetadata(),
"Metadata must be cleared after log call");
} finally {
ProjectBuildLogAppender.setReportCapture(null);
}
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit a004638e (force-push squash, 2026-09-18 15:25).
Both CHANGES_REQUESTED findings from 6ccbc27a are confirmed fixed:
@param contentintrace(Supplier<String>)— restored: Javadoc now reads@param content the message supplier, matching all other supplier-based overloads. ✅@param content+@param errorintrace(Supplier<String>, Throwable)— restored: both tags present, consistent with the rest of the interface. ✅
New code reviewed: quiet-mode early MavenJulHandler install in configureLogging()
The Windows CI fix is correct. The root-cause (JLine terminal-init JUL events firing between configureLogging() and activateLogging() before MavenJulHandler was installed) is properly diagnosed. The fix:
- Installs
MavenJulHandlerearly viaisInstalled()guard (idempotent —activateLogging()re-checks safely) ✅ - Sets JUL root to
Level.SEVEREimmediately — this is the correct JUL equivalent of SLF4JERRORand acts as the primary gate, blockingWARNING/INFO/DEBUGJUL events at source before they can reachisLevelEnabled()on any SLF4J logger that might still have a pre-reconfiguration level ✅ install()intentionally does NOT setLevel.ALL, avoiding theConcurrentHashMap.computeIfAbsentreentrancy flood during SLF4J bootstrap ✅activateLogging()redundantly setsLevel.SEVEREagain for quiet mode — harmless no-op, consistent
No new issues. Prior APPROVE stands.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
56a2502 to
98cb72b
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push rebase onto master, 2026-09-18).
All three findings from the previous CHANGES_REQUESTED review (6ccbc27a) are confirmed addressed:
@param contenttag restored —trace(Supplier<String>)atLog.javaline 91 now carries@param content the message supplier, matching the fix fromcaeeeb4that had been dropped in the squash.@param content+@param errortags restored —trace(Supplier<String>, Throwable)at lines 101–102 now carries both@paramtags.LogEvent.projectId()+mojoId()added to API interface — the interface now exposes these asdefault-returning-nullmethods (matching thethreadId()/sequenceNumber()pattern), andDefaultLogEventandProjectBuildLogAppender.accept()wire them through correctly. The memory-note requirement forfield("execution")inDefaultLogEventis satisfied.
The pom.xml property-name fix (version.maven-*-plugin → lifecycle.maven-*-plugin) is correct — aligns with plugin-versions.properties key names.
No new issues. Prior APPROVE stands.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push, 2026-09-18).
The pom.xml property-reference fix is correct — all 13 ${lifecycle.maven-*-plugin} references now match the <lifecycle.maven-*-plugin> property declarations (and the comment text was updated too). Version bumps look consistent.
Two pre-existing findings carry over from prior review (not yet fixed):
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| /** | ||
| * The log message, without level prefix or timestamp formatting. | ||
| * | ||
| * @return the formatted message, never {@code null} |
There was a problem hiding this comment.
[low] Javadoc copy-paste: @return says "the formatted message" but this is message(), not formattedMessage().
The field comment says "The log message, without level prefix or timestamp formatting" — the @return tag should match:
| * @return the formatted message, never {@code null} | |
| * @return the log message, never {@code null} |
| log.info("test message"); | ||
|
|
||
| // After the call completes, metadata should be cleared | ||
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); |
There was a problem hiding this comment.
[low] Trivially-true assertion — doesn't test the finally { remove() } path.
ProjectBuildLogAppender.hasReportCapture() returns false in unit tests (no capture installed), so withMetadata() takes the else branch:
} else {
logAction.run();
}LOG_API_METADATA.set() is never called → the ThreadLocal was never populated → assertNull trivially passes regardless of whether the finally { remove() } block is present or correct.
To actually exercise the cleanup path, install a no-op capture before the call:
void logApiMetadataIsClearedAfterCall() {
Logger mockLogger = mock(Logger.class);
when(mockLogger.isInfoEnabled()).thenReturn(true);
when(mockLogger.getName()).thenReturn("com.example.MyMojo");
// Install a capture so hasReportCapture() returns true,
// forcing withMetadata() through the set/walk/remove path
ProjectBuildLogAppender appender = mock(ProjectBuildLogAppender.class);
ProjectBuildLogAppender.setInstance(appender);
try {
DefaultLog log = new DefaultLog(mockLogger);
log.info("test message");
assertNull(DefaultLog.getLogApiMetadata(),
"LOG_API_METADATA must be removed in the finally block");
} finally {
ProjectBuildLogAppender.setInstance(null);
}
}(Exact API depends on how ProjectBuildLogAppender exposes its capture flag — adapt accordingly.)
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b (squash-rebase onto master, 2026-09-18).
The two @param content tags that were flagged in the previous CHANGES_REQUESTED review (on 6ccbc27a) are present in this commit — trace(Supplier<String> content) and trace(Supplier<String>, Throwable error) both carry their @param tags matching the other supplier-based overloads. All other items from prior reviews remain in place:
SEVEREfor quiet-mode JUL root level (notWARNING) ✓IN_PUBLISHreentrancy guard ✓@return the sequence number, or {@code -1} if unavailable✓- Fork-aware
FORKING_MOJO_ID✓ pendingEarlyLogsdrain inactivateLogging()✓- Null
loggerNameguard ✓ - Throwable rendering via
writeThrowable()✓
Three low-severity observations (carried from prior APPROVE — none blocking):
-
publishIsReentrantSafetest comment — the Javadoc says "Install a custom SLF4J logger that fires a second JUL event" but the test externally setsIN_PUBLISH=truevia reflection then callspublish(). The guard is still correctly exercised, but the comment misleads about the scenario being simulated. -
DefaultLog.child(name)blank-name guard —requireNonNullcovers null but not blank. A blank name produces a trailing-dot logger name. Harmless in practice but worth aPreconditions.checkArgument(!name.isBlank(), …)if the contract means to exclude blanks. -
LogEvent.formattedMessage()Javadoc — says "the fully formatted log line" but when a throwable is present,MavenSimpleLogger.write()appends the ANSI-styled stack trace, making it a multi-line block. Worth noting in the doc.
None of these block this PR. APPROVE stands.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push squash rebase onto master, 2026-09-18).
The @param content and @param error tags that were missing from trace(Supplier<String>) and trace(Supplier<String>, Throwable) in the previous squash (6ccbc27a) are restored in this commit — that finding is addressed.
One new low-severity finding on the new test.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| log.info("test message"); | ||
|
|
||
| // After the call completes, metadata should be cleared | ||
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); |
There was a problem hiding this comment.
[low] logApiMetadataIsClearedAfterCall() is trivially true — never exercises the cleanup path
The test calls log.info("test message") without setting reportCapture. Inside withMetadata(), the guard ProjectBuildLogAppender.hasReportCapture() returns false, so the method takes the else branch and calls logAction.run() directly — the LOG_API_METADATA ThreadLocal is never set. assertNull(getLogApiMetadata()) therefore passes trivially regardless of whether the finally { LOG_API_METADATA.remove(); } block is correct.
To actually test the cleanup, reportCapture must be set before the call (so hasReportCapture() returns true) and cleared afterward:
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); | |
| ProjectBuildLogAppender.setReportCapture(event -> {}); | |
| try { | |
| log.info("test message"); | |
| } finally { | |
| ProjectBuildLogAppender.setReportCapture(null); | |
| } | |
| // After the call completes, metadata should be cleared | |
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); |
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push rebase onto master, 2026-09-18).
One blocking bug introduced in this squash: the property key rename in plugin-versions.properties / PluginVersions.java is not matched by a corresponding rename in impl/maven-core/pom.xml, causing all lifecycle plugin version lookups to fail at startup.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| lifecycle.maven-site-plugin=${lifecycle.maven-site-plugin} | ||
| lifecycle.maven-surefire-plugin=${lifecycle.maven-surefire-plugin} | ||
| lifecycle.maven-war-plugin=${lifecycle.maven-war-plugin} | ||
| version.maven-clean-plugin=${version.maven-clean-plugin} |
There was a problem hiding this comment.
[high] Property key rename not matched in pom.xml — CI breaks with ExceptionInInitializerError
This file now uses version.maven-*-plugin keys, and PluginVersions.java looks up "version." + pluginArtifactId. But impl/maven-core/pom.xml (head commit 98cb72b4) still declares the properties under the old lifecycle.maven-*-plugin names:
<lifecycle.maven-clean-plugin>3.5.0</lifecycle.maven-clean-plugin>Maven's resource filtering substitutes ${version.maven-clean-plugin} only if a property named version.maven-clean-plugin exists in the POM. Since it doesn't, the placeholder is left unresolved, and PluginVersions.version() hits the version.startsWith("${") guard and throws ExceptionInInitializerError. This is confirmed by the CI failure:
Tests run: 14, Failures: 0, Errors: 14 — in LifecycleExecutorTest
at org.apache.maven.lifecycle.PluginVersions.version(PluginVersions.java:60)
at org.apache.maven.lifecycle.PluginVersions.<clinit>(PluginVersions.java:71)
Fix: rename the 13 <lifecycle.maven-*-plugin> properties in impl/maven-core/pom.xml to <version.maven-*-plugin>, and update the ${lifecycle.maven-*-plugin} expressions in the <pluginManagement> section to ${version.maven-*-plugin}. The versions-update profile (now removed) also referenced these names, but that's deleted in this PR.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push squash, 2026-09-18).
Previous CHANGES_REQUESTED fixed: Both @param tags for trace(Supplier<String>) and trace(Supplier<String>, Throwable) are present. The two new default methods projectId() and mojoId() on LogEvent are cleanly added with correct Javadoc. The pendingEarlyLogs drain refactor (moved back into activateLogging() now that createTerminal runs first) is correct — comments are consistent with doInvoke() call order.
One blocking issue: The squash has a rebase conflict in plugin-versions.properties that breaks CI — all lifecycle tests fail with ExceptionInInitializerError on class load.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| # POM properties (version.maven-<name>-plugin), making them visible to | ||
| # dependency-update bots such as Dependabot and Renovate. | ||
|
|
||
| lifecycle.maven-clean-plugin=${lifecycle.maven-clean-plugin} | ||
| lifecycle.maven-compiler-plugin=${lifecycle.maven-compiler-plugin} | ||
| lifecycle.maven-deploy-plugin=${lifecycle.maven-deploy-plugin} | ||
| lifecycle.maven-ear-plugin=${lifecycle.maven-ear-plugin} | ||
| lifecycle.maven-ejb-plugin=${lifecycle.maven-ejb-plugin} | ||
| lifecycle.maven-install-plugin=${lifecycle.maven-install-plugin} | ||
| lifecycle.maven-jar-plugin=${lifecycle.maven-jar-plugin} | ||
| lifecycle.maven-plugin-plugin=${lifecycle.maven-plugin-plugin} | ||
| lifecycle.maven-rar-plugin=${lifecycle.maven-rar-plugin} | ||
| lifecycle.maven-resources-plugin=${lifecycle.maven-resources-plugin} | ||
| lifecycle.maven-site-plugin=${lifecycle.maven-site-plugin} | ||
| lifecycle.maven-surefire-plugin=${lifecycle.maven-surefire-plugin} | ||
| lifecycle.maven-war-plugin=${lifecycle.maven-war-plugin} | ||
| version.maven-clean-plugin=${version.maven-clean-plugin} | ||
| version.maven-compiler-plugin=${version.maven-compiler-plugin} | ||
| version.maven-deploy-plugin=${version.maven-deploy-plugin} | ||
| version.maven-ear-plugin=${version.maven-ear-plugin} | ||
| version.maven-ejb-plugin=${version.maven-ejb-plugin} | ||
| version.maven-install-plugin=${version.maven-install-plugin} | ||
| version.maven-jar-plugin=${version.maven-jar-plugin} | ||
| version.maven-plugin-plugin=${version.maven-plugin-plugin} | ||
| version.maven-rar-plugin=${version.maven-rar-plugin} | ||
| version.maven-resources-plugin=${version.maven-resources-plugin} | ||
| version.maven-site-plugin=${version.maven-site-plugin} | ||
| version.maven-surefire-plugin=${version.maven-surefire-plugin} | ||
| version.maven-war-plugin=${version.maven-war-plugin} |
There was a problem hiding this comment.
[high] Rebase conflict: version.* key names don't match POM properties — CI fails on class load
The properties file references ${version.maven-*-plugin} placeholders, but the POM declares <lifecycle.maven-*-plugin> properties (e.g. <lifecycle.maven-ejb-plugin>3.3.0</lifecycle.maven-ejb-plugin>). Maven resource filtering substitutes ${lifecycle.maven-ejb-plugin} but leaves ${version.maven-ejb-plugin} as a literal string, so PluginVersions.version() throws:
ExceptionInInitializerError: plugin-versions.properties was not filtered at build time;
version.maven-ejb-plugin still contains placeholder: ${version.maven-ejb-plugin}
This brings down DefaultLifecycleRegistry$CleanLifecycle at static init time, causing 100+ test failures across maven-core.
The fix is to rename both the property file keys and the PluginVersions.java key prefix from version.* to lifecycle.*, aligning with the POM property names that master uses:
| # POM properties (version.maven-<name>-plugin), making them visible to | |
| # dependency-update bots such as Dependabot and Renovate. | |
| lifecycle.maven-clean-plugin=${lifecycle.maven-clean-plugin} | |
| lifecycle.maven-compiler-plugin=${lifecycle.maven-compiler-plugin} | |
| lifecycle.maven-deploy-plugin=${lifecycle.maven-deploy-plugin} | |
| lifecycle.maven-ear-plugin=${lifecycle.maven-ear-plugin} | |
| lifecycle.maven-ejb-plugin=${lifecycle.maven-ejb-plugin} | |
| lifecycle.maven-install-plugin=${lifecycle.maven-install-plugin} | |
| lifecycle.maven-jar-plugin=${lifecycle.maven-jar-plugin} | |
| lifecycle.maven-plugin-plugin=${lifecycle.maven-plugin-plugin} | |
| lifecycle.maven-rar-plugin=${lifecycle.maven-rar-plugin} | |
| lifecycle.maven-resources-plugin=${lifecycle.maven-resources-plugin} | |
| lifecycle.maven-site-plugin=${lifecycle.maven-site-plugin} | |
| lifecycle.maven-surefire-plugin=${lifecycle.maven-surefire-plugin} | |
| lifecycle.maven-war-plugin=${lifecycle.maven-war-plugin} | |
| version.maven-clean-plugin=${version.maven-clean-plugin} | |
| version.maven-compiler-plugin=${version.maven-compiler-plugin} | |
| version.maven-deploy-plugin=${version.maven-deploy-plugin} | |
| version.maven-ear-plugin=${version.maven-ear-plugin} | |
| version.maven-ejb-plugin=${version.maven-ejb-plugin} | |
| version.maven-install-plugin=${version.maven-install-plugin} | |
| version.maven-jar-plugin=${version.maven-jar-plugin} | |
| version.maven-plugin-plugin=${version.maven-plugin-plugin} | |
| version.maven-rar-plugin=${version.maven-rar-plugin} | |
| version.maven-resources-plugin=${version.maven-resources-plugin} | |
| version.maven-site-plugin=${version.maven-site-plugin} | |
| version.maven-surefire-plugin=${version.maven-surefire-plugin} | |
| version.maven-war-plugin=${version.maven-war-plugin} | |
| # POM properties (lifecycle.<name>-plugin), making them visible to | |
| # dependency-update bots such as Dependabot and Renovate. | |
| lifecycle.maven-clean-plugin=${lifecycle.maven-clean-plugin} | |
| lifecycle.maven-compiler-plugin=${lifecycle.maven-compiler-plugin} | |
| lifecycle.maven-deploy-plugin=${lifecycle.maven-deploy-plugin} | |
| lifecycle.maven-ear-plugin=${lifecycle.maven-ear-plugin} | |
| lifecycle.maven-ejb-plugin=${lifecycle.maven-ejb-plugin} | |
| lifecycle.maven-install-plugin=${lifecycle.maven-install-plugin} | |
| lifecycle.maven-jar-plugin=${lifecycle.maven-jar-plugin} | |
| lifecycle.maven-plugin-plugin=${lifecycle.maven-plugin-plugin} | |
| lifecycle.maven-rar-plugin=${lifecycle.maven-rar-plugin} | |
| lifecycle.maven-resources-plugin=${lifecycle.maven-resources-plugin} | |
| lifecycle.maven-site-plugin=${lifecycle.maven-site-plugin} | |
| lifecycle.maven-surefire-plugin=${lifecycle.maven-surefire-plugin} | |
| lifecycle.maven-war-plugin=${lifecycle.maven-war-plugin} |
Also update PluginVersions.java line String key = "version." + pluginArtifactId; → String key = "lifecycle." + pluginArtifactId; and the Javadoc comment in the same file.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit `98cb72b` (squash-rebase onto master, 2026-09-18).
Summary
The two `@param` regressions from `6ccbc27a` are fixed. Three correctness improvements since the last APPROVE (`65cff258`) are included in this squash: (1) `projectId()`/`mojoId()` added to `LogEvent` API and wired through `DefaultLogEvent`+`ProjectBuildLogAppender`, (2) `DefaultLog.withMetadata()` refactored to skip `ThreadLocal` setup entirely when no report capture is active, (3) the quiet-mode JUL pre-installation fix for `MavenITmng4387` on Windows.
However the squash carries a blocking rebase conflict: `plugin-versions.properties` and `PluginVersions.java` use the old `version.maven-` key scheme while master has moved to `lifecycle.maven-` — this is the direct cause of the CI `ExceptionInInitializerError` on all 100+ lifecycle tests.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b4 (force-push rebase onto master, 2026-09-18).
Correction of prior CHANGES_REQUESTED: The three CHANGES_REQUESTED reviews on this commit (16:25, 16:26, 16:29) were based on a false positive. The plugin-versions.properties / PluginVersions.java / pom.xml triple rename from lifecycle.maven-*-plugin → version.maven-*-plugin is internally consistent — all three files move together:
pom.xml:<lifecycle.maven-*-plugin>→<version.maven-*-plugin>✅plugin-versions.properties:lifecycle.maven-*-pluginkeys →version.maven-*-pluginkeys ✅PluginVersions.java:"lifecycle." + artifactId→"version." + artifactId✅
The bot confused the rename direction (master still uses lifecycle.*; the PR consistently uses version.*). No ExceptionInInitializerError — all 13 lifecycle plugin lookups resolve correctly.
All prior findings confirmed fixed:
- ✅
@param content/@param errorontrace(Supplier<String>)andtrace(Supplier<String>, Throwable)— present - ✅
projectId()/mojoId()default methods onLogEvent— cleanly added - ✅
DefaultLog.withMetadata()— ThreadLocal cost gated behindhasReportCapture() - ✅ Quiet-mode JUL pre-installation in
configureLogging()—MavenJulHandler.install()+Level.SEVEREbeforecreateTerminal() - ✅
PluginVersions.javakey scheme consistent withplugin-versions.propertiesandpom.xml
Two pre-existing low-severity observations (non-blocking, carried from prior review):
LogEvent.message()@returntag says "the formatted message" — copy-paste fromformattedMessage(). Should be "the log message".DefaultLogTest.logApiMetadataIsClearedAfterCall()doesn't activateProjectBuildLogAppenderreport capture, sowithMetadata()takes theelsebranch and never touches theThreadLocal. The assertionassertNull(getLogApiMetadata())is always true regardless of whetherremove()is called. Fix: callProjectBuildLogAppender.setReportCapture(true)before andfalseafter, or restructure to use aMockBuildEventListener.
Neither item is blocking. The logging foundation is sound.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| /** | ||
| * The log message, without level prefix or timestamp formatting. | ||
| * | ||
| * @return the formatted message, never {@code null} |
There was a problem hiding this comment.
[low] Javadoc copy-paste: @return says "the formatted message" but this is message(), not formattedMessage().
The field comment says "The log message, without level prefix or timestamp formatting" — the @return tag should match:
| * @return the formatted message, never {@code null} | |
| * @return the log message, never {@code null} |
| log.info("test message"); | ||
|
|
||
| // After the call completes, metadata should be cleared | ||
| assertNull(DefaultLog.getLogApiMetadata(), "Log API metadata should be cleared after the log call"); |
There was a problem hiding this comment.
[low] logApiMetadataIsClearedAfterCall() is trivially true — never exercises the cleanup path
The test calls log.info("test message") without activating report capture. Inside withMetadata(), ProjectBuildLogAppender.hasReportCapture() returns false, so it takes the else branch and calls logAction.run() directly — LOG_API_METADATA.set() and .remove() are never called. assertNull(getLogApiMetadata()) is always true whether or not the finally { remove() } block works.
To actually test the cleanup path, activate capture before the call:
ProjectBuildLogAppender.setReportCapture(consumer -> {});
try {
log.info("test message");
assertNull(DefaultLog.getLogApiMetadata(), "ThreadLocal must be cleared after call");
} finally {
ProjectBuildLogAppender.setReportCapture(null);
}Or use a @TempDir-based MockBuildEventListener if that's how capture is activated in tests.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 98cb72b (squash-rebase onto master, 2026-09-18).
CI is red: ExceptionInInitializerError in 100+ lifecycle tests — property key mismatch between impl/maven-core/pom.xml and plugin-versions.properties introduced by this squash-rebase. See inline comments.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
| version.maven-compiler-plugin=${version.maven-compiler-plugin} | ||
| version.maven-deploy-plugin=${version.maven-deploy-plugin} | ||
| version.maven-ear-plugin=${version.maven-ear-plugin} | ||
| version.maven-ejb-plugin=${version.maven-ejb-plugin} |
There was a problem hiding this comment.
[high] Property key mismatch — filtering placeholder will never be substituted, breaking 100+ lifecycle tests
This file now uses version.maven-*-plugin placeholders (e.g. ${version.maven-ejb-plugin} on this line), but impl/maven-core/pom.xml still defines the properties as <lifecycle.maven-ejb-plugin>3.3.0</lifecycle.maven-ejb-plugin> (new file line 43). Maven resource filtering can only substitute a placeholder if the property name in the POM exactly matches the ${...} expression in the template.
Because there is no <version.maven-ejb-plugin> property in the POM, the placeholder survives filtering as a literal string. PluginVersions.java then reads version.maven-ejb-plugin=\${version.maven-ejb-plugin} and throws:
ExceptionInInitializerError: plugin-versions.properties was not filtered at build time;
version.maven-ejb-plugin still contains placeholder: ${version.maven-ejb-plugin}
CI confirms: every test that touches PluginVersions fails with this error (LifecycleExecutorTest, BuildPlanCreatorTest, etc.).
Root cause: The squash-rebase onto master picked up commit ab8947053d (chore: add versions:update-properties workflow) which renamed the POM properties from version.* → lifecycle.*. This PR simultaneously reverts those POM names back to lifecycle.* (bumped versions from master) but also changes plugin-versions.properties and PluginVersions.java to use version.* — leaving the three files inconsistent.
Fix: In impl/maven-core/pom.xml, rename all 13 <lifecycle.maven-*-plugin> properties to <version.maven-*-plugin> (keeping the updated version numbers from master):
| version.maven-ejb-plugin=${version.maven-ejb-plugin} | |
| version.maven-ejb-plugin=${version.maven-ejb-plugin} |
| <lifecycle.maven-clean-plugin>3.5.0</lifecycle.maven-clean-plugin> | ||
| <lifecycle.maven-compiler-plugin>3.16.0</lifecycle.maven-compiler-plugin> | ||
| <lifecycle.maven-deploy-plugin>3.2.0</lifecycle.maven-deploy-plugin> | ||
| <lifecycle.maven-ear-plugin>3.4.0</lifecycle.maven-ear-plugin> | ||
| <lifecycle.maven-ejb-plugin>3.3.0</lifecycle.maven-ejb-plugin> | ||
| <lifecycle.maven-install-plugin>3.2.0</lifecycle.maven-install-plugin> | ||
| <lifecycle.maven-jar-plugin>3.5.1</lifecycle.maven-jar-plugin> | ||
| <lifecycle.maven-plugin-plugin>3.16.0</lifecycle.maven-plugin-plugin> | ||
| <lifecycle.maven-rar-plugin>3.1.0</lifecycle.maven-rar-plugin> | ||
| <lifecycle.maven-resources-plugin>3.5.0</lifecycle.maven-resources-plugin> | ||
| <lifecycle.maven-site-plugin>3.22.0</lifecycle.maven-site-plugin> | ||
| <lifecycle.maven-surefire-plugin>3.6.0</lifecycle.maven-surefire-plugin> | ||
| <lifecycle.maven-war-plugin>3.5.1</lifecycle.maven-war-plugin> |
There was a problem hiding this comment.
[high] POM property names must match plugin-versions.properties placeholders
This block defines <lifecycle.maven-*-plugin> properties, but plugin-versions.properties (changed in this same PR) uses ${version.maven-*-plugin} placeholders. Maven resource filtering looks up properties by exact name — these never match, so all 13 placeholders survive unresolved.
Rename all 13 properties here from lifecycle.* to version.* to align with the new plugin-versions.properties and PluginVersions.java:
| <lifecycle.maven-clean-plugin>3.5.0</lifecycle.maven-clean-plugin> | |
| <lifecycle.maven-compiler-plugin>3.16.0</lifecycle.maven-compiler-plugin> | |
| <lifecycle.maven-deploy-plugin>3.2.0</lifecycle.maven-deploy-plugin> | |
| <lifecycle.maven-ear-plugin>3.4.0</lifecycle.maven-ear-plugin> | |
| <lifecycle.maven-ejb-plugin>3.3.0</lifecycle.maven-ejb-plugin> | |
| <lifecycle.maven-install-plugin>3.2.0</lifecycle.maven-install-plugin> | |
| <lifecycle.maven-jar-plugin>3.5.1</lifecycle.maven-jar-plugin> | |
| <lifecycle.maven-plugin-plugin>3.16.0</lifecycle.maven-plugin-plugin> | |
| <lifecycle.maven-rar-plugin>3.1.0</lifecycle.maven-rar-plugin> | |
| <lifecycle.maven-resources-plugin>3.5.0</lifecycle.maven-resources-plugin> | |
| <lifecycle.maven-site-plugin>3.22.0</lifecycle.maven-site-plugin> | |
| <lifecycle.maven-surefire-plugin>3.6.0</lifecycle.maven-surefire-plugin> | |
| <lifecycle.maven-war-plugin>3.5.1</lifecycle.maven-war-plugin> | |
| <version.maven-clean-plugin>3.5.0</version.maven-clean-plugin> | |
| <version.maven-compiler-plugin>3.16.0</version.maven-compiler-plugin> | |
| <version.maven-deploy-plugin>3.2.0</version.maven-deploy-plugin> | |
| <version.maven-ear-plugin>3.4.0</version.maven-ear-plugin> | |
| <version.maven-ejb-plugin>3.3.0</version.maven-ejb-plugin> | |
| <version.maven-install-plugin>3.2.0</version.maven-install-plugin> | |
| <version.maven-jar-plugin>3.5.1</version.maven-jar-plugin> | |
| <version.maven-plugin-plugin>3.16.0</version.maven-plugin-plugin> | |
| <version.maven-rar-plugin>3.1.0</version.maven-rar-plugin> | |
| <version.maven-resources-plugin>3.5.0</version.maven-resources-plugin> | |
| <version.maven-site-plugin>3.22.0</version.maven-site-plugin> | |
| <version.maven-surefire-plugin>3.6.0</version.maven-surefire-plugin> | |
| <version.maven-war-plugin>3.5.1</version.maven-war-plugin> |
Also update the pluginManagement comment on line 284 (currently says lifecycle.maven-*-plugin) to say version.maven-*-plugin for consistency.
…enhancements - Add LogEvent interface (maven-api-core) with projectId/mojoId context fields - Add DefaultLogEvent record (maven-core) implementing LogEvent - Add MavenJulHandler (maven-core): bridge JUL→SLF4J for plugin logging - Enhance DefaultLog (maven-api-core): carry LOG_API_METADATA for mojo log capture - Add ProjectBuildLogAppender (maven-core): MDC-aware log sink feeding LogEvent stream - Suppress JLine terminal-init DEBUG logs before activateLogging in quiet mode
98cb72b to
473a044
Compare
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 473a044526 (force-push rebase onto master, 2026-09-18).
Both findings from the previous REQUEST_CHANGES review are confirmed addressed:
-
@param contenttag restored —trace(Supplier<String> content)now has@param content the message supplier, consistent with all other supplier-based overloads in the interface. -
@param content+@param errortags restored —trace(Supplier<String> content, Throwable error)now carries both@paramtags, matching the other supplier+throwable overloads.
New code in this commit reviewed and clean:
LogEvent.projectId()andmojoId()— properly@Nullable-annotated default methods with accurate Javadoc;DefaultLogEventrecord components wired correctly inProjectBuildLogAppender.accept()viaMOJO_ID.get().DefaultLog.withMetadata()refactor — the fast path (else { logAction.run(); }when report capture is off) correctly avoids all ThreadLocal and StackWalker cost.LookupInvokerdrain reorg — drain now happens inactivateLogging()aftercreateTerminal()has installedProjectBuildLogAppender; the ordering (drain into newSlf4jLoggerbeforecontext.logger = logger) is correct. ThefailOnSeveritymessage accumulates in the old logger and drains into the new one in the same call — no double-replay risk.configureLogging()earlySEVEREguard —MavenJulHandler.install()+Level.SEVEREon the JUL root logger in quiet mode is correctly placed beforecreateTerminal()runs.MavenJulHandlerTestcomment corrected to accurately describe the reentrancy simulation.
Two earlier non-blocking observations remain open (DefaultLog.child(name) blank-name guard; LogEvent.formattedMessage() Javadoc multi-line throwable note) — neither blocks this PR.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 473a0445 (force-push rebase, 2026-09-18).
Both findings from the CHANGES_REQUESTED review on 6ccbc27a are confirmed fixed:
-
@param content/@param errortags — restored inLog.javafor bothtrace(Supplier<String>)(line 93) andtrace(Supplier<String>, Throwable)(line 104). Matches every other supplier overload in the interface. ✅ -
version.*→lifecycle.*property rename — consistent acrosspom.xmlproperty declarations,plugin-versions.propertieskeys/values, andPluginVersions.version()key construction. ✅
New additions in this commit:
-
Windows quiet-mode JUL fix —
configureLogging()now eagerly installsMavenJulHandlerand sets JUL root toSEVEREin quiet mode, beforecreateTerminal()runs. This closes the race window where JLine terminal-init JUL events (fired betweenconfigureLogging()andactivateLogging()) leaked into quiet output. The approach is sound: SLF4J is already bootstrapped at this point (viaLoggerFactory.getILoggerFactory()earlier inconfigureLogging()), so the install doesn't trigger thecomputeIfAbsentreentrancy flood that installing before SLF4J bootstrap would.activateLogging()idempotently skips re-installation viaisInstalled(). ✅ -
LogEventAPI moved tomaven-api-core(org.apache.maven.api.build.report) —LogEventinterface promoted frommaven-internalto public API.projectId()andmojoId()default methods added.DefaultLogEventrecord updated with matching fields, populated inProjectBuildLogAppender.accept()from thePROJECT_ID/MOJO_IDThreadLocals. TheLogEventis now self-contained with execution context without requiring callers to correlate against lifecycle events. ✅ -
DefaultLog.withMetadata()optimization — ThreadLocal + StackWalker cost now fully avoided whenhasReportCapture()is false. The branch is clean: report-capture path sets and removes the ThreadLocal in try/finally; non-capture path callslogAction.run()directly.ProjectBuildLogAppender.accept()correctly falls through to null source metadata whengetLogApiMetadata()returns null (no-capture case). ✅ -
pendingEarlyLogsremoval — drain now happens directly inactivateLogging()aftercreateTerminal()has installed theProjectBuildLogAppenderlogSink. The field is gone fromLookupContext. Simpler and correct — no more dead-code drain increateTerminal(). ✅ -
MavenJulHandlerTestcomment fix — comment now accurately describes the test: simulating re-entrance by settingIN_PUBLISH=truedirectly, rather than the misleading "install a custom SLF4J logger that fires a second JUL event". ✅
One low-severity observation (not blocking):
In configureLogging(), there is a tiny window between MavenJulHandler.install() (which leaves JUL root level at the JUL default, typically INFO) and the subsequent LogManager.getLogManager().getLogger("").setLevel(Level.SEVERE). Any JUL event at INFO or WARNING fired in that two-line gap on a concurrent thread would pass through MavenJulHandler.publish(). In practice this is not exploitable (the startup is effectively single-threaded at this point and no concurrent threads are emitting JUL events yet), but the two lines could be reordered — set level first, then install — to eliminate even the theoretical gap. Not worth a follow-up PR; just noting it for completeness.
APPROVED. Both prior findings addressed, new changes correct.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
gnodet-bot
left a comment
There was a problem hiding this comment.
Re-review of commit 473a0445 (final squash, 2026-09-18T16:59:37Z).
The two @param tags flagged in the previous CHANGES_REQUESTED (on 6ccbc27a) are confirmed present in this squash:
trace(Supplier<String> content)—@param content the message supplierat line 91 ✅trace(Supplier<String> content, Throwable error)— both@param contentand@param errorat lines 101-102 ✅
All prior findings from previous review cycles are addressed. No new findings.
This review was generated by an AI agent, Hermès on behalf of @gnodet.
Summary
Master-only logging infrastructure additions, layered on top of the forward-port (#12929) of the shared Log API enhancements from 4.0.x (#12690).
What's included (master-only)
Structured LogEvent (
maven-api-core,maven-core)LogEvent/LogLevelAPI for structured log event representationsourceClassName(),sourceMethodName(),threadId()— populated for both Log API events (viaDefaultLog.withMetadata()) and JUL events (viaMavenJulHandler);null/-1for direct SLF4J loggingprojectId()andmojoId()— populated byProjectBuildLogAppenderfrom thePROJECT_ID/MOJO_IDthread-locals at event capture time, making eachLogEventself-contained without requiring callers to bracket againstmojo.started/mojo.succeededeventsCustom JUL Handler (
maven-logging)MavenJulHandlerreplacesSLF4JBridgeHandler— all JUL events always route through SLF4J so thatMavenSimpleLoggerproduces a consistentformattedMessage(with timestamp, logger name, and ANSI styling) regardless of originsourceClassName,sourceMethodName,threadId) is stashed in a ThreadLocal before the SLF4J call and read byProjectBuildLogAppenderduring the same synchronous call chain — no metadata is lostloggerNameguard per JUL spec (falls back to root logger)MavenJulHandler→ SLF4J — all converge on the same structuredLogEventStructured LogSink (
maven-logging,maven-core)MavenSimpleLogger.LogSink— structured callback with(level, loggerName, cleanMessage, formattedMessage, throwable)replacing the oldConsumer<String>sinkwrite()reuses the existingwriteThrowable()method instead of duplicating rendering logicProjectBuildLogAppenderproducesLogEventobjects (with source metadata when available) instead of raw stringsBuildEventListener.projectLogMessage()now takesLogEventinstead ofStringLog API metadata (
maven-core)DefaultLog.withMetadata()— captures caller class/method/thread via StackWalker, gated behindProjectBuildLogAppender.hasReportCapture()for zero overhead in normal builds (~1-5μs per-call cost only when build report capture is active)Shared with 4.0.x (via forward-port #12929)
The following changes are in the forward-port commit and are not duplicated in this PR's diff:
Log.trace()— default no-op methods preventingAbstractMethodErrorLog.child(name)— hierarchical sub-loggersmaven.mojo.id) — fork-aware save/restoreDefaultLogwarn bug fix +isXxxEnabled()guardsDefaultLogTest— 6 tests (warn regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compat)PR chain
mvnlogviewerRelated
maven-4.0.x(milestone 4.1.0)Test plan
DefaultLogTest— 6 tests: warn/supplier regression, metadata lifecycle, trace delegation, trace no-op, child logger, default trace backward compatMavenJulHandlerTest— 10 tests: parameterized JUL→SLF4J level mapping, FINEST→TRACE, CONFIG→INFO, metadata null checkMachineBuildEventListenerTest—testProjectLogMessageIncludesExecutionWhenMojoIdPresent: verifiesexecutionJSON field emitted whenmojoIdis set;testProjectLogMessageOmitsExecutionWhenMojoIdAbsent: verifies field absent for project-level log linesmvn test -pl impl/maven-core,impl/maven-logging— all tests pass