Skip to content

Commit ebeb63e

Browse files
cliffhallclaude
andcommitted
ci: a superseded dispatched run posts no pending status either
The new DCO and Everything diff pending jobs posted unconditionally, so an older dispatch whose pending job ran late, or that was re-run (a re-run keeps its run ID), could reset the newer run's result to pending, and its own result job, finding the newer run, would leave it there. The pending jobs now run the same newer-run check. The CI workflows' pending jobs have the same gap; that is #5223. Copilot round 1 on #5222. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: cliffhall <[email protected]>
1 parent 8600738 commit ebeb63e

3 files changed

Lines changed: 66 additions & 9 deletions

File tree

‎.github/workflows/dco.yml‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,34 @@ jobs:
6767
# A dispatched run's checks are left out of the status rollup a pull request
6868
# reads, so on a dispatch these two jobs mirror the run onto the commit as a
6969
# status, as in typescript.yml (whose comments hold the reasoning): pending
70-
# at the start, then the result. A `pull_request` run skips both.
70+
# at the start, then the result. A `pull_request` run skips both. Unlike
71+
# typescript.yml's (#5223), the pending job also stays silent when a newer
72+
# dispatched run exists, so a late or re-run older run cannot reset the
73+
# newer run's result to pending, which its own result job would then leave.
7174
dispatch-status-pending:
7275
name: Report the dispatched run as pending
7376
if: github.event_name == 'workflow_dispatch'
7477
timeout-minutes: 5
7578
runs-on: ubuntu-latest
7679
permissions:
7780
statuses: write
81+
# To look for a newer dispatched run of this workflow on the commit.
82+
actions: read
7883
steps:
7984
- name: Set the commit status to pending
8085
env:
8186
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87+
GH_REPO: ${{ github.repository }}
8288
run: |
89+
# A re-run keeps its run ID, so a newer run means this one is
90+
# superseded and the status is no longer its to set.
91+
NEWER=$(gh run list --workflow dco.yml --event workflow_dispatch \
92+
--commit "$GITHUB_SHA" --json databaseId \
93+
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
94+
if [ "${NEWER:-0}" -gt 0 ]; then
95+
echo "Superseded by a newer dispatched run, which reports instead"
96+
exit 0
97+
fi
8398
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
8499
-f state=pending -f context="DCO (dispatched)" \
85100
-f description="Dispatched run in progress" \

‎.github/workflows/everything-mcp-diff.yml‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,19 +203,34 @@ jobs:
203203
# A dispatched run's checks are left out of the status rollup a pull request
204204
# reads, so on a dispatch these two jobs mirror the run onto the commit as a
205205
# status, as in typescript.yml (whose comments hold the reasoning): pending
206-
# at the start, then the result. Every other event skips both.
206+
# at the start, then the result. Every other event skips both. Unlike
207+
# typescript.yml's (#5223), the pending job also stays silent when a newer
208+
# dispatched run exists, so a late or re-run older run cannot reset the
209+
# newer run's result to pending, which its own result job would then leave.
207210
dispatch-status-pending:
208211
name: Report the dispatched run as pending
209212
if: github.event_name == 'workflow_dispatch'
210213
timeout-minutes: 5
211214
runs-on: ubuntu-latest
212215
permissions:
213216
statuses: write
217+
# To look for a newer dispatched run of this workflow on the commit.
218+
actions: read
214219
steps:
215220
- name: Set the commit status to pending
216221
env:
217222
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
223+
GH_REPO: ${{ github.repository }}
218224
run: |
225+
# A re-run keeps its run ID, so a newer run means this one is
226+
# superseded and the status is no longer its to set.
227+
NEWER=$(gh run list --workflow everything-mcp-diff.yml --event workflow_dispatch \
228+
--commit "$GITHUB_SHA" --json databaseId \
229+
--jq "[.[] | select(.databaseId > $GITHUB_RUN_ID)] | length") || NEWER=0
230+
if [ "${NEWER:-0}" -gt 0 ]; then
231+
echo "Superseded by a newer dispatched run, which reports instead"
232+
exit 0
233+
fi
219234
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
220235
-f state=pending -f context="Everything diff (dispatched)" \
221236
-f description="Dispatched run in progress" \

‎scripts/version-pr-ci.test.mjs‎

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,27 @@ const workflow = (name) => parse(read(`.github/workflows/${name}`));
3636

3737
// `trigger` is the event the workflow runs on when a person pushes; the
3838
// workflow token's version of that event starts no run, hence the dispatch.
39+
// `pendingChecksNewer`: the pending job, like the result job, stays silent
40+
// when a newer dispatched run exists (#5222); the CI workflows' pending jobs
41+
// do not yet (#5223).
3942
const CI = [
4043
{
4144
file: "typescript.yml",
4245
context: "TypeScript (dispatched)",
4346
trigger: "push",
4447
},
4548
{ file: "python.yml", context: "Python (dispatched)", trigger: "push" },
46-
{ file: "dco.yml", context: "DCO (dispatched)", trigger: "pull_request" },
49+
{
50+
file: "dco.yml",
51+
context: "DCO (dispatched)",
52+
trigger: "pull_request",
53+
pendingChecksNewer: true,
54+
},
4755
{
4856
file: "everything-mcp-diff.yml",
4957
context: "Everything diff (dispatched)",
5058
trigger: "pull_request",
59+
pendingChecksNewer: true,
5160
},
5261
];
5362
// The Python stamps touch none of the Everything diff's paths, so its
@@ -104,10 +113,10 @@ for (const { file, context, trigger } of CI) {
104113
});
105114
}
106115

