Skip to content

Commit b1466d8

Browse files
authored
[MNG-8685] Better support for CI systems (#2254)
Improve CI detection of Maven, make possible to propagate some options from CI to Maven. Changes: * `InvokerRequest` carries new info: `Optional<CIInfo>`: if present, we run on CI, if empty, not. * `Parser` (creating `InvokerRequest`) already inspects env and assembles invoker request, so make it detect CI as well * `CIDetector` is a Java Service (this all happens early, no DI yet) that can be extended (like adding CI specific detectors). Core had one implementation that became the "generic" that is what Maven 4 had so far. * Added "jenkins", "github", "teamcity", "circle" and "travis" support. --- https://issues.apache.org/jira/browse/MNG-8685
1 parent c7f22fe commit b1466d8

File tree

27 files changed

+661
-26
lines changed

27 files changed

+661
-26
lines changed

api/maven-api-cli/src/main/java/org/apache/maven/api/cli/InvokerRequest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.maven.api.annotations.Experimental;
2929
import org.apache.maven.api.annotations.Immutable;
3030
import org.apache.maven.api.annotations.Nonnull;
31+
import org.apache.maven.api.cli.cisupport.CIInfo;
3132
import org.apache.maven.api.services.Lookup;
3233
import org.apache.maven.api.services.MessageBuilderFactory;
3334

@@ -182,11 +183,28 @@ default Optional<OutputStream> stdErr() {
182183
@Nonnull
183184
Optional<List<CoreExtensions>> coreExtensions();
184185

186+
/**
187+
* Returns detected CI system, if any.
188+
*
189+
* @return an {@link Optional} containing the {@link CIInfo} collected from CI system. or empty if CI not
190+
* detected.
191+
*/
192+
@Nonnull
193+
Optional<CIInfo> ciInfo();
194+
185195
/**
186196
* Returns the options associated with this invocation request.
187197
*
188198
* @return the options object
189199
*/
190200
@Nonnull
191201
Options options();
202+
203+
/**
204+
* This method returns "verbose" option value derived from multiple places: CLI options, but also CI detection,
205+
* if applicable.
206+
*/
207+
default boolean effectiveVerbose() {
208+
return options().verbose().orElse(ciInfo().isPresent() && ciInfo().get().isVerbose());
209+
}
192210
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api.cli.cisupport;
20+
21+
import org.apache.maven.api.annotations.Nonnull;
22+
23+
/**
24+
* CI support: this class contains gathered information and more from CI that Maven process runs on.
25+
*
26+
* @since 4.0.0
27+
*/
28+
public interface CIInfo {
29+
/**
30+
* Short distinct name of CI system: "GH", "Jenkins", etc.
31+
*/
32+
@Nonnull
33+
String name();
34+
35+
/**
36+
* May return a message that will be logged by Maven explaining why it was detected (and possibly more).
37+
*/
38+
@Nonnull
39+
default String message() {
40+
return "";
41+
}
42+
43+
/**
44+
* Some CI systems may allow running jobs in "debug" (or some equivalent) mode.
45+
*/
46+
default boolean isVerbose() {
47+
return false;
48+
}
49+
}

impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseInvokerRequest.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.apache.maven.api.cli.CoreExtensions;
2929
import org.apache.maven.api.cli.InvokerRequest;
3030
import org.apache.maven.api.cli.ParserRequest;
31+
import org.apache.maven.api.cli.cisupport.CIInfo;
3132

3233
import static java.util.Objects.requireNonNull;
3334

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

4648
@SuppressWarnings("ParameterNumber")
4749
public BaseInvokerRequest(
@@ -54,7 +56,8 @@ public BaseInvokerRequest(
5456
@Nonnull Map<String, String> systemProperties,
5557
@Nonnull Path topDirectory,
5658
@Nullable Path rootDirectory,
57-
@Nullable List<CoreExtensions> coreExtensions) {
59+
@Nullable List<CoreExtensions> coreExtensions,
60+
@Nullable CIInfo ciInfo) {
5861
this.parserRequest = requireNonNull(parserRequest);
5962
this.parsingFailed = parsingFailed;
6063
this.cwd = requireNonNull(cwd);
@@ -66,6 +69,7 @@ public BaseInvokerRequest(
6669
this.topDirectory = requireNonNull(topDirectory);
6770
this.rootDirectory = rootDirectory;
6871
this.coreExtensions = coreExtensions;
72+
this.ciInfo = ciInfo;
6973
}
7074

7175
@Override
@@ -117,4 +121,9 @@ public Optional<Path> rootDirectory() {
117121
public Optional<List<CoreExtensions>> coreExtensions() {
118122
return Optional.ofNullable(coreExtensions);
119123
}
124+
125+
@Override
126+
public Optional<CIInfo> ciInfo() {
127+
return Optional.ofNullable(ciInfo);
128+
}
120129
}

impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/BaseParser.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@
4343
import org.apache.maven.api.cli.Options;
4444
import org.apache.maven.api.cli.Parser;
4545
import org.apache.maven.api.cli.ParserRequest;
46+
import org.apache.maven.api.cli.cisupport.CIInfo;
4647
import org.apache.maven.api.cli.extensions.CoreExtension;
4748
import org.apache.maven.api.cli.extensions.InputLocation;
4849
import org.apache.maven.api.cli.extensions.InputSource;
4950
import org.apache.maven.api.services.Interpolator;
5051
import org.apache.maven.cling.internal.extension.io.CoreExtensionsStaxReader;
52+
import org.apache.maven.cling.invoker.cisupport.CIDetectorHelper;
5153
import org.apache.maven.cling.props.MavenPropertiesLoader;
5254
import org.apache.maven.cling.utils.CLIReportingUtils;
5355
import org.apache.maven.properties.internal.EnvironmentUtils;
@@ -86,6 +88,9 @@ public LocalContext(ParserRequest parserRequest) {
8688
@Nullable
8789
public List<CoreExtensions> extensions;
8890

91+
@Nullable
92+
public CIInfo ciInfo;
93+
8994
public Options options;
9095

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

198+
// CI detection
199+
context.ciInfo = detectCI(context);
200+
193201
// only if not failed so far; otherwise we may have no options to validate
194202
if (!context.parsingFailed) {
195203
validate(context);
@@ -500,4 +508,19 @@ protected List<CoreExtension> validateCoreExtensionsDescriptorFromFile(
500508
.collect(Collectors.joining(", ")))
501509
.collect(Collectors.joining("; ")));
502510
}
511+
512+
@Nullable
513+
protected CIInfo detectCI(LocalContext context) {
514+
List<CIInfo> detected = CIDetectorHelper.detectCI();
515+
if (detected.isEmpty()) {
516+
return null;
517+
} else if (detected.size() > 1) {
518+
// warn
519+
context.parserRequest
520+
.logger()
521+
.warn("Multiple CI systems detected: "
522+
+ detected.stream().map(CIInfo::name).collect(Collectors.joining(", ")));
523+
}
524+
return detected.get(0);
525+
}
503526
}

impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.apache.maven.api.cli.InvokerRequest;
4747
import org.apache.maven.api.cli.Logger;
4848
import org.apache.maven.api.cli.Options;
49+
import org.apache.maven.api.cli.cisupport.CIInfo;
4950
import org.apache.maven.api.cli.logging.AccumulatingLogger;
5051
import org.apache.maven.api.services.BuilderProblem;
5152
import org.apache.maven.api.services.Interpolator;
@@ -278,7 +279,7 @@ protected void configureLogging(C context) throws Exception {
278279
context.slf4jConfiguration = Slf4jConfigurationFactory.getConfiguration(context.loggerFactory);
279280

280281
context.loggerLevel = Slf4jConfiguration.Level.INFO;
281-
if (mavenOptions.verbose().orElse(false)) {
282+
if (context.invokerRequest.effectiveVerbose()) {
282283
context.loggerLevel = Slf4jConfiguration.Level.DEBUG;
283284
} else if (mavenOptions.quiet().orElse(false)) {
284285
context.loggerLevel = Slf4jConfiguration.Level.ERROR;
@@ -465,7 +466,7 @@ protected void showVersion(C context) {
465466
InvokerRequest invokerRequest = context.invokerRequest;
466467
if (invokerRequest.options().quiet().orElse(false)) {
467468
writer.accept(CLIReportingUtils.showVersionMinimal());
468-
} else if (invokerRequest.options().verbose().orElse(false)) {
469+
} else if (invokerRequest.effectiveVerbose()) {
469470
writer.accept(CLIReportingUtils.showVersion(
470471
ProcessHandle.current().info().commandLine().orElse(null), describe(context.terminal)));
471472

@@ -493,9 +494,8 @@ protected String describe(Terminal terminal) {
493494
}
494495

495496
protected void preCommands(C context) throws Exception {
496-
Options mavenOptions = context.invokerRequest.options();
497-
boolean verbose = mavenOptions.verbose().orElse(false);
498-
boolean version = mavenOptions.showVersion().orElse(false);
497+
boolean verbose = context.invokerRequest.effectiveVerbose();
498+
boolean version = context.invokerRequest.options().showVersion().orElse(false);
499499
if (verbose || version) {
500500
showVersion(context);
501501
}
@@ -726,11 +726,11 @@ protected boolean mayDisableInteractiveMode(C context, boolean proposedInteracti
726726
if (context.invokerRequest.options().nonInteractive().orElse(false)) {
727727
return false;
728728
} else {
729-
boolean runningOnCI = isRunningOnCI(context);
730-
if (runningOnCI) {
729+
if (context.invokerRequest.ciInfo().isPresent()) {
730+
CIInfo ci = context.invokerRequest.ciInfo().get();
731731
context.logger.info(
732-
"Making this build non-interactive, because the environment variable CI equals \"true\"."
733-
+ " Disable this detection by removing that variable or adding --force-interactive.");
732+
"Making this build non-interactive, because CI detected. Disable this detection by adding --force-interactive.");
733+
context.logger.info("Detected CI system: '" + ci.name() + "': " + ci.message());
734734
return false;
735735
}
736736
}
@@ -935,10 +935,5 @@ protected int calculateDegreeOfConcurrency(String threadConfiguration) {
935935
}
936936
}
937937

938-
protected boolean isRunningOnCI(C context) {
939-
String ciEnv = context.protoSession.getSystemProperties().get("env.CI");
940-
return ciEnv != null && !"false".equals(ciEnv);
941-
}
942-
943938
protected abstract int execute(C context) throws Exception;
944939
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.cling.invoker.cisupport;
20+
21+
import java.util.Optional;
22+
23+
import org.apache.maven.api.cli.cisupport.CIInfo;
24+
25+
/**
26+
* Service interface to detect CI system process runs on, if any.
27+
*
28+
* @since 4.0.0
29+
*/
30+
public interface CIDetector {
31+
/**
32+
* Returns non-empty optional with CI information, if CI is detected, empty otherwise.
33+
*/
34+
Optional<CIInfo> detectCI();
35+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.cling.invoker.cisupport;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
import java.util.Optional;
24+
import java.util.ServiceLoader;
25+
import java.util.stream.Collectors;
26+
27+
import org.apache.maven.api.cli.cisupport.CIInfo;
28+
29+
/**
30+
* CI detector helper: it uses service discovery to discover {@link CIDetector}s. If resulting list has more than
31+
* one element, it will remove the {@link GenericCIDetector} result, assuming a more specific one is also present.
32+
*/
33+
public final class CIDetectorHelper {
34+
private CIDetectorHelper() {}
35+
36+
public static List<CIInfo> detectCI() {
37+
ArrayList<CIInfo> result = ServiceLoader.load(CIDetector.class).stream()
38+
.map(ServiceLoader.Provider::get)
39+
.map(CIDetector::detectCI)
40+
.filter(Optional::isPresent)
41+
.map(Optional::get)
42+
.collect(Collectors.toCollection(ArrayList::new));
43+
44+
if (result.size() > 1) {
45+
// remove generic
46+
result.removeIf(c -> GenericCIDetector.NAME.equals(c.name()));
47+
}
48+
return result;
49+
}
50+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.cling.invoker.cisupport;
20+
21+
import java.util.Optional;
22+
23+
import org.apache.maven.api.cli.cisupport.CIInfo;
24+
25+
/**
26+
* Circle CI support.
27+
*/
28+
public class CircleCIDetector implements CIDetector {
29+
public static final String NAME = "CircleCI";
30+
31+
private static final String CIRCLECI = "CIRCLECI";
32+
33+
@Override
34+
public Optional<CIInfo> detectCI() {
35+
String ciEnv = System.getenv(CIRCLECI);
36+
if ("true".equals(ciEnv)) {
37+
return Optional.of(new CIInfo() {
38+
@Override
39+
public String name() {
40+
return NAME;
41+
}
42+
});
43+
}
44+
return Optional.empty();
45+
}
46+
}

0 commit comments

Comments
 (0)