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

gnodet pushed a commit to branch backport/maven-4.0.x/pr-11140
in repository https://gitbox.apache.org/repos/asf/maven.git

commit 0e1faeac63e0eb6704e62ec7cac3dc58ed03a518
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Tue Oct 7 16:54:26 2025 +0200

    Allow repository URL interpolation with improved validation (#11140)
    
    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>

Reply via email to