Skip to content

Commit 6198b4e

Browse files
committed
build: collapse chains of skipped workflow runs
The validate job's 'Look for prior successful runs' step finds a prior successful run that is commit- or tree-same and records its URL via core.notice and as the validate.outputs.skip output, which downstream jobs surface to users. When the prior run was itself a skip-run (i.e. its only successful work was recording a notice pointing at a yet earlier run), we end up with a chain of skip-runs each pointing at the previous skip-run, rather than at the run that actually built and tested the tree. Following such a chain by hand to find the real build is tedious. Bottom out that chain in the script: after identifying a candidate run, inspect its Validation job's annotations for the same 'Skipping: There already is a successful run: <url>' notice. If present, fetch the referenced run, verify it is still completed and successful, and repeat until we reach a run that is not itself a skip-run. Use that URL for both the notice and the job output, so consumers always see the real underlying run. Also add console logs along the way (initial match, each chain hop, stop conditions) to make future debugging straightforward, and grant the workflow checks: read so the script can call checks.listAnnotations. Signed-off-by: Matthew John Cheetham <mjcheetham@outlook.com>
1 parent e4016c8 commit 6198b4e

1 file changed

Lines changed: 88 additions & 2 deletions

File tree

.github/workflows/build.yaml

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ on:
2121
permissions:
2222
contents: read
2323
actions: read
24+
checks: read
2425

2526
env:
2627
GIT_VERSION: ${{ github.event.inputs.git_version || 'v2.53.0.vfs.0.7' }}
@@ -67,6 +68,89 @@ jobs:
6768
const head_sha = run.head_sha;
6869
const tree_id = run.head_commit.tree_id;
6970
71+
/*
72+
* If the given workflow run was itself a "skip" run that references another
73+
* run, follow the chain to find the deepest *actual* successful, non-skipped
74+
* run, so we don't end up pointing at a chain of skip-runs.
75+
*/
76+
const SKIP_NOTICE_RE = /^Skipping: There already is a successful run: (.+)$/
77+
const RUN_URL_RE = /\/actions\/runs\/(\d+)/
78+
const MAX_CHAIN_LENGTH = 10
79+
async function resolveSkipChain(initialRunId, initialRunUrl) {
80+
let currentRunId = initialRunId
81+
let currentRunUrl = initialRunUrl
82+
console.log(`Resolving skip chain starting at ${currentRunUrl}`)
83+
for (let i = 0; i < MAX_CHAIN_LENGTH; i++) {
84+
let referencedUrl = null
85+
try {
86+
const { data: jobsData } = await github.rest.actions.listJobsForWorkflowRun({
87+
owner: context.repo.owner,
88+
repo: context.repo.repo,
89+
run_id: currentRunId,
90+
})
91+
const validationJob = jobsData.jobs.find((j) => j.name === 'Validation')
92+
if (!validationJob) {
93+
console.log(`No 'Validation' job found on ${currentRunUrl}; treating as the real run`)
94+
return currentRunUrl
95+
}
96+
97+
const { data: annotations } = await github.rest.checks.listAnnotations({
98+
owner: context.repo.owner,
99+
repo: context.repo.repo,
100+
check_run_id: validationJob.id,
101+
})
102+
for (const annotation of annotations) {
103+
const match = annotation.message && annotation.message.match(SKIP_NOTICE_RE)
104+
if (match) {
105+
referencedUrl = match[1]
106+
break
107+
}
108+
}
109+
} catch (e) {
110+
// If we cannot inspect this run, fall back to the URL we already have
111+
console.log(`Failed to inspect ${currentRunUrl}: ${e.message}; stopping chain resolution`)
112+
return currentRunUrl
113+
}
114+
115+
if (!referencedUrl) {
116+
console.log(`${currentRunUrl} is not a skip-run; using it as the resolved run`)
117+
return currentRunUrl
118+
}
119+
120+
console.log(`${currentRunUrl} is a skip-run referencing ${referencedUrl}; following the chain`)
121+
122+
const idMatch = referencedUrl.match(RUN_URL_RE)
123+
if (!idMatch) {
124+
console.log(`Could not parse a run ID from ${referencedUrl}; stopping chain resolution`)
125+
return referencedUrl
126+
}
127+
128+
const referencedId = Number(idMatch[1])
129+
let referencedRun
130+
try {
131+
const response = await github.rest.actions.getWorkflowRun({
132+
owner: context.repo.owner,
133+
repo: context.repo.repo,
134+
run_id: referencedId,
135+
})
136+
referencedRun = response.data
137+
} catch (e) {
138+
console.log(`Could not fetch referenced run ${referencedUrl}: ${e.message}; stopping chain resolution`)
139+
return currentRunUrl
140+
}
141+
142+
if (referencedRun.status !== 'completed' || referencedRun.conclusion !== 'success') {
143+
console.log(`Referenced run ${referencedUrl} is no longer usable (status=${referencedRun.status}, conclusion=${referencedRun.conclusion}); stopping chain resolution`)
144+
return currentRunUrl
145+
}
146+
147+
currentRunId = referencedRun.id
148+
currentRunUrl = referencedRun.html_url
149+
}
150+
console.log(`Reached MAX_CHAIN_LENGTH (${MAX_CHAIN_LENGTH}); stopping at ${currentRunUrl}`)
151+
return currentRunUrl
152+
}
153+
70154
// See whether there is a successful run for that commit or tree
71155
const { data: runs } = await github.rest.actions.listWorkflowRuns({
72156
owner: context.repo.owner,
@@ -101,8 +185,10 @@ jobs:
101185
}
102186
103187
if (run.status === 'completed' && run.conclusion === 'success') {
104-
core.notice(`Skipping: There already is a successful run: ${run.html_url}`)
105-
return run.html_url
188+
console.log(`Found previous successful run at ${run.html_url} (head_sha=${run.head_sha}, tree_id=${run.head_commit?.tree_id})`)
189+
const resolvedUrl = await resolveSkipChain(run.id, run.html_url)
190+
core.notice(`Skipping: There already is a successful run: ${resolvedUrl}`)
191+
return resolvedUrl
106192
}
107193
}
108194
return ''

0 commit comments

Comments
 (0)