From 77467b8d33e8d7b20dbdd882810188d36d7532e1 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Wed, 10 Jun 2026 15:11:16 -0400 Subject: [PATCH 1/2] Unify CI concurrency group across trigger events workflow_dispatch runs grouped by github.ref (refs/heads/branch) while pull_request runs grouped by head_ref (bare branch name), so a dispatched run was never cancelled when a push superseded it. Use ref_name so all trigger events on the same branch share a group. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 95e2508..8fe5a58 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,11 @@ on: permissions: contents: read +# ref_name (not ref) so workflow_dispatch, push, and pull_request runs on the +# same branch share one group: a dispatched run must be cancelled when a push +# supersedes it, and vice versa. concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} cancel-in-progress: true jobs: From 4e8dc9e956520145887216f5c51342401e594552 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Wed, 10 Jun 2026 15:16:56 -0400 Subject: [PATCH 2/2] Key PR concurrency groups by PR number, not head_ref head_ref is attacker-controlled on fork PRs: a fork branch named "main" produced the same concurrency group as trusted push and workflow_dispatch runs on this repo's main, and with cancel-in-progress: true a fork PR could cancel trusted CI runs. The PR number is assigned by the repo, so fork authors cannot steer a PR run into a branch group. Push and dispatch runs keep sharing a group per ref_name so a dispatched run is still superseded by a push. Flagged by roborev review on PR #15. Generated with Claude Code (claude-fable-5[1m]) Co-authored-by: Claude Fable 5 --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8fe5a58..5b70ec9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,11 +9,12 @@ on: permissions: contents: read -# ref_name (not ref) so workflow_dispatch, push, and pull_request runs on the -# same branch share one group: a dispatched run must be cancelled when a push -# supersedes it, and vice versa. +# PR runs are keyed by PR number (repo-assigned, so a fork branch named "main" +# cannot collide with and cancel trusted runs on this repo's main). Push and +# workflow_dispatch runs share a group per ref_name so a dispatched run is +# cancelled when a push supersedes it, and vice versa. concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref_name }} cancel-in-progress: true jobs: