diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java index b3356a22080b..2b6c65079649 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java @@ -1114,6 +1114,21 @@ private Model readParentLocally( try { ModelBuilderSessionState derived = derive(candidateSource); + + // Check GA match BEFORE readAsParentModel() which recursively resolves + // the candidate's parent chain and can trigger false cycle detection (GH-12074). + Model fileModel = derived.readFileModel(); + String fileGroupId = getGroupId(fileModel); + String fileArtifactId = fileModel.getArtifactId(); + + if (fileGroupId == null + || !fileGroupId.equals(parent.getGroupId()) + || fileArtifactId == null + || !fileArtifactId.equals(parent.getArtifactId())) { + mismatchRelativePathAndGA(childModel, fileGroupId, fileArtifactId); + return null; + } + Model candidateModel = derived.readAsParentModel(profileActivationContext, parentChain); // Add profiles from parent, preserving model ID tracking for (Map.Entry> entry : @@ -1121,19 +1136,8 @@ private Model readParentLocally( addActivePomProfiles(entry.getKey(), entry.getValue()); } - String groupId = getGroupId(candidateModel); - String artifactId = candidateModel.getArtifactId(); String version = getVersion(candidateModel); - // Ensure that relative path and GA match, if both are provided - if (groupId == null - || !groupId.equals(parent.getGroupId()) - || artifactId == null - || !artifactId.equals(parent.getArtifactId())) { - mismatchRelativePathAndGA(childModel, groupId, artifactId); - return null; - } - if (version != null && parent.getVersion() != null && !version.equals(parent.getVersion())) { try { VersionRange parentRange = versionParser.parseVersionRange(parent.getVersion()); diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/ParentCycleDetectionTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/ParentCycleDetectionTest.java index f0db48c998e3..c133ffb1abbb 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/ParentCycleDetectionTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/ParentCycleDetectionTest.java @@ -187,6 +187,69 @@ void testDirectCycleDetection(@TempDir Path tempDir) throws IOException { } } + /** + * Reproduces GH-12074: dependency-reduced-pom.xml from shade plugin causes false parent cycle. + * + * When a POM's parent relative path resolves to a POM with a different GA (e.g., the + * default ".." from dependency-reduced-pom.xml resolves to a sibling project POM), the model + * builder should detect the GA mismatch early and skip local resolution — not recursively + * read the candidate's parents and trigger a false cycle. + */ + @Test + void testNoFalseCycleWhenRelativePathResolvesToWrongPom(@TempDir Path tempDir) throws IOException { + Files.createDirectories(tempDir.resolve(".mvn")); + + // Root POM found by resolving ".." from the project directory. + // It has a different GA than the expected parent but shares the same parent reference. + Path rootPom = tempDir.resolve("pom.xml"); + Files.writeString(rootPom, """ + + 4.0.0 + + test + parent + 1.0 + + + root + + """); + + // Project POM in a subdirectory, referencing the same parent. + // Default relativePath ".." resolves to rootPom above, which has GA test:root (not test:parent). + Path projectPom = tempDir.resolve("project").resolve("pom.xml"); + Files.createDirectories(projectPom.getParent()); + Files.writeString(projectPom, """ + + 4.0.0 + + test + parent + 1.0 + + project + + """); + + // Use BUILD_EFFECTIVE to match what the shade plugin triggers via compat ProjectBuilder + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .source(Sources.buildSource(projectPom)) + .requestType(ModelBuilderRequest.RequestType.BUILD_EFFECTIVE) + .build(); + + try { + modelBuilder.newSession().build(request); + } catch (StackOverflowError error) { + fail("StackOverflowError — cycle detection not working"); + } catch (ModelBuilderException exception) { + // Parent not found externally is expected; a cycle error is not + if (exception.getMessage().contains("cycle")) { + fail("False parent cycle detected: " + exception.getMessage()); + } + } + } + @Test void testMultipleModulesWithSameParentDoNotCauseCycle(@TempDir Path tempDir) throws IOException { // Create .mvn directory to mark root