From 3ef52a9e3a97ec0305e0b488ed58d5597be88b5a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 21:16:05 +0000 Subject: [PATCH 1/3] fix(ci): add concurrency groups so the Actions queue can drain The repository's Actions queue is saturated and not draining: 285 runs queued, the oldest sitting unstarted for ~24 minutes with only 2 runs in progress. Pushes to `main` are affected as well as PRs, so no PR can reach a green CI gate and nothing is mergeable. Cause: 17 of 30 workflows carried no `concurrency:` block, including the heaviest and most-triggered ones (ci, codeql-analysis, security, e2e-tests, pr-checks, auto-label, dependency-review). #1447 correctly removed their base-branch filters so every PR runs the gate, but without a concurrency group each push stacks a brand-new full run beside the runs it obsoletes. With ~30 workflows and dozens of open PRs the arrival rate exceeds the drain rate and the backlog grows without bound. Add a concurrency group to the 14 workflows that trigger on pull_request, pull_request_target, push, or issues: - Group is keyed on the PR number where one exists, falling back to github.ref. Keying on github.ref alone would be wrong for the pull_request_target workflows (pr-checks, dependabot-auto-merge), where github.ref resolves to the base branch and every PR would collide into a single group and cancel its siblings. - cancel-in-progress is true only for pull_request and pull_request_target events. Push and schedule runs queue rather than cancel, so each merged commit keeps its own verdict instead of being cancelled by the next merge. - auto-label triggers on both PRs and issues, so its key falls through pull_request.number -> issue.number -> github.ref. Left untouched: bulk-issue-processor, real-processing, and stale, which are workflow_dispatch- or schedule-only and are not queue pressure. Group names are prefixed by workflow slug and do not collide with the existing groups in coverage, secret-scan, pr-governance, verification, phase-goal-tracker, or the gh-aw lock files. This changes scheduling only. No job, step, trigger, or permission is modified; all 28 workflow files still parse. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV --- .github/workflows/anthropic-wif-test.yml | 10 ++++++++++ .github/workflows/api-cost-postgres.yml | 10 ++++++++++ .github/workflows/auto-assign.yml | 10 ++++++++++ .github/workflows/auto-label.yml | 10 ++++++++++ .github/workflows/branch-cleanup.yml | 10 ++++++++++ .github/workflows/ci.yml | 10 ++++++++++ .github/workflows/codeql-analysis.yml | 10 ++++++++++ .github/workflows/dependabot-auto-merge.yml | 10 ++++++++++ .github/workflows/dependency-review.yml | 10 ++++++++++ .github/workflows/e2e-tests.yml | 10 ++++++++++ .github/workflows/gh-aw-validation.yml | 10 ++++++++++ .github/workflows/issue-triage.yml | 10 ++++++++++ .github/workflows/pr-checks.yml | 10 ++++++++++ .github/workflows/security.yml | 10 ++++++++++ 14 files changed, 140 insertions(+) diff --git a/.github/workflows/anthropic-wif-test.yml b/.github/workflows/anthropic-wif-test.yml index 297cf510b..a5bcbaf89 100644 --- a/.github/workflows/anthropic-wif-test.yml +++ b/.github/workflows/anthropic-wif-test.yml @@ -12,6 +12,16 @@ permissions: id-token: write contents: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: anthropic-wif-test-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: call-claude: runs-on: ubuntu-latest diff --git a/.github/workflows/api-cost-postgres.yml b/.github/workflows/api-cost-postgres.yml index 422b6f204..31676e971 100644 --- a/.github/workflows/api-cost-postgres.yml +++ b/.github/workflows/api-cost-postgres.yml @@ -29,6 +29,16 @@ on: permissions: contents: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: api-cost-postgres-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: migration-matrix: name: PostgreSQL migration matrix (${{ matrix.scenario }}) diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index c716e8e46..1c9318e19 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -8,6 +8,16 @@ on: permissions: issues: write + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: auto-assign-${{ github.event.issue.number || github.ref }} + cancel-in-progress: true + jobs: auto-assign: runs-on: ubuntu-latest diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index 46ff31e6a..b421a5a4a 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -9,6 +9,16 @@ on: permissions: pull-requests: write issues: write + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: auto-label-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} + cancel-in-progress: true + jobs: label: runs-on: ubuntu-latest diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index 4aa29fd1f..7c67603dc 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -49,6 +49,16 @@ permissions: contents: write pull-requests: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: branch-cleanup-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: cleanup: runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7400d8f4..2e48b8603 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,16 @@ permissions: contents: read actions: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: guards: # Fail fast on the class of breakage that shipped to main un-caught: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index bdad9e215..cf97aedce 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -14,6 +14,16 @@ permissions: security-events: write actions: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: codeql-analysis-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: analyze: name: "Security Scan - ${{ matrix.language }}" diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index acb002814..1604b178c 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -16,6 +16,16 @@ permissions: pull-requests: write statuses: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: dependabot-auto-merge-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: approve: if: >- diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 1ff5a5eb5..3c2aa805c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -9,6 +9,16 @@ permissions: contents: read pull-requests: write + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: dependency-review-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: dependency-review: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3bc529fd7..0e36165a5 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -30,6 +30,16 @@ env: TEST_YOUTUBE_URL: https://www.youtube.com/watch?v=auJzb1D-fag VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: e2e-tests-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: e2e: name: E2E Pipeline Tests diff --git a/.github/workflows/gh-aw-validation.yml b/.github/workflows/gh-aw-validation.yml index 8f0b98fcc..8c46eafb5 100644 --- a/.github/workflows/gh-aw-validation.yml +++ b/.github/workflows/gh-aw-validation.yml @@ -20,6 +20,16 @@ on: permissions: contents: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: gh-aw-validation-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: validate-gh-aw: runs-on: ubuntu-latest diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index f991e9e32..93c179e85 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -4,6 +4,16 @@ on: types: [opened] permissions: issues: write + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: issue-triage-${{ github.event.issue.number || github.ref }} + cancel-in-progress: true + jobs: triage: runs-on: ubuntu-latest diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index b9e6a5ec1..2a209b339 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -6,6 +6,16 @@ on: permissions: {} + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: pr-checks-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: validate: runs-on: ubuntu-latest diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index dee5f5ce8..0ceea1bfc 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -15,6 +15,16 @@ permissions: security-events: write actions: read + +# Supersede superseded work instead of stacking it. Without this, every push +# to a PR branch queued a brand-new full run alongside the ones it obsoleted; +# with ~30 workflows and dozens of open PRs the Actions queue could not drain. +# PR runs cancel their predecessors; push/schedule runs queue instead, so each +# merged commit keeps its own verdict. +concurrency: + group: security-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} + jobs: npm-audit: runs-on: ubuntu-latest From 20cc78e0d4f3902f3f0e999871a5e4f3be302ef2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 21:19:07 +0000 Subject: [PATCH 2/3] fix(ci): key non-PR concurrency by SHA so merge bursts keep their verdicts Self-review correction to the previous commit, plus the same fix applied to two workflows that already had the defect. The previous commit keyed every group on `github.event.pull_request.number || github.ref` and set `cancel-in-progress` false for push events, on the reasoning that push runs would queue rather than cancel and so "each merged commit keeps its own verdict". That claim is wrong. `cancel-in-progress: false` prevents a new run from cancelling a *running* one. It does not prevent GitHub from cancelling a *pending* one: when a run is queued into a group that already has one in progress and one pending, the pending run is cancelled. Because `github.ref` is `refs/heads/main` for every merge, all main pushes shared one group, so a burst of merges -- exactly what this repository does, ten in twenty minutes -- would have silently dropped the middle commits' verdicts. That is a quieter version of the failure being fixed. Key non-PR runs on `github.sha` instead. Each push and scheduled run then lands in its own singleton group and can neither cancel nor be cancelled. This costs nothing: every `push:` trigger in these workflows is already filtered to `main` (verified across all 13), so pushes to PR branches fire no push runs at all and all queue pressure comes from `pull_request`/`pull_request_target` events, which still cancel their predecessors by PR number. The conditional `cancel-in-progress` is kept as a second, explicit safeguard. Also apply the same key to coverage.yml and secret-scan.yml. Both predate this change and both carried the bare `github.ref` group with `cancel-in-progress: true` -- the more aggressive form of the same bug, where a merge to main cancels the previous merge's coverage and secret scan outright. Leaving them as-is while documenting the hazard in fourteen other files would have been incoherent. Issue-triggered workflows (auto-assign, issue-triage, auto-label) keep their `github.event.issue.number` key; `github.sha` is meaningless for issues events and would collide every issue into one group. Scheduling only. No job, step, trigger, or permission changed; all 28 workflow files parse. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV --- .github/workflows/anthropic-wif-test.yml | 11 ++++++++--- .github/workflows/api-cost-postgres.yml | 11 ++++++++--- .github/workflows/auto-assign.yml | 9 +++++++-- .github/workflows/auto-label.yml | 9 +++++++-- .github/workflows/branch-cleanup.yml | 11 ++++++++--- .github/workflows/ci.yml | 11 ++++++++--- .github/workflows/codeql-analysis.yml | 11 ++++++++--- .github/workflows/coverage.yml | 8 ++++++-- .github/workflows/dependabot-auto-merge.yml | 11 ++++++++--- .github/workflows/dependency-review.yml | 11 ++++++++--- .github/workflows/e2e-tests.yml | 11 ++++++++--- .github/workflows/gh-aw-validation.yml | 11 ++++++++--- .github/workflows/issue-triage.yml | 9 +++++++-- .github/workflows/pr-checks.yml | 11 ++++++++--- .github/workflows/secret-scan.yml | 8 ++++++-- .github/workflows/security.yml | 11 ++++++++--- 16 files changed, 121 insertions(+), 43 deletions(-) diff --git a/.github/workflows/anthropic-wif-test.yml b/.github/workflows/anthropic-wif-test.yml index a5bcbaf89..e57dd084e 100644 --- a/.github/workflows/anthropic-wif-test.yml +++ b/.github/workflows/anthropic-wif-test.yml @@ -16,10 +16,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: anthropic-wif-test-${{ github.event.pull_request.number || github.ref }} + group: anthropic-wif-test-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/api-cost-postgres.yml b/.github/workflows/api-cost-postgres.yml index 31676e971..6d3b0dcd3 100644 --- a/.github/workflows/api-cost-postgres.yml +++ b/.github/workflows/api-cost-postgres.yml @@ -33,10 +33,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: api-cost-postgres-${{ github.event.pull_request.number || github.ref }} + group: api-cost-postgres-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml index 1c9318e19..469b48d49 100644 --- a/.github/workflows/auto-assign.yml +++ b/.github/workflows/auto-assign.yml @@ -12,8 +12,13 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: group: auto-assign-${{ github.event.issue.number || github.ref }} cancel-in-progress: true diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml index b421a5a4a..ececcb602 100644 --- a/.github/workflows/auto-label.yml +++ b/.github/workflows/auto-label.yml @@ -13,8 +13,13 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: group: auto-label-${{ github.event.pull_request.number || github.event.issue.number || github.ref }} cancel-in-progress: true diff --git a/.github/workflows/branch-cleanup.yml b/.github/workflows/branch-cleanup.yml index 7c67603dc..210e14ba2 100644 --- a/.github/workflows/branch-cleanup.yml +++ b/.github/workflows/branch-cleanup.yml @@ -53,10 +53,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: branch-cleanup-${{ github.event.pull_request.number || github.ref }} + group: branch-cleanup-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e48b8603..e1a64e03d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,10 +17,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: ci-${{ github.event.pull_request.number || github.ref }} + group: ci-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cf97aedce..6ff1873fb 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -18,10 +18,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: codeql-analysis-${{ github.event.pull_request.number || github.ref }} + group: codeql-analysis-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 417bb83d7..27be14a4b 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,9 +17,13 @@ permissions: contents: read actions: read +# Keyed by PR number for PR runs and by commit SHA otherwise. The previous +# github.ref key collided every merge to main into one group, where GitHub +# cancels the pending run -- a burst of merges silently dropped the middle +# commits' verdicts. See the fuller note in ci.yml. concurrency: - group: coverage-${{ github.ref }} - cancel-in-progress: true + group: coverage-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: coverage: diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 1604b178c..ffced72cc 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -20,10 +20,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: dependabot-auto-merge-${{ github.event.pull_request.number || github.ref }} + group: dependabot-auto-merge-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 3c2aa805c..56bf29340 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -13,10 +13,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: dependency-review-${{ github.event.pull_request.number || github.ref }} + group: dependency-review-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 0e36165a5..1f84fcc22 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -34,10 +34,15 @@ env: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: e2e-tests-${{ github.event.pull_request.number || github.ref }} + group: e2e-tests-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/gh-aw-validation.yml b/.github/workflows/gh-aw-validation.yml index 8c46eafb5..34212ed01 100644 --- a/.github/workflows/gh-aw-validation.yml +++ b/.github/workflows/gh-aw-validation.yml @@ -24,10 +24,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: gh-aw-validation-${{ github.event.pull_request.number || github.ref }} + group: gh-aw-validation-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 93c179e85..eaca00393 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -8,8 +8,13 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: group: issue-triage-${{ github.event.issue.number || github.ref }} cancel-in-progress: true diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 2a209b339..ae4737ca4 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -10,10 +10,15 @@ permissions: {} # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: pr-checks-${{ github.event.pull_request.number || github.ref }} + group: pr-checks-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: diff --git a/.github/workflows/secret-scan.yml b/.github/workflows/secret-scan.yml index 25735e1f6..d77229ff2 100644 --- a/.github/workflows/secret-scan.yml +++ b/.github/workflows/secret-scan.yml @@ -13,9 +13,13 @@ on: permissions: contents: read +# Keyed by PR number for PR runs and by commit SHA otherwise. The previous +# github.ref key collided every merge to main into one group, where GitHub +# cancels the pending run -- a burst of merges silently dropped the middle +# commits' verdicts. See the fuller note in ci.yml. concurrency: - group: secret-scan-${{ github.ref }} - cancel-in-progress: true + group: secret-scan-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: gitleaks: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 0ceea1bfc..8f1d55bc4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,10 +19,15 @@ permissions: # Supersede superseded work instead of stacking it. Without this, every push # to a PR branch queued a brand-new full run alongside the ones it obsoleted; # with ~30 workflows and dozens of open PRs the Actions queue could not drain. -# PR runs cancel their predecessors; push/schedule runs queue instead, so each -# merged commit keeps its own verdict. +# +# PR runs are keyed by PR number and cancel their predecessors -- that is where +# all the queue pressure comes from, since every `push:` trigger here is already +# filtered to main. Non-PR runs (push, schedule) are keyed by commit SHA so they +# land in singleton groups: keying them on github.ref would collide every merge +# to main into one group, and GitHub cancels the *pending* run in a group, so a +# burst of merges would silently drop the middle commits' verdicts. concurrency: - group: security-${{ github.event.pull_request.number || github.ref }} + group: security-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: From c00a664822de0014ede7c9a8110c0f28f9aacfcc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 21:23:37 +0000 Subject: [PATCH 3/3] fix(ci): key dependabot-auto-merge check_suite runs by head SHA Review finding from the Vercel review bot on #1510, confirmed and applied. dependabot-auto-merge is the one workflow here that triggers on check_suite, and on that event GITHUB_SHA is the tip of the default branch rather than the commit under test. It is therefore identical for every PR, so the `|| github.sha` fallback collapsed all concurrent check-suite runs into a single group. Since cancel-in-progress is false for that event, they would not cancel a running peer -- but GitHub still cancels the *pending* run in a group, so one PR's merge-gate verdict could be dropped by another PR's check suite arriving. Fall back to github.event.check_suite.head_sha first, which is the PR head and keeps the runs isolated, before github.sha. Audited the other events reached by these groups for the same class of degeneracy: - issues (auto-assign, auto-label, issue-triage) already key on github.event.issue.number, which is unique. - schedule (codeql-analysis, security) resolves github.sha to the default branch tip, which can coincide with that same workflow's push run at the same commit. That collision is between two runs of the same scan over the same tree, so collapsing them is harmless dedup rather than a lost verdict. check_suite was the only real case. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV --- .github/workflows/dependabot-auto-merge.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index ffced72cc..4af537153 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -28,7 +28,12 @@ permissions: # to main into one group, and GitHub cancels the *pending* run in a group, so a # burst of merges would silently drop the middle commits' verdicts. concurrency: - group: dependabot-auto-merge-${{ github.event.pull_request.number || github.sha }} + # check_suite is the exception to the github.sha fallback used elsewhere: + # on that event GITHUB_SHA is the default branch tip, identical for every + # PR, so github.sha alone would collapse all concurrent check-suite runs + # into one group and drop merge-gate verdicts. check_suite.head_sha is the + # PR head and keeps them isolated. + group: dependabot-auto-merge-${{ github.event.pull_request.number || github.event.check_suite.head_sha || github.sha }} cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }} jobs: