Skip to content

ci: run one workflow per pull request with concurrency groups - #722

Open
owenpearson wants to merge 1 commit into
mainfrom
ci/workflow-concurrency-groups
Open

owenpearson wants to merge 1 commit into
mainfrom
ci/workflow-concurrency-groups

Conversation

@owenpearson

@owenpearson owenpearson commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

What

Adds a top-level concurrency block to check.yml, lint.yml and features.yml:

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

check.yml also gains name: Check. Without a name: key its github.workflow evaluates to the literal string .github/workflows/check.yml, which is both the run name the Actions UI shows and, now, part of the concurrency group key. lint.yml and features.yml already carry names.

release.yml is deliberately left alone — it runs on tags, not on the PR/push triggers this addresses.

Why

All three workflows fire on the same triggers: pull_request plus push to main (check.yml adds workflow_dispatch). In a stacked-PR chain, one git push that updates several branches makes GitHub fire a pull_request synchronize both for the PR whose head moved and for the PR whose base moved. The result is two runs of the same workflow, for the same PR, on the same commit.

Every run currently on PR #716's head SHA b9a4f00a, straight from the API:

Run Workflow Created
36569873822 .github/workflows/check.yml 2026-09-29T12:42:07Z
36569876066 .github/workflows/check.yml 2026-09-29T12:42:08Z
36569873775 Linting check 2026-09-29T12:42:07Z
36569875678 Linting check 2026-09-29T12:42:08Z
36569876933 Features 2026-09-29T12:42:08Z
36569878145 Features 2026-09-29T12:42:09Z

Six runs where three would do — every workflow duplicated, one second apart, all pull_request events on an identical commit. PR #717 shows the same pattern: runs 36569872967 (12:42:06Z) and 36569873206 (12:42:07Z), both on head ec5d1b19.

Each duplicated check run costs a full 7-version matrix (~16-19 minutes), plus the duplicated lint and build.

Design notes

  • github.workflow belongs in the group key. Without it the three workflows share one group and cancel each other. Lint finishes in ~22 seconds and would kill the 19-minute check run.
  • Keying on github.event.pull_request.number — the PR, not the branch — is what actually de-duplicates, because the duplicate events are two synchronizes for the same PR.
  • The || github.ref fallback is required. pull_request.number is empty for push: main and for workflow_dispatch; without a fallback every non-PR run collapses into one group named <workflow>-, and consecutive pushes to main would cancel each other. With it, main gets refs/heads/main and a workflow_dispatch on a branch gets refs/heads/<branch> — a different group from that branch's PR group, so a manual re-run cannot be cancelled by a PR event.
  • cancel-in-progress is gated on github.event_name == 'pull_request' on purpose. On push: main every merge commit should get a run that completes; cancelling would leave commits on main with no finished check and degrade required-status-check history.
  • Not keyed on the head SHA. That would stop a newer push from superseding an older run, which is the main benefit being bought here.

Tradeoff

When merging down a stack one PR at a time with minutes between merges, a base-branch update can cancel a run that is already 15 minutes into the 19-minute suite and restart it from zero. Total CI minutes still drop, but time-to-green for that one PR can get worse.

cancel-in-progress: false is the alternative: it collapses the duplicate by queueing instead of replacing, which saves concurrency slots but not minutes. The cancelling variant is the right default — a superseded run is testing a commit nobody is waiting on.

Note for tooling

Superseded runs conclude cancelled, not success. The PR status rollup is unaffected, since GitHub uses the latest run per check name, but any tooling that asserts "every run on this SHA succeeded" needs to treat cancelled as skippable.

Validation

  • All three files parse: yaml.safe_load succeeds on each. (on: parsing as the boolean key True under YAML 1.1 is expected.)
  • concurrency is top-level (column 0) in exactly the three files, not nested under jobs:.
  • ruff check . passes.
  • Diff touches exactly check.yml, lint.yml and features.yml.

🤖 Generated with Claude Code

All three CI workflows fire on `pull_request` and on `push` to main. A
single push that moves several branches in a stacked chain makes GitHub
fire a `pull_request` synchronize both for the PR whose head moved and
for the PR whose base moved, so one PR gets two runs on the identical
head SHA a second apart. Each duplicate costs a full seven-version check
matrix plus lint and build.

Group by workflow and pull request number so those two synchronizes
share a group and the later one supersedes the earlier. Keying on
`github.workflow` holds the three workflows in separate groups, so the
22-second lint run cannot cancel the 19-minute check run. The
`github.ref` fallback covers `push` to main and `workflow_dispatch`,
where `pull_request.number` is empty.

`cancel-in-progress` is limited to pull request events: every merge
commit on main gets a run that finishes, which keeps the required status
check history intact.

`check.yml` gains a name, so the Actions UI and the concurrency group
use `Check` rather than the workflow's file path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 30 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 48d9b84a-ec7c-42a5-a915-fdab8c4fb6da

📥 Commits

Reviewing files that changed from the base of the PR and between 2d583bf and 8a479bc.

📒 Files selected for processing (3)
  • .github/workflows/check.yml
  • .github/workflows/features.yml
  • .github/workflows/lint.yml

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.

This branch was successfully deployed

1 active deployment
staging/pull/722/features — 8a479bc3 Deployed Sep 29, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant