This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch ci-issue-11796-default-phases
in repository https://gitbox.apache.org/repos/asf/maven.git

commit ce628236122c7a85c91447258f67aa96a9b33fae
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Apr 3 23:09:46 2026 +0200

    Fix #11796: Preserve cross-lifecycle default phase bindings from 
components.xml
    
    When a plugin registers a custom lifecycle via components.xml with
    <default-phases> binding goals to standard lifecycle phases (e.g.
    process-sources), these cross-lifecycle bindings were silently dropped
    during the legacy-to-API Lifecycle conversion. The wrap() method only
    created Phase objects for the custom lifecycle's own phases, losing
    any bindings to phases from other lifecycles.
    
    The fix separates v3phases() (used by computePhases() for phase
    ordering) from phases() (used by allPhases() for extracting plugin
    bindings). Cross-lifecycle phase bindings are now included in phases()
    so they survive the round-trip conversion back to a legacy Lifecycle,
    while v3phases() returns only the lifecycle's own phases to avoid
    polluting the phase-to-lifecycle map.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../internal/impl/DefaultLifecycleRegistry.java    | 138 +++++++++++++--------
 .../maven/lifecycle/DefaultLifecyclesTest.java     |  48 +++++++
 2 files changed, 131 insertions(+), 55 deletions(-)

diff --git 
a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
 
b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
index a0b9ff8a0a..a5532a69a3 100644
--- 
a/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/internal/impl/DefaultLifecycleRegistry.java
@@ -225,73 +225,101 @@ public String id() {
                     return lifecycle.getId();
                 }
 
+                @Override
+                public Collection<Phase> v3phases() {
+                    return buildOwnPhases();
+                }
+
                 @Override
                 public Collection<Phase> phases() {
+                    List<Phase> phases = new ArrayList<>(buildOwnPhases());
+                    // Include phases from getDefaultLifecyclePhases() that 
bind to phases
+                    // from other lifecycles (e.g. process-sources from the 
default lifecycle).
+                    // In Maven 3, <default-phases> could bind plugin goals to 
standard lifecycle
+                    // phases. These cross-lifecycle bindings must be 
preserved so they survive
+                    // the round-trip conversion back to a legacy Lifecycle.
+                    Map<String, LifecyclePhase> defaultPhases = 
lifecycle.getDefaultLifecyclePhases();
+                    if (defaultPhases != null) {
+                        Set<String> ownPhases = new 
HashSet<>(lifecycle.getPhases());
+                        for (String phaseName : defaultPhases.keySet()) {
+                            if (!ownPhases.contains(phaseName)) {
+                                phases.add(createPhase(phaseName, null));
+                            }
+                        }
+                    }
+                    return phases;
+                }
+
+                private List<Phase> buildOwnPhases() {
                     List<String> names = lifecycle.getPhases();
                     List<Phase> phases = new ArrayList<>();
                     for (int i = 0; i < names.size(); i++) {
                         String name = names.get(i);
                         String prev = i > 0 ? names.get(i - 1) : null;
-                        phases.add(new Phase() {
-                            @Override
-                            public String name() {
-                                return name;
-                            }
-
-                            @Override
-                            public List<Phase> phases() {
-                                return List.of();
-                            }
+                        phases.add(createPhase(name, prev));
+                    }
+                    return phases;
+                }
 
-                            @Override
-                            public Stream<Phase> allPhases() {
-                                return Stream.concat(
-                                        Stream.of(this), 
phases().stream().flatMap(Lifecycle.Phase::allPhases));
+                private Phase createPhase(String name, String prev) {
+                    return new Phase() {
+                        @Override
+                        public String name() {
+                            return name;
+                        }
+
+                        @Override
+                        public List<Phase> phases() {
+                            return List.of();
+                        }
+
+                        @Override
+                        public Stream<Phase> allPhases() {
+                            return Stream.concat(
+                                    Stream.of(this), 
phases().stream().flatMap(Lifecycle.Phase::allPhases));
+                        }
+
+                        @Override
+                        public List<Plugin> plugins() {
+                            Map<String, LifecyclePhase> lfPhases = 
lifecycle.getDefaultLifecyclePhases();
+                            LifecyclePhase phase = lfPhases != null ? 
lfPhases.get(name) : null;
+                            if (phase != null) {
+                                Map<String, Plugin> plugins = new 
LinkedHashMap<>();
+                                
DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase);
+                                return plugins.values().stream().toList();
                             }
+                            return List.of();
+                        }
 
-                            @Override
-                            public List<Plugin> plugins() {
-                                Map<String, LifecyclePhase> lfPhases = 
lifecycle.getDefaultLifecyclePhases();
-                                LifecyclePhase phase = lfPhases != null ? 
lfPhases.get(name) : null;
-                                if (phase != null) {
-                                    Map<String, Plugin> plugins = new 
LinkedHashMap<>();
-                                    
DefaultPackagingRegistry.parseLifecyclePhaseDefinitions(plugins, name, phase);
-                                    return plugins.values().stream().toList();
-                                }
+                        @Override
+                        public Collection<Link> links() {
+                            if (prev == null) {
                                 return List.of();
+                            } else {
+                                return List.of(new Link() {
+                                    @Override
+                                    public Kind kind() {
+                                        return Kind.AFTER;
+                                    }
+
+                                    @Override
+                                    public Pointer pointer() {
+                                        return new Pointer() {
+                                            @Override
+                                            public String phase() {
+                                                return prev;
+                                            }
+
+                                            @Override
+                                            public Type type() {
+                                                return Type.PROJECT;
+                                            }
+                                        };
+                                    }
+                                });
                             }
-
-                            @Override
-                            public Collection<Link> links() {
-                                if (prev == null) {
-                                    return List.of();
-                                } else {
-                                    return List.of(new Link() {
-                                        @Override
-                                        public Kind kind() {
-                                            return Kind.AFTER;
-                                        }
-
-                                        @Override
-                                        public Pointer pointer() {
-                                            return new Pointer() {
-                                                @Override
-                                                public String phase() {
-                                                    return prev;
-                                                }
-
-                                                @Override
-                                                public Type type() {
-                                                    return Type.PROJECT;
-                                                }
-                                            };
-                                        }
-                                    });
-                                }
-                            }
-                        });
-                    }
-                    return phases;
+                        }
+                    };
                 }
 
                 @Override
