Skip to content
Merged
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
230 changes: 177 additions & 53 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ if(env.BRANCH_NAME == "master") {

env.MAVEN_NTP = true

def fullTestLabel = pullRequest.labels.contains('full-test')
def weeklyTestLabel = pullRequest.labels.contains('weekly-test')
Comment on lines +9 to +10

@lemeurherve lemeurherve Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common location for labels retrieval, quite useful when using replays to reproduce/trigger certain type of builds.

Note: hotfixed in 84e4da8

-def fullTestLabel = pullRequest.labels.contains('full-test')
-def weeklyTestLabel = pullRequest.labels.contains('weekly-test')
+def fullTestLabel
+def weeklyTestLabel
+if (env.CHANGE_ID) {
+  fullTestLabel = pullRequest.labels.contains('full-test')
+  weeklyTestLabel = pullRequest.labels.contains('weekly-test')
+}


def fixedPrepArchiveName = '' // can be set to a specific prep archive name in case last commits aren't impacting it

properties([
disableConcurrentBuilds(abortPrevious: true),
buildDiscarder(logRotator(numToKeepStr: '7')),
Expand All @@ -25,22 +30,16 @@ def mavenEnv(Map params = [:], Closure body) {
// no Dockerized tests; https://github.com/jenkins-infra/documentation/blob/master/ci.adoc#container-agents
node('maven-bom') {
timeout(120) {
withChecks(name: 'Tests', includeStage: true) {
infra.withArtifactCachingProxy {
withEnv([
'JAVA_HOME=/opt/jdk-' + params['jdk'],
'PATH+JDK=/opt/jdk-' + params['jdk'] + '/bin',
"MAVEN_ARGS=${env.MAVEN_ARGS != null ? MAVEN_ARGS : ''} -B ${env.MAVEN_NTP != null ? '-ntp' : ''} -Dmaven.repo.local=${WORKSPACE_TMP}/m2repo",
"MVN_LOCAL_REPO=${WORKSPACE_TMP}/m2repo",
]) {
infra.loadMavenLocalCacheIfAny(env.MVN_LOCAL_REPO)
infra.withArtifactCachingProxy {
withEnv([
'JAVA_HOME=/opt/jdk-' + params['jdk'],
'PATH+JDK=/opt/jdk-' + params['jdk'] + '/bin',
"MAVEN_ARGS=${env.MAVEN_ARGS != null ? MAVEN_ARGS : ''} -B ${env.MAVEN_NTP != null ? '-ntp' : ''} -Dmaven.repo.local=${WORKSPACE_TMP}/m2repo",
"MVN_LOCAL_REPO=${WORKSPACE_TMP}/m2repo",
]) {
infra.loadMavenLocalCacheIfAny(env.MVN_LOCAL_REPO)

body()
}
}
if (junit(testResults: '**/target/surefire-reports/TEST-*.xml,**/target/failsafe-reports/TEST-*.xml').failCount > 0) {
// TODO JENKINS-27092 throw up UNSTABLE status in this case
error 'Some test failures, not going to continue'
body()
}
}
}
Expand All @@ -64,62 +63,146 @@ def fullTestMarkerFile
def weeklyTestMarkerFile
def durations = [:]

stage('prep') {
mavenEnv(jdk: 21) {
checkout scm
withEnv(['SAMPLE_PLUGIN_OPTS=-Dset.changelist']) {
sh '''
mvn -v
bash prep.sh
'''
mavenEnv(jdk: 21) {
def scmVars = checkout scm
commitId = scmVars.GIT_COMMIT

fullTestMarkerFile = fileExists 'full-test'
weeklyTestMarkerFile = fileExists 'weekly-test'

// Ensure prep archive corresponds to the current state
def prepArchiveName = "bom-prep-${commitId}.tar.gz"
def prepFoundInBuildNumber = 0

stage('retrieve prep archive') {
if (fixedPrepArchiveName) {
prepArchiveName = fixedPrepArchiveName
echo "[WARNING] Using fixed prep archive name ${fixedPrepArchiveName} instead of bom-prep-${commitId}.tar.gz"
}
fullTestMarkerFile = fileExists 'full-test'
weeklyTestMarkerFile = fileExists 'weekly-test'
prepFoundInBuildNumber = copyArtifactsFromAnyPreviousBuild(prepArchiveName, env.JOB_NAME)
if (prepFoundInBuildNumber == 0) {
catchError(buildResult: 'SUCCESS', stageResult: 'NOT_BUILT') {
error("[INFO] ${prepArchiveName} not found")
}
return
Comment on lines +84 to +87

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark stage as skipped.

}
}

stage('prep') {
if (prepFoundInBuildNumber == 0) {
withChecks(name: 'Tests', includeStage: true) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withCheck moved from mavenNode to here and to L182, as those are the parts we want to record test results from. (Also, avoid dangling GitHub status checks when skipping stages inside mavenNode)

withEnv(['SAMPLE_PLUGIN_OPTS=-Dset.changelist']) {
sh '''
mvn -v
bash prep.sh
'''
if (junit(testResults: '**/target/surefire-reports/TEST-*.xml,**/target/failsafe-reports/TEST-*.xml').failCount > 0) {
error 'Some test failures during prep.sh, not going to continue'
}
Comment on lines +99 to +101

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from mavenNode, we want to stop here if there is an error in prep.sh.

// Publish incrementals before prep archive preparation to avoid dirty git status
infra.prepareToPublishIncrementals()

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved from the end of "prep" stage as we can prepare incrementals publication as soon as prep.sh finished, and before dirtying the staging area when we create the new prep archive.

}
}
} else {
withEnv(["ARCHIVE_NAME=${prepArchiveName}"]) {
sh '''
tar xzfv "${ARCHIVE_NAME}"
rm "${ARCHIVE_NAME}"
'''
echo "[INFO] ${prepArchiveName} retrieved and extracted, no need to run prep.sh"
}
}
}

stage('parse prep') {
dir('target') {
def plugins = readFile('plugins.txt').split('\n')
pluginsByRepository = parsePlugins(plugins)

lines = readFile('lines.txt').split('\n')
lines = [lines[0], lines[-1]] // Save resources by running PCT only on newest and oldest lines
def allLines = readFile('lines.txt').split('\n')
newestAndOldestLines = [allLines[0], allLines[-1]] // Save resources by running PCT only on newest and oldest lines
echo "[INFO] ${allLines.size()} lines retrieved from lines.txt: ${allLines.join(' ')} "

// For archival, keep track of newest and oldest lines as PR labels may change accross builds
// For stashes, we only care about the lines of the current build
lines = newestAndOldestLines
if (weeklyTestMarkerFile || weeklyTestLabel) {
echo "[INFO] Keeping only 'weekly' line as there is a 'weekly-test' label or marker file"
lines = ['weekly']
} else {
echo "[INFO] Keeping only newest and oldest lines to save resources: ${lines.join(' ')} "
}
}
lines.each { line ->
stash name: line, includes: "pct.sh,excludes.txt,bom-*/excludes.txt,target/pct.jar,target/megawar-${line}.war"
}

stage('archive new prep') {
if (prepFoundInBuildNumber == 0) {
def prepArchiveGlob = 'pct.sh excludes.txt bom-*/excludes.txt target/pct.jar target/plugins.txt target/lines.txt'
// Both newest and oldest lines in the prep archive, in case labels change on PR accross builds
// ex: from weekly-test to full-test
newestAndOldestLines.each { line ->
prepArchiveGlob += " target/megawar-${line}.war"
}
withEnv(["ARCHIVE_NAME=${prepArchiveName}", "ARCHIVE_GLOB=${prepArchiveGlob}",]) {
sh 'tar czfv "${ARCHIVE_NAME}" ${ARCHIVE_GLOB}'
archiveArtifacts artifacts: prepArchiveName, fingerprint: true
echo "[INFO] New ${prepArchiveName} archived"
}
} else {
catchError(buildResult: 'SUCCESS', stageResult: 'NOT_BUILT') {
error("[INFO] No new prep to archive")
}
return
}
}

stage('stash prep lines') {
if (lines.size() > 0) {
lines.each { line ->
stash name: line, includes: "pct.sh,excludes.txt,bom-*/excludes.txt,target/pct.jar,target/megawar-${line}.war"
}
} else {
catchError(buildResult: 'SUCCESS', stageResult: 'NOT_BUILT') {
error('[INFO] No line to stash')
}
return
}
infra.prepareToPublishIncrementals()
}
}

if (BRANCH_NAME == 'master' || fullTestMarkerFile || weeklyTestMarkerFile || env.CHANGE_ID && (pullRequest.labels.contains('full-test') || pullRequest.labels.contains('weekly-test'))) {
if (BRANCH_NAME == 'master' || fullTestMarkerFile || weeklyTestMarkerFile || env.CHANGE_ID && (fullTestLabel || weeklyTestLabel)) {
def branches = [failFast: false]
lines.each {line ->
if (line != 'weekly' && (weeklyTestMarkerFile || env.CHANGE_ID && pullRequest.labels.contains('weekly-test'))) {
if (line != 'weekly' && (weeklyTestMarkerFile || env.CHANGE_ID && weeklyTestLabel)) {
return
}
pluginsByRepository.each { repository, plugins ->
branches["pct-$repository-$line"] = {
def jdk = line == 'weekly' || line == '2.555.x' ? 21 : 17
mavenEnv(jdk: jdk) {
unstash line
withEnv([
"PLUGINS=${plugins.join(',')}",
"LINE=$line",
'EXTRA_MAVEN_PROPERTIES=maven.test.failure.ignore=true:surefire.rerunFailingTestsCount=1'
]) {
def start = System.currentTimeMillis()
try {
sh '''
mvn -v
bash pct.sh
'''
} catch (e) {
if (!(e instanceof InterruptedException) && !(e instanceof org.jenkinsci.plugins.workflow.support.steps.AgentOfflineException)) {
unstable('PCT failed in ' + repository + ' - line ' + line)
} else {
throw e
withChecks(name: 'Tests', includeStage: true) {
mavenEnv(jdk: jdk) {
unstash line
withEnv([
"PLUGINS=${plugins.join(',')}",
"LINE=$line",
'EXTRA_MAVEN_PROPERTIES=maven.test.failure.ignore=true:surefire.rerunFailingTestsCount=1'
]) {
def start = System.currentTimeMillis()
try {
sh '''
mvn -v
bash pct.sh
'''
} catch (e) {
if (!(e instanceof InterruptedException) && !(e instanceof org.jenkinsci.plugins.workflow.support.steps.AgentOfflineException)) {
unstable('PCT failed in ' + repository + ' - line ' + line)
} else {
throw e
}
} finally {
def elapsed = System.currentTimeMillis() - start
durations["pct-$repository-$line"] = (elapsed / 1000.0)
}
} finally {
def elapsed = System.currentTimeMillis() - start
durations["pct-$repository-$line"] = (elapsed / 1000.0)
}
}
}
Expand Down Expand Up @@ -154,3 +237,44 @@ if (fullTestMarkerFile) {
}

infra.maybePublishIncrementals()


// === Helper functions

// Search and copy an artifact from builds of a job
// Returns the build number where it has been found, zero otherwise
def copyArtifactsFromAnyPreviousBuild(archiveName, jobName) {
def foundInBuildNumber = 0
def archiveExists = false
def buildNumber = env.BUILD_NUMBER.toInteger()
if (buildNumber == 1) {
echo "[INFO] First build of ${jobName}, no ${archiveName} available yet"
} else {
// Loop over builds to retrieve the prep archive as previous build can have (only) other archive(s)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use lastSuccessfulBuild (note that this includes UNSTABLE) rather than looping?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with lastSuccessFulBuild is that I would not be able to retrieve the bom-report from an "unstable" build (one with a marker for example).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I got it looping over more than 100 builds in less than a second in #7037, I was surprised how quick it can be.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with lastSuccessFulBuild is that I would not be able to retrieve the bom-report from an "unstable" build

Why? These would match lastSuccessfulBuild (not lastStableBuild).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got it looping over more than 100 builds in less than a second

Sure, if they are already loaded into memory! The problem is the case that the SoftReferences have been cleared and you are forcing large objects to reread from disk.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider this a blocker?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could probably use something like a S3 archive, but that would require some non trivial infra setup as we're trying to avoid write credentials on ci.jenkins.io as it can't be 100% trusted.

@lemeurherve lemeurherve Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I'm counting later on on this trick to retrieve the bom report across previous builds too.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider this a blocker?

Probably not, just caught my eye and alarmed me: in general, any code which loops over historical (completed) build records should be a red flag.

@lemeurherve lemeurherve Jul 15, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

During my (numerous) tests I tried several selectors, but as a build could have archived something else, copyArtifact would use that build as ref to find the archive I wanted, archive not always in that build, sometime in earlier ones. Hence this loop.

I may have missed something, but I'd like to start that way if possible. Let me know if you think of an alternative 🙂

loops over historical (completed) build records should be a red flag

I need to do this to be able to rerun multiple builds profiting from the last available prep:

  • Build n°1, initial commit: run bash prep.sh and create new archive, archiveArtifact it, publish other artifacts (like test reports)
  • Build n°2, replay or rerun: retrieve archive from build n°1, don't create new archive, publish other artifacts
  • Build n°3, replay or rerun: retrieve archive from build n°1, ...
  • Build n° 4, new commit: run bash prep.sh and create new archive, archiveArtifact it, publish other artifacts
  • Build n°5, replay or rerun: retrieve archive from build n°4, ...
  • Build n°6, replay or rerun: retrieve archive from build n°4, ...
  • etc.

def checkBuildNumber = buildNumber - 1
// Don't loop until the first build of master '^^
def limit = jobName.endsWith('master') ? buildNumber - 50 : 0
while (!archiveExists && checkBuildNumber > limit) {
echo "[INFO] Trying to retrieve ${archiveName} from ${jobName}#${checkBuildNumber}..."
try {
copyArtifacts(projectName: jobName,
selector: specific("${checkBuildNumber}"),
filter: archiveName,
fingerprintArtifacts: true,
optional: false,
)
archiveExists = true
} catch(e) {}
if (!archiveExists) {
checkBuildNumber = checkBuildNumber - 1
}
}
if (!archiveExists) {
echo "[INFO] No ${archiveName} found in any build of ${jobName}"
} else {
foundInBuildNumber = checkBuildNumber
echo "[INFO] ${archiveName} found in ${jobName}#${checkBuildNumber}"
}
}
return foundInBuildNumber
}