107-
// Run a `dispatch-status` script with a stub `gh` on PATH. The stub answers
116+
// Run a `dispatch-status` (or `-pending`) script with a stub `gh` on PATH. The stub answers
108117
// `gh run list` with the number of newer dispatched runs and records every
109118
// other call, so a test sees exactly which status, if any, the run posts.
110-
function runResultScript(script, { needs, newer }) {
119+
function runStatusScript(script, { needs = {}, newer }) {
111120
const dir = mkdtempSync(path.join(tmpdir(), "dispatch-status-"));
112121
try {
113122
const calls = path.join(dir, "calls");
@@ -171,7 +180,7 @@ for (const { file } of CI) {
171180
},
172181
() => {
173182
for (const needs of [FAILED, PASSED, CANCELLED])
174-
assert.equal(runResultScript(script, { needs, newer: 1 }), null);
183+
assert.equal(runStatusScript(script, { needs, newer: 1 }), null);
175184
},
176185
);
177186

@@ -182,21 +191,39 @@ for (const { file } of CI) {
182191
},
183192
() => {
184193
assert.equal(
185-
runResultScript(script, { needs: PASSED, newer: 0 }),
194+
runStatusScript(script, { needs: PASSED, newer: 0 }),
186195
"success",
187196
);
188197
assert.equal(
189-
runResultScript(script, { needs: FAILED, newer: 0 }),
198+
runStatusScript(script, { needs: FAILED, newer: 0 }),
190199
"failure",
191200
);
192201
assert.equal(
193-
runResultScript(script, { needs: CANCELLED, newer: 0 }),
202+
runStatusScript(script, { needs: CANCELLED, newer: 0 }),
194203
"error",
195204
);
196205
},
197206
);
198207
}
199208

209+
for (const { file } of CI.filter((c) => c.pendingChecksNewer)) {
210+
const pending = workflow(file).jobs["dispatch-status-pending"];
211+
212+
// A superseded run's pending job (a late start, or a re-run, which keeps
213+
// its run ID) must not reset the newer run's result to pending: its own
214+
// result job would stay silent and leave it there.
215+
test(
216+
`${file}: a superseded run posts no pending status`,
217+
{ skip: SCRIPT_SKIP },
218+
() => {
219+
assert.equal(pending.permissions.actions, "read");
220+
const script = pending.steps[0].run;
221+
assert.equal(runStatusScript(script, { newer: 1 }), null);
222+
assert.equal(runStatusScript(script, { newer: 0 }), "pending");
223+
},
224+
);
225+
}
226+
200227
for (const { file, job, dispatches } of VERSION_WORKFLOWS) {
201228
const steps = workflow(file).jobs[job];
202229

0 commit comments

Comments
 (0)