diff --git 
a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java
 
b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java
index 0b36378871..d5a3b9433c 100644
--- 
a/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java
+++ 
b/impl/maven-core/src/test/java/org/apache/maven/lifecycle/DefaultLifecyclesTest.java
@@ -23,18 +23,23 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
 import org.apache.maven.internal.impl.DefaultLifecycleRegistry;
 import org.apache.maven.internal.impl.DefaultLookup;
+import org.apache.maven.lifecycle.mapping.LifecyclePhase;
 import org.codehaus.plexus.PlexusContainer;
 import 
org.codehaus.plexus.component.repository.exception.ComponentLookupException;
 import org.codehaus.plexus.testing.PlexusTest;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -96,6 +101,49 @@ void testCustomLifecycle() throws ComponentLookupException {
         assertEquals("etl", dl.getLifeCycles().get(3).getId());
     }
 
+    @Test
+    void testCustomLifecycleWithCrossLifecycleDefaultPhases() throws 
ComponentLookupException {
+        // Simulates a plugin that registers a custom lifecycle via 
components.xml
+        // with <default-phases> binding goals to standard lifecycle phases 
(e.g. process-sources)
+        // rather than to phases of the custom lifecycle itself. This is the 
Maven 3 mechanism
+        // for extension plugins to bind goals to standard phases without 
requiring <executions>.
+        Map<String, LifecyclePhase> defaultPhases = new HashMap<>();
+        defaultPhases.put("process-sources", new 
LifecyclePhase("com.example:my-plugin:1.0:touch"));
+
+        Lifecycle customLifecycle = new Lifecycle("my-custom-lifecycle", 
Arrays.asList("custom-phase"), defaultPhases);
+
+        List<Lifecycle> myLifecycles = new ArrayList<>();
+        myLifecycles.add(customLifecycle);
+        myLifecycles.addAll(defaultLifeCycles.getLifeCycles());
+
+        Map<String, Lifecycle> lifeCycles = 
myLifecycles.stream().collect(Collectors.toMap(Lifecycle::getId, l -> l));
+        PlexusContainer mockedPlexusContainer = mock(PlexusContainer.class);
+        
when(mockedPlexusContainer.lookupMap(Lifecycle.class)).thenReturn(lifeCycles);
+
+        DefaultLifecycles dl = new DefaultLifecycles(
+                new DefaultLifecycleRegistry(
+                        List.of(new 
DefaultLifecycleRegistry.LifecycleWrapperProvider(mockedPlexusContainer))),
+                new DefaultLookup(mockedPlexusContainer));
+
+        Lifecycle resolved = dl.getLifeCycles().stream()
+                .filter(l -> "my-custom-lifecycle".equals(l.getId()))
+                .findFirst()
+                .orElseThrow();
+
+        // Cross-lifecycle default phase bindings must survive the round-trip 
conversion
+        Map<String, LifecyclePhase> resolvedDefaultPhases = 
resolved.getDefaultLifecyclePhases();
+        assertNotNull(resolvedDefaultPhases);
+        assertTrue(
+                resolvedDefaultPhases.containsKey("process-sources"),
+                "Cross-lifecycle binding to 'process-sources' should be 
preserved");
+
+        // The lifecycle's own phase list should NOT include cross-lifecycle 
phases
+        assertFalse(
+                resolved.getPhases().contains("process-sources"),
+                "Cross-lifecycle phase should not appear in the lifecycle's 
own phase list");
+        assertTrue(resolved.getPhases().contains("custom-phase"), "Lifecycle's 
own phase should be present");
+    }
+
     private Lifecycle getLifeCycleById(String id) {
         return defaultLifeCycles.getLifeCycles().stream()
                 .filter(l -> id.equals(l.getId()))

Reply via email to