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 f773e6665908..12e53b05aa78 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 @@ -598,6 +598,93 @@ String replaceCiFriendlyVersion(Map properties, String version) return version != null ? interpolator.interpolate(version, properties::get) : null; } + /** + * Get enhanced properties that include profile-aware property resolution. + * This method activates profiles to ensure that properties defined in profiles + * are available for CI-friendly version processing and repository URL interpolation. + * It also includes directory-related properties that may be needed during profile activation. + */ + private Map getEnhancedProperties(Model model, Path rootDirectory) { + Map properties = new HashMap<>(); + + // Add directory-specific properties first, as they may be needed for profile activation + 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 = rootDirectory.toString(); + String rootUri = rootDirectory.toUri().toString(); + properties.put("project.rootDirectory", root); + properties.put("project.rootDirectory.uri", rootUri); + } catch (IllegalStateException e) { + // Root directory not available, continue without it + } + + // Handle root vs non-root project properties with profile activation + if (!Objects.equals(rootDirectory, model.getProjectDirectory())) { + Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory); + if (rootModelPath != null) { + Model rootModel = derive(Sources.buildSource(rootModelPath)).readFileModel(); + properties.putAll(getPropertiesWithProfiles(rootModel, properties)); + } + } else { + properties.putAll(getPropertiesWithProfiles(model, properties)); + } + + return properties; + } + + /** + * Get properties from a model including properties from activated profiles. + * This performs lightweight profile activation to merge profile properties. + * + * @param model the model to get properties from + * @param baseProperties base properties (including directory properties) to include in profile activation context + */ + private Map getPropertiesWithProfiles(Model model, Map baseProperties) { + Map properties = new HashMap<>(); + + // Start with base properties (including directory properties) + properties.putAll(baseProperties); + + // Add model properties + properties.putAll(model.getProperties()); + + try { + // Create a profile activation context for this model with base properties available + DefaultProfileActivationContext profileContext = getProfileActivationContext(request, model); + + // Activate profiles and merge their properties + List activeProfiles = getActiveProfiles(model.getProfiles(), profileContext); + + for (Profile profile : activeProfiles) { + properties.putAll(profile.getProperties()); + } + } catch (Exception e) { + // If profile activation fails, log a warning but continue with base properties + // This ensures that CI-friendly versions still work even if profile activation has issues + logger.warn("Failed to activate profiles for CI-friendly version processing: {}", e.getMessage()); + logger.debug("Profile activation failure details", e); + } + + // User properties override everything + properties.putAll(session.getEffectiveProperties()); + + return properties; + } + + /** + * Convenience method for getting properties with profiles without additional base properties. + * This is a backward compatibility method that provides an empty base properties map. + */ + private Map getPropertiesWithProfiles(Model model) { + return getPropertiesWithProfiles(model, new HashMap<>()); + } + private void buildBuildPom() throws ModelBuilderException { // Retrieve and normalize the source path, ensuring it's non-null and in absolute form Path top = request.getSource().getPath(); @@ -1394,21 +1481,11 @@ Model doReadFileModel() throws ModelBuilderException { } } - // CI friendly version - // All expressions are interpolated using user properties and properties - // defined on the root project. - Map properties = new HashMap<>(); - if (!Objects.equals(rootDirectory, model.getProjectDirectory())) { - Path rootModelPath = modelProcessor.locateExistingPom(rootDirectory); - if (rootModelPath != null) { - Model rootModel = - derive(Sources.buildSource(rootModelPath)).readFileModel(); - properties.putAll(rootModel.getProperties()); - } - } else { - properties.putAll(model.getProperties()); - } - properties.putAll(session.getEffectiveProperties()); + // Enhanced property resolution with profile activation for CI-friendly versions and repository URLs + // This includes directory properties, profile properties, and user properties + Map properties = getEnhancedProperties(model, rootDirectory); + + // CI friendly version processing with profile-aware properties model = model.with() .version(replaceCiFriendlyVersion(properties, model.getVersion())) .parent( @@ -1419,22 +1496,8 @@ 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) { - } + + // Repository URL interpolation with the same profile-aware properties UnaryOperator callback = properties::get; model = model.with() .repositories(interpolateRepository(model.getRepositories(), callback)) diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java index a35835d128b3..51ba8b722308 100644 --- a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultModelBuilderTest.java @@ -122,6 +122,94 @@ public void testMergeRepositories() throws Exception { assertEquals("central", repositories.get(3).getId()); // default } + @Test + public void testCiFriendlyVersionWithProfiles() { + // Test case 1: Default profile should set revision to baseVersion+dev + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("ci-friendly-profiles"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertEquals("0.2.0+dev", result.getEffectiveModel().getVersion()); + + // Test case 2: Release profile should set revision to baseVersion only + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("ci-friendly-profiles"))) + .activeProfileIds(List.of("releaseBuild")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals("0.2.0", result.getEffectiveModel().getVersion()); + } + + @Test + public void testRepositoryUrlInterpolationWithProfiles() { + // Test case 1: Default properties should be used + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://default.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + + // Test case 2: Development profile should override repository URL + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .activeProfileIds(List.of("development")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://dev.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + + // Test case 3: Production profile should override repository URL + request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("repository-url-profiles"))) + .activeProfileIds(List.of("production")) + .build(); + result = builder.newSession().build(request); + assertNotNull(result); + assertEquals( + "http://prod.repo.com/repository/maven-public/", + result.getEffectiveModel().getRepositories().get(0).getUrl()); + } + + @Test + public void testDirectoryPropertiesInProfilesAndRepositories() { + // Test that directory properties (like ${project.basedir}) are available + // during profile activation and repository URL interpolation + ModelBuilderRequest request = ModelBuilderRequest.builder() + .session(session) + .requestType(ModelBuilderRequest.RequestType.BUILD_PROJECT) + .source(Sources.buildSource(getPom("directory-properties-profiles"))) + .activeProfileIds(List.of("local-repo")) + .build(); + ModelBuilderResult result = builder.newSession().build(request); + assertNotNull(result); + + // Verify CI-friendly version was resolved with profile properties + assertEquals("1.0.0-LOCAL", result.getEffectiveModel().getVersion()); + + // Verify repository URL was interpolated with directory properties from profile + String expectedUrl = + "file://" + getPom("directory-properties-profiles").getParent().toString() + "/local-repo"; + assertEquals( + expectedUrl, result.getEffectiveModel().getRepositories().get(0).getUrl()); + } + private Path getPom(String name) { return Paths.get("src/test/resources/poms/factory/" + name + ".xml").toAbsolutePath(); } diff --git a/impl/maven-impl/src/test/resources/poms/factory/ci-friendly-profiles.xml b/impl/maven-impl/src/test/resources/poms/factory/ci-friendly-profiles.xml new file mode 100644 index 000000000000..d1edb98f4e9a --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/ci-friendly-profiles.xml @@ -0,0 +1,43 @@ + + + + 4.1.0 + + org.apache.maven.test + ci-friendly-profiles-test + ${revision} + pom + + + 0.2.0 + ${baseVersion}+dev + + + + + releaseBuild + + ${baseVersion} + + + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/directory-properties-profiles.xml b/impl/maven-impl/src/test/resources/poms/factory/directory-properties-profiles.xml new file mode 100644 index 000000000000..ba87dbef97c0 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/directory-properties-profiles.xml @@ -0,0 +1,53 @@ + + + + 4.1.0 + + org.apache.maven.test + directory-properties-profiles-test + ${revision} + pom + + + 1.0.0 + ${baseVersion}-SNAPSHOT + http://default.repo.com + + + + + + local-repo + + ${baseVersion}-LOCAL + file://${project.basedir}/local-repo + + + + + + + test-repo + ${repo.url} + + + + diff --git a/impl/maven-impl/src/test/resources/poms/factory/repository-url-profiles.xml b/impl/maven-impl/src/test/resources/poms/factory/repository-url-profiles.xml new file mode 100644 index 000000000000..45c81ff5fbc7 --- /dev/null +++ b/impl/maven-impl/src/test/resources/poms/factory/repository-url-profiles.xml @@ -0,0 +1,56 @@ + + + + 4.1.0 + + org.apache.maven.test + repository-url-profiles-test + 1.0-SNAPSHOT + pom + + + http://default.repo.com + + + + + development + + http://dev.repo.com + + + + + production + + http://prod.repo.com + + + + + + + company-repo + ${repo.base.url}/repository/maven-public/ + + + + diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11196CIFriendlyProfilesTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11196CIFriendlyProfilesTest.java new file mode 100644 index 000000000000..0f1150a8ed27 --- /dev/null +++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITgh11196CIFriendlyProfilesTest.java @@ -0,0 +1,84 @@ +/* + * 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.util.Properties; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This is a test set for #11196. + * It verifies that changes to ${revision} in profiles propagate to the final project version. + * + * @author Apache Maven Team + */ +class MavenITgh11196CIFriendlyProfilesTest extends AbstractMavenIntegrationTestCase { + + MavenITgh11196CIFriendlyProfilesTest() { + super("[4.0.0-rc-4,)"); + } + + /** + * Verify that CI-friendly version resolution works correctly with profile properties. + * Without profile activation, the version should be "0.2.0+dev". + * + * @throws Exception in case of failure + */ + @Test + void testCiFriendlyVersionWithoutProfile() throws Exception { + File testDir = extractResources("/gh-11196-ci-friendly-profiles"); + + Verifier verifier = newVerifier(testDir.getAbsolutePath()); + verifier.setAutoclean(false); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Properties props = verifier.loadProperties("target/versions.properties"); + assertEquals("0.2.0+dev", props.getProperty("project.version")); + assertEquals("0.2.0+dev", props.getProperty("project.properties.revision")); + assertEquals("0.2.0", props.getProperty("project.properties.baseVersion")); + } + + /** + * Verify that CI-friendly version resolution works correctly with profile properties. + * With the releaseBuild profile activated, the version should be "0.2.0" (without +dev). + * + * @throws Exception in case of failure + */ + @Test + void testCiFriendlyVersionWithReleaseProfile() throws Exception { + File testDir = extractResources("/gh-11196-ci-friendly-profiles"); + + Verifier verifier = newVerifier(testDir.getAbsolutePath()); + verifier.setAutoclean(false); + verifier.addCliArgument("-PreleaseBuild"); + verifier.addCliArgument("validate"); + verifier.execute(); + verifier.verifyErrorFreeLog(); + + Properties props = verifier.loadProperties("target/release-profile.properties"); + assertEquals("0.2.0", props.getProperty("project.version")); + assertEquals("0.2.0", props.getProperty("project.properties.revision")); + assertEquals("0.2.0", props.getProperty("project.properties.baseVersion")); + } +} diff --git a/its/core-it-suite/src/test/resources/gh-11196-ci-friendly-profiles/pom.xml b/its/core-it-suite/src/test/resources/gh-11196-ci-friendly-profiles/pom.xml new file mode 100644 index 000000000000..d8573c3e3018 --- /dev/null +++ b/its/core-it-suite/src/test/resources/gh-11196-ci-friendly-profiles/pom.xml @@ -0,0 +1,95 @@ + + + + 4.1.0 + + org.apache.maven.its.mng11196 + ci-friendly-profiles-test + ${revision} + pom + + + 0.2.0 + ${baseVersion}+dev + + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + + reportVersions + + eval + + validate + + target/versions.properties + + project/version + project/properties/revision + project/properties/baseVersion + + + + + + + + + + + releaseBuild + + ${baseVersion} + + + + + org.apache.maven.its.plugins + maven-it-plugin-expression + 2.1-SNAPSHOT + + + reportReleaseProfile + + eval + + validate + + target/release-profile.properties + + project/version + project/properties/revision + project/properties/baseVersion + + + + + + + + + + +