Skip to content

fix(ci): add concurrency groups so the Actions queue can drain - #1510

Merged
groupthinking merged 3 commits into
mainfrom
claude/clever-heisenberg-l8nagb
Aug 13, 2026
Merged

fix(ci): add concurrency groups so the Actions queue can drain#1510
groupthinking merged 3 commits into
mainfrom
claude/clever-heisenberg-l8nagb

Conversation

@groupthinking

@groupthinking groupthinking commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Canonical issue

Closes #1519

Outcome

Removes the structural amplifier that lets this repository's Actions queue reach hundreds of runs deep and stay there.

Measured via the Actions API:

Signal 21:13 UTC 21:19 UTC 21:26 UTC
Runs queued 285 256 207
Runs in_progress 2 23
Oldest queued run 20:49:28Z (~24 min, never started)

Be precise about what this PR does and does not claim. Between the first two samples, runner capacity recovered on its own and the queue began draining. So the acute stall was a capacity condition, not something missing concurrency: blocks caused. I am not claiming this PR would have prevented it.

What the missing blocks do cause is the standing run volume: with ~26 open PRs, ~30 workflows, and no deduplication, every push stacks a brand-new full run beside the ones it obsoletes. That is why a capacity dip produced a 285-deep backlog rather than a shallow one, why it took ~24 minutes to start moving, and why it will recur on the next dip. This PR shrinks the amplifier. It does not add runners.

The consequence while it lasted was concrete: no PR could reach a green CI gate. Ready PRs all reported mergeable_state: unstable purely because their checks were queued — nothing failed, nothing ran.

Why the volume is unbounded

17 of 30 workflows carried no concurrency: block, including the heaviest and most-triggered: ci, codeql-analysis, security, e2e-tests, pr-checks, auto-label, dependency-review.

#1447 correctly removed those workflows' base-branch filters so every PR runs the gate. That was right and this PR does not touch it — but it raised per-PR run volume without adding any deduplication to absorb it.

Overlap with #1508 — read this before merging either

#1508 was opened one minute before this PR and independently adds a concurrency: block to ci.yml. An earlier revision of this body claimed no competing PR existed; that was written before #1508 appeared and is corrected here rather than left as a false checkbox.

They are complementary, and neither needs to be closed:

Suggested split: #1508 keeps .coderabbit.yaml and drops (or re-keys) its ci.yml hunk; this PR keeps the workflow scheduling change across all 16 files. The reverse split is equally fine — the only thing that matters is that whichever version lands does not key main's post-merge runs on github.ref.

Scope

  • Included: a concurrency: block on the 14 workflows triggering on pull_request, pull_request_target, push, or issuesci, codeql-analysis, security, e2e-tests, pr-checks, dependency-review, api-cost-postgres, gh-aw-validation, anthropic-wif-test, branch-cleanup, dependabot-auto-merge, auto-label, auto-assign, issue-triage. Plus a re-key of coverage and secret-scan, which already had groups carrying the defect below.
  • Explicitly excluded:

The design, and two corrections found in review

The first commit (3ef52a9) keyed groups on github.event.pull_request.number || github.ref with cancel-in-progress: false for push events, claiming push runs would "queue, so each merged commit keeps its own verdict."

That claim was wrong, and 20cc78e fixes it. cancel-in-progress: false stops a new run cancelling a running one. It does not stop GitHub 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. Since github.ref is refs/heads/main for every merge, all main pushes shared one group, so a burst of merges — ten in twenty minutes here — would have silently dropped the middle commits' verdicts. (This is the same bug #1508's ci.yml hunk currently carries.)

A second defect, caught by the Vercel review bot, is fixed in c00a664. On check_suite events GITHUB_SHA is the default-branch tip rather than the commit under test, so the github.sha fallback collapsed every PR's check suite into one group. dependabot-auto-merge now falls back to github.event.check_suite.head_sha first.

