Fix #12704: Do not report an independent, built module as skipped - #12705
Conversation
When the reactor is halted after a module fails, the concurrent builder turned every project that had not yet run its whole plan into a skipped one. The plan holds a step for every phase of the lifecycle, so a project that had already run everything the requested tasks asked of it still had empty steps left for the phases beyond them; those got skipped along with the rest, and the project was reported as ProjectSkipped even though it had built successfully and written its output. Base the outcome on the steps that carry mojo executions instead: a project that ran all of its work is reported as succeeded, one that still had work pending stays skipped, and a project whose setup never ran was never started and stays skipped as well. The accompanying log line always blamed a failed dependency, which is the only reason the legacy builder ever had. A halted project usually has no failed dependency at all, so tell the two cases apart and say which one it is.
There was a problem hiding this comment.
Pull request overview
Fixes #12704 by improving how the concurrent builder reports project outcomes when the reactor halts: projects that completed all requested mojo work are now reported as succeeded (not skipped), and the CLI “skipped” message is refined to avoid blaming dependencies when the build was stopped due to an unrelated failure.
Changes:
- Add
BuildStep.hasExecutions()to identify lifecycle-plan steps that actually contain mojo work. - In
BuildPlanExecutorTEARDOWN, compute “success vs skipped” based on completion of mojo-bearing steps plus whether the project’s SETUP actually ran. - In
ExecutionEventLogger, differentiate skip messaging between “dependency failed” vs “build stopped”; add unit tests for both reasons.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildStep.java | Adds hasExecutions() predicate to distinguish work steps from ordering/no-op steps. |
| impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlanExecutor.java | Adjusts TEARDOWN outcome logic to treat “all requested mojo work completed” as success, even if out-of-scope steps are later skipped due to halt. |
| impl/maven-cli/src/main/java/org/apache/maven/cling/event/ExecutionEventLogger.java | Updates skipped-project logging to choose an accurate reason instead of always blaming dependencies. |
| impl/maven-cli/src/test/java/org/apache/maven/cling/event/ExecutionEventLoggerTest.java | Adds unit tests covering the two skip reasons (dependency failure vs unrelated earlier failure). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| @Test | ||
| void testProjectSkippedBecauseTheBuildWasStopped() { |
There was a problem hiding this comment.
Same as the other thread: the "summaries not recorded yet" state cannot be observed here, because the failed project's BuildFailure is added before the halt or blacklist that makes any other project skippable. A test for it would have to assert behaviour for a state the builder never produces.
I have added unit tests for the two reachable cases already, and BuildStep.hasExecutions() is now covered by BuildStepTest in d9a7f69.
| private boolean dependsOnFailedProject(ExecutionEvent event) { | ||
| MavenSession session = event.getSession(); | ||
| MavenProject project = event.getProject(); | ||
| if (session == null || project == null || session.getProjectDependencyGraph() == null) { | ||
| return true; | ||
| } | ||
| MavenExecutionResult result = session.getResult(); | ||
| return result == null | ||
| || session.getProjectDependencyGraph().getUpstreamProjects(project, true).stream() | ||
| .anyMatch(upstream -> result.getBuildSummary(upstream) instanceof BuildFailure); | ||
| } |
There was a problem hiding this comment.
I checked this and the race it describes cannot happen.
A project can only be skipped after the failure that causes it, and handleBuildError records the failed project's summary before it halts or blacklists anything:
buildContext.getResult().addException(t);
buildContext.getResult().addBuildSummary(new BuildFailure(mavenProject, ...)); // first
...
buildContext.getReactorBuildStatus().blackList(mavenProject); // or halt(), afterThe legacy builder does the same in BuilderCommon.handleBuildError. So by the time any other project can observe the halt or the blacklist and reach its own TEARDOWN, the BuildFailure is already in the result.
Visibility across threads is covered too: DefaultMavenExecutionResult.buildSummaries is a Collections.synchronizedMap(new IdentityHashMap<>()), so the write happens-before the read.
The "cannot be established" fallback in the javadoc is about the session, the dependency graph or the result being absent altogether, not about a summary arriving late — those are the three null checks above the stream.
gnodet
left a comment
There was a problem hiding this comment.
Well-scoped fix for the regression introduced in MNG-8670. The logic correctly distinguishes completed-but-halted projects from genuinely skipped ones by checking only mojo-bearing steps (hasExecutions()), and the improved skip messages accurately attribute the cause (dependency failure vs. halt).
The fix handles edge cases correctly:
- Aggregator/POM-only modules: If SETUP ran but the project has no mojo-bearing steps, it reports as succeeded (started, nothing to do). If SETUP never ran, it stays skipped.
dependsOnFailedProject: Falls back to the original message wording when session state is unavailable, preserving backward compatibility.- Ordering: The failing project's
BuildFailureis added to the result before the skipped project's TEARDOWN fires, so the dependency check is reliable.
The PR description is exceptionally thorough — traces the root cause, explains the mechanism, documents before/after behavior, and scopes out remaining IT flakiness.
One minor note: hasExecutions() is a trivial public API addition (return !mojos.isEmpty()) with no dedicated unit test, but the behavior is covered indirectly by MavenITmng8648ProjectEventsTest.
This review was generated by an AI agent and may contain inaccuracies. Please verify all suggestions before applying.
Claude Code on behalf of gnodet
Fixes #12704.
1. The event
BuildPlanExecutor'sTEARDOWNdecided betweenProjectSucceededandProjectSkippedonallStepsExecuted, which required every step of the project's plan to have reachedEXECUTED. The plan holds a step for every phase of the lifecycle regardless of the requested tasks, so formvn compilea project keeps emptypackage,installanddeploysteps that normally run as no-ops. As soon as the reactor is halted those get skipped,allStepsExecutedturns false, and a project that had already finished everything asked of it is reported as skipped.The outcome is now based on the steps that actually carry mojo executions. A project that ran all of its work is reported as succeeded; one that still had mojo-bearing steps pending stays skipped. A project whose
SETUPstep never ran was never started at all, so it stays skipped too — this keeps aggregators and other modules with no work of their own from being reported as succeeded when they were genuinely never built.BuildStep.hasExecutions()is added for that predicate. (BuildStep.skip()looks like it was meant for this, but it has no callers, so out-of-scope phases are distinguishable only by having no mojos.)2. The message
ExecutionEventLogger.projectSkippedalways printed:That is the only reason the legacy builder ever produced, because
LifecycleModuleBuilderfiresProjectSkippedbeforeProjectStartedand only for projects that never started. The concurrent builder also fires it for projects stopped by an unrelated failure, where the sentence is simply untrue. The logger now checks whether any upstream project actually failed and says which of the two happened; when the answer cannot be established the existing wording is kept.Before / after
Two sibling modules with no dependency between them, plus one that does depend on the failing module.
slowhas 4000 generated sources so it is still compiling whenfailsbreaks:before:
after:
slowcompiles its 4001 sources, writes its 4000 class files and is reported as succeeded;dependentis still skipped, with the reason that is true for it. Reproduced 5 runs out of 5 in both directions.Testing
ExecutionEventLoggerTestcovering both skip reasons.MavenITmng8648ProjectEventsTestpasses against the patched distribution —subproject-aandsubproject-bsucceed,subproject-cfails,subproject-dis skipped and keeps the dependency message.impl/maven-coreandimpl/maven-cliare green (635 tests).Not addressed here
MavenITmng8648ProjectEventsTestremains timing-sensitive: ifsubproject-cfails beforesubproject-b's steps are scheduled at all,bis legitimately skipped and the assertion onProjectSucceededstill fails. That needs an ordering guarantee in the test rather than a change in core, so I left it out of this PR. It is the flake described in #12704.