blob: 2754d16eb1a429790ef3a7110dabe2712866d6c2 [file] [log] [blame]
import org.apache.sling.jenkins.SlingJenkinsHelper;
def call(Map params = [:]) {
def globalConfig = [
availableJDKs : [ 8: 'jdk_1.8_latest', 9: 'jdk_1.9_latest', 10: 'jdk_10_latest', 11: 'jdk_11_latest',
12: 'jdk_12_latest', 13: 'jdk_13_latest', 14: 'jdk_14_latest', 15: 'jdk_15_latest',
16: 'jdk_16_latest', 17: 'jdk_17_latest', 18: 'jdk_18_latest', 19: 'jdk_19_latest'],
mvnVersion : 'maven_3_latest',
// maps values to node labels (available ones in https://cwiki.apache.org/confluence/display/INFRA/ci-builds.apache.org)
availableOperatingSystems : ['windows' : 'Windows', 'linux': 'ubuntu', 'linux-arm': 'arm', 'ubuntu': 'ubuntu'],
mainNodeLabel : 'ubuntu',
githubCredentialsId: 'sling-github-token'
]
def jobConfig = [
jdks: [8],
operatingSystems: ['linux'],
upstreamProjects: [],
archivePatterns: [],
mavenGoal: '',
additionalMavenParams: '',
rebuildFrequency: '@weekly',
enabled: true,
emailRecipients: [],
sonarQubeEnabled: true,
sonarQubeUseAdditionalMavenParams: true,
sonarQubeAdditionalParams: ''
]
boolean shouldDeploy = false
node(globalConfig.mainNodeLabel) {
timeout(time:5, unit: 'MINUTES') {
stage('Init') {
checkout scm
def url
if (isUnix()) {
sh "git clean -fdx"
url = sh(returnStdout: true, script: 'git config remote.origin.url').trim()
} else {
bat "git clean -fdx"
url = bat(returnStdout: true, script: 'git config remote.origin.url').trim()
}
jobConfig.repoName = url.substring(url.lastIndexOf('/') + 1).replace('.git', '');
if ( fileExists('.sling-module.json') ) {
overrides = readJSON file: '.sling-module.json'
echo "Jenkins overrides: ${overrides.jenkins}"
overrides.jenkins.each { key,value ->
jobConfig[key] = value;
}
}
echo "Final job config: ${jobConfig}"
shouldDeploy = getShouldDeploy()
}
}
}
node(globalConfig.mainNodeLabel) {
timeout(time:30, unit: 'MINUTES', activity: true) {
stage('Configure Job') {
def upstreamProjectsCsv = jobConfig.upstreamProjects ?
jsonArrayToCsv(jobConfig.upstreamProjects) : ''
def jobTriggers = []
if ( isOnMainBranch() )
jobTriggers.add(cron(jobConfig.rebuildFrequency))
if ( upstreamProjectsCsv )
jobTriggers.add(upstream(upstreamProjects: upstreamProjectsCsv, threshold: hudson.model.Result.SUCCESS))
properties([
pipelineTriggers(jobTriggers),
buildDiscarder(logRotator(numToKeepStr: '10'))
])
}
}
}
if ( jobConfig.enabled ) {
def helper = new SlingJenkinsHelper()
helper.runWithErrorHandling(jobConfig, {
// the reference build is always the first one, and the only one to deploy, archive artifacts, etc
// usually this is the build done with the oldest JDK version, to ensure maximum compatibility
boolean isReferenceStage = true
// contains the label as key and a closure to execute as value
def stepsMap = [failFast: true] // fail-fast, https://stackoverflow.com/a/37356318
def referenceJdkVersion
// parallel execution of all build jobs
jobConfig.jdks.each { jdkVersion ->
jobConfig.operatingSystems.each { operatingSystem ->
stageDefinition = defineStage(globalConfig, jobConfig, jdkVersion, operatingSystem, isReferenceStage, shouldDeploy)
if ( isReferenceStage ) {
referenceJdkVersion = jdkVersion
}
stepsMap["Build (Java ${jdkVersion} on ${operatingSystem})"] = stageDefinition
isReferenceStage = false
currentBuild.result = "SUCCESS"
}
}
// do a quick sanity check first without tests if multiple parallel builds are required
if ( stepsMap.size() > 1 ) {
node(globalConfig.mainNodeLabel) {
stage("Sanity Check") {
checkout scm
withMaven(maven: globalConfig.mvnVersion,
jdk: jenkinsJdkLabel(referenceJdkVersion, globalConfig),
publisherStrategy: 'EXPLICIT') {
String mvnCommand = "mvn -U -B -e clean compile ${additionalMavenParams(jobConfig)}"
if (isUnix()) {
sh mvnCommand
} else {
bat mvnCommand
}
}
}
}
}
// execute the actual Maven builds
parallel stepsMap
// last stage is deploy
def goal = jobConfig.mavenGoal ?: "deploy"
if ( goal == "deploy" && shouldDeploy ) {
node(globalConfig.mainNodeLabel) {
stage("Deploy to Nexus") {
deployToNexus(globalConfig)
}
}
}
})
} else {
echo "Job is disabled, not building"
}
}
def jenkinsJdkLabel(int jdkVersion, def globalConfig) {
def label = globalConfig.availableJDKs[jdkVersion]
if ( !label )
error("Unknown JDK version ${jdkVersion}. Available JDKs: ${globalConfig.availableJDKs}")
return label
}
def jenkinsNodeLabel(String operatingSystem, def jobConfig, def globalConfig) {
def branchConfig = jobConfig?.branches?."$env.BRANCH_NAME" ?: [:]
if ( branchConfig.nodeLabel ) {
echo "Using branch specific node label ${branchConfig.nodeLabel}"
return branchConfig.nodeLabel
} else {
def label = globalConfig.availableOperatingSystems[operatingSystem]
if ( !label )
error("Unknown operating system ${operatingSystem}. Available operating systems: ${globalConfig.availableOperatingSystems}")
echo "Using operating-system ${operatingSystem} specific node label ${label}"
return label
}
}
def additionalMavenParams(def jobConfig) {
def branchConfig = jobConfig?.branches?."$env.BRANCH_NAME" ?: [:]
return branchConfig.additionalMavenParams ?
branchConfig.additionalMavenParams : jobConfig.additionalMavenParams
}
def defineStage(def globalConfig, def jobConfig, def jdkVersion, def operatingSystem, boolean isReferenceStage, boolean shouldDeploy) {
def goal = jobConfig.mavenGoal ? jobConfig.mavenGoal : ( isReferenceStage ? "deploy" : "verify" )
def additionalMavenParams = additionalMavenParams(jobConfig)
def jenkinsJdkLabel = jenkinsJdkLabel(jdkVersion, globalConfig)
// do not deploy artifacts built from PRs or feature branches
// also do not deploy non-SNAPSHOT versions
if ( goal == "deploy" && !shouldDeploy ) {
goal = "verify"
echo "Maven goal set to ${goal} since branch is not master ( ${env.BRANCH_NAME} ) or version is not snapshot"
}
def invocation = {
if ( isReferenceStage ) {
if ( goal == "deploy" && shouldDeploy ) {
// this must be an absolute path to always refer to the same directory (for each Maven module in a reactor)
String localRepoPath = "${pwd()}/.local-snapshots-dir" // must also be outside target, as target is cleaned too late
// Make sure the directory is wiped.
dir(localRepoPath) {
deleteDir()
}
// deploy to local directory (all artifacts from a reactor)
additionalMavenParams = "${additionalMavenParams} -DaltDeploymentRepository=snapshot-repo::default::file:${localRepoPath}"
}
// calculate coverage with jacoco (for subsequent evaluation by SonarQube)
additionalMavenParams = "${additionalMavenParams} -Pjacoco-report"
}
checkout scm
withMaven(maven: globalConfig.mvnVersion, jdk: jenkinsJdkLabel,
mavenLocalRepo: '.repository', // use dedicated Maven repository as long as proper locking is not supported, https://lists.apache.org/thread/yovswz70v3f4d2b5ofyoqymvg9lbmzrg
options: [
artifactsPublisher(disabled: !isReferenceStage),
junitPublisher(disabled: !isReferenceStage),
openTasksPublisher(disabled: !isReferenceStage),
dependenciesFingerprintPublisher(disabled: !isReferenceStage)
] ) {
String mvnCommand = "mvn -U -B -e clean ${goal} ${additionalMavenParams} -Dci"
if (isUnix()) {
sh mvnCommand
} else {
bat mvnCommand
}
}
if ( isReferenceStage && jobConfig.archivePatterns ) {
archiveArtifacts(artifacts: SlingJenkinsHelper.jsonArrayToCsv(jobConfig.archivePatterns), allowEmptyArchive: true)
}
if ( isReferenceStage && goal == 'deploy' && shouldDeploy ) {
// Stash the build results from the local deployment directory so we can deploy them on another node
stash name: 'local-snapshots-dir', includes: '.local-snapshots-dir/**'
}
}
def jenkinsNodeLabel = jenkinsNodeLabel(operatingSystem, jobConfig, globalConfig)
return {
node(jenkinsNodeLabel) {
dir(jenkinsJdkLabel) { // isolate parallel builds on same node
timeout(time: 30, unit: 'MINUTES') {
checkout scm
stage("Maven Build (Java ${jdkVersion}, ${goal})") {
echo "Running on node ${env.NODE_NAME}"
invocation.call()
}
}
if ( isReferenceStage ) {
// SonarQube must be executed on the same node in order to reuse artifact from the Maven build
if ( jobConfig.sonarQubeEnabled ) {
stage('Analyse with SonarCloud') {
timeout(time: 30, unit: 'MINUTES') {
analyseWithSonarCloud(globalConfig, jobConfig)
}
}
}
}
}
}
}
}
def analyseWithSonarCloud(def globalConfig, def jobConfig) {
// this might fail if there are no jdks defined, but that's always an error
// also, we don't activate any Maven publisher since we don't want this part of the
// build tracked, but using withMaven(...) allows us to easily reuse the same
// Maven and JDK versions
def additionalMavenParams = additionalMavenParams(jobConfig)
def isPrBuild = env.BRANCH_NAME.startsWith("PR-")
// As we don't have the global SonarCloud conf for now, we can't use #withSonarQubeEnv so we need to set the following props manually
def sonarcloudParams="-Dsonar.host.url=https://sonarcloud.io -Dsonar.organization=apache -Dsonar.projectKey=apache_${jobConfig.repoName} -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco-merged/jacoco.xml ${jobConfig.sonarQubeAdditionalParams}"
if ( jobConfig.sonarQubeUseAdditionalMavenParams ) {
sonarcloudParams="${sonarcloudParams} ${additionalMavenParams}"
}
// Params are different if it's a PR or if it's not
// Note: soon we won't have to handle that manually, see https://jira.sonarsource.com/browse/SONAR-11853
if ( isPrBuild ) {
sonarcloudParams="${sonarcloudParams} -Dsonar.pullrequest.branch=${CHANGE_BRANCH} -Dsonar.pullrequest.base=${CHANGE_TARGET} -Dsonar.pullrequest.key=${CHANGE_ID}"
} else if ( isOnMainBranch() ) {
sonarcloudParams="${sonarcloudParams} -Dsonar.branch.name=${BRANCH_NAME}"
}
static final String SONAR_PLUGIN_GAV = 'org.sonarsource.scanner.maven:sonar-maven-plugin:3.9.1.2184'
// Alls params are set, let's execute using #withCrendentials to hide and mask Robert's token
withCredentials([string(credentialsId: 'sonarcloud-token-rombert', variable: 'SONAR_TOKEN')]) {
// always build with Java 11 (that is the minimum version supported: https://sonarcloud.io/documentation/appendices/end-of-support/)
withMaven(maven: globalConfig.mvnVersion,
jdk: jenkinsJdkLabel(11, globalConfig),
publisherStrategy: 'EXPLICIT') {
try {
String mvnCommand = "mvn -B -e ${SONAR_PLUGIN_GAV}:sonar ${sonarcloudParams}"
if (isUnix()) {
sh mvnCommand
} else {
bat mvnCommand
}
} catch ( Exception e ) {
// TODO - we should check the actual failure cause here, but see
// https://stackoverflow.com/questions/55742773/get-the-cause-of-a-maven-build-failure-inside-a-jenkins-pipeline/55744122
echo "Marking build unstable due to mvn sonar:sonar failing. See https://cwiki.apache.org/confluence/display/SLING/SonarCloud+analysis for more info."
currentBuild.result = 'UNSTABLE'
}
}
}
}
def deployToNexus(def globalConfig) {
node('nexus-deploy') {
timeout(60) {
echo "Running on node ${env.NODE_NAME}"
// first clear workspace
deleteDir()
// Nexus deployment needs pom.xml
checkout scm
// Unstash the previously stashed build results.
unstash name: 'local-snapshots-dir'
// https://www.mojohaus.org/wagon-maven-plugin/merge-maven-repos-mojo.html
static final String WAGON_PLUGIN_GAV = "org.codehaus.mojo:wagon-maven-plugin:2.0.2"
String mavenArguments = "${WAGON_PLUGIN_GAV}:merge-maven-repos -Dwagon.target=https://repository.apache.org/content/repositories/snapshots -Dwagon.targetId=apache.snapshots.https -Dwagon.source=file:${pwd()}/.local-snapshots-dir"
withMaven(maven: globalConfig.mvnVersion,
jdk: jenkinsJdkLabel(11, globalConfig),
publisherStrategy: 'EXPLICIT') {
String mvnCommand = "mvn ${mavenArguments}"
if (isUnix()) {
sh mvnCommand
} else {
bat mvnCommand
}
}
}
}
}
boolean getShouldDeploy() {
// check branch name
if ( !isOnMainBranch() ) {
return false
}
// check version
def mavenPom = readMavenPom()
def mavenVersion = mavenPom.version ?: mavenPom.parent.version
def isSnapshot = mavenVersion.endsWith('-SNAPSHOT')
if ( !isSnapshot ) {
return false
}
return true
}
boolean isOnMainBranch() {
return env.BRANCH_NAME == 'master'
}