/
slingOsgiBundleBuild.groovy
336 lines (313 loc) · 15.6 KB
/
slingOsgiBundleBuild.groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
import org.apache.sling.jenkins.SlingJenkinsHelper;
def call(Map params = [:]) {
def globalConfig = [
// https://cwiki.apache.org/confluence/x/kRLiAw (most are Eclipse Temurin distributions of Open JDK from Adoptium)
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',
20: 'jdk_20_latest', 21: 'jdk_21_latest', 22: 'jdk_22_latest'],
// https://cwiki.apache.org/confluence/x/cRTiAw
mvnVersion : 'maven_3_latest',
// maps values to node labels (available ones in https://cwiki.apache.org/confluence/x/ViZ4CQ)
availableOperatingSystems : ['windows' : 'Windows', 'linux': 'ubuntu', 'linux-arm': 'arm', 'ubuntu': 'ubuntu'],
mainNodeLabel : 'ubuntu',
githubCredentialsId: 'sling-github-token'
]
def jobConfig = [
jdks: [11,17,21],
operatingSystems: ['linux','windows'],
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
// the stepsMap has at least one entry due to the failFast entry
if ( stepsMap.size() > 2 ) {
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"
// generate javadocs to detect illegal javadoc markup in sources
additionalMavenParams = "javadoc:javadoc ${additionalMavenParams}"
}
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 17 (that is the minimum version supported: https://docs.sonarcloud.io/appendices/scanner-environment/)
withMaven(maven: globalConfig.mvnVersion,
jdk: jenkinsJdkLabel(17, 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'
}