The shipped design:

  1. PR runs key on github.event.pull_request.number and cancel their predecessors. This is where all queue pressure comes from — verified: every push: trigger across these workflows is already filtered to main (or claude/branch-cleanup-*), so pushes to PR branches fire no push runs at all.
  2. Non-PR runs (push, schedule) key on github.sha, landing in singleton groups where they can neither cancel nor be cancelled. Every merged commit keeps its own verdict. This costs nothing given (1).
  3. Not github.ref alone — besides the burst problem, on the two pull_request_target workflows github.ref resolves to the base branch, collapsing every open PR into one group where they would cancel each other.
  4. Issue-triggered workflows keep github.event.issue.number; github.sha is meaningless for issues events. auto-label fires on both, so it falls through PR → issue → ref.
  5. coverage and secret-scan predate this PR and both had the bare github.ref group with cancel-in-progress: true — the more aggressive form of the same bug. Re-keyed to match.

Risk

  • Risk level: low
  • Failure mode: the change is a scheduling constraint. The realistic risk is over-cancellation — a run cancelled that someone wanted. Bounded by the design above: only PR-event runs cancel, and only by a newer push to the same PR, whose result supersedes it by definition. No workflow gains or loses a trigger, so no gate becomes skippable.
  • Rollback: git revert. No migration, config, schema, or permission change.

Verification

