Skip to content

Commit 94cfe61

Browse files
authored
Don't Skip builds if the previous one failed (#81)
* add previos build and update the logic * add cron check to doc only changes * add inverse stage for build skip on changelog * remove the skip stage * add debug logging * [copilot] unify X-only skip logic * combine X-only changes into one pattern * refactor debug params * Clarify comments in isChangeOnlyMatching function Updated comments for clarity regarding empty commits. * change skip result to success * remove return * remove env activation * add build result check * Update CHANGELOG for version 2.2.4 * fix date
1 parent ba66902 commit 94cfe61

File tree

7 files changed

+216
-80
lines changed

7 files changed

+216
-80
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
**2.2.4 - 02/10/26**
2+
3+
- Fix doc only / changelog only skipping behavior
4+
15
**2.2.3 - 02/09/26**
26

37
- Increase the cleanup Jenkins job timeout to 120 minutes

vars/build_stages.groovy

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def call() {
1616
}
1717

1818
// Individual function implementations
19-
def runDebugInfo() {
19+
def runDebugInfo(Map skipEval = [:]) {
2020
stage("Debug Info - Python ${PYTHON_VERSION}") {
2121

2222
echo "Jenkins pipeline run timestamp: ${env.TIMESTAMP}"
@@ -26,7 +26,8 @@ def runDebugInfo() {
2626
SKIP_DEPLOY: ${params.SKIP_DEPLOY}
2727
RUN_SLOW: ${params.RUN_SLOW}
2828
SLACK_TO: ${params.SLACK_TO}
29-
DEBUG: ${params.DEBUG}"""
29+
DEBUG: ${params.DEBUG}
30+
FORCE_FULL_BUILD: ${params.FORCE_FULL_BUILD}"""
3031

3132
// Display environment variables from Jenkins.
3233
echo """Environment:
@@ -44,7 +45,22 @@ def runDebugInfo() {
4445
WORKSPACE: '${WORKSPACE}'
4546
XDG_CACHE_HOME: '${XDG_CACHE_HOME}'
4647
IS_CRON: '${IS_CRON}'
47-
CRON_SCHEDULE: '${env.CRON_SCHEDULE}'"""
48+
CRON_SCHEDULE: '${env.CRON_SCHEDULE}'
49+
GIT_COMMIT: '${env.GIT_COMMIT}'
50+
GIT_PREVIOUS_COMMIT: '${env.GIT_PREVIOUS_COMMIT}'
51+
"""
52+
53+
// Display skip evaluation results (evaluated after checkout)
54+
if (skipEval) {
55+
echo """Skip Evaluation:
56+
previousBuildPassed: ${skipEval.previousBuildPassed}
57+
isDocOnlyChange: ${skipEval.isDocOnlyChange}
58+
isChangelogOnlyChange: ${skipEval.isChangelogOnlyChange}
59+
canSkipFullBuild: ${skipEval.canSkipFullBuild}
60+
skipForDocOnly: ${skipEval.skipForDocOnly}
61+
skipForChangelogOnly: ${skipEval.skipForChangelogOnly}
62+
"""
63+
}
4864
}
4965
}
5066

@@ -158,7 +174,7 @@ def deployDocs() {
158174
}
159175

160176
def cleanup() {
161-
sh "${ACTIVATE} && make clean"
177+
sh "make clean"
162178
cleanWs()
163179
dir("${WORKSPACE}@tmp") {
164180
deleteDir()

vars/git_utils.groovy

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def getPreviousCommit() {
66
}
77

88
def getCurrentCommit() {
9-
return sh(
9+
return env.GIT_COMMIT ?: sh(
1010
script: "git rev-parse HEAD",
1111
returnStdout: true
1212
).trim()
@@ -30,4 +30,125 @@ def getCommitInfo() {
3030
currentCommit: currentCommit,
3131
changedFiles: changedFiles
3232
]
33-
}
33+
}
34+
35+
/**
36+
* Get the commit SHA from the previous Jenkins build.
37+
*
38+
* This checks multiple sources in order:
39+
* 1. GIT_PREVIOUS_COMMIT environment variable (set by Jenkins Git plugin)
40+
* 2. Previous build's GIT_COMMIT from build variables
41+
*
42+
* Returns null if no previous build exists or commit cannot be determined.
43+
*/
44+
def getPreviousBuildCommit() {
45+
// First try the Jenkins Git plugin environment variable
46+
if (env.GIT_PREVIOUS_COMMIT) {
47+
echo "Using GIT_PREVIOUS_COMMIT: ${env.GIT_PREVIOUS_COMMIT}"
48+
return env.GIT_PREVIOUS_COMMIT
49+
}
50+
51+
// Fall back to accessing previous build's variables
52+
def previousBuild = currentBuild.previousBuild
53+
if (previousBuild != null) {
54+
try {
55+
def buildVars = previousBuild.getBuildVariables()
56+
if (buildVars['GIT_COMMIT']) {
57+
echo "Using previous build's GIT_COMMIT: ${buildVars['GIT_COMMIT']}"
58+
return buildVars['GIT_COMMIT']
59+
}
60+
} catch (Exception e) {
61+
echo "Could not get previous build commit: ${e.message}"
62+
}
63+
}
64+
65+
echo "No previous build commit found"
66+
return null
67+
}
68+
69+
/**
70+
* Get the list of files changed since the last Jenkins build.
71+
*
72+
* Returns an empty string if:
73+
* - No previous build exists (first build)
74+
* - The previous commit cannot be determined
75+
* - Git diff fails (e.g., shallow clone without the previous commit)
76+
*
77+
* An empty string should be treated as "run full build" by callers.
78+
*/
79+
def getChangedFilesSinceLastBuild() {
80+
def previousCommit = getPreviousBuildCommit()
81+
def currentCommit = getCurrentCommit()
82+
83+
if (!previousCommit) {
84+
echo "No previous build commit found. Cannot determine changed files."
85+
return ''
86+
}
87+
88+
// Check if the previous commit exists in the current clone
89+
def commitExists = sh(
90+
script: "git cat-file -e ${previousCommit} 2>/dev/null && echo 'exists' || echo 'missing'",
91+
returnStdout: true
92+
).trim()
93+
94+
if (commitExists == 'missing') {
95+
echo "Previous build commit ${previousCommit} not found in current clone (possibly shallow clone). Cannot determine changed files."
96+
return ''
97+
}
98+
99+
def changedFiles = sh(
100+
script: "git diff --name-only ${previousCommit} ${currentCommit} 2>/dev/null || echo ''",
101+
returnStdout: true
102+
).trim()
103+
104+
return changedFiles
105+
}
106+
107+
/**
108+
* Check if all changes since the last Jenkins build match a specific pattern.
109+
*
110+
* This function compares the current commit against the commit from the
111+
* previous Jenkins build to determine if all changed files match the
112+
* given grep pattern.
113+
*
114+
* @param pattern The grep pattern to match (e.g., '^docs/' or '^CHANGELOG')
115+
* @param description Human-readable description for logging (e.g., "docs-only" or "changelog-only")
116+
*
117+
* Returns false (run full build) if:
118+
* - No previous build exists (first build)
119+
* - The previous build's commit cannot be determined
120+
* - There are no changed files
121+
* - Any files do NOT match the pattern
122+
*
123+
* Returns true (can skip non-essential steps) if:
124+
* - All changed files since the last build match the pattern
125+
*/
126+
def isChangeOnlyMatching(String pattern, String description) {
127+
def changedFiles = getChangedFilesSinceLastBuild()
128+
129+
// If no files are found (first build, shallow clone, empty commit, manual rerun),
130+
// return false to trigger a full build
131+
if (changedFiles == '') {
132+
echo "No changed files found since last build. Running full build."
133+
return false
134+
}
135+
136+
echo "Files changed since last build:\n${changedFiles}"
137+
138+
// Check if all changed files match the pattern
139+
def hasNonMatchingChanges = sh(
140+
script: """
141+
echo '${changedFiles}' |
142+
grep -v '${pattern}' |
143+
wc -l || echo '0'
144+
""",
145+
returnStdout: true
146+
).trim().toInteger() > 0
147+
148+
if (!hasNonMatchingChanges) {
149+
echo "All changes are ${description}."
150+
}
151+
152+
// Return true if all changes match the pattern
153+
return !hasNonMatchingChanges
154+
}

vars/is_changelog_only_commit.groovy

Lines changed: 0 additions & 26 deletions
This file was deleted.

vars/is_doc_only_change.groovy

Lines changed: 0 additions & 31 deletions
This file was deleted.

vars/previous_build_passed.groovy

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
def call() {
2+
// Check if the previous build completed successfully.
3+
// Returns true if the previous build passed.
4+
// Returns false if the previous build doesn't exist, failed, was aborted, or is unstable.
5+
//
6+
// This is used to determine whether we can safely skip build steps for
7+
// doc-only or changelog-only changes. If the previous build failed, we
8+
// should run the full build to ensure the failure is addressed.
9+
10+
def previousBuild = currentBuild.previousBuild
11+
12+
if (previousBuild == null) {
13+
// No previous build exists, so we should run the entire build.
14+
echo "No previous build found. Treating as failure."
15+
return false
16+
}
17+
18+
def previousResult = previousBuild.result
19+
echo "Previous build (#${previousBuild.number}) result: ${previousResult}"
20+
21+
// SUCCESS is the only result we consider as "passed"
22+
// Other results include: FAILURE, UNSTABLE, ABORTED, NOT_BUILT, null.
23+
if (previousResult == 'SUCCESS') {
24+
return true
25+
} else {
26+
echo "Previous build did not pass (result: ${previousResult}). Will not skip build steps."
27+
return false
28+
}
29+
}

vars/reusable_pipeline.groovy

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,6 @@ def call(Map config = [:]){
8484
// Jenkins commands run in separate processes, so need to activate the environment every
8585
// time we run pip, poetry, etc.
8686
ACTIVATE_BASE = "source ${CONDA_BIN_PATH}/activate &> /dev/null"
87-
IS_DOC_ONLY_CHANGE = "${is_doc_only_change()}"
88-
IS_CHANGELOG_ONLY_COMMIT = "${is_changelog_only_commit()}"
8987
}
9088

9189
agent { label "coordinator" }
@@ -109,6 +107,11 @@ def call(Map config = [:]){
109107
defaultValue: false,
110108
description: "Whether to run slow tests as part of pytest suite."
111109
)
110+
booleanParam(
111+
name: "FORCE_FULL_BUILD",
112+
defaultValue: false,
113+
description: "Force a complete build regardless of change type (overrides doc-only and changelog-only skip logic)."
114+
)
112115
string(
113116
name: "SLACK_TO",
114117
defaultValue: "",
@@ -153,18 +156,7 @@ def call(Map config = [:]){
153156
}
154157
}
155158
}
156-
157159
stage("Python Versions") {
158-
// Skip builds if this commit only contains changelog changes
159-
// FIXME [MIC-6729]. Commenting out temporarily
160-
// when {
161-
// anyOf {
162-
// environment name: 'IS_CRON', value: 'true'
163-
// not {
164-
// environment name: 'IS_CHANGELOG_ONLY_COMMIT', value: 'true'
165-
// }
166-
// }
167-
// }
168160
steps {
169161
script {
170162

@@ -186,14 +178,45 @@ def call(Map config = [:]){
186178
try {
187179
checkout scm
188180
load_shared_files()
189-
buildStages.runDebugInfo()
190-
buildStages.buildEnvironment()
191-
if (IS_DOC_ONLY_CHANGE.toBoolean() == true) {
192-
echo "This is a doc-only change. Skipping everything except doc build and doc tests."
181+
182+
// Evaluate skip conditions after checkout (GIT_PREVIOUS_COMMIT is now available)
183+
def previousBuildPassed = previous_build_passed()
184+
def isDocOnlyChange = git_utils.isChangeOnlyMatching('^docs/', 'docs-only')
185+
def isChangelogOnlyChange = git_utils.isChangeOnlyMatching('^CHANGELOG', 'changelog-only')
186+
def isCron = env.IS_CRON.toBoolean()
187+
def forceFullBuild = params.FORCE_FULL_BUILD
188+
189+
echo "Skip evaluation: previousBuildPassed=${previousBuildPassed}, isDocOnlyChange=${isDocOnlyChange}, isChangelogOnlyChange=${isChangelogOnlyChange}, isCron=${isCron}, forceFullBuild=${forceFullBuild}"
190+
191+
// Determine if we should skip the full build
192+
def canSkipFullBuild = previousBuildPassed && !isCron && !forceFullBuild
193+
def skipForChangelogOnly = canSkipFullBuild && isChangelogOnlyChange
194+
def skipForDocOnly = canSkipFullBuild && isDocOnlyChange
195+
196+
// Prepare skip evaluation info for debug output
197+
def skipEval = [
198+
previousBuildPassed: previousBuildPassed,
199+
isDocOnlyChange: isDocOnlyChange,
200+
isChangelogOnlyChange: isChangelogOnlyChange,
201+
canSkipFullBuild: canSkipFullBuild,
202+
skipForDocOnly: skipForDocOnly,
203+
skipForChangelogOnly: skipForChangelogOnly
204+
]
205+
206+
if (skipForChangelogOnly) {
207+
echo "This is a changelog-only change since last build and previous build passed. Skipping entire build."
208+
// No build steps needed - just let it fall through to cleanup
209+
currentBuild.result = 'SUCCESS' // Mark build as successful since we're intentionally skipping
210+
} else if (skipForDocOnly) {
211+
echo "This is a doc-only change since last build and previous build passed. Skipping everything except doc build and doc tests."
212+
buildStages.runDebugInfo(skipEval)
213+
buildStages.buildEnvironment()
193214
buildStages.installPackage("docs")
194215
buildStages.buildDocs()
195216
buildStages.testDocs()
196217
} else {
218+
buildStages.runDebugInfo(skipEval)
219+
buildStages.buildEnvironment()
197220
buildStages.installPackage()
198221
buildStages.installDependencies(upstream_repos)
199222
buildStages.checkFormatting(run_mypy)

0 commit comments

Comments
 (0)