Skip to content

Commit

Permalink
#523: Add auto-detection for all CIs in Sonarqube commercial editions
Browse files Browse the repository at this point in the history
The plugin previously only provided support for auto-detecting and configuring the scanner properties for a Pull Request in Azure Devops and a Merge Request or Branch in Gitlab CI. The Sonarqube documentation also stated that Bitbucket Pipelines, Github Actions, CodeMagic, Jenkins Branch API, and Cirrus CI could also be used to auto-discover Pull Request or Branch information although the plugin did not provide these.

This change adds support for detecting these additional CIs based on the various environment variables they provide, and to auto-configure Pull Request or Branch parameters in the scanner when a suitable build job is detected.

Includes the general clean-up of the creation of Branch and Pull Request configuration to force fail-fast behaviour where target branches are not provided or can't be matched against known branches, to ensure the correct reference branch is selected for Pull Request analysis, and to force an error to be displayed if a user mixes Pull Rrequest and Branch parameters in their launch properties.
  • Loading branch information
mc1arke committed Dec 29, 2021
1 parent dc99260 commit 92ad73a
Show file tree
Hide file tree
Showing 20 changed files with 1,277 additions and 337 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@
import com.github.mc1arke.sonarqube.plugin.almclient.github.v3.RestApplicationAuthenticationProvider;
import com.github.mc1arke.sonarqube.plugin.almclient.gitlab.DefaultGitlabClientFactory;
import com.github.mc1arke.sonarqube.plugin.ce.CommunityReportAnalysisComponentProvider;
import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory;
import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchConfigurationLoader;
import com.github.mc1arke.sonarqube.plugin.scanner.CommunityBranchParamsValidator;
import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectBranchesLoader;
import com.github.mc1arke.sonarqube.plugin.scanner.CommunityProjectPullRequestsLoader;
import com.github.mc1arke.sonarqube.plugin.scanner.ScannerPullRequestPropertySensor;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.AzureDevopsAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.BitbucketPipelinesAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.CirrusCiAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.CodeMagicAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.GithubActionsAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.GitlabCiAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration.JenkinsAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchFeatureExtension;
import com.github.mc1arke.sonarqube.plugin.server.CommunityBranchSupportDelegate;
import com.github.mc1arke.sonarqube.plugin.server.pullrequest.validator.AzureDevopsValidator;
Expand Down Expand Up @@ -143,7 +151,11 @@ public void define(Plugin.Context context) {
if (SonarQubeSide.SCANNER == context.getRuntime().getSonarQubeSide()) {
context.addExtensions(CommunityProjectBranchesLoader.class, CommunityProjectPullRequestsLoader.class,
CommunityBranchConfigurationLoader.class, CommunityBranchParamsValidator.class,
ScannerPullRequestPropertySensor.class);
ScannerPullRequestPropertySensor.class, BranchConfigurationFactory.class,
AzureDevopsAutoConfigurer.class, BitbucketPipelinesAutoConfigurer.class,
CirrusCiAutoConfigurer.class, CodeMagicAutoConfigurer.class,
GithubActionsAutoConfigurer.class, GitlabCiAutoConfigurer.class,
JenkinsAutoConfigurer.class);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2021 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.scanner;

import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.utils.System2;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonar.scanner.scan.branch.ProjectBranches;

import java.util.Optional;

@ScannerSide
public interface BranchAutoConfigurer {

Optional<BranchConfiguration> detectConfiguration(System2 system, ProjectBranches projectBranches);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2021 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.scanner;

import org.sonar.api.scanner.ScannerSide;
import org.sonar.api.utils.MessageException;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonar.scanner.scan.branch.BranchInfo;
import org.sonar.scanner.scan.branch.BranchType;
import org.sonar.scanner.scan.branch.ProjectBranches;

import java.util.Optional;

@ScannerSide
public class BranchConfigurationFactory {

public BranchConfiguration createBranchConfiguration(String branchName, ProjectBranches branches) {
if (branches.isEmpty()) {
return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, null, null, null);
}

String targetBranchName = branches.get(branchName) == null ? branches.defaultBranchName() : branchName;
return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, targetBranchName, null, null);
}

public BranchConfiguration createPullRequestConfiguration(String pullRequestKey, String pullRequestBranch, String pullRequestBase, ProjectBranches branches) {
if (branches.isEmpty()) {
return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, null, pullRequestBase, pullRequestKey);
}

String referenceBranch = branches.get(pullRequestBase) == null ? branches.defaultBranchName() : findReferenceBranch(pullRequestBase, branches);
return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, referenceBranch, pullRequestBase, pullRequestKey);

}

private static String findReferenceBranch(String targetBranch, ProjectBranches branches) {
BranchInfo target = Optional.ofNullable(branches.get(targetBranch))
.orElseThrow(() -> MessageException.of("No branch exists in Sonarqube with the name " + targetBranch));

if (target.type() == BranchType.BRANCH) {
return targetBranch;
}

String targetBranchTarget = target.branchTargetName();
if (targetBranchTarget == null) {
throw MessageException.of(String.format("The branch '%s' of type %s does not have a target", target.name(), target.type()));
}

return findReferenceBranch(targetBranchTarget, branches);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020 Michael Clarke
* Copyright (C) 2020-2021 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
Expand All @@ -18,26 +18,27 @@
*/
package com.github.mc1arke.sonarqube.plugin.scanner;

import org.apache.commons.lang.StringUtils;
import org.sonar.api.notifications.AnalysisWarnings;
import org.sonar.api.utils.MessageException;
import org.sonar.api.utils.System2;
import org.sonar.api.utils.log.Logger;
import org.sonar.api.utils.log.Loggers;
import org.sonar.core.config.ScannerProperties;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonar.scanner.scan.branch.BranchConfigurationLoader;
import org.sonar.scanner.scan.branch.BranchInfo;
import org.sonar.scanner.scan.branch.BranchType;
import org.sonar.scanner.scan.branch.DefaultBranchConfiguration;
import org.sonar.scanner.scan.branch.ProjectBranches;
import org.sonar.scanner.scan.branch.ProjectPullRequests;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

/**
* @author Michael Clarke
Expand All @@ -55,89 +56,67 @@ public class CommunityBranchConfigurationLoader implements BranchConfigurationLo

private final System2 system2;
private final AnalysisWarnings analysisWarnings;
private final BranchConfigurationFactory branchConfigurationFactory;
private final List<BranchAutoConfigurer> autoConfigurers;

public CommunityBranchConfigurationLoader(System2 system2, AnalysisWarnings analysisWarnings) {
public CommunityBranchConfigurationLoader(System2 system2, AnalysisWarnings analysisWarnings,
BranchConfigurationFactory branchConfigurationFactory,
List<BranchAutoConfigurer> autoConfigurers) {
super();
this.system2 = system2;
this.analysisWarnings = analysisWarnings;
this.branchConfigurationFactory = branchConfigurationFactory;
this.autoConfigurers = autoConfigurers;
}

@Override
public BranchConfiguration load(Map<String, String> localSettings, ProjectBranches projectBranches,
ProjectPullRequests pullRequests) {
localSettings = autoConfigure(localSettings);
List<String> nonEmptyParameters = localSettings.entrySet().stream()
.filter(e -> StringUtils.isNotEmpty(e.getValue()))
.map(Map.Entry::getKey)
.collect(Collectors.toList());
boolean hasBranchParameters = BRANCH_ANALYSIS_PARAMETERS.stream()
.anyMatch(nonEmptyParameters::contains);
boolean hasPullRequestParameters = PULL_REQUEST_ANALYSIS_PARAMETERS.stream()
.anyMatch(nonEmptyParameters::contains);

if (hasPullRequestParameters && hasBranchParameters) {
throw MessageException.of("sonar.pullrequest and sonar.branch parameters should not be specified in the same scan");
}

if (!hasBranchParameters && !hasPullRequestParameters) {
for (BranchAutoConfigurer branchAutoConfigurer : autoConfigurers) {
Optional<BranchConfiguration> optionalBranchConfiguration = branchAutoConfigurer.detectConfiguration(system2, projectBranches);
if (optionalBranchConfiguration.isPresent()) {
BranchConfiguration branchConfiguration = optionalBranchConfiguration.get();
LOGGER.info("Auto detected {} configuration with source {} using {}", branchConfiguration.branchType(), branchConfiguration.branchName(), branchAutoConfigurer.getClass().getName());
return branchConfiguration;
}
}
}

if (null != localSettings.get(ScannerProperties.BRANCH_TARGET)) { //NOSONAR - purposefully checking for a deprecated parameter
String warning = String.format("Property '%s' is no longer supported", ScannerProperties.BRANCH_TARGET); //NOSONAR - reporting use of deprecated parameter
analysisWarnings.addUnique(warning);
LOGGER.warn(warning);
}
if (BRANCH_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) {
return createBranchConfiguration(localSettings.get(ScannerProperties.BRANCH_NAME),
projectBranches);
} else if (PULL_REQUEST_ANALYSIS_PARAMETERS.stream().anyMatch(localSettings::containsKey)) {
return createPullRequestConfiguration(localSettings.get(ScannerProperties.PULL_REQUEST_KEY),
localSettings.get(ScannerProperties.PULL_REQUEST_BRANCH),
localSettings.get(ScannerProperties.PULL_REQUEST_BASE),
projectBranches);
}

return new DefaultBranchConfiguration();
}

private Map<String, String> autoConfigure(Map<String, String> localSettings) {
Map<String, String> mutableLocalSettings = new HashMap<>(localSettings);
if (Boolean.parseBoolean(system2.envVariable("GITLAB_CI"))) {
//GitLab CI auto configuration
if (system2.envVariable("CI_MERGE_REQUEST_IID") != null) {
// we are inside a merge request
Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_IID")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_KEY, v));
Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BRANCH, v));
Optional.ofNullable(system2.envVariable("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BASE, v));
} else {
// branch or tag
Optional.ofNullable(system2.envVariable("CI_COMMIT_REF_NAME")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.BRANCH_NAME, v));
}
if (hasBranchParameters) {
String branch = StringUtils.trimToNull(localSettings.get(ScannerProperties.BRANCH_NAME));
return branchConfigurationFactory.createBranchConfiguration(branch, projectBranches);
}
if (Boolean.parseBoolean(system2.envVariable("TF_BUILD"))) {
Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_PULLREQUESTID")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_KEY, v));
Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BRANCH, v));
Optional.ofNullable(system2.envVariable("SYSTEM_PULLREQUEST_TARGETBRANCH")).ifPresent(
v -> mutableLocalSettings.putIfAbsent(ScannerProperties.PULL_REQUEST_BASE, v));

}
return Collections.unmodifiableMap(mutableLocalSettings);
}

private static BranchConfiguration createBranchConfiguration(String branchName, ProjectBranches branches) {
BranchInfo existingBranch = branches.get(branchName);

if (null == existingBranch) {
return new CommunityBranchConfiguration(branchName, BranchType.BRANCH, branches.defaultBranchName(), null, null);
if (hasPullRequestParameters) {
String key = Optional.ofNullable(StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_KEY)))
.orElseThrow(() -> MessageException.of(ScannerProperties.PULL_REQUEST_KEY + " is required for a pull request analysis"));
String branch = Optional.ofNullable(StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_BRANCH)))
.orElseThrow(() -> MessageException.of(ScannerProperties.PULL_REQUEST_BRANCH + " is required for a pull request analysis"));
String target = StringUtils.trimToNull(localSettings.get(ScannerProperties.PULL_REQUEST_BASE));
return branchConfigurationFactory.createPullRequestConfiguration(key, branch, target, projectBranches);
}

return new CommunityBranchConfiguration(branchName, existingBranch.type(), existingBranch.name(), null, null);
}

private static BranchConfiguration createPullRequestConfiguration(String pullRequestKey, String pullRequestBranch,
String pullRequestBase,
ProjectBranches branches) {
if (null == pullRequestBase || pullRequestBase.isEmpty()) {
return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST, branches.defaultBranchName(),
branches.defaultBranchName(), pullRequestKey);
} else {
return new CommunityBranchConfiguration(pullRequestBranch, BranchType.PULL_REQUEST,
Optional.ofNullable(branches.get(pullRequestBase))
.map(b -> pullRequestBase)
.orElse(null),
pullRequestBase, pullRequestKey);
}
return new DefaultBranchConfiguration();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2021 Michael Clarke
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
package com.github.mc1arke.sonarqube.plugin.scanner.autoconfiguration;

import com.github.mc1arke.sonarqube.plugin.scanner.BranchAutoConfigurer;
import com.github.mc1arke.sonarqube.plugin.scanner.BranchConfigurationFactory;
import org.sonar.api.utils.System2;
import org.sonar.scanner.scan.branch.BranchConfiguration;
import org.sonar.scanner.scan.branch.ProjectBranches;

import java.util.Optional;

public class AzureDevopsAutoConfigurer implements BranchAutoConfigurer {

private final BranchConfigurationFactory branchConfigurationFactory;

public AzureDevopsAutoConfigurer(BranchConfigurationFactory branchConfigurationFactory) {
this.branchConfigurationFactory = branchConfigurationFactory;
}

@Override
public Optional<BranchConfiguration> detectConfiguration(System2 system2, ProjectBranches projectBranches) {
if (!Boolean.parseBoolean(system2.envVariable("TF_BUILD"))) {
return Optional.empty();
}

String key = system2.envVariable("SYSTEM_PULLREQUEST_PULLREQUESTID");
if (key != null) {
String sourceBranch = system2.envVariable("SYSTEM_PULLREQUEST_SOURCEBRANCH");
String targetBranch = system2.envVariable("SYSTEM_PULLREQUEST_TARGETBRANCH");

return Optional.of(branchConfigurationFactory.createPullRequestConfiguration(key, sourceBranch, targetBranch, projectBranches));
}

return Optional.empty();
}
}
Loading

0 comments on commit 92ad73a

Please sign in to comment.