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 4aacaee410 Allow repository URL interpolation with improved validation
(#11140) (#11210)
4aacaee410 is described below
commit 4aacaee410949280d4662740fb894786a14fa59b
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Oct 8 13:19:19 2025 +0200
Allow repository URL interpolation with improved validation (#11140)
(#11210)
This commit enables repository URL interpolation in Maven 4 while
maintaining
backward compatibility and providing early validation of unresolved
expressions.
Repository URLs can now use expressions like ${env.REPO_URL} and
${project.basedir.uri}
which are interpolated during model building.
Key changes:
1. DefaultModelBuilder: Add repository URL interpolation during model
building
- Support for repositories, pluginRepositories, profiles, and
distributionManagement
- Provide basedir, project.basedir, project.basedir.uri,
project.rootDirectory,
and project.rootDirectory.uri properties for interpolation
- Enable environment variable and project property interpolation in
repository URLs
2. DefaultModelValidator: Validate interpolated repository URLs for
unresolved expressions
- Repository URL expressions are interpolated during model building
- After interpolation, any remaining ${...} expressions cause validation
errors
- Early failure during model validation provides clear error messages
3. CompatibilityFixStrategy: Remove repository disabling logic, replace with
informational logging for interpolated URLs
4. Add integration tests for repository URL interpolation:
- Test successful interpolation from environment variables and project
properties
- Test early failure when expressions cannot be resolved during model
building
The new approach enables legitimate use cases while providing early, clear
error
messages for unresolved expressions during the validate phase rather than
later
during repository resolution.
(cherry picked from commit 210dbdcb7e77b5bd549d2b6263a92cca4179ec2d)
# Conflicts:
#
impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelBuilder.java
#
impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
---
.../mvnup/goals/CompatibilityFixStrategy.java | 22 +---
.../maven/impl/model/DefaultModelBuilder.java | 79 +++++++++++++
.../maven/impl/model/DefaultModelValidator.java | 129 ++++++++++++---------
.../impl/model/DefaultModelValidatorTest.java | 19 ++-
.../repository-with-unsupported-expression.xml | 41 +++++++
.../it/MavenITgh11140RepoDmUnresolvedTest.java | 48 ++++++++
.../it/MavenITgh11140RepoInterpolationTest.java | 90 ++++++++++++++
.../resources/gh-11140-repo-dm-unresolved/pom.xml | 42 +++++++
.../resources/gh-11140-repo-interpolation/pom.xml | 51 ++++++++
9 files changed, 444 insertions(+), 77 deletions(-)
diff --git
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
index 426981a09a..69e09e0a0d 100644
---
a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
+++
b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvnup/goals/CompatibilityFixStrategy.java
@@ -33,7 +33,6 @@
import org.apache.maven.api.di.Singleton;
import org.apache.maven.cling.invoker.mvnup.UpgradeContext;
import org.jdom2.Attribute;
-import org.jdom2.Comment;
import org.jdom2.Content;
import org.jdom2.Document;
import org.jdom2.Element;
@@ -498,25 +497,12 @@ private boolean fixRepositoryExpressions(Element
repositoriesElement, Namespace
Element urlElement = repository.getChild("url", namespace);
if (urlElement != null) {
String url = urlElement.getTextTrim();
- if (url.contains("${")
- && !url.contains("${project.basedir}")
- && !url.contains("${project.rootDirectory}")) {
+ if (url.contains("${")) {
+ // Allow repository URL interpolation; do not disable.
+ // Keep a gentle warning to help users notice unresolved
placeholders at build time.
String repositoryId = getChildText(repository, "id",
namespace);
- context.warning("Found unsupported expression in " +
elementType + " URL (id: " + repositoryId
+ context.info("Detected interpolated expression in " +
elementType + " URL (id: " + repositoryId
+ "): " + url);
- context.warning(
- "Maven 4 only supports ${project.basedir} and
${project.rootDirectory} expressions in repository URLs");
-
- // Comment out the problematic repository
- Comment comment =
- new Comment(" Repository disabled due to
unsupported expression in URL: " + url + " ");
- Element parent = repository.getParentElement();
- parent.addContent(parent.indexOf(repository), comment);
- removeElementWithFormatting(repository);
-
- context.detail("Fixed: " + "Commented out " + elementType
+ " with unsupported URL expression (id: "
- + repositoryId + ")");
- fixed = true;
}
}
}
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 9ab2505871..f773e66659 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
@@ -42,6 +42,7 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
@@ -63,12 +64,15 @@
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement;
+import org.apache.maven.api.model.DeploymentRepository;
+import org.apache.maven.api.model.DistributionManagement;
import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent;
import org.apache.maven.api.model.Profile;
+import org.apache.maven.api.model.Repository;
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.Interpolator;
@@ -1415,6 +1419,29 @@ Model doReadFileModel() throws ModelBuilderException {
model.getParent().getVersion()))
: null)
.build();
+ // Interpolate repository URLs
+ if (model.getProjectDirectory() != null) {
+ String basedir = model.getProjectDirectory().toString();
+ String basedirUri =
model.getProjectDirectory().toUri().toString();
+ properties.put("basedir", basedir);
+ properties.put("project.basedir", basedir);
+ properties.put("project.basedir.uri", basedirUri);
+ }
+ try {
+ String root =
request.getSession().getRootDirectory().toString();
+ String rootUri =
+
request.getSession().getRootDirectory().toUri().toString();
+ properties.put("project.rootDirectory", root);
+ properties.put("project.rootDirectory.uri", rootUri);
+ } catch (IllegalStateException e) {
+ }
+ UnaryOperator<String> callback = properties::get;
+ model = model.with()
+
.repositories(interpolateRepository(model.getRepositories(), callback))
+
.pluginRepositories(interpolateRepository(model.getPluginRepositories(),
callback))
+ .profiles(map(model.getProfiles(),
this::interpolateRepository, callback))
+
.distributionManagement(interpolateRepository(model.getDistributionManagement(),
callback))
+ .build();
// Override model properties with user properties
Map<String, String> newProps = merge(model.getProperties(),
session.getUserProperties());
if (newProps != null) {
@@ -1445,6 +1472,41 @@ Model doReadFileModel() throws ModelBuilderException {
return model;
}
+ private DistributionManagement interpolateRepository(
+ DistributionManagement distributionManagement,
UnaryOperator<String> callback) {
+ return distributionManagement == null
+ ? null
+ : distributionManagement
+ .with()
+ .repository((DeploymentRepository)
+
interpolateRepository(distributionManagement.getRepository(), callback))
+ .snapshotRepository((DeploymentRepository)
+
interpolateRepository(distributionManagement.getSnapshotRepository(), callback))
+ .build();
+ }
+
+ private Profile interpolateRepository(Profile profile,
UnaryOperator<String> callback) {
+ return profile == null
+ ? null
+ : profile.with()
+
.repositories(interpolateRepository(profile.getRepositories(), callback))
+
.pluginRepositories(interpolateRepository(profile.getPluginRepositories(),
callback))
+ .build();
+ }
+
+ private List<Repository> interpolateRepository(List<Repository>
repositories, UnaryOperator<String> callback) {
+ return map(repositories, this::interpolateRepository, callback);
+ }
+
+ private Repository interpolateRepository(Repository repository,
UnaryOperator<String> callback) {
+ return repository == null
+ ? null
+ : repository
+ .with()
+ .url(interpolator.interpolate(repository.getUrl(),
callback))
+ .build();
+ }
+
/**
* Merges a list of model profiles with user-defined properties.
* For each property defined in both the model and user properties,
the user property value
@@ -2250,4 +2312,21 @@ Set<String> getContexts() {
return contexts;
}
}
+
+ private static <T, A> List<T> map(List<T> resources, BiFunction<T, A, T>
mapper, A argument) {
+ List<T> newResources = null;
+ if (resources != null) {
+ for (int i = 0; i < resources.size(); i++) {
+ T resource = resources.get(i);
+ T newResource = mapper.apply(resource, argument);
+ if (newResource != resource) {
+ if (newResources == null) {
+ newResources = new ArrayList<>(resources);
+ }
+ newResources.set(i, newResource);
+ }
+ }
+ }
+ return newResources;
+ }
}
diff --git
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
index 576ef1bf23..18be13c560 100644
---
a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
+++
b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultModelValidator.java
@@ -494,15 +494,6 @@ public void validateFileModel(Session s, Model m, int
validationLevel, ModelProb
validationLevel);
}
- validateRawRepositories(problems, m.getRepositories(),
"repositories.repository.", EMPTY, validationLevel);
-
- validateRawRepositories(
- problems,
- m.getPluginRepositories(),
- "pluginRepositories.pluginRepository.",
- EMPTY,
- validationLevel);
-
Build build = m.getBuild();
if (build != null) {
validate20RawPlugins(problems, build.getPlugins(),
"build.plugins.plugin.", EMPTY, validationLevel);
@@ -556,16 +547,6 @@ public void validateFileModel(Session s, Model m, int
validationLevel, ModelProb
validationLevel);
}
- validateRawRepositories(
- problems, profile.getRepositories(), prefix,
"repositories.repository.", validationLevel);
-
- validateRawRepositories(
- problems,
- profile.getPluginRepositories(),
- prefix,
- "pluginRepositories.pluginRepository.",
- validationLevel);
-
BuildBase buildBase = profile.getBuild();
if (buildBase != null) {
validate20RawPlugins(problems, buildBase.getPlugins(),
prefix, "plugins.plugin.", validationLevel);
@@ -635,6 +616,43 @@ public void validateRawModel(Session s, Model m, int
validationLevel, ModelProbl
parent);
}
}
+
+ if (validationLevel > VALIDATION_LEVEL_MINIMAL) {
+ validateRawRepositories(problems, m.getRepositories(),
"repositories.repository.", EMPTY, validationLevel);
+
+ validateRawRepositories(
+ problems,
+ m.getPluginRepositories(),
+ "pluginRepositories.pluginRepository.",
+ EMPTY,
+ validationLevel);
+
+ for (Profile profile : m.getProfiles()) {
+ String prefix = "profiles.profile[" + profile.getId() + "].";
+
+ validateRawRepositories(
+ problems, profile.getRepositories(), prefix,
"repositories.repository.", validationLevel);
+
+ validateRawRepositories(
+ problems,
+ profile.getPluginRepositories(),
+ prefix,
+ "pluginRepositories.pluginRepository.",
+ validationLevel);
+ }
+
+ DistributionManagement distMgmt = m.getDistributionManagement();
+ if (distMgmt != null) {
+ validateRawRepository(
+ problems, distMgmt.getRepository(),
"distributionManagement.repository.", "", true);
+ validateRawRepository(
+ problems,
+ distMgmt.getSnapshotRepository(),
+ "distributionManagement.snapshotRepository.",
+ "",
+ true);
+ }
+ }
}
private void validate30RawProfileActivation(ModelProblemCollector
problems, Activation activation, String prefix) {
@@ -1444,40 +1462,7 @@ private void validateRawRepositories(
Map<String, Repository> index = new HashMap<>();
for (Repository repository : repositories) {
- validateStringNotEmpty(
- prefix, prefix2, "id", problems, Severity.ERROR,
Version.V20, repository.getId(), null, repository);
-
- if (validateStringNotEmpty(
- prefix,
- prefix2,
- "[" + repository.getId() + "].url",
- problems,
- Severity.ERROR,
- Version.V20,
- repository.getUrl(),
- null,
- repository)) {
- // only allow ${basedir} and ${project.basedir}
- Matcher m =
EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
- while (m.find()) {
- String expr = m.group(1);
- if (!("basedir".equals(expr)
- || "project.basedir".equals(expr)
- || expr.startsWith("project.basedir.")
- || "project.rootDirectory".equals(expr)
- || expr.startsWith("project.rootDirectory."))) {
- addViolation(
- problems,
- Severity.ERROR,
- Version.V40,
- prefix + prefix2 + "[" + repository.getId() +
"].url",
- null,
- "contains an unsupported expression (only
expressions starting with 'project.basedir' or 'project.rootDirectory' are
supported).",
- repository);
- break;
- }
- }
- }
+ validateRawRepository(problems, repository, prefix, prefix2,
false);
String key = repository.getId();
@@ -1501,6 +1486,44 @@ private void validateRawRepositories(
}
}
+ private void validateRawRepository(
+ ModelProblemCollector problems,
+ Repository repository,
+ String prefix,
+ String prefix2,
+ boolean allowEmptyUrl) {
+ if (repository == null) {
+ return;
+ }
+ validateStringNotEmpty(
+ prefix, prefix2, "id", problems, Severity.ERROR, Version.V20,
repository.getId(), null, repository);
+
+ if (!allowEmptyUrl
+ && validateStringNotEmpty(
+ prefix,
+ prefix2,
+ "[" + repository.getId() + "].url",
+ problems,
+ Severity.ERROR,
+ Version.V20,
+ repository.getUrl(),
+ null,
+ repository)) {
+ // Check for uninterpolated expressions - these should have been
interpolated by now
+ Matcher matcher =
EXPRESSION_NAME_PATTERN.matcher(repository.getUrl());
+ if (matcher.find()) {
+ addViolation(
+ problems,
+ Severity.ERROR,
+ Version.V40,
+ prefix + prefix2 + "[" + repository.getId() + "].url",
+ null,
+ "contains an uninterpolated expression.",
+ repository);
+ }
+ }
+ }
+
private void validate20EffectiveRepository(
ModelProblemCollector problems, Repository repository, String
prefix, int validationLevel) {
if (repository != null) {
diff --git
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
index 6dc7a05851..9948814d4e 100644
---
a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
+++
b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelValidatorTest.java
@@ -336,7 +336,7 @@ void testEmptyPluginVersion() throws Exception {
@Test
void testMissingRepositoryId() throws Exception {
SimpleProblemCollector result =
- validateFile("missing-repository-id-pom.xml",
ModelValidator.VALIDATION_LEVEL_STRICT);
+ validateRaw("missing-repository-id-pom.xml",
ModelValidator.VALIDATION_LEVEL_STRICT);
assertViolations(result, 0, 4, 0);
@@ -855,16 +855,23 @@ void testParentVersionRELEASE() throws Exception {
@Test
void repositoryWithExpression() throws Exception {
SimpleProblemCollector result =
validateFile("raw-model/repository-with-expression.xml");
- assertViolations(result, 0, 1, 0);
- assertEquals(
- "'repositories.repository.[repo].url' contains an unsupported
expression (only expressions starting with 'project.basedir' or
'project.rootDirectory' are supported).",
- result.getErrors().get(0));
+ // Interpolation in repository URLs is allowed; unresolved
placeholders will fail later during resolution
+ assertViolations(result, 0, 0, 0);
}
@Test
void repositoryWithBasedirExpression() throws Exception {
SimpleProblemCollector result =
validateRaw("raw-model/repository-with-basedir-expression.xml");
- assertViolations(result, 0, 0, 0);
+ // This test runs on raw model without interpolation, so all
expressions appear uninterpolated
+ // In the real flow, supported expressions would be interpolated
before validation
+ assertViolations(result, 0, 3, 0);
+ }
+
+ @Test
+ void repositoryWithUnsupportedExpression() throws Exception {
+ SimpleProblemCollector result =
validateRaw("raw-model/repository-with-unsupported-expression.xml");
+ // Unsupported expressions should cause validation errors
+ assertViolations(result, 0, 1, 0);
}
@Test
diff --git
a/impl/maven-impl/src/test/resources/poms/validation/raw-model/repository-with-unsupported-expression.xml
b/impl/maven-impl/src/test/resources/poms/validation/raw-model/repository-with-unsupported-expression.xml
new file mode 100644
index 0000000000..ed61d566aa
--- /dev/null
+++
b/impl/maven-impl/src/test/resources/poms/validation/raw-model/repository-with-unsupported-expression.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.1.0</modelVersion>
+
+ <groupId>org.apache.maven.its.mng0000</groupId>
+ <artifactId>test</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <packaging>pom</packaging>
+
+ <name>Maven Integration Test :: Test</name>
+ <description>Test unsupported repository URL expressions that should cause
validation errors.</description>
+
+ <repositories>
+ <repository>
+ <id>repo-unsupported</id>
+ <url>${project.baseUri}/sdk/maven/repo</url>
+ </repository>
+ </repositories>
+
+</project>
diff --git
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoDmUnresolvedTest.java
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoDmUnresolvedTest.java
new file mode 100644
index 0000000000..b3a2929239
--- /dev/null
+++
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoDmUnresolvedTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * IT to assert unresolved placeholders cause failure when used.
+ */
+class MavenITgh11140RepoDmUnresolvedTest extends
AbstractMavenIntegrationTestCase {
+
+ MavenITgh11140RepoDmUnresolvedTest() {
+ super("(4.0.0-rc-3,)");
+ }
+
+ @Test
+ void testFailsOnUnresolvedPlaceholders() throws Exception {
+ File testDir = extractResources("/gh-11140-repo-dm-unresolved");
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+
+ try {
+ verifier.addCliArgument("validate");
+ verifier.execute();
+ } catch (VerificationException expected) {
+ // Expected to fail due to unresolved placeholders during model
validation
+ }
+ // We expect error mentioning uninterpolated expression
+ verifier.verifyTextInLog("contains an uninterpolated expression");
+ }
+}
diff --git
a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoInterpolationTest.java
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoInterpolationTest.java
new file mode 100644
index 0000000000..d354b33f2e
--- /dev/null
+++
b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11140RepoInterpolationTest.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.it;
+
+import java.io.File;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * ITs for repository/distributionManagement URL interpolation.
+ */
+class MavenITgh11140RepoInterpolationTest extends
AbstractMavenIntegrationTestCase {
+
+ MavenITgh11140RepoInterpolationTest() {
+ super("(4.0.0-rc-3,)");
+ }
+
+ @Test
+ void testInterpolationFromEnvAndProps() throws Exception {
+ File testDir = extractResources("/gh-11140-repo-interpolation");
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+
+ // Provide env vars consumed by POM via ${env.*}
+ Path base = testDir.toPath().toAbsolutePath();
+ String baseUri = getBaseUri(base);
+ verifier.setEnvironmentVariable("IT_REPO_BASE", baseUri);
+ verifier.setEnvironmentVariable("IT_DM_BASE", baseUri);
+
+ // Use a cheap goal that prints effective POM
+ verifier.addCliArgument("help:effective-pom");
+ verifier.execute();
+ verifier.verifyErrorFreeLog();
+
+ List<String> lines = verifier.loadLogLines();
+ // Expect resolved file:// URLs, not placeholders
+ assertTrue(lines.stream().anyMatch(s ->
s.contains("<id>envRepo</id>")), "envRepo present");
+ assertTrue(lines.stream().anyMatch(s -> s.contains("<url>" + baseUri +
"/repo</url>")), "envRepo url resolved");
+ assertTrue(lines.stream().anyMatch(s ->
s.contains("<id>propRepo</id>")), "propRepo present");
+ assertTrue(
+ lines.stream().anyMatch(s -> s.contains("<url>" + baseUri +
"/custom</url>")),
+ "propRepo url resolved via property");
+ assertTrue(lines.stream().anyMatch(s ->
s.contains("<id>distRepo</id>")), "distRepo present");
+ assertTrue(
+ lines.stream().anyMatch(s -> s.contains("<url>" + baseUri +
"/dist</url>")), "distRepo url resolved");
+ }
+
+ private static String getBaseUri(Path base) {
+ String baseUri = base.toUri().toString();
+ if (baseUri.endsWith("/")) {
+ baseUri = baseUri.substring(0, baseUri.length() - 1);
+ }
+ return baseUri;
+ }
+
+ @Test
+ void testUnresolvedPlaceholderFailsResolution() throws Exception {
+ File testDir = extractResources("/gh-11140-repo-interpolation");
+ Verifier verifier = newVerifier(testDir.getAbsolutePath());
+
+ // Do NOT set env vars, so placeholders stay
+ verifier.addCliArgument("validate");
+ try {
+ verifier.execute();
+ } catch (VerificationException expected) {
+ // Expected to fail due to unresolved placeholders during model
validation
+ }
+ // We expect error mentioning uninterpolated expression
+ verifier.verifyTextInLog("contains an uninterpolated expression");
+ }
+}
diff --git
a/its/core-it-suite/src/test/resources/gh-11140-repo-dm-unresolved/pom.xml
b/its/core-it-suite/src/test/resources/gh-11140-repo-dm-unresolved/pom.xml
new file mode 100644
index 0000000000..106bb79dc3
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11140-repo-dm-unresolved/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.1.0" root="true">
+ <groupId>org.apache.maven.its.repointerp</groupId>
+ <artifactId>repo-dm-unresolved</artifactId>
+ <version>1.0</version>
+ <packaging>pom</packaging>
+
+ <name>Maven Integration Test :: Unresolved placeholders must fail</name>
+ <description>Verify that unresolved placeholders in
repository/distributionManagement cause failure when used.</description>
+
+ <distributionManagement>
+ <repository>
+ <id>badDist</id>
+ <url>${env.MISSING_VAR}/dist</url>
+ </repository>
+ </distributionManagement>
+
+ <repositories>
+ <repository>
+ <id>badRepo</id>
+ <url>${env.MISSING_VAR}/repo</url>
+ </repository>
+ </repositories>
+</project>
diff --git
a/its/core-it-suite/src/test/resources/gh-11140-repo-interpolation/pom.xml
b/its/core-it-suite/src/test/resources/gh-11140-repo-interpolation/pom.xml
new file mode 100644
index 0000000000..5f07980e74
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/gh-11140-repo-interpolation/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+<project xmlns="http://maven.apache.org/POM/4.1.0" root="true">
+ <groupId>org.apache.maven.its.repointerp</groupId>
+ <artifactId>repo-interpolation</artifactId>
+ <version>1.0</version>
+ <packaging>pom</packaging>
+
+ <name>Maven Integration Test :: Repository and DistributionManagement URL
interpolation</name>
+ <description>Verify that repository and distributionManagement URLs are
interpolated from env and project properties.</description>
+
+ <distributionManagement>
+ <repository>
+ <id>distRepo</id>
+ <url>${env.IT_DM_BASE}/dist</url>
+ </repository>
+ </distributionManagement>
+
+ <properties>
+ <!-- Property sourced from env via model interpolation in test class -->
+ <customRepoUrl>${env.IT_REPO_BASE}/custom</customRepoUrl>
+ </properties>
+
+ <repositories>
+ <repository>
+ <id>envRepo</id>
+ <url>${env.IT_REPO_BASE}/repo</url>
+ </repository>
+ <repository>
+ <id>propRepo</id>
+ <url>${customRepoUrl}</url>
+ </repository>
+ </repositories>
+</project>