Skip to content

fix(ci): a skipped draft run can no longer cancel or mask the real run - #9

Merged
jayleaton merged 3 commits into
mainfrom
fix/ci-concurrency
Oct 22, 2026
Merged

jayleaton merged 3 commits into
mainfrom
fix/ci-concurrency

Conversation

@jayleaton

@jayleaton jayleaton commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Problem

A force-push to a draft PR and gh pr ready about a second apart start two runs in the same concurrency group (ci-<workflow>-<ref>, cancel-in-progress). When the draft run (every job skipped) is queued last, it cancels the real run. Its skipped ci-ok then becomes the latest check on the head SHA, and GitHub counts a skipped required check as passing. So the head can look green although nothing ran.

A second failure mode in the same scenario: when the push and the ready event land together, GitHub can attach the ready_for_review run to the previous head, leaving the new SHA with only the draft run and no ci-ok at all.

How

pull_request event ──► concurrency group  ci-CI-<ref>-{draft|run}    (draft runs never cancel real runs)
                       cancel-in-progress only for pull_request   (main push, merge queue, dispatch always finish)

draft-payload run:  draft job ── PR ready now AND no other CI run on this SHA? ── yes ─► real checks, reports ci-ok
                                                                               └─ no ─► reports "ci (draft, not run)"
real run:           changes ─► go / mobile ─► ci-ok   (cancelled by a newer push ⇒ no green ci-ok on the old SHA)
  • ci-ok always runs, so a skipped job never shows its raw name expression. In a run that stays a draft it is named ci (draft, not run), so it can never satisfy the required ci-ok check.
  • The go, changes and mobile jobs keep static names.

Evidence

Scratch PR #5 (now closed, branch deleted): force-push, then gh pr ready 0–1 s later.

Before (main's workflow), at gap 0.5 s: real run 39739182843 ended with changes=cancelled go=cancelled ci-ok=failure. The head's checks then showed only the draft run's skipped ci-ok. In other attempts, the draft run cancelled itself or the real run won.

After (this branch):

gap real run draft run head checks
0 s success, ci-ok ✓ stood down (ci (draft, not run)) ci-ok pass
0.3 s success, ci-ok ✓ stood down ci-ok pass
0.5 s success, ci-ok ✓ stood down ci-ok pass
1 s success, ci-ok ✓ stood down ci-ok pass
0 s success, ci-ok ✓ stood down ci-ok pass
  • Ready run lands on the old head: I deleted the head's real run and reran its draft run. The draft job logged PR #5 ready-now run=true and ran changes and go for real; ci-ok passed.
  • Plain draft push: one run, ci (draft, not run), and no ci-ok on the SHA.
  • Superseded real run: on a ready PR, a second push 20 s later cancelled the first run. The old SHA has no check runs, so no green ci-ok; the new SHA is green.
  • Open draft, then ready right away: the first CI run on this PR showed the draft run checking before GitHub had created the ready run, so both ran the full checks. The draft job now waits 15 s when the PR is already ready. Re-checked on scratch PR scratch: draft→ready dedupe check (do not merge) #10 (closed): the draft run logged ready-now run=false, and only one full run happened.
  • actionlint is clean and make ci passes. CI on this PR's head is green: one run, 1m25s.

Merge Danger

Door: two-way. This only changes the workflow file.

Blast Radius: CI. Draft pushes now start two tiny jobs (draft, ci (draft, not run), about 4 s each) instead of zero. The draft job waits an extra 15 s when the PR is already ready at check time. Pushes to main and merge-queue runs are no longer cancelled by later ones.

🤖 Generated with Claude Code

jayleaton and others added 2 commits October 22, 2026 14:17
A force-push to a draft PR followed by "ready for review" a second later
started two runs in one concurrency group; when the draft run arrived
last it cancelled the real run, and its skipped ci-ok then counted as
the passing required check for the head SHA.

Draft and real runs now use separate concurrency groups, only PR runs
cancel superseded runs, and jobs in draft runs are renamed
"<job> (draft, not run)" so they never report as ci-ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…revious head

When a push and "ready for review" land together, GitHub can attach the
ready_for_review run to the old head, leaving the new SHA with only the
skipped draft run and no ci-ok. A small draft job now checks the live
PR state: if the PR is ready and no other CI run covers this SHA, the
run does the real checks. ci-ok always runs and reports as
"ci (draft, not run)" in runs that stayed drafts, which also replaces
the job renames whose expressions showed raw on skipped jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton marked this pull request as ready for review October 22, 2026 07:40
Opening a draft PR and marking it ready seconds later let the draft run
check before GitHub had created the ready run, so both ran the full
checks. Waiting 15 s (only when the PR is already ready) avoids the
duplicate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton merged commit 3408579 into main Oct 22, 2026
5 checks passed
@jayleaton
jayleaton deleted the fix/ci-concurrency branch October 22, 2026 07:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant