gnodet-bot commented on code in PR #13544:
URL: https://github.com/apache/maven/pull/13544#discussion_r4282888027


##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java:
##########
@@ -96,22 +96,86 @@ public void then(BuildPlan step) {
     }
 
     private Map<String, BuildStep> merge(Map<String, BuildStep> org, 
Map<String, BuildStep> add) {
-        // all new phases should be added after the existing ones
-        List<BuildStep> lasts =
-                org.values().stream().filter(b -> 
b.successors.isEmpty()).toList();
-        List<BuildStep> firsts =
-                add.values().stream().filter(b -> 
b.predecessors.isEmpty()).toList();
-        firsts.stream()
-                .filter(addNode -> !org.containsKey(addNode.name))
-                .forEach(addNode -> lasts.forEach(orgNode -> 
addNode.executeAfter(orgNode)));
-        add.forEach((name, node) -> org.merge(name, node, this::merge));
+        // When add contains infrastructure steps ($plan$, $setup$, 
$teardown$), this merge
+        // is from computeForkPlan (execution plan merged into a fork). In 
that case, use the
+        // original terminal-step chaining to avoid creating cycles with the 
frontier approach.
+        boolean isSegmentMerge = !add.containsKey(BuildStep.PLAN);

Review Comment:
   💡 **Nit:** The comment mentions `$plan$, $setup$, $teardown$` but only 
`$plan$` is checked. They are always added together so this is functionally 
correct, but the comment overstates the check.
   
   ```suggestion
           // When add contains the infrastructure $plan$ step, this merge is 
from
           // computeForkPlan (execution plan merged into a fork). In that 
case, use the
           // original terminal-step chaining to avoid creating cycles with the 
frontier approach.
           boolean isSegmentMerge = !add.containsKey(BuildStep.PLAN);
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/lifecycle/internal/concurrent/BuildPlan.java:
##########
@@ -96,22 +96,86 @@ public void then(BuildPlan step) {
     }
 
     private Map<String, BuildStep> merge(Map<String, BuildStep> org, 
Map<String, BuildStep> add) {
-        // all new phases should be added after the existing ones
-        List<BuildStep> lasts =
-                org.values().stream().filter(b -> 
b.successors.isEmpty()).toList();
-        List<BuildStep> firsts =
-                add.values().stream().filter(b -> 
b.predecessors.isEmpty()).toList();
-        firsts.stream()
-                .filter(addNode -> !org.containsKey(addNode.name))
-                .forEach(addNode -> lasts.forEach(orgNode -> 
addNode.executeAfter(orgNode)));
-        add.forEach((name, node) -> org.merge(name, node, this::merge));
+        // When add contains infrastructure steps ($plan$, $setup$, 
$teardown$), this merge
+        // is from computeForkPlan (execution plan merged into a fork). In 
that case, use the
+        // original terminal-step chaining to avoid creating cycles with the 
frontier approach.
+        boolean isSegmentMerge = !add.containsKey(BuildStep.PLAN);
+
+        if (isSegmentMerge) {
+            // Track which steps are currently skipped before the merge
+            Set<String> skippedInOrg = org.entrySet().stream()
+                    .filter(e -> e.getValue().skip.get())
+                    .map(Map.Entry::getKey)
+                    .collect(Collectors.toSet());
+
+            // The "active frontier" consists of non-skipped steps that have 
at least one
+            // skipped same-project successor. This is the boundary between 
the active and
+            // skipped regions — the right insertion point for new 
non-lifecycle steps
+            // (e.g., aggregator goals). Using the frontier avoids creating 
transitive
+            // dependencies on all lifecycle phases through wrapper steps like 
after:ALL.
+            List<BuildStep> activeFrontier = org.values().stream()
+                    .filter(b -> !b.skip.get())
+                    .filter(b -> b.successors.stream().anyMatch(s -> s.project 
== b.project && s.skip.get()))
+                    .toList();
+            if (activeFrontier.isEmpty()) {
+                activeFrontier = org.values().stream()
+                        .filter(b -> b.successors.isEmpty())
+                        .toList();
+            }

Review Comment:
   💡 **Nit:** The primary frontier computation filters `!b.skip.get()`, but 
this fallback doesn't. In the degenerate case where all steps are skipped, it 
would pick skipped terminal steps as insertion points. Consider filtering for 
consistency:
   
   ```suggestion
               if (activeFrontier.isEmpty()) {
                   activeFrontier = org.values().stream()
                           .filter(b -> !b.skip.get())
                           .filter(b -> b.successors.isEmpty())
                           .toList();
               }
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to