1
0
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:
Christopher Fenner 2019-05-13 12:29:07 +02:00 committed by GitHub
parent f9047bc37a
commit c8f9db71eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 456 additions and 1 deletions

View 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

View File

@ -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:

View 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()
}
}

View File

@ -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()

View 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
"""
}