From f6e0c1e00aae4812024741bf30b06e9b4509ae77 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Mon, 8 Sep 2025 19:09:11 +0000 Subject: [PATCH 1/2] Add CI optimizations for Maven 4.0 Automatically enable CI-specific optimizations when CI environment is detected: - Auto-enable batch mode (non-interactive) to prevent hanging on prompts - Auto-enable show-version for better CI build identification - Auto-enable show-errors for improved CI debugging Key features: - Respects explicit user options (user choices override CI defaults) - Works with existing CI detection infrastructure (GitHub Actions, Travis CI, Jenkins, etc.) - Provides informative logging about applied optimizations - Maintains full backward compatibility - Comprehensive test coverage with unit and integration tests Implementation: - CIAwareMavenOptions: Wrapper class providing CI-specific defaults - Modified MavenInvoker: Integrates CI-aware options with logging - Added comprehensive test suite covering all scenarios Addresses GitHub issue #11088 and provides additional valuable CI optimizations beyond just batch mode for improved CI/CD experience. --- .../invoker/mvn/CIAwareMavenOptions.java | 324 ++++++++++++++++++ .../maven/cling/invoker/mvn/MavenInvoker.java | 48 ++- .../invoker/mvn/CIAwareMavenOptionsTest.java | 173 ++++++++++ .../mvn/CIOptimizationsIntegrationTest.java | 207 +++++++++++ 4 files changed, 750 insertions(+), 2 deletions(-) create mode 100644 impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptions.java create mode 100644 impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptionsTest.java create mode 100644 impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptions.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptions.java new file mode 100644 index 000000000000..3057dc634415 --- /dev/null +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptions.java @@ -0,0 +1,324 @@ +/* + * 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.cling.invoker.mvn; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.UnaryOperator; + +import org.apache.maven.api.cli.ParserRequest; +import org.apache.maven.api.cli.cisupport.CIInfo; +import org.apache.maven.api.cli.mvn.MavenOptions; + +/** + * A wrapper around MavenOptions that provides CI-specific defaults when CI is detected. + * This class applies the following optimizations for CI environments: + * + * + * All CI optimizations can be overridden by explicit command-line flags. + * + * @since 4.0.0 + */ +public class CIAwareMavenOptions implements MavenOptions { + private final MavenOptions delegate; + private final CIInfo ciInfo; + private final boolean hasExplicitShowVersion; + private final boolean hasExplicitShowErrors; + private final boolean hasExplicitNonInteractive; + + public CIAwareMavenOptions(MavenOptions delegate, CIInfo ciInfo) { + this.delegate = delegate; + this.ciInfo = ciInfo; + + // Check if user has explicitly set these options + this.hasExplicitShowVersion = delegate.showVersion().isPresent(); + this.hasExplicitShowErrors = delegate.showErrors().isPresent(); + this.hasExplicitNonInteractive = delegate.nonInteractive().isPresent(); + } + + @Override + public String source() { + return "CI-aware(" + delegate.source() + ")"; + } + + @Override + public Optional showVersion() { + // If user explicitly set show-version, respect their choice + if (hasExplicitShowVersion) { + return delegate.showVersion(); + } + // In CI, default to showing version for better build identification + return Optional.of(true); + } + + @Override + public Optional showErrors() { + // If user explicitly set show-errors, respect their choice + if (hasExplicitShowErrors) { + return delegate.showErrors(); + } + // In CI, default to showing errors for better debugging + return Optional.of(true); + } + + @Override + public Optional nonInteractive() { + // If user explicitly set non-interactive/batch-mode, respect their choice + if (hasExplicitNonInteractive) { + return delegate.nonInteractive(); + } + // In CI, default to non-interactive mode (batch mode) + return Optional.of(true); + } + + // Delegate all other methods to the wrapped options + @Override + public Optional> userProperties() { + return delegate.userProperties(); + } + + @Override + public Optional showVersionAndExit() { + return delegate.showVersionAndExit(); + } + + @Override + public Optional quiet() { + return delegate.quiet(); + } + + @Override + public Optional verbose() { + return delegate.verbose(); + } + + @Override + public Optional failOnSeverity() { + return delegate.failOnSeverity(); + } + + @Override + public Optional forceInteractive() { + return delegate.forceInteractive(); + } + + @Override + public Optional altUserSettings() { + return delegate.altUserSettings(); + } + + @Override + public Optional altProjectSettings() { + return delegate.altProjectSettings(); + } + + @Override + public Optional altInstallationSettings() { + return delegate.altInstallationSettings(); + } + + @Override + public Optional altUserToolchains() { + return delegate.altUserToolchains(); + } + + @Override + public Optional altInstallationToolchains() { + return delegate.altInstallationToolchains(); + } + + @Override + public Optional logFile() { + return delegate.logFile(); + } + + @Override + public Optional rawStreams() { + return delegate.rawStreams(); + } + + @Override + public Optional color() { + return delegate.color(); + } + + @Override + public Optional offline() { + return delegate.offline(); + } + + @Override + public Optional help() { + return delegate.help(); + } + + @Override + public Optional alternatePomFile() { + return delegate.alternatePomFile(); + } + + @Override + public Optional> goals() { + return delegate.goals(); + } + + @Override + public Optional> activatedProfiles() { + return delegate.activatedProfiles(); + } + + @Override + public Optional suppressSnapshotUpdates() { + return delegate.suppressSnapshotUpdates(); + } + + @Override + public Optional strictChecksums() { + return delegate.strictChecksums(); + } + + @Override + public Optional relaxedChecksums() { + return delegate.relaxedChecksums(); + } + + @Override + public Optional failFast() { + return delegate.failFast(); + } + + @Override + public Optional failAtEnd() { + return delegate.failAtEnd(); + } + + @Override + public Optional failNever() { + return delegate.failNever(); + } + + @Override + public Optional resume() { + return delegate.resume(); + } + + @Override + public Optional resumeFrom() { + return delegate.resumeFrom(); + } + + @Override + public Optional> projects() { + return delegate.projects(); + } + + @Override + public Optional alsoMake() { + return delegate.alsoMake(); + } + + @Override + public Optional alsoMakeDependents() { + return delegate.alsoMakeDependents(); + } + + @Override + public Optional threads() { + return delegate.threads(); + } + + @Override + public Optional builder() { + return delegate.builder(); + } + + @Override + public Optional noTransferProgress() { + return delegate.noTransferProgress(); + } + + @Override + public Optional cacheArtifactNotFound() { + return delegate.cacheArtifactNotFound(); + } + + @Override + public Optional strictArtifactDescriptorPolicy() { + return delegate.strictArtifactDescriptorPolicy(); + } + + @Override + public Optional ignoreTransitiveRepositories() { + return delegate.ignoreTransitiveRepositories(); + } + + @Override + public Optional atFile() { + return delegate.atFile(); + } + + @Override + public Optional updateSnapshots() { + return delegate.updateSnapshots(); + } + + @Override + public Optional nonRecursive() { + return delegate.nonRecursive(); + } + + @Override + public MavenOptions interpolate(UnaryOperator callback) { + return new CIAwareMavenOptions((MavenOptions) delegate.interpolate(callback), ciInfo); + } + + @Override + public void warnAboutDeprecatedOptions(ParserRequest request, Consumer printWriter) { + delegate.warnAboutDeprecatedOptions(request, printWriter); + } + + @Override + public void displayHelp(ParserRequest request, Consumer printWriter) { + delegate.displayHelp(request, printWriter); + } + + /** + * Returns the CI information that triggered the CI-aware behavior. + * + * @return the CI information + */ + public CIInfo getCIInfo() { + return ciInfo; + } + + /** + * Returns the underlying delegate options. + * + * @return the delegate options + */ + public MavenOptions getDelegate() { + return delegate; + } +} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java index 426e30d8ec1e..fa4a36d68db3 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java @@ -38,6 +38,7 @@ import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.cli.InvokerRequest; import org.apache.maven.api.cli.Logger; +import org.apache.maven.api.cli.cisupport.CIInfo; import org.apache.maven.api.cli.mvn.MavenOptions; import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.api.services.Lookup; @@ -85,12 +86,23 @@ public MavenInvoker(Lookup protoLookup, @Nullable Consumer contex @Override protected MavenContext createContext(InvokerRequest invokerRequest) { - return new MavenContext( - invokerRequest, true, (MavenOptions) invokerRequest.options().orElse(null)); + MavenOptions options = (MavenOptions) invokerRequest.options().orElse(null); + + // Apply CI-specific defaults if CI is detected and not overridden by user + if (invokerRequest.ciInfo().isPresent() && options != null) { + options = new CIAwareMavenOptions(options, invokerRequest.ciInfo().get()); + } + + return new MavenContext(invokerRequest, true, options); } @Override protected int execute(MavenContext context) throws Exception { + // Log CI optimizations if applied + if (context.options() instanceof CIAwareMavenOptions ciOptions) { + logCIOptimizations(context, ciOptions); + } + MavenExecutionRequest request = prepareMavenExecutionRequest(); toolchains(context, request); populateRequest(context, context.lookup, request); @@ -613,4 +625,36 @@ protected void logSummary( logSummary(context, child, references, indent); } } + + /** + * Logs information about CI optimizations that have been applied. + */ + private void logCIOptimizations(MavenContext context, CIAwareMavenOptions ciOptions) { + CIInfo ciInfo = ciOptions.getCIInfo(); + MavenOptions delegate = ciOptions.getDelegate(); + + // Check which optimizations were applied + boolean appliedBatchMode = !delegate.nonInteractive().isPresent() + && ciOptions.nonInteractive().orElse(false); + boolean appliedShowVersion = + !delegate.showVersion().isPresent() && ciOptions.showVersion().orElse(false); + boolean appliedShowErrors = + !delegate.showErrors().isPresent() && ciOptions.showErrors().orElse(false); + + if (appliedBatchMode || appliedShowVersion || appliedShowErrors) { + context.logger.info("Applying CI optimizations for detected CI system: '" + ciInfo.name() + "'"); + + if (appliedBatchMode) { + context.logger.info(" - Enabled batch mode (non-interactive)"); + } + if (appliedShowVersion) { + context.logger.info(" - Enabled show-version for build identification"); + } + if (appliedShowErrors) { + context.logger.info(" - Enabled show-errors for better debugging"); + } + + context.logger.info("To disable CI optimizations, use --force-interactive or set explicit options."); + } + } } diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptionsTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptionsTest.java new file mode 100644 index 000000000000..41dbcddc12e1 --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIAwareMavenOptionsTest.java @@ -0,0 +1,173 @@ +/* + * 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.cling.invoker.mvn; + +import java.util.Optional; + +import org.apache.maven.api.cli.cisupport.CIInfo; +import org.apache.maven.api.cli.mvn.MavenOptions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link CIAwareMavenOptions}. + */ +class CIAwareMavenOptionsTest { + + private final CIInfo mockCIInfo = mock(CIInfo.class); + + @Test + void testCIOptimizationsAppliedWhenNotExplicitlySet() { + // Given: A delegate with no explicit CI-related options set + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.showVersion()).thenReturn(Optional.empty()); + when(delegate.showErrors()).thenReturn(Optional.empty()); + when(delegate.nonInteractive()).thenReturn(Optional.empty()); + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: CI optimizations should be applied + assertTrue(ciOptions.showVersion().orElse(false), "showVersion should be enabled in CI"); + assertTrue(ciOptions.showErrors().orElse(false), "showErrors should be enabled in CI"); + assertTrue(ciOptions.nonInteractive().orElse(false), "nonInteractive should be enabled in CI"); + } + + @Test + void testExplicitOptionsRespected() { + // Given: A delegate with explicit options set + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.showVersion()).thenReturn(Optional.of(false)); + when(delegate.showErrors()).thenReturn(Optional.of(false)); + when(delegate.nonInteractive()).thenReturn(Optional.of(false)); + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: Explicit user choices should be respected + assertFalse(ciOptions.showVersion().orElse(true), "Explicit showVersion=false should be respected"); + assertFalse(ciOptions.showErrors().orElse(true), "Explicit showErrors=false should be respected"); + assertFalse(ciOptions.nonInteractive().orElse(true), "Explicit nonInteractive=false should be respected"); + } + + @Test + void testMixedExplicitAndDefaultOptions() { + // Given: A delegate with some explicit options and some defaults + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.showVersion()).thenReturn(Optional.of(false)); // explicit + when(delegate.showErrors()).thenReturn(Optional.empty()); // default + when(delegate.nonInteractive()).thenReturn(Optional.empty()); // default + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: Mix of explicit and CI defaults + assertFalse(ciOptions.showVersion().orElse(true), "Explicit showVersion=false should be respected"); + assertTrue(ciOptions.showErrors().orElse(false), "showErrors should default to true in CI"); + assertTrue(ciOptions.nonInteractive().orElse(false), "nonInteractive should default to true in CI"); + } + + @Test + void testDelegationOfOtherMethods() { + // Given: A delegate with various options + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.quiet()).thenReturn(Optional.of(true)); + when(delegate.verbose()).thenReturn(Optional.of(false)); + when(delegate.offline()).thenReturn(Optional.of(true)); + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: Other methods should be delegated unchanged + assertEquals(Optional.of(true), ciOptions.quiet()); + assertEquals(Optional.of(false), ciOptions.verbose()); + assertEquals(Optional.of(true), ciOptions.offline()); + } + + @Test + void testSourceDescription() { + // Given: A delegate with a source + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.source()).thenReturn("CLI"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: Source should indicate CI-aware wrapper + assertEquals("CI-aware(CLI)", ciOptions.source()); + } + + @Test + void testGetCIInfo() { + // Given: A delegate and CI info + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: CI info should be accessible + assertSame(mockCIInfo, ciOptions.getCIInfo()); + } + + @Test + void testGetDelegate() { + // Given: A delegate + MavenOptions delegate = mock(MavenOptions.class); + when(delegate.source()).thenReturn("test"); + + // When: Creating CI-aware options + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + + // Then: Delegate should be accessible + assertSame(delegate, ciOptions.getDelegate()); + } + + @Test + void testInterpolate() { + // Given: A delegate that supports interpolation + MavenOptions delegate = mock(MavenOptions.class); + MavenOptions interpolatedDelegate = mock(MavenOptions.class); + when(delegate.source()).thenReturn("test"); + when(delegate.interpolate(any())).thenReturn(interpolatedDelegate); + when(interpolatedDelegate.source()).thenReturn("interpolated"); + + // When: Creating CI-aware options and interpolating + CIAwareMavenOptions ciOptions = new CIAwareMavenOptions(delegate, mockCIInfo); + MavenOptions interpolated = ciOptions.interpolate(s -> s); + + // Then: Should return new CI-aware options with interpolated delegate + assertInstanceOf(CIAwareMavenOptions.class, interpolated); + CIAwareMavenOptions ciInterpolated = (CIAwareMavenOptions) interpolated; + assertSame(interpolatedDelegate, ciInterpolated.getDelegate()); + assertSame(mockCIInfo, ciInterpolated.getCIInfo()); + } +} diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java new file mode 100644 index 000000000000..9b19e25f985a --- /dev/null +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java @@ -0,0 +1,207 @@ +/* + * 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.cling.invoker.mvn; + +import java.nio.file.Path; +import java.util.Map; + +import org.apache.maven.api.cli.InvokerRequest; +import org.apache.maven.api.cli.cisupport.CIInfo; +import org.apache.maven.api.cli.mvn.MavenOptions; +import org.apache.maven.cling.invoker.ProtoLookup; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Integration tests for CI optimizations in Maven. + */ +class CIOptimizationsIntegrationTest { + + @TempDir + Path tempDir; + + @Test + void testCIOptimizationsAppliedWhenCIDetected() throws Exception { + // Given: A mock CI environment + CIInfo mockCIInfo = mock(CIInfo.class); + when(mockCIInfo.name()).thenReturn("TestCI"); + when(mockCIInfo.message()).thenReturn("Test CI detected"); + + // Create a mock InvokerRequest with CI info + InvokerRequest mockRequest = mock(InvokerRequest.class); + when(mockRequest.ciInfo()).thenReturn(java.util.Optional.of(mockCIInfo)); + when(mockRequest.topDirectory()).thenReturn(tempDir); + when(mockRequest.rootDirectory()).thenReturn(java.util.Optional.of(tempDir)); + when(mockRequest.userProperties()).thenReturn(Map.of()); + when(mockRequest.systemProperties()).thenReturn(Map.of()); + when(mockRequest.cwd()).thenReturn(tempDir); + when(mockRequest.installationDirectory()).thenReturn(tempDir); + when(mockRequest.userHomeDirectory()).thenReturn(tempDir); + org.apache.maven.api.cli.ParserRequest mockParserRequest = mock(org.apache.maven.api.cli.ParserRequest.class); + when(mockParserRequest.logger()).thenReturn(mock(org.apache.maven.api.cli.Logger.class)); + when(mockRequest.parserRequest()).thenReturn(mockParserRequest); + + // Create mock options without explicit CI settings + MavenOptions mockOptions = mock(MavenOptions.class); + when(mockOptions.showVersion()).thenReturn(java.util.Optional.empty()); + when(mockOptions.showErrors()).thenReturn(java.util.Optional.empty()); + when(mockOptions.nonInteractive()).thenReturn(java.util.Optional.empty()); + when(mockOptions.forceInteractive()).thenReturn(java.util.Optional.empty()); + when(mockOptions.source()).thenReturn("CLI"); + when(mockRequest.options()).thenReturn(java.util.Optional.of(mockOptions)); + + // When: Creating MavenInvoker and context + MavenInvoker invoker = new MavenInvoker(ProtoLookup.builder().build(), null); + MavenContext context = invoker.createContext(mockRequest); + + // Then: Options should be CI-aware + assertInstanceOf(CIAwareMavenOptions.class, context.options()); + CIAwareMavenOptions ciOptions = (CIAwareMavenOptions) context.options(); + + // Verify CI optimizations are applied + assertTrue(ciOptions.showVersion().orElse(false), "showVersion should be enabled in CI"); + assertTrue(ciOptions.showErrors().orElse(false), "showErrors should be enabled in CI"); + assertTrue(ciOptions.nonInteractive().orElse(false), "nonInteractive should be enabled in CI"); + + // Verify CI info is preserved + assertSame(mockCIInfo, ciOptions.getCIInfo()); + } + + @Test + void testCIOptimizationsNotAppliedWhenNoCIDetected() throws Exception { + // Given: No CI environment + InvokerRequest mockRequest = mock(InvokerRequest.class); + when(mockRequest.ciInfo()).thenReturn(java.util.Optional.empty()); + when(mockRequest.topDirectory()).thenReturn(tempDir); + when(mockRequest.rootDirectory()).thenReturn(java.util.Optional.of(tempDir)); + when(mockRequest.userProperties()).thenReturn(Map.of()); + when(mockRequest.systemProperties()).thenReturn(Map.of()); + when(mockRequest.cwd()).thenReturn(tempDir); + when(mockRequest.installationDirectory()).thenReturn(tempDir); + when(mockRequest.userHomeDirectory()).thenReturn(tempDir); + org.apache.maven.api.cli.ParserRequest mockParserRequest2 = mock(org.apache.maven.api.cli.ParserRequest.class); + when(mockParserRequest2.logger()).thenReturn(mock(org.apache.maven.api.cli.Logger.class)); + when(mockRequest.parserRequest()).thenReturn(mockParserRequest2); + + // Create mock options + MavenOptions mockOptions = mock(MavenOptions.class); + when(mockOptions.showVersion()).thenReturn(java.util.Optional.empty()); + when(mockOptions.showErrors()).thenReturn(java.util.Optional.empty()); + when(mockOptions.nonInteractive()).thenReturn(java.util.Optional.empty()); + when(mockOptions.source()).thenReturn("CLI"); + when(mockRequest.options()).thenReturn(java.util.Optional.of(mockOptions)); + + // When: Creating MavenInvoker and context + MavenInvoker invoker = new MavenInvoker(ProtoLookup.builder().build(), null); + MavenContext context = invoker.createContext(mockRequest); + + // Then: Options should NOT be CI-aware + assertFalse( + context.options() instanceof CIAwareMavenOptions, + "Options should not be CI-aware when no CI is detected"); + assertSame(mockOptions, context.options()); + } + + @Test + void testExplicitOptionsOverrideCIDefaults() throws Exception { + // Given: A mock CI environment + CIInfo mockCIInfo = mock(CIInfo.class); + when(mockCIInfo.name()).thenReturn("TestCI"); + + InvokerRequest mockRequest = mock(InvokerRequest.class); + when(mockRequest.ciInfo()).thenReturn(java.util.Optional.of(mockCIInfo)); + when(mockRequest.topDirectory()).thenReturn(tempDir); + when(mockRequest.rootDirectory()).thenReturn(java.util.Optional.of(tempDir)); + when(mockRequest.userProperties()).thenReturn(Map.of()); + when(mockRequest.systemProperties()).thenReturn(Map.of()); + when(mockRequest.cwd()).thenReturn(tempDir); + when(mockRequest.installationDirectory()).thenReturn(tempDir); + when(mockRequest.userHomeDirectory()).thenReturn(tempDir); + org.apache.maven.api.cli.ParserRequest mockParserRequest3 = mock(org.apache.maven.api.cli.ParserRequest.class); + when(mockParserRequest3.logger()).thenReturn(mock(org.apache.maven.api.cli.Logger.class)); + when(mockRequest.parserRequest()).thenReturn(mockParserRequest3); + + // Create mock options with explicit settings that contradict CI defaults + MavenOptions mockOptions = mock(MavenOptions.class); + when(mockOptions.showVersion()).thenReturn(java.util.Optional.of(false)); // explicit false + when(mockOptions.showErrors()).thenReturn(java.util.Optional.of(false)); // explicit false + when(mockOptions.nonInteractive()).thenReturn(java.util.Optional.of(false)); // explicit false + when(mockOptions.forceInteractive()).thenReturn(java.util.Optional.empty()); + when(mockOptions.source()).thenReturn("CLI"); + when(mockRequest.options()).thenReturn(java.util.Optional.of(mockOptions)); + + // When: Creating MavenInvoker and context + MavenInvoker invoker = new MavenInvoker(ProtoLookup.builder().build(), null); + MavenContext context = invoker.createContext(mockRequest); + + // Then: Explicit user choices should be respected + CIAwareMavenOptions ciOptions = (CIAwareMavenOptions) context.options(); + assertFalse(ciOptions.showVersion().orElse(true), "Explicit showVersion=false should be respected"); + assertFalse(ciOptions.showErrors().orElse(true), "Explicit showErrors=false should be respected"); + assertFalse(ciOptions.nonInteractive().orElse(true), "Explicit nonInteractive=false should be respected"); + } + + @Test + void testForceInteractiveOverridesCIDefaults() throws Exception { + // Given: A mock CI environment with force-interactive + CIInfo mockCIInfo = mock(CIInfo.class); + when(mockCIInfo.name()).thenReturn("TestCI"); + + InvokerRequest mockRequest = mock(InvokerRequest.class); + when(mockRequest.ciInfo()).thenReturn(java.util.Optional.of(mockCIInfo)); + when(mockRequest.topDirectory()).thenReturn(tempDir); + when(mockRequest.rootDirectory()).thenReturn(java.util.Optional.of(tempDir)); + when(mockRequest.userProperties()).thenReturn(Map.of()); + when(mockRequest.systemProperties()).thenReturn(Map.of()); + when(mockRequest.cwd()).thenReturn(tempDir); + when(mockRequest.installationDirectory()).thenReturn(tempDir); + when(mockRequest.userHomeDirectory()).thenReturn(tempDir); + org.apache.maven.api.cli.ParserRequest mockParserRequest4 = mock(org.apache.maven.api.cli.ParserRequest.class); + when(mockParserRequest4.logger()).thenReturn(mock(org.apache.maven.api.cli.Logger.class)); + when(mockRequest.parserRequest()).thenReturn(mockParserRequest4); + + // Create mock options with force-interactive + MavenOptions mockOptions = mock(MavenOptions.class); + when(mockOptions.showVersion()).thenReturn(java.util.Optional.empty()); + when(mockOptions.showErrors()).thenReturn(java.util.Optional.empty()); + when(mockOptions.nonInteractive()).thenReturn(java.util.Optional.empty()); + when(mockOptions.forceInteractive()).thenReturn(java.util.Optional.of(true)); // force interactive + when(mockOptions.source()).thenReturn("CLI"); + when(mockRequest.options()).thenReturn(java.util.Optional.of(mockOptions)); + + // When: Creating MavenInvoker and context + MavenInvoker invoker = new MavenInvoker(ProtoLookup.builder().build(), null); + MavenContext context = invoker.createContext(mockRequest); + + // Then: CI optimizations should still be applied (force-interactive affects interactive mode, not these + // options) + CIAwareMavenOptions ciOptions = (CIAwareMavenOptions) context.options(); + assertTrue(ciOptions.showVersion().orElse(false), "showVersion should still be enabled in CI"); + assertTrue(ciOptions.showErrors().orElse(false), "showErrors should still be enabled in CI"); + assertTrue(ciOptions.nonInteractive().orElse(false), "nonInteractive should still be enabled in CI"); + assertTrue(ciOptions.forceInteractive().orElse(false), "forceInteractive should be preserved"); + } +} From 95c92ef1e9c65b91883ff3440e0245b0d199b151 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Wed, 10 Sep 2025 21:58:35 +0000 Subject: [PATCH 2/2] Fix CI-aware options to respect --force-interactive flag - Modified MavenInvoker to skip CI optimizations when --force-interactive is specified - Updated CIOptimizationsIntegrationTest to reflect correct behavior where --force-interactive completely disables CI optimizations - This ensures users can override CI detection and get normal interactive behavior even in CI environments --- .../maven/cling/invoker/mvn/MavenInvoker.java | 5 +++- .../mvn/CIOptimizationsIntegrationTest.java | 24 +++++++++++++------ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java index fa4a36d68db3..b6d8a4d58299 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java @@ -89,7 +89,10 @@ protected MavenContext createContext(InvokerRequest invokerRequest) { MavenOptions options = (MavenOptions) invokerRequest.options().orElse(null); // Apply CI-specific defaults if CI is detected and not overridden by user - if (invokerRequest.ciInfo().isPresent() && options != null) { + // Skip CI optimizations if --force-interactive is specified + if (invokerRequest.ciInfo().isPresent() + && options != null + && !options.forceInteractive().orElse(false)) { options = new CIAwareMavenOptions(options, invokerRequest.ciInfo().get()); } diff --git a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java index 9b19e25f985a..c5fa63bc2104 100644 --- a/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java +++ b/impl/maven-cli/src/test/java/org/apache/maven/cling/invoker/mvn/CIOptimizationsIntegrationTest.java @@ -196,12 +196,22 @@ void testForceInteractiveOverridesCIDefaults() throws Exception { MavenInvoker invoker = new MavenInvoker(ProtoLookup.builder().build(), null); MavenContext context = invoker.createContext(mockRequest); - // Then: CI optimizations should still be applied (force-interactive affects interactive mode, not these - // options) - CIAwareMavenOptions ciOptions = (CIAwareMavenOptions) context.options(); - assertTrue(ciOptions.showVersion().orElse(false), "showVersion should still be enabled in CI"); - assertTrue(ciOptions.showErrors().orElse(false), "showErrors should still be enabled in CI"); - assertTrue(ciOptions.nonInteractive().orElse(false), "nonInteractive should still be enabled in CI"); - assertTrue(ciOptions.forceInteractive().orElse(false), "forceInteractive should be preserved"); + // Then: CI optimizations should NOT be applied when force-interactive is specified + // The context should contain the original options, not the CI-aware wrapper + assertFalse( + context.options() instanceof CIAwareMavenOptions, + "CI optimizations should not be applied when --force-interactive is specified"); + + // The original options should be preserved + MavenOptions options = context.options(); + assertFalse( + options.showVersion().orElse(false), + "showVersion should not be enabled when force-interactive is used"); + assertFalse( + options.showErrors().orElse(false), "showErrors should not be enabled when force-interactive is used"); + assertFalse( + options.nonInteractive().orElse(false), + "nonInteractive should not be enabled when force-interactive is used"); + assertTrue(options.forceInteractive().orElse(false), "forceInteractive should be preserved"); } }