Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
* Fix branch mapping logic to ensure correct environment assignment ([#1263](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1263))
* Fix Nexus not accepting npm artifacts when uploaded through odsComponentStageUploadToNexus ([#1268](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1268))
* Aqua Stage is being skipped when branches are not eligable ([#1170](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1170))

## [4.12.4] - 2026-06-29
### Changed
* Tune SonarQube scan to work at scale ([#1275](https://github.com/opendevstack/ods-jenkins-shared-library/pull/1275))

## [4.12.3] - 2026-06-19
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ _List<String>_
all other editions (which are capable of handling multiple branches).


| *filesizeLimit* +
_int_
|Sets the limit in megabytes (MB) for files to be discarded from the analysis scope
if the file size is greater than specified.
Defaults to 2 MB.


| *longLivedBranches* +
_List<String>_
|Branch(es) for which no PR analysis should be performed. If not set, it
Expand All @@ -70,6 +77,13 @@ _String_
to be the same as in BuildOpenShiftImageStage.


| *scanTimeout* +
_int_
|Timeout in minutes for the SonarQube scanner execution.
When reached, the scan is aborted but the pipeline continues.
Defaults to 10.


| *sonarQubeNexusRepository* +
_String_
|Nexus repository to upload the SonarQube report to.
Expand Down
12 changes: 12 additions & 0 deletions src/org/ods/component/ScanWithSonarOptions.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,16 @@ class ScanWithSonarOptions extends Options {
*/
String sonarQubeNexusRepository

/**
* Timeout in minutes for the SonarQube scanner execution.
* When reached, the scan is aborted but the pipeline continues.
* Defaults to 10. */
int scanTimeout

/**
* Sets the limit in megabytes (MB) for files to be discarded from the analysis scope
* if the file size is greater than specified.
* Defaults to 2 MB. */
int filesizeLimit

}
39 changes: 30 additions & 9 deletions src/org/ods/component/ScanWithSonarStage.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ class ScanWithSonarStage extends Stage {
if (!config.containsKey('requireQualityGatePass')) {
config.requireQualityGatePass = false
}
if (!config.containsKey('scanTimeout')) {
config.scanTimeout = 10
}
if (!config.containsKey('filesizeLimit')) {
config.filesizeLimit = 2
}
if (configurationSonarCluster['nexusRepository']) {
config.sonarQubeNexusRepository = configurationSonarCluster['nexusRepository']
}
Expand Down Expand Up @@ -102,6 +108,7 @@ class ScanWithSonarStage extends Stage {
sonarProperties['sonar.projectKey'] = sonarProjectKey
sonarProperties['sonar.projectName'] = sonarProjectKey
sonarProperties['sonar.branch.name'] = context.gitBranch
sonarProperties['sonar.filesize.limit'] = options.filesizeLimit

def pullRequestInfo = assemblePullRequestInfo()
def ocSecretName = "sonarqube-private-token"
Expand All @@ -119,21 +126,35 @@ class ScanWithSonarStage extends Stage {
)
]) {
logger.info("SonarQube private projects enabled, using private token.")
executeWithTimeout {
runSonarQubeScanAndReport(
sonarProjectKey,
sonarProperties,
pullRequestInfo,
steps.env.privateToken as String
)
}
}
} else {
logger.info("SonarQube private projects disabled, using public token.")
executeWithTimeout {
runSonarQubeScanAndReport(
sonarProjectKey,
sonarProperties,
pullRequestInfo,
steps.env.privateToken as String
""
)
}
} else {
logger.info("SonarQube private projects disabled, using public token.")
runSonarQubeScanAndReport(
sonarProjectKey,
sonarProperties,
pullRequestInfo,
""
)
}
}

private void executeWithTimeout(Closure closure) {
try {
steps.timeout(time: options.scanTimeout, unit: 'MINUTES') {
closure()
}
} catch (Exception e) {
logger.warn "SonarQube scan timed out after ${options.scanTimeout} minutes. Continuing pipeline."
}
}

Expand Down
1 change: 1 addition & 0 deletions src/org/ods/services/SonarQubeService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class SonarQubeService {
'-Dsonar.scm.provider=git',
"-Dsonar.projectKey=${properties['sonar.projectKey']}",
"-Dsonar.projectName=${properties['sonar.projectName']}",
"-Dsonar.filesize.limit=${properties['sonar.filesize.limit']}",
"-Dsonar.sources=.",
]
if (exclusions?.trim()) {
Expand Down
68 changes: 68 additions & 0 deletions test/groovy/org/ods/component/ScanWithSonarStageSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,72 @@ class ScanWithSonarStageSpec extends PipelineSpockTestBase {
stage.sonarQubeProjectsPrivate == true
}

def "stage uses default scanTimeout and filesizeLimit values"() {
given:
def tempFolderPath = tempFolder.getRoot().absolutePath
def script = Spy(PipelineSteps)
script.env.WORKSPACE = tempFolderPath
def logger = Spy(new Logger(script, false))
def config = [:]
def configurationSonarCluster = [:]
def stage = new ScanWithSonarStage(
script,
new Context(script, [
componentId: "component1",
projectId: "prj1",
buildUrl: "http://build",
buildNumber: "56",
repoName: "component1",
gitCommit: "12112121212121",
cdProject: "prj1-cd",
credentialsId: "cd-user",
branchToEnvironmentMapping: ['*': 'dev']
], logger),
config,
Spy(new BitbucketService(script, 'https://bitbucket.example.com', 'FOO', 'foo-cd-cd-user-with-password', logger)),
Spy(new SonarQubeService(script, logger, "SonarServerConfig")),
Spy(new NexusService("http://nexus", script, "foo-cd-cd-user-with-password")),
logger,
configurationSonarCluster,
[:]
)
expect:
stage.options.scanTimeout == 10
stage.options.filesizeLimit == 2
}

def "stage uses custom scanTimeout and filesizeLimit values"() {
given:
def tempFolderPath = tempFolder.getRoot().absolutePath
def script = Spy(PipelineSteps)
script.env.WORKSPACE = tempFolderPath
def logger = Spy(new Logger(script, false))
def config = [scanTimeout: 30, filesizeLimit: 5]
def configurationSonarCluster = [:]
def stage = new ScanWithSonarStage(
script,
new Context(script, [
componentId: "component1",
projectId: "prj1",
buildUrl: "http://build",
buildNumber: "56",
repoName: "component1",
gitCommit: "12112121212121",
cdProject: "prj1-cd",
credentialsId: "cd-user",
branchToEnvironmentMapping: ['*': 'dev']
], logger),
config,
Spy(new BitbucketService(script, 'https://bitbucket.example.com', 'FOO', 'foo-cd-cd-user-with-password', logger)),
Spy(new SonarQubeService(script, logger, "SonarServerConfig")),
Spy(new NexusService("http://nexus", script, "foo-cd-cd-user-with-password")),
logger,
configurationSonarCluster,
[:]
)
expect:
stage.options.scanTimeout == 30
stage.options.filesizeLimit == 5
}

}
40 changes: 36 additions & 4 deletions test/groovy/org/ods/services/SonarQubeServiceSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ class SonarQubeServiceSpec extends PipelineSpockTestBase {
logger.debugMode >> false
def service = new SonarQubeService(steps, logger, "env")
def optionsWithPrivateToken = [
properties: ['sonar.projectKey': 'key', 'sonar.projectName': 'name'],
properties: ['sonar.projectKey': 'key', 'sonar.projectName': 'name', 'sonar.filesize.limit': '2'],
gitCommit: "abcdef123456",
pullRequestInfo: [:],
sonarQubeEdition: "community",
exclusions: "test/**",
privateToken: "my-private-token"
]
def optionsWithoutPrivateToken = [
properties: ['sonar.projectKey': 'key', 'sonar.projectName': 'name'],
properties: ['sonar.projectKey': 'key', 'sonar.projectName': 'name', 'sonar.filesize.limit': '2'],
gitCommit: "abcdef123456",
pullRequestInfo: [:],
sonarQubeEdition: "community",
Expand All @@ -109,8 +109,8 @@ class SonarQubeServiceSpec extends PipelineSpockTestBase {

then:
2 * steps.sh(label: 'Set Java 17 for SonarQube scan', script: "source use-j17.sh")
1 * steps.sh(label: 'Run SonarQube scan', script: { it.contains("/opt/sonar-scanner/bin/sonar-scanner") && it.contains("-Dsonar.projectKey=key") && it.contains("-Dsonar.projectName=name") && it.contains("-Dsonar.exclusions=test/**") && it.contains("-Dsonar.auth.token=my-private-token") })
1 * steps.sh(label: 'Run SonarQube scan', script: { it.contains("/opt/sonar-scanner/bin/sonar-scanner") && it.contains("-Dsonar.projectKey=key") && it.contains("-Dsonar.projectName=name") && it.contains("-Dsonar.exclusions=test/**") && it.contains("-Dsonar.auth.token=public-token") })
1 * steps.sh(label: 'Run SonarQube scan', script: { it.contains("/opt/sonar-scanner/bin/sonar-scanner") && it.contains("-Dsonar.projectKey=key") && it.contains("-Dsonar.projectName=name") && it.contains("-Dsonar.filesize.limit=2") && it.contains("-Dsonar.exclusions=test/**") && it.contains("-Dsonar.auth.token=my-private-token") })
1 * steps.sh(label: 'Run SonarQube scan', script: { it.contains("/opt/sonar-scanner/bin/sonar-scanner") && it.contains("-Dsonar.projectKey=key") && it.contains("-Dsonar.projectName=name") && it.contains("-Dsonar.filesize.limit=2") && it.contains("-Dsonar.exclusions=test/**") && it.contains("-Dsonar.auth.token=public-token") })
}

def "generateReport calls script.sh with correct params and uses correct token"() {
Expand Down Expand Up @@ -209,6 +209,38 @@ class SonarQubeServiceSpec extends PipelineSpockTestBase {
service.generateAndStoreSonarQubeToken("credId", "namespace", "secretName") == ""
}

def "scan includes sonar.filesize.limit parameter from properties"() {
given:
def steps = GroovyMock(util.PipelineSteps)
steps.tool('SonarScanner') >> '/opt/sonar-scanner'
steps.withSonarQubeEnv(_, _) >> { String env, Closure block -> block() }
steps.getSONAR_HOST_URL() >> 'https://sonarqube.example.com'
steps.getSONAR_AUTH_TOKEN() >> 'public-token'
steps.sh(_) >> { Map args ->
if (args.returnStatus) {
return 1
}
return null
}
def logger = Mock(org.ods.util.ILogger)
logger.debugMode >> false
def service = new SonarQubeService(steps, logger, "env")
def options = [
properties: ['sonar.projectKey': 'key', 'sonar.projectName': 'name', 'sonar.filesize.limit': '5'],
gitCommit: "abcdef123456",
pullRequestInfo: [:],
sonarQubeEdition: "community",
exclusions: "",
privateToken: ""
]

when:
service.scan(options)

then:
1 * steps.sh(label: 'Run SonarQube scan', script: { it.contains("-Dsonar.filesize.limit=5") })
}

def "getScannerBinary returns tool path if which fails"() {
given:
def steps = GroovyMock(util.PipelineSteps)
Expand Down
Loading