Head c00a664. Measured, not inferred.

  • All 28 workflow files parseyaml.safe_load over .github/workflows/*.yml, clean at every commit.
  • Group names checked for collisions — all 27 group expressions in the repo are distinct.
  • Trigger surface unchanged — the parsed on: keys of all 28 files are byte-identical before and after; the whole diff lives inside new or rewritten concurrency: blocks.
  • Push filters audited — all 13 push: triggers confirmed filtered to main, except branch-cleanup (claude/branch-cleanup-*). This is what makes point (2) free rather than a tradeoff.
  • Observed working in production, same SHA, 46-second window. Two events hit c00a664: a push at 21:23:43 and ready_for_review at 21:24:29. Dependency Review (pull_request, uses the PR head's workflow file — has the block) cancelled the superseded run. PR Checks (pull_request_target, uses the base branch's file — no block yet) stacked both. Same PR, same commit, same window; the only variable is whether the file in play carries the block.
  • Dependency Review — passed on this head. (Empty scan is correct: this PR touches no dependency manifests.)
  • Remaining required CI — still queued behind the backlog at last check.
  • CodeRabbit review — blocked by the inherited label gate that fix(review): make the CodeRabbit label-gate override actually bind #1508 diagnoses, not by anything in this diff.

A limitation this evidence exposes: the fix is partially self-testing but not self-deploying. pull_request workflows take effect from the PR head and are already deduplicating here. pull_request_target workflows (pr-checks, dependabot-auto-merge) read the base branch, so they keep stacking until this lands on main, and their behaviour cannot be verified from this PR. A green run here is not evidence for that half.

Production evidence

Not applicable as a deployed artefact — CI configuration, no runtime or apps/web/** surface. The Vercel preview built green on all three heads.

The runtime evidence is the queue measurement and the same-SHA A/B above, both taken from the live Actions API rather than reasoned about — including the part that cuts against this PR's original framing. The post-merge check is direct and cheap: standing queue depth should sit materially lower at comparable PR volume.

Agent handoff

Recommended companion action (human)

Merging does not clear runs already queued — concurrency groups apply to new runs only. To drain immediately, cancel the existing backlog. I did not do this unilaterally: cancelling discards CI verdicts for commits already merged to main, which is a call for a human, not an unattended routine.

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-uvai Ready Ready Preview, v0 Aug 7, 2026 9:25pm

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • [‘architecture-gap’, ‘bug’, ‘ci-cd’, ‘ci/cd’, ‘copilot-rabbit’, ‘documentation’, ‘duplicate’, ‘enhancement’, ‘frontend’, ‘github_actions’, ‘good first issue’, ‘help wanted’, ‘high-priority’, ‘invalid’, ‘javascript’, ‘ml-model’, ‘needs-triage’, ‘pipeline-critical’, ‘placeholder-code’, ‘priority:high’, ‘python’, ‘python:uv’, ‘question’, ‘styling’, ‘tests’, ‘v0’]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 75b2a600-b779-4b67-8f10-ea26312af2ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@groupthinking groupthinking added ci/cd github_actions Pull requests that update GitHub Actions code labels Aug 7, 2026 — with Claude

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

Labels added (ci/cd, github_actions) so the label gate no longer skips this.

Reviewer context: this is a scheduling-only change — 14 workflows each gain a concurrency: block, +140 lines, 0 deletions, no job/step/trigger/permission touched. The three points worth scrutiny are called out in the PR body: the group key uses github.event.pull_request.number || github.ref (a bare github.ref would collide every PR into one group on the two pull_request_target workflows), cancel-in-progress is conditional so push/schedule runs queue rather than cancel, and auto-label falls through PR → issue → ref because it triggers on both.


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@groupthinking: I will run a complete review of #1510. I will check the concurrency group keys, cancellation conditions, trigger compatibility, and workflow YAML validity.

⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 32 minutes.

Copy link
Copy Markdown
Owner Author

Kept as canonical over #1511 (gate 6), with one defect to fix first

#1511 was the same change from a concurrent session, five seconds apart. Closed in favour of this one — 14 workflows vs 7, and this PR's key handles the pull_request_target base-ref trap structurally rather than per-workflow. Rationale recorded on #1511.

This PR is the unblock for the whole repo right now, so it is worth getting right. Two things before it lands.

1. Defect — dependabot-auto-merge.yml collapses all check_suite runs into one group

That workflow triggers on pull_request_target and check_suite. On a check_suite event there is no github.event.pull_request, so the key falls through to github.ref, which for check_suite is the default branch. Every check_suite-triggered run in the repo therefore lands in the single group:

dependabot-auto-merge-refs/heads/main

cancel-in-progress is false here (the event is neither pull_request nor pull_request_target), but that does not make this safe. GitHub only keeps one pending run per concurrency group: when a third run arrives while one is running and one is pending, the pending one is cancelled. So a check-suite completion for PR A can be silently dropped by one for PR B, and the merge gate never evaluates A.

That is a merge gate skipping PRs it was supposed to act on — the same class of bug #1487 exists to fix.

Suggested key, which recovers the PR number on the check_suite path:

concurrency:
  group: dependabot-auto-merge-${{ github.event.pull_request.number || github.event.check_suite.head_sha || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'pull_request_target' }}

Keying on head_sha gives one group per head commit, which is the granularity the merge gate actually reasons about. The same fall-through is worth a look on any other workflow here with a non-PR trigger.

2. Merge-order conflict with #1487

Both this PR and #1487 modify .github/workflows/dependabot-auto-merge.yml. #1487 rewrites the permissions: block (adds checks: read) and renames both jobs to dependabot-auto-merge-*; this PR inserts a concurrency: block immediately after that same permissions: block. They will conflict textually, and #1487's job rename interacts with the concurrency key above.

Not a gate-6 overlap — they are different changes that happen to share a file — but they should not both be merged blind. Suggest landing this one first (it is the queue unblock, and is Class A CI config), then rebasing #1487 onto it.

Context for whoever picks this up

Measured while reviewing: 281 workflow runs queued against 20 executing, and the queue grew by 19 runs in the two minutes I was watching. It is not draining on its own. This PR is the fix.


Generated by Claude Code

…dicts

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV
Comment thread .github/workflows/dependabot-auto-merge.yml Outdated
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FoRdXUHXgtt3U6HiHdcosV

Copy link
Copy Markdown
Owner Author

Unplanned live validation of this change, worth recording — and it comes with a caveat reviewers should know before merging.

Two events landed on head c00a664 46 seconds apart: my push at 21:23:43, then ready_for_review from linear-code[bot] at 21:24:29. That produced a natural A/B on the same SHA:

Workflow Trigger Which workflow file GitHub uses 21:23 run 21:24 run
🔍 Dependency Review pull_request PR head — has this PR's concurrency: block cancelled queued
PR Checks pull_request_target base branch — block not there yet queued queued

The pull_request workflow deduplicated: the superseded run was cancelled the moment the newer event arrived. The pull_request_target workflow stacked both runs, exactly as it does on main today. Same PR, same commit, same 46-second window — the only variable is whether the workflow file in play carries the block.

That is the mechanism this PR is built on, observed rather than argued.

The caveat: it also shows the fix is partially self-testing and not self-deploying. pull_request workflows (ci, codeql-analysis, security, e2e-tests, coverage, secret-scan, dependency-review, auto-label, api-cost-postgres, gh-aw-validation) take effect from the PR head, so they are already deduplicating here and their behaviour is verifiable pre-merge. pull_request_target workflows (pr-checks, dependabot-auto-merge) read the base branch, so they will keep stacking until this lands on main — and their behaviour cannot be verified from this PR at all. Nobody should read a green run here as evidence for that half.

Queue depth meanwhile: 285 → 256 → 236 → 207 since 21:13. Still draining on recovered capacity; this PR's own required checks remain queued behind it, which is why the CI box above is still unchecked.


Generated by Claude Code

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA c00a664.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

Scanned Files

None

Copy link
Copy Markdown
Owner Author

I opened #1508 a minute before this and independently added a concurrency group to ci.yml. This PR does the same thing across 16 workflows instead of one, so I've dropped my concurrency commits and this PR should own the work#1508 is back to being a single-file CodeRabbit config fix. Flagging rather than leaving two PRs racing on the same file.

Two things from that work worth handing over.

1. The github.sha choice is load-bearing — please keep it

My first attempt keyed the push side by github.ref, which is the obvious spelling and is wrong in a way that presents as success. Every main commit lands in one group, and GitHub does not simply run same-group workflows in parallel: a run goes pending while another in the group is in progress, and queueing a third cancels the pending one. So main's post-merge CI would serialize, and commits in the middle of a merge burst would lose their run outright.

cancel-in-progress: false does not prevent this — it governs the in-progress run; cancelling the pending one is the grouping semantics themselves. CodeRabbit independently confirmed this against GitHub's concurrency docs when I asked it to check.

This PR already gets it right (github.event.pull_request.number || github.sha). It's worth a comment in at least ci.yml saying why, because github.ref is exactly what someone will "simplify" it back to — and the symptom would look like the queue draining faster, not like a regression.

2. Scheduled runs can collide on github.sha

security.yml and codeql-analysis.yml both carry a schedule trigger. On a scheduled run github.event.pull_request.number is null, so the group key falls through to github.sha — the default branch head. That means a scheduled run and a push run for the same commit share a group. With cancel-in-progress false for non-PR events, the later one goes pending, and it can then be cancelled by a subsequent entrant.

The effect is a security scan that silently doesn't run, which is the failure mode you least want from a scanner — it reports nothing rather than reporting a problem. Same class of trap as the one above: it looks like green.

Adding the event name to the key separates them:

group: security-${{ github.event_name }}-${{ github.event.pull_request.number || github.sha }}

Worth applying to any workflow here with both schedule and push/pull_request triggers — from a quick pass that's security.yml and codeql-analysis.yml; the other 14 look unaffected since their triggers can't collide on a shared SHA.

Neither point blocks this PR — the first is already correct, and the second is a narrow edge case. Both are cheap to address now and annoying to diagnose later.


Generated by Claude Code

@groupthinking
groupthinking merged commit 8dc737a into main Aug 13, 2026
37 of 40 checks passed
@groupthinking
groupthinking deleted the claude/clever-heisenberg-l8nagb branch August 13, 2026 03:18
@linear-code

linear-code Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

GRV-406

groupthinking pushed a commit that referenced this pull request Aug 29, 2026
The open risk recorded when this PR was written -- that the merge gate
hard-codes gate 2's check list and will drift from the policy it claims to
enforce -- materialised in three weeks.

#1449/#1480 made `test-frontend` a required check and updated
MERGE_POLICY.md, but nothing pointed at this gate. Its list still ended at
`test`, which runs Python pytest only, so it would have merged a Dependabot
PR with the apps/web vitest suite unrun -- including the CWE-209 and
billing-disclosure regressions that suite carries. That is the same
fail-open class this PR exists to close, arriving through drift rather
than through a coding error.

Adds `test-frontend` to REQUIRED_CHECKS, and adds the test I previously
declined to write. I argued then that parsing the policy prose was "its own
small parser with its own failure mode"; having now seen the drift happen,
that trade is clearly wrong -- the parse is one regex over a stable
sentence, and the alternative is a gate that silently under-enforces. The
test fails with the exact missing names, so the next addition to gate 2
lands here instead of in production.

Also merges origin/main (56 commits), which brings the concurrency group
added to this workflow by #1510. Verified present after the merge.

17 tests pass. Removing `test-frontend` from the gate fails the new test
with `missing from gate: ['test-frontend']`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: workflows have no concurrency groups, so obsolete runs stack instead of superseding

2 participants