mirror of
https://github.com/SAP/jenkins-library.git
synced 2025-01-30 05:59:39 +02:00
add step for Sonar scan (#183)
* add new step sonarExecute * simplify list * add general credentials for SonarQube * Update default_pipeline_environment.yml * Update sonarExecute.groovy * correct worker invocation * switch step/stage config order * add tests * add webhook handling * Update default_pipeline_environment.yml * use withMandatoryProperty with condition * Update ConfigurationHelper.groovy * Update sonarExecute.groovy * Update ConfigurationHelper.groovy * rename step to sonarExecuteScan * rename step to sonarExecuteScan * rename step to sonarExecuteScan * Update sonarExecuteScan.groovy * change return type * Update sonarExecuteScan.groovy * stash * update defaults * update install path * use quiet unzip * use long option names * optimize filename retrival * rework PR voting * fix path * remove accitentially checked-in file * add documentation, optimise coding * correct test case * add documentation * remove option prefix * rename config variable * update docs * update docs * rename download url * fix typo * adjust test cases * add test cases * update docs
This commit is contained in:
parent
f9047bc37a
commit
c8f9db71eb
18
documentation/docs/steps/sonarExecuteScan.md
Normal file
18
documentation/docs/steps/sonarExecuteScan.md
Normal file
@ -0,0 +1,18 @@
|
||||
# ${docGenStepName}
|
||||
|
||||
## ${docGenDescription}
|
||||
|
||||
## Prerequsites
|
||||
|
||||
- The project needs a `sonar-project.properties` file that describes the project and defines certain settings, see [here](https://docs.sonarqube.org/display/SCAN/Advanced+SonarQube+Scanner+Usages#AdvancedSonarQubeScannerUsages-Multi-moduleProjectStructure).
|
||||
- A SonarQube instance needs to be defined in the Jenkins.
|
||||
|
||||
## ${docGenParameters}
|
||||
|
||||
## ${docGenConfiguration}
|
||||
|
||||
## Exceptions
|
||||
|
||||
none
|
||||
|
||||
## Examples
|
@ -463,6 +463,12 @@ steps:
|
||||
- 'opensourceConfiguration'
|
||||
toJson: false
|
||||
toHtml: false
|
||||
sonarExecuteScan:
|
||||
dockerImage: 'maven:3.5-jdk-8'
|
||||
instance: 'SonarCloud'
|
||||
options: []
|
||||
pullRequestProvider: 'github'
|
||||
sonarScannerDownloadUrl: 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-3.3.0.1492-linux.zip'
|
||||
testsPublishResults:
|
||||
failOnError: false
|
||||
junit:
|
||||
|
237
test/groovy/SonarExecuteTest.groovy
Normal file
237
test/groovy/SonarExecuteTest.groovy
Normal file
@ -0,0 +1,237 @@
|
||||
import static org.hamcrest.Matchers.containsString
|
||||
import static org.hamcrest.Matchers.hasItem
|
||||
import static org.hamcrest.Matchers.is
|
||||
import static org.hamcrest.Matchers.allOf
|
||||
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.RuleChain
|
||||
import org.junit.rules.ExpectedException
|
||||
import static org.junit.Assert.assertThat
|
||||
|
||||
import util.BasePiperTest
|
||||
import util.JenkinsDockerExecuteRule
|
||||
import util.JenkinsShellCallRule
|
||||
import util.JenkinsReadYamlRule
|
||||
import util.JenkinsStepRule
|
||||
import util.JenkinsLoggingRule
|
||||
import util.Rules
|
||||
|
||||
class SonarExecuteScanTest extends BasePiperTest {
|
||||
private ExpectedException thrown = ExpectedException.none()
|
||||
private JenkinsReadYamlRule readYamlRule = new JenkinsReadYamlRule(this)
|
||||
private JenkinsStepRule jsr = new JenkinsStepRule(this)
|
||||
private JenkinsLoggingRule jlr = new JenkinsLoggingRule(this)
|
||||
private JenkinsShellCallRule jscr = new JenkinsShellCallRule(this)
|
||||
private JenkinsDockerExecuteRule jedr = new JenkinsDockerExecuteRule(this)
|
||||
|
||||
@Rule
|
||||
public RuleChain rules = Rules
|
||||
.getCommonRules(this)
|
||||
.around(readYamlRule)
|
||||
.around(thrown)
|
||||
.around(jedr)
|
||||
.around(jscr)
|
||||
.around(jlr)
|
||||
.around(jsr)
|
||||
|
||||
def sonarInstance
|
||||
|
||||
@Before
|
||||
void init() throws Exception {
|
||||
sonarInstance = null
|
||||
helper.registerAllowedMethod("withSonarQubeEnv", [String.class, Closure.class], { string, closure ->
|
||||
sonarInstance = string
|
||||
return closure()
|
||||
})
|
||||
helper.registerAllowedMethod("unstash", [String.class], { stashInput -> return []})
|
||||
helper.registerAllowedMethod("fileExists", [String.class], { file -> return file })
|
||||
helper.registerAllowedMethod('string', [Map], { m -> m })
|
||||
helper.registerAllowedMethod('withCredentials', [List, Closure], { l, c ->
|
||||
try {
|
||||
binding.setProperty(l[0].variable, 'TOKEN_'+l[0].credentialsId)
|
||||
c()
|
||||
} finally {
|
||||
binding.setProperty(l[0].variable, null)
|
||||
}
|
||||
})
|
||||
nullScript.commonPipelineEnvironment.setArtifactVersion('1.2.3-20180101')
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithDefaults() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat('Sonar instance is not set to the default value', sonarInstance, is('SonarCloud'))
|
||||
assertThat('Sonar project version is not set to the default value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.projectVersion=1')))
|
||||
assertThat('Docker image is not set to the default value', jedr.dockerParams.dockerImage, is('maven:3.5-jdk-8'))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithCustomVersion() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
projectVersion: '2'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat('Sonar project version is not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.projectVersion=2')))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithCustomOptions() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
options: '-Dsonar.host.url=localhost'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat('Sonar options are not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.host.url=localhost')))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithCustomOptionsList() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
options: ['sonar.host.url=localhost']
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat('Sonar options are not set to the custom value', jscr.shell, hasItem(containsString('sonar-scanner -Dsonar.host.url=localhost')))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithCustomInstance() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
instance: 'MySonarInstance'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat('Sonar instance is not set to the custom value', sonarInstance.toString(), is('MySonarInstance'))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithPRHandling() throws Exception {
|
||||
binding.setVariable('env', [
|
||||
'CHANGE_ID': '42',
|
||||
'CHANGE_TARGET': 'master',
|
||||
'BRANCH_NAME': 'feature/anything'
|
||||
])
|
||||
nullScript.commonPipelineEnvironment.setGithubOrg('testOrg')
|
||||
//nullScript.commonPipelineEnvironment.setGithubRepo('testRepo')
|
||||
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
//githubOrg: 'testOrg',
|
||||
githubRepo: 'testRepo'
|
||||
)
|
||||
// asserts
|
||||
assertThat(jscr.shell, hasItem(allOf(
|
||||
containsString('-Dsonar.pullrequest.key=42'),
|
||||
containsString('-Dsonar.pullrequest.base=master'),
|
||||
containsString('-Dsonar.pullrequest.branch=feature/anything'),
|
||||
containsString('-Dsonar.pullrequest.provider=github'),
|
||||
containsString('-Dsonar.pullrequest.github.repository=testOrg/testRepo')
|
||||
)))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithPRHandlingWithoutMandatory() throws Exception {
|
||||
thrown.expect(Exception)
|
||||
thrown.expectMessage('ERROR - NO VALUE AVAILABLE FOR githubRepo')
|
||||
|
||||
binding.setVariable('env', ['CHANGE_ID': '42'])
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
githubOrg: 'testOrg'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertJobStatusFailure()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithLegacyPRHandling() throws Exception {
|
||||
binding.setVariable('env', ['CHANGE_ID': '42'])
|
||||
nullScript.commonPipelineEnvironment.setGithubOrg('testOrg')
|
||||
//nullScript.commonPipelineEnvironment.setGithubRepo('testRepo')
|
||||
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
legacyPRHandling: true,
|
||||
githubTokenCredentialsId: 'githubId',
|
||||
//githubOrg: 'testOrg',
|
||||
githubRepo: 'testRepo'
|
||||
)
|
||||
// asserts
|
||||
assertThat(jscr.shell, hasItem(allOf(
|
||||
containsString('-Dsonar.analysis.mode=preview'),
|
||||
containsString('-Dsonar.github.pullRequest=42'),
|
||||
containsString('-Dsonar.github.oauth=TOKEN_githubId'),
|
||||
containsString('-Dsonar.github.repository=testOrg/testRepo')
|
||||
)))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithLegacyPRHandlingWithoutMandatory() throws Exception {
|
||||
thrown.expect(Exception)
|
||||
thrown.expectMessage('ERROR - NO VALUE AVAILABLE FOR githubTokenCredentialsId')
|
||||
|
||||
binding.setVariable('env', ['CHANGE_ID': '42'])
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
legacyPRHandling: true,
|
||||
githubOrg: 'testOrg',
|
||||
githubRepo: 'testRepo'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertJobStatusFailure()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithSonarAuth() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
sonarTokenCredentialsId: 'githubId'
|
||||
)
|
||||
// asserts
|
||||
assertThat(jscr.shell, hasItem(containsString('-Dsonar.login=TOKEN_githubId')))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithSonarCloudOrganization() throws Exception {
|
||||
jsr.step.sonarExecuteScan(
|
||||
script: nullScript,
|
||||
juStabUtils: utils,
|
||||
organization: 'TestOrg-github'
|
||||
)
|
||||
|
||||
// asserts
|
||||
assertThat(jscr.shell, hasItem(containsString('-Dsonar.organization=TestOrg-github')))
|
||||
assertJobStatusSuccess()
|
||||
}
|
||||
}
|
@ -41,7 +41,8 @@ class JenkinsSetupRule implements TestRule {
|
||||
JOB_NAME : 'p',
|
||||
BUILD_NUMBER: '1',
|
||||
BUILD_URL : 'http://build.url',
|
||||
BRANCH_NAME: 'master'
|
||||
BRANCH_NAME: 'master',
|
||||
WORKSPACE: 'any/path'
|
||||
])
|
||||
|
||||
base.evaluate()
|
||||
|
193
vars/sonarExecuteScan.groovy
Normal file
193
vars/sonarExecuteScan.groovy
Normal file
@ -0,0 +1,193 @@
|
||||
import com.sap.piper.ConfigurationHelper
|
||||
import com.sap.piper.GenerateDocumentation
|
||||
import com.sap.piper.Utils
|
||||
|
||||
import static com.sap.piper.Prerequisites.checkScript
|
||||
|
||||
import groovy.transform.Field
|
||||
import groovy.text.SimpleTemplateEngine
|
||||
|
||||
@Field String STEP_NAME = getClass().getName()
|
||||
|
||||
@Field Set GENERAL_CONFIG_KEYS = [
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* The URL to the Github API. see https://docs.sonarqube.org/display/PLUG/GitHub+Plugin#GitHubPlugin-Usage
|
||||
* deprecated: only supported in LTS / < 7.2
|
||||
*/
|
||||
'githubApiUrl',
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* The Github organization.
|
||||
* @default: `commonPipelineEnvironment.getGithubOrg()`
|
||||
*/
|
||||
'githubOrg',
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* The Github repository.
|
||||
* @default: `commonPipelineEnvironment.getGithubRepo()`
|
||||
*/
|
||||
'githubRepo',
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* The Jenkins credentialId for a Github token. It is needed to report findings back to the pull-request.
|
||||
* deprecated: only supported in LTS / < 7.2
|
||||
* @possibleValues Jenkins credential id
|
||||
*/
|
||||
'githubTokenCredentialsId',
|
||||
/**
|
||||
* The Jenkins credentialsId for a SonarQube token. It is needed for non-anonymous analysis runs. see https://sonarcloud.io/account/security
|
||||
* @possibleValues Jenkins credential id
|
||||
*/
|
||||
'sonarTokenCredentialsId',
|
||||
]
|
||||
@Field Set STEP_CONFIG_KEYS = GENERAL_CONFIG_KEYS.plus([
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* Disables the pull-request decoration with inline comments.
|
||||
* deprecated: only supported in LTS / < 7.2
|
||||
* @possibleValues `true`, `false`
|
||||
*/
|
||||
'disableInlineComments',
|
||||
/**
|
||||
* Name of the docker image that should be used. If empty, Docker is not used and the command is executed directly on the Jenkins system.
|
||||
* see dockerExecute
|
||||
*/
|
||||
'dockerImage',
|
||||
/**
|
||||
* The name of the SonarQube instance defined in the Jenkins settings.
|
||||
*/
|
||||
'instance',
|
||||
/**
|
||||
* Pull-Request voting only:
|
||||
* Activates the pull-request handling using the [GitHub Plugin](https://docs.sonarqube.org/display/PLUG/GitHub+Plugin) (deprecated).
|
||||
* deprecated: only supported in LTS / < 7.2
|
||||
* @possibleValues `true`, `false`
|
||||
*/
|
||||
'legacyPRHandling',
|
||||
/**
|
||||
* A list of options which are passed to the `sonar-scanner`.
|
||||
*/
|
||||
'options',
|
||||
/**
|
||||
* Organization that the project will be assigned to in SonarCloud.io.
|
||||
*/
|
||||
'organization',
|
||||
/**
|
||||
* The project version that is reported to SonarQube.
|
||||
* @default: major number of `commonPipelineEnvironment.getArtifactVersion()`
|
||||
*/
|
||||
'projectVersion'
|
||||
])
|
||||
@Field Set PARAMETER_KEYS = STEP_CONFIG_KEYS
|
||||
|
||||
/**
|
||||
* The step executes the [sonar-scanner](https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner) cli command to scan the defined sources and publish the results to a SonarQube instance.
|
||||
*/
|
||||
@GenerateDocumentation
|
||||
void call(Map parameters = [:]) {
|
||||
handlePipelineStepErrors(stepName: STEP_NAME, stepParameters: parameters) {
|
||||
def utils = parameters.juStabUtils ?: new Utils()
|
||||
def script = checkScript(this, parameters) ?: this
|
||||
// load default & individual configuration
|
||||
Map configuration = ConfigurationHelper.newInstance(this)
|
||||
.loadStepDefaults()
|
||||
.mixinGeneralConfig(script.commonPipelineEnvironment, GENERAL_CONFIG_KEYS)
|
||||
.mixinStepConfig(script.commonPipelineEnvironment, STEP_CONFIG_KEYS)
|
||||
.mixinStageConfig(script.commonPipelineEnvironment, parameters.stageName?:env.STAGE_NAME, GENERAL_CONFIG_KEYS)
|
||||
.mixin(parameters, PARAMETER_KEYS)
|
||||
.addIfEmpty('projectVersion', script.commonPipelineEnvironment.getArtifactVersion()?.tokenize('.')?.get(0))
|
||||
.addIfEmpty('githubOrg', script.commonPipelineEnvironment.getGithubOrg())
|
||||
.addIfEmpty('githubRepo', script.commonPipelineEnvironment.getGithubRepo())
|
||||
// check mandatory parameters
|
||||
.withMandatoryProperty('githubTokenCredentialsId', null, { config -> config.legacyPRHandling && isPullRequest() })
|
||||
.withMandatoryProperty('githubOrg', null, { isPullRequest() })
|
||||
.withMandatoryProperty('githubRepo', null, { isPullRequest() })
|
||||
.use()
|
||||
|
||||
if(configuration.options instanceof String)
|
||||
configuration.options = [].plus(configuration.options)
|
||||
|
||||
def worker = { config ->
|
||||
withSonarQubeEnv(config.instance) {
|
||||
loadSonarScanner(config)
|
||||
|
||||
if(config.organization) config.options.add("sonar.organization=${config.organization}")
|
||||
if(config.projectVersion) config.options.add("sonar.projectVersion=${config.projectVersion}")
|
||||
// prefix options
|
||||
config.options = config.options.collect { it.startsWith('-D') ? it : "-D${it}" }
|
||||
|
||||
sh "PATH=\$PATH:${env.WORKSPACE}/.sonar-scanner/bin sonar-scanner ${config.options.join(' ')}"
|
||||
}
|
||||
}
|
||||
|
||||
if(configuration.sonarTokenCredentialsId){
|
||||
def workerForSonarAuth = worker
|
||||
worker = { config ->
|
||||
withCredentials([string(
|
||||
credentialsId: config.sonarTokenCredentialsId,
|
||||
variable: 'SONAR_TOKEN'
|
||||
)]){
|
||||
config.options.add("sonar.login=$SONAR_TOKEN")
|
||||
workerForSonarAuth(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(isPullRequest()){
|
||||
def workerForGithubAuth = worker
|
||||
worker = { config ->
|
||||
if(config.legacyPRHandling) {
|
||||
withCredentials([string(
|
||||
credentialsId: config.githubTokenCredentialsId,
|
||||
variable: 'GITHUB_TOKEN'
|
||||
)]){
|
||||
// support for https://docs.sonarqube.org/display/PLUG/GitHub+Plugin
|
||||
config.options.add('sonar.analysis.mode=preview')
|
||||
config.options.add("sonar.github.oauth=$GITHUB_TOKEN")
|
||||
config.options.add("sonar.github.pullRequest=${env.CHANGE_ID}")
|
||||
config.options.add("sonar.github.repository=${config.githubOrg}/${config.githubRepo}")
|
||||
if(config.githubApiUrl) config.options.add("sonar.github.endpoint=${config.githubApiUrl}")
|
||||
if(config.disableInlineComments) config.options.add("sonar.github.disableInlineComments=${config.disableInlineComments}")
|
||||
workerForGithubAuth(config)
|
||||
}
|
||||
} else {
|
||||
// see https://sonarcloud.io/documentation/analysis/pull-request/
|
||||
config.options.add("sonar.pullrequest.key=${env.CHANGE_ID}")
|
||||
config.options.add("sonar.pullrequest.base=${env.CHANGE_TARGET}")
|
||||
config.options.add("sonar.pullrequest.branch=${env.BRANCH_NAME}")
|
||||
config.options.add("sonar.pullrequest.provider=${config.pullRequestProvider}")
|
||||
switch(config.pullRequestProvider){
|
||||
case 'github':
|
||||
config.options.add("sonar.pullrequest.github.repository=${config.githubOrg}/${config.githubRepo}")
|
||||
break;
|
||||
default: error "Pull-Request provider '${config.pullRequestProvider}' is not supported!"
|
||||
}
|
||||
workerForGithubAuth(config)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dockerExecute(
|
||||
script: script,
|
||||
dockerImage: configuration.dockerImage
|
||||
){
|
||||
worker(configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean isPullRequest(){
|
||||
return env.CHANGE_ID
|
||||
}
|
||||
|
||||
private void loadSonarScanner(config){
|
||||
def filename = new File(config.sonarScannerDownloadUrl).getName()
|
||||
def foldername = filename.replace('.zip', '').replace('cli-', '')
|
||||
|
||||
sh """
|
||||
curl --remote-name --remote-header-name --location --silent --show-error ${config.sonarScannerDownloadUrl}
|
||||
unzip -q ${filename}
|
||||
mv ${foldername} .sonar-scanner
|
||||
"""
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user