From 69910d19ef105c10076a4979604026a7233480ac Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 05:43:05 +0000 Subject: [PATCH] Keep late draft CI runs from cancelling real ones An agent pushes a commit and runs `gh pr ready` seconds later. GitHub can deliver that push's `synchronize` event (payload still draft: true) after `ready_for_review`. Both runs shared one concurrency group with cancel-in-progress, so the late draft run cancelled the real run, skipped every job on the draft gate, and reported ci-ok green on a head nothing had tested. Seen on #1706 and #1707 today; both needed a manual re-run to get a real verdict. Give draft PR runs their own group so they can never cancel a ready-state run. Draft pushes still supersede each other, as do ready pushes. Add a scheduling test that pins the split. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01EtzNTGaPfJK39cMX68F1qw --- .github/workflows/ci.yml | 8 ++++++-- scripts/tests/test_ci_scheduling.py | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bd976e031..2c91ab4ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,9 +39,13 @@ permissions: # queued push but the newest during a merge burst, and most main commits never # got a verdict. Merge-group refs (gh-readonly-queue/...) are unique per queue # entry already. The nightly keeps its own group so it never queues behind (or -# cancels) a push. +# cancels) a push. Draft PR runs get their own group too: a push followed at +# once by `gh pr ready` can deliver the push's `synchronize` (payload still +# draft) after `ready_for_review`. In a shared group that late draft run +# cancelled the real run, skipped every job, and left ci-ok green on a head +# nothing had tested. concurrency: - group: ci-${{ github.event.pull_request.number || (github.event_name == 'push' && github.sha) || github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }} + group: ci-${{ github.event.pull_request.number || (github.event_name == 'push' && github.sha) || github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }}${{ github.event.pull_request.draft && '-draft' || '' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Linux jobs run on Depot's runners (depot-ubuntu-*-4: 4 vCPU, like GitHub's diff --git a/scripts/tests/test_ci_scheduling.py b/scripts/tests/test_ci_scheduling.py index e49271e89..ae0cb7951 100644 --- a/scripts/tests/test_ci_scheduling.py +++ b/scripts/tests/test_ci_scheduling.py @@ -203,6 +203,13 @@ def test_unchanged_pr_merge_skips_only_what_the_queue_re_tests(self): self.assertIn("if: github.event_name == 'pull_request' && github.base_ref == 'main'" " && github.head_ref != 'release/v5-prerelease'", owner) + def test_late_draft_run_cannot_cancel_the_ready_run(self): + # A push's draft-payload `synchronize` can land after `ready_for_review` + # on the same head; sharing a group, it cancelled the real run and left + # ci-ok green with every job skipped. + group = re.search(r"^concurrency:\n group: (.*)$", TEXT, re.M)[1] + self.assertIn("github.event.pull_request.draft && '-draft'", group) + def test_row_reader_preserves_both_os_siblings(self): jobs = reader.jobs("""jobs: example: