This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git
The following commit(s) were added to refs/heads/maven-4.0.x by this push:
new 8fffa0e526 [Backport 4.0.x] Fix mvnup spurious pluginManagement
injection for remote parent plugins (#12351)
8fffa0e526 is described below
commit 8fffa0e5268a6ae2670189567cb2a74c76bd0b83
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Jun 25 15:17:33 2026 +0200
[Backport 4.0.x] Fix mvnup spurious pluginManagement injection for remote
parent plugins (#12351)
* [#11606] Fix mvnup spurious pluginManagement injection for plugins
inherited from remote parent POM
When a project inherits from a remote parent POM, plugins declared only
in the remote parent's build/plugins or pluginManagement should not have
local pluginManagement entries injected by mvnup. The project does not
control the remote parent, so overriding its plugin versions locally is
spurious.
The fix collects all plugin keys declared in local POMs (build/plugins
and build/pluginManagement/plugins) and skips plugins in the effective
model analysis that are not locally declared — they come from remote
parents that the project does not control.
Plugins that ARE declared locally (even without a version, inheriting
from a remote parent's pluginManagement) are still eligible for
pluginManagement injection, since the project explicitly uses them.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
* Override remote parent plugins with comment instead of skipping
Instead of skipping plugins inherited from remote parent POMs,
mvnup now adds pluginManagement overrides with a comment
"Override version inherited from parent". This ensures Maven 4
incompatible plugin versions get upgraded even when they come
from a parent POM the project does not control.
Co-Authored-By: Claude Opus 4.6 <[email protected]>
---------
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../invoker/mvnup/goals/PluginUpgradeStrategy.java | 79 ++++++++++++++---
.../mvnup/goals/PluginUpgradeStrategyTest.java | 98 +++++++++++++++++++---
2 files changed, 153 insertions(+), 24 deletions(-)
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
index 3402b78c2f..8df694b2e2 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategy.java
@@ -26,6 +26,7 @@
import java.util.Set;
import java.util.stream.Collectors;
+import eu.maveniverse.domtrip.Comment;
import eu.maveniverse.domtrip.Document;
import eu.maveniverse.domtrip.Editor;
import eu.maveniverse.domtrip.Element;
@@ -132,6 +133,9 @@ public UpgradeResult doApply(UpgradeContext context,
Map<Path, Document> pomMap)
// Phase 2: For each POM, build effective model using the session
and analyze plugins
PluginAnalysisResults analysisResults =
analyzePluginsUsingEffectiveModels(context, pomMap, tempDir);
+ // Collect locally declared plugin keys so we can add comments for
remote-parent overrides
+ Set<String> localPluginKeys =
collectLocallyDeclaredPluginKeys(pomMap);
+
// Phase 3: Add plugin management and direct overrides to the last
local parent in hierarchy
for (Map.Entry<Path, Document> entry : pomMap.entrySet()) {
Path pomPath = entry.getKey();
@@ -151,8 +155,8 @@ public UpgradeResult doApply(UpgradeContext context,
Map<Path, Document> pomMap)
Set<String> pluginsForManagement =
analysisResults.pluginsNeedingManagement().get(pomPath);
if (pluginsForManagement != null &&
!pluginsForManagement.isEmpty()) {
- hasUpgrades |=
-
addPluginManagementForEffectivePlugins(context, pomDocument,
pluginsForManagement);
+ hasUpgrades |= addPluginManagementForEffectivePlugins(
+ context, pomDocument, pluginsForManagement,
localPluginKeys);
context.detail("Added plugin management to " + pomPath
+ " (target parent for "
+ pluginsForManagement.size() + " plugins)");
}
@@ -162,7 +166,8 @@ public UpgradeResult doApply(UpgradeContext context,
Map<Path, Document> pomMap)
Set<String> pluginsForDirectOverride =
analysisResults.pluginsNeedingDirectOverride().get(pomPath);
if (pluginsForDirectOverride != null &&
!pluginsForDirectOverride.isEmpty()) {
- hasUpgrades |= addDirectPluginOverrides(context,
pomDocument, pluginsForDirectOverride);
+ hasUpgrades |= addDirectPluginOverrides(
+ context, pomDocument,
pluginsForDirectOverride, localPluginKeys);
}
if (hasUpgrades) {
@@ -697,7 +702,7 @@ private Path findParentInPomMap(Parent parent, Map<Path,
Document> pomMap) {
* Adds plugin management entries for plugins found through effective
model analysis.
*/
private boolean addPluginManagementForEffectivePlugins(
- UpgradeContext context, Document pomDocument, Set<String>
pluginKeys) {
+ UpgradeContext context, Document pomDocument, Set<String>
pluginKeys, Set<String> localPluginKeys) {
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
boolean hasUpgrades = false;
@@ -728,7 +733,8 @@ private boolean addPluginManagementForEffectivePlugins(
if (upgrade != null) {
// Check if plugin is already managed
if (!isPluginAlreadyManagedInElement(managedPluginsElement,
upgrade)) {
- addPluginManagementEntryFromUpgrade(managedPluginsElement,
upgrade, context);
+ boolean fromRemoteParent =
!localPluginKeys.contains(pluginKey);
+ addPluginManagementEntryFromUpgrade(managedPluginsElement,
upgrade, context, fromRemoteParent);
hasUpgrades = true;
}
}
@@ -762,12 +768,18 @@ private boolean isPluginAlreadyManagedInElement(Element
pluginsElement, PluginUp
* Adds a plugin management entry from a PluginUpgrade.
*/
private void addPluginManagementEntryFromUpgrade(
- Element managedPluginsElement, PluginUpgrade upgrade,
UpgradeContext context) {
- // Create plugin element using DomUtils convenience method for proper
formatting
- DomUtils.createPlugin(managedPluginsElement, upgrade.groupId(),
upgrade.artifactId(), upgrade.minVersion());
-
- context.detail("Added plugin management for " + upgrade.groupId() +
":" + upgrade.artifactId() + " version "
- + upgrade.minVersion() + " (found through effective model
analysis)");
+ Element managedPluginsElement, PluginUpgrade upgrade,
UpgradeContext context, boolean fromRemoteParent) {
+ Element plugin = DomUtils.createPlugin(
+ managedPluginsElement, upgrade.groupId(),
upgrade.artifactId(), upgrade.minVersion());
+
+ if (fromRemoteParent) {
+ managedPluginsElement.insertChildBefore(plugin, Comment.of("
Override version inherited from parent "));
+ context.detail("Added plugin management for " + upgrade.groupId()
+ ":" + upgrade.artifactId() + " version "
+ + upgrade.minVersion() + " (overrides version inherited
from parent)");
+ } else {
+ context.detail("Added plugin management for " + upgrade.groupId()
+ ":" + upgrade.artifactId() + " version "
+ + upgrade.minVersion() + " (found through effective model
analysis)");
+ }
}
/**
@@ -775,7 +787,8 @@ private void addPluginManagementEntryFromUpgrade(
* This is necessary when a parent POM sets an explicit version in its
build/plugins
* that pluginManagement alone cannot override.
*/
- private boolean addDirectPluginOverrides(UpgradeContext context, Document
pomDocument, Set<String> pluginKeys) {
+ private boolean addDirectPluginOverrides(
+ UpgradeContext context, Document pomDocument, Set<String>
pluginKeys, Set<String> localPluginKeys) {
Map<String, PluginUpgrade> pluginUpgrades = getPluginUpgradesAsMap();
boolean hasUpgrades = false;
@@ -795,8 +808,12 @@ private boolean addDirectPluginOverrides(UpgradeContext
context, Document pomDoc
PluginUpgrade upgrade = pluginUpgrades.get(pluginKey);
if (upgrade != null) {
if (!isPluginAlreadyManagedInElement(pluginsElement, upgrade))
{
- DomUtils.createPlugin(
+ Element plugin = DomUtils.createPlugin(
pluginsElement, upgrade.groupId(),
upgrade.artifactId(), upgrade.minVersion());
+ if (!localPluginKeys.contains(pluginKey)) {
+ pluginsElement.insertChildBefore(
+ plugin, Comment.of(" Override version
inherited from parent "));
+ }
hasUpgrades = true;
context.detail("Added " + upgrade.groupId() + ":" +
upgrade.artifactId() + " version "
+ upgrade.minVersion()
@@ -808,6 +825,42 @@ private boolean addDirectPluginOverrides(UpgradeContext
context, Document pomDoc
return hasUpgrades;
}
+ private Set<String> collectLocallyDeclaredPluginKeys(Map<Path, Document>
pomMap) {
+ Set<String> localPluginKeys = new HashSet<>();
+ for (Document doc : pomMap.values()) {
+ Element root = doc.root();
+ Element buildElement = root.childElement(BUILD).orElse(null);
+ if (buildElement != null) {
+ Element pluginsElement =
buildElement.childElement(PLUGINS).orElse(null);
+ if (pluginsElement != null) {
+ collectPluginKeysFromElement(pluginsElement,
localPluginKeys);
+ }
+ Element pmElement =
buildElement.childElement(PLUGIN_MANAGEMENT).orElse(null);
+ if (pmElement != null) {
+ Element managedPluginsElement =
+ pmElement.childElement(PLUGINS).orElse(null);
+ if (managedPluginsElement != null) {
+ collectPluginKeysFromElement(managedPluginsElement,
localPluginKeys);
+ }
+ }
+ }
+ }
+ return localPluginKeys;
+ }
+
+ private void collectPluginKeysFromElement(Element pluginsElement,
Set<String> keys) {
+ pluginsElement.childElements(PLUGIN).forEach(pluginElement -> {
+ String groupId = getChildText(pluginElement, GROUP_ID);
+ String artifactId = getChildText(pluginElement, ARTIFACT_ID);
+ if (groupId == null && artifactId != null &&
artifactId.startsWith(MAVEN_PLUGIN_PREFIX)) {
+ groupId = DEFAULT_MAVEN_PLUGIN_GROUP_ID;
+ }
+ if (groupId != null && artifactId != null) {
+ keys.add(groupId + ":" + artifactId);
+ }
+ });
+ }
+
private record PluginAnalysis(Set<String> needsManagement, Set<String>
needsDirectOverride) {}
private record PluginAnalysisResults(
diff --git
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategyTest.java
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategyTest.java
index 3702a07e05..5315cf1ad8 100644
---
a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategyTest.java
+++
b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvnup/goals/PluginUpgradeStrategyTest.java
@@ -773,11 +773,12 @@ void shouldHaveValidPluginUpgradeDefinitions() throws
Exception {
class InheritedPluginDetectionTests {
@Test
- @DisplayName("should detect inherited plugins from remote parent POM
and add pluginManagement")
- void shouldDetectInheritedPluginsFromRemoteParent() throws Exception {
- // org.apache:apache:23 defines maven-enforcer-plugin:1.4.1 in
pluginManagement.
- // A child POM that inherits from this parent should get
pluginManagement overrides
- // added by mvnup for plugins that need Maven 4 compatibility
upgrades.
+ @DisplayName("should inject pluginManagement with comment for plugins
inherited from remote parent")
+ void shouldInjectPluginManagementWithCommentForRemoteParentPlugins()
throws Exception {
+ // org.apache:apache:23 defines maven-enforcer-plugin in
pluginManagement and
+ // build/plugins. A child POM that does NOT itself declare the
plugin should
+ // still get a pluginManagement override with a comment explaining
it overrides
+ // the parent, so that Maven 4 incompatible plugin versions get
upgraded.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
@@ -806,12 +807,18 @@ void shouldDetectInheritedPluginsFromRemoteParent()
throws Exception {
UpgradeResult result = strategy.doApply(context, pomMap);
assertTrue(result.success(), "Strategy should succeed");
- assertTrue(result.modifiedCount() > 0, "Should have added
plugin management for inherited plugins");
+ assertTrue(result.modifiedCount() > 0, "Should have modified
POM for remote parent plugin upgrade");
String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<pluginManagement>"),
+ "Should inject pluginManagement for plugins from
remote parent");
+ assertTrue(
+ xml.contains("Override version inherited from parent"),
+ "Should add comment explaining the override");
assertTrue(
xml.contains("<artifactId>maven-enforcer-plugin</artifactId>"),
- "Should add pluginManagement for maven-enforcer-plugin
inherited from parent");
+ "Should add pluginManagement for
maven-enforcer-plugin");
} finally {
try (var walk = Files.walk(tempDir)) {
walk.sorted(java.util.Comparator.reverseOrder()).forEach(p
-> {
@@ -825,12 +832,12 @@ void shouldDetectInheritedPluginsFromRemoteParent()
throws Exception {
}
@Test
- @DisplayName("should not add direct build/plugins override when plugin
version comes from pluginManagement")
- void shouldNotAddDirectOverrideWhenVersionFromPluginManagement()
throws Exception {
+ @DisplayName("should override remote parent plugin via
pluginManagement with comment, not direct build/plugins")
+ void shouldOverrideRemoteParentPluginViaPluginManagementWithComment()
throws Exception {
// org.apache:apache:23 has maven-enforcer-plugin in build/plugins
WITHOUT
// an explicit version — the version (1.4.1) comes from
pluginManagement.
- // In this case, adding a pluginManagement override in the child
is sufficient;
- // no direct build/plugins entry should be added for enforcer.
+ // In this case, adding a pluginManagement override with a comment
in the child
+ // is sufficient; no direct build/plugins entry should be added
for enforcer.
String pomXml = """
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
@@ -868,6 +875,11 @@ void
shouldNotAddDirectOverrideWhenVersionFromPluginManagement() throws Exceptio
.orElse("")));
assertTrue(hasEnforcerInPM, "Should have enforcer in
pluginManagement");
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("Override version inherited from parent"),
+ "Should add comment explaining the override");
+
// Verify NO direct build/plugins entry for enforcer (PM override
is sufficient)
Element buildPlugins = root.childElement("build")
.flatMap(b -> b.childElement("plugins"))
@@ -949,6 +961,70 @@ void
shouldNotDuplicatePluginInBuildPluginsWhenAlreadyDeclared() throws Exceptio
.orElse(null);
assertEquals("3.5.0", version, "Existing enforcer-plugin version
should be upgraded to 3.5.0");
}
+
+ @Test
+ @DisplayName("should inject pluginManagement when plugin is locally
declared without version")
+ void
shouldInjectPluginManagementForLocallyDeclaredPluginWithoutVersion() throws
Exception {
+ // Child POM explicitly declares maven-enforcer-plugin in
build/plugins without
+ // a version. The version comes from the remote parent's
pluginManagement.
+ // Since the child does declare the plugin, mvnup should add a
pluginManagement
+ // entry to override the inherited version.
+ String pomXml = """
+ <?xml version="1.0" encoding="UTF-8"?>
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache</groupId>
+ <artifactId>apache</artifactId>
+ <version>23</version>
+ </parent>
+ <groupId>org.example</groupId>
+ <artifactId>test-child</artifactId>
+ <version>1.0.0-SNAPSHOT</version>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-enforcer-plugin</artifactId>
+ </plugin>
+ </plugins>
+ </build>
+ </project>
+ """;
+
+ Path tempDir = Files.createTempDirectory("mvnup-test-");
+ try {
+ Files.createDirectories(tempDir.resolve(".mvn"));
+ Path pomPath = tempDir.resolve("pom.xml");
+ Files.writeString(pomPath, pomXml);
+
+ Document document = Document.of(pomXml);
+ Map<Path, Document> pomMap = Map.of(pomPath, document);
+
+ UpgradeContext context = createMockContext();
+ UpgradeResult result = strategy.doApply(context, pomMap);
+
+ assertTrue(result.success(), "Strategy should succeed");
+ assertTrue(
+ result.modifiedCount() > 0, "Should have added
pluginManagement for locally declared plugin");
+
+ String xml = DomUtils.toXml(document);
+ assertTrue(
+ xml.contains("<pluginManagement>"), "Should add
pluginManagement for locally declared plugin");
+ assertTrue(
+
xml.contains("<artifactId>maven-enforcer-plugin</artifactId>"),
+ "Should add pluginManagement for
maven-enforcer-plugin");
+ } finally {
+ try (var walk = Files.walk(tempDir)) {
+ walk.sorted(java.util.Comparator.reverseOrder()).forEach(p
-> {
+ try {
+ Files.delete(p);
+ } catch (IOException ignored) {
+ }
+ });
+ }
+ }
+ }
}
@Nested