Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Immutable;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.cli.cisupport.CIInfo;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.MessageBuilderFactory;

Expand Down Expand Up @@ -182,11 +183,28 @@ default Optional<OutputStream> stdErr() {
@Nonnull
Optional<List<CoreExtensions>> coreExtensions();

/**
* Returns detected CI system, if any.
*
* @return an {@link Optional} containing the {@link CIInfo} collected from CI system. or empty if CI not
* detected.
*/
@Nonnull
Optional<CIInfo> ciInfo();

/**
* Returns the options associated with this invocation request.
*
* @return the options object
*/
@Nonnull
Options options();

/**
* This method returns "verbose" option value derived from multiple places: CLI options, but also CI detection,
* if applicable.
*/
default boolean effectiveVerbose() {
return options().verbose().orElse(ciInfo().isPresent() && ciInfo().get().isVerbose());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.api.cli.cisupport;

import org.apache.maven.api.annotations.Nonnull;

/**
* CI support: this class contains gathered information and more from CI that Maven process runs on.
*/
public interface CIInfo {
/**
* Short distinct name of CI system: "GH", "Jenkins", etc.
*/
@Nonnull
String name();

/**
* May return a message that will be logged by Maven explaining why it was detected (and possibly more).
*/
@Nonnull
default String message() {
return "";
}

/**
* Some CI systems may allow running jobs in "debug" (or some equivalent) mode.
*/
default boolean isVerbose() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.apache.maven.api.cli.CoreExtensions;
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.cli.cisupport.CIInfo;

import static java.util.Objects.requireNonNull;

Expand All @@ -42,6 +43,7 @@ public abstract class BaseInvokerRequest implements InvokerRequest {
private final Path topDirectory;
private final Path rootDirectory;
private final List<CoreExtensions> coreExtensions;
private final CIInfo ciInfo;

@SuppressWarnings("ParameterNumber")
public BaseInvokerRequest(
Expand All @@ -54,7 +56,8 @@ public BaseInvokerRequest(
@Nonnull Map<String, String> systemProperties,
@Nonnull Path topDirectory,
@Nullable Path rootDirectory,
@Nullable List<CoreExtensions> coreExtensions) {
@Nullable List<CoreExtensions> coreExtensions,
@Nullable CIInfo ciInfo) {
this.parserRequest = requireNonNull(parserRequest);
this.parsingFailed = parsingFailed;
this.cwd = requireNonNull(cwd);
Expand All @@ -66,6 +69,7 @@ public BaseInvokerRequest(
this.topDirectory = requireNonNull(topDirectory);
this.rootDirectory = rootDirectory;
this.coreExtensions = coreExtensions;
this.ciInfo = ciInfo;
}

@Override
Expand Down Expand Up @@ -117,4 +121,9 @@ public Optional<Path> rootDirectory() {
public Optional<List<CoreExtensions>> coreExtensions() {
return Optional.ofNullable(coreExtensions);
}

@Override
public Optional<CIInfo> ciInfo() {
return Optional.ofNullable(ciInfo);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
import org.apache.maven.api.cli.Options;
import org.apache.maven.api.cli.Parser;
import org.apache.maven.api.cli.ParserRequest;
import org.apache.maven.api.cli.cisupport.CIInfo;
import org.apache.maven.api.cli.extensions.CoreExtension;
import org.apache.maven.api.cli.extensions.InputLocation;
import org.apache.maven.api.cli.extensions.InputSource;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
import org.apache.maven.cling.invoker.cisupport.CIDetectorHelper;
import org.apache.maven.cling.props.MavenPropertiesLoader;
import org.apache.maven.cling.utils.CLIReportingUtils;
import org.apache.maven.properties.internal.EnvironmentUtils;
Expand Down Expand Up @@ -86,6 +88,9 @@ public LocalContext(ParserRequest parserRequest) {
@Nullable
public List<CoreExtensions> extensions;

@Nullable
public CIInfo ciInfo;

public Options options;

public Map<String, String> extraInterpolationSource() {
Expand Down Expand Up @@ -190,6 +195,9 @@ public InvokerRequest parseInvocation(ParserRequest parserRequest) {
parserRequest.logger().error("Error reading core extensions descriptor", e);
}

// CI detection
context.ciInfo = detectCI(context);

// only if not failed so far; otherwise we may have no options to validate
if (!context.parsingFailed) {
validate(context);
Expand Down Expand Up @@ -500,4 +508,19 @@ protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
.collect(Collectors.joining(", ")))
.collect(Collectors.joining("; ")));
}

@Nullable
protected CIInfo detectCI(LocalContext context) {
List<CIInfo> detected = CIDetectorHelper.detectCI();
if (detected.isEmpty()) {
return null;
} else if (detected.size() > 1) {
// warn
context.parserRequest
.logger()
.warn("Multiple CI systems detected: "
+ detected.stream().map(CIInfo::name).collect(Collectors.joining(", ")));
}
return detected.get(0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.apache.maven.api.cli.InvokerRequest;
import org.apache.maven.api.cli.Logger;
import org.apache.maven.api.cli.Options;
import org.apache.maven.api.cli.cisupport.CIInfo;
import org.apache.maven.api.cli.logging.AccumulatingLogger;
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.Interpolator;
Expand Down Expand Up @@ -278,7 +279,7 @@ protected void configureLogging(C context) throws Exception {
context.slf4jConfiguration = Slf4jConfigurationFactory.getConfiguration(context.loggerFactory);

context.loggerLevel = Slf4jConfiguration.Level.INFO;
if (mavenOptions.verbose().orElse(false)) {
if (context.invokerRequest.effectiveVerbose()) {
context.loggerLevel = Slf4jConfiguration.Level.DEBUG;
} else if (mavenOptions.quiet().orElse(false)) {
context.loggerLevel = Slf4jConfiguration.Level.ERROR;
Expand Down Expand Up @@ -465,7 +466,7 @@ protected void showVersion(C context) {
InvokerRequest invokerRequest = context.invokerRequest;
if (invokerRequest.options().quiet().orElse(false)) {
writer.accept(CLIReportingUtils.showVersionMinimal());
} else if (invokerRequest.options().verbose().orElse(false)) {
} else if (context.invokerRequest.effectiveVerbose()) {
writer.accept(CLIReportingUtils.showVersion(
ProcessHandle.current().info().commandLine().orElse(null), describe(context.terminal)));

Expand Down Expand Up @@ -493,9 +494,8 @@ protected String describe(Terminal terminal) {
}

protected void preCommands(C context) throws Exception {
Options mavenOptions = context.invokerRequest.options();
boolean verbose = mavenOptions.verbose().orElse(false);
boolean version = mavenOptions.showVersion().orElse(false);
boolean verbose = context.invokerRequest.effectiveVerbose();
boolean version = context.invokerRequest.options().showVersion().orElse(false);
if (verbose || version) {
showVersion(context);
}
Expand Down Expand Up @@ -726,11 +726,11 @@ protected boolean mayDisableInteractiveMode(C context, boolean proposedInteracti
if (context.invokerRequest.options().nonInteractive().orElse(false)) {
return false;
} else {
boolean runningOnCI = isRunningOnCI(context);
if (runningOnCI) {
if (context.invokerRequest.ciInfo().isPresent()) {
CIInfo ci = context.invokerRequest.ciInfo().get();
context.logger.info(
"Making this build non-interactive, because the environment variable CI equals \"true\"."
+ " Disable this detection by removing that variable or adding --force-interactive.");
"Making this build non-interactive, because CI detected. Disable this detection by adding --force-interactive.");
context.logger.info("Detected CI system: '" + ci.name() + "': " + ci.message());
return false;
}
}
Expand Down Expand Up @@ -935,10 +935,5 @@ protected int calculateDegreeOfConcurrency(String threadConfiguration) {
}
}

protected boolean isRunningOnCI(C context) {
String ciEnv = context.protoSession.getSystemProperties().get("env.CI");
return ciEnv != null && !"false".equals(ciEnv);
}

protected abstract int execute(C context) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.cisupport;

import java.util.Optional;

import org.apache.maven.api.cli.cisupport.CIInfo;

/**
* Service interface to detect CI system process runs on, if any.
*/
public interface CIDetector {
/**
* Returns non-empty optional with CI information, if CI is detected, empty otherwise.
*/
Optional<CIInfo> detectCI();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.cisupport;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;

import org.apache.maven.api.cli.cisupport.CIInfo;

/**
* CI detector helper: it uses service discovery to discover {@link CIDetector}s. If resulting list has more than
* one element, it will remove the {@link GenericCIDetector} result, assuming a more specific one is also present.
*/
public final class CIDetectorHelper {
private CIDetectorHelper() {}

public static List<CIInfo> detectCI() {
List<CIInfo> result = new ArrayList<>(ServiceLoader.load(CIDetector.class).stream()
.map(ServiceLoader.Provider::get)
.map(CIDetector::detectCI)
.filter(Optional::isPresent)
.map(Optional::get)
.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html#toList():

The returned List is unmodifiable

so not sure about removeIf later.

Maybe Collectors.toCollection(ArrayList::new)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think is fine, this happens "on boot", is not some sort of "hot" spot. The reason why I remove "generic" if there are 2 or more is that I assume there is more specific hit (like GH, sets CI=true but also sets GITHUB_RUNNER=true)... we can refine this later anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, to keep things simple I don't want to fiddle with order, as other solution would be to test all, and THEN as last, if no hit, "generic"... but that would assume there is no (and never will be) overlap between all CI systems, which IMO we never know and cannot know (future wise).


if (result.size() > 1) {
// remove generic
result.removeIf(c -> GenericCIDetector.NAME.equals(c.name()));
}
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.cisupport;

import java.util.Optional;

import org.apache.maven.api.cli.cisupport.CIInfo;

/**
* Circle CI support.
*/
public class CircleCIDetector implements CIDetector {
public static final String NAME = "CircleCI";

private static final String CIRCLECI = "CIRCLECI";

@Override
public Optional<CIInfo> detectCI() {
String ciEnv = System.getenv(CIRCLECI);
if ("true".equals(ciEnv)) {
return Optional.of(new CIInfo() {
@Override
public String name() {
return NAME;
}
});
}
return Optional.empty();
}
}
Loading