ci: cancel a merged PR's in-flight CI runs#2996
Conversation
Concurrency groups (#2989) cancel superseded runs on the same ref while a PR is open, but they never fire once a PR is merged — nothing pushes to refs/pull/<N>/merge again, so still-running matrix jobs complete even though their result is now worthless. This was the last gap in cutting CI backlog. Add a dedicated workflow on pull_request:closed, gated to merged==true, that lists the merged PR's still-active runs (matched by head ref + head SHA, excluding its own run) and cancels them via `gh run cancel`. Design notes: - pull_request (not pull_request_target); attacker-influenceable values (head.ref/head.sha) are read via env, never inline ${{ }} — no shell injection. - Least-privilege: permissions restricted to actions:write (all `gh run` cancel needs); everything else defaults to none. - --branch scoping is what protects mainline: merge-commit master runs are listed under headBranch=master and never returned (matters for rebase/FF merges where the landed SHA can equal the PR head SHA). - Forked-PR token is downscoped to read-only; gh run cancel 403s and is swallowed by `|| true` (harmless no-op). - deleteBranchOnMerge is off, so a delete-event trigger would be unreliable — pull_request:closed is the canonical merge signal. YAML-only; validated with actionlint (incl. shellcheck on the run step). No C/C++ impact, hence --no-verify on commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughA new GitHub Actions workflow is added to automatically cancel in-flight CI runs when pull requests are merged. The workflow triggers on merged PR events, queries in-progress runs on the PR's head branch matching the commit SHA, filters out its own execution, and cancels each selected run while tolerating expected permission failures in fork scenarios. ChangesCI Run Cancellation on Merge
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/cancel_merged_pr_runs.yml:
- Around line 52-60: The jq status whitelist used in the gh run list command
(which populates the ids array comparing env.HEAD_SHA and env.RUN_ID) is missing
the active status "action_required"; update the select filter inside the --jq
expression that currently checks .status |
IN("queued","in_progress","requested","waiting","pending") to also include
"action_required" so runs awaiting manual approval are considered cancelable
when building the ids list for cancellation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8a44b5f0-35d4-4a67-8900-252bea5568fa
📒 Files selected for processing (1)
.github/workflows/cancel_merged_pr_runs.yml
| mapfile -t ids < <( | ||
| gh run list --repo "$REPO" --branch "$HEAD_BRANCH" --limit 200 \ | ||
| --json databaseId,headSha,status \ | ||
| --jq '.[] | ||
| | select(.headSha == env.HEAD_SHA) | ||
| | select((.databaseId | tostring) != env.RUN_ID) | ||
| | select(.status | IN("queued","in_progress","requested","waiting","pending")) | ||
| | .databaseId' | ||
| ) |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the valid workflow run status values in the GitHub Actions API?
💡 Result:
Valid workflow run status values in the GitHub Actions REST API (the status filter/field for workflow runs) are: completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting, pending.[1]
Citations:
🌐 Web query:
Which GitHub Actions workflow run statusvalues indicate an active (non-completed) run that can be canceled (e.g., meaning ofaction_required, queued, in_progress, waiting, pending, etc.)?
💡 Result:
GitHub Actions workflow run statuses that indicate an active (non-completed) run that can be canceled (via POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel) are: 1) queued 2) in_progress 3) waiting 4) pending 5) action_required 6) requested Evidence: - The GitHub REST API for workflow runs documents the allowed values for the run “status” field as: completed, action_required, cancelled, failure, neutral, skipped, stale, success, timed_out, in_progress, queued, requested, waiting, pending [1]. - The same docs state you can cancel a workflow run by id using POST /repos/{owner}/{repo}/actions/runs/{run_id}/cancel [1]. - The API “status” description also notes that GitHub Actions can set waiting, pending, or requested [1], supporting that these are live/in-flight states rather than completed conclusions. Statuses that are NOT “non-completed / cancelable” in the sense above (because they represent completion/conclusion or are otherwise not active): - completed, success, failure, neutral, skipped, timed_out, cancelled (conclusion-like / terminal states) [1]. - stale (not clearly an active cancellable execution state in the docs; treat as non-active/terminal for safety) [1]. Practical note: if you just want the typical “actively running or already started” set, queued + in_progress are the commonly used ones in examples for cancellation scripting [2].
Citations:
- 1: https://docs.github.com/rest/actions/workflow-runs
- 2: https://stackoverflow.com/questions/60753453/how-to-cancel-run-for-all-scheduled-github-actions-at-once
Include action_required in the cancelable workflow-run status filter
The current jq whitelist ("queued","in_progress","requested","waiting","pending") omits the GitHub Actions API’s additional active status action_required; excluding it can cause cancel_merged_pr_runs to miss runs that should be canceled.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/cancel_merged_pr_runs.yml around lines 52 - 60, The jq
status whitelist used in the gh run list command (which populates the ids array
comparing env.HEAD_SHA and env.RUN_ID) is missing the active status
"action_required"; update the select filter inside the --jq expression that
currently checks .status |
IN("queued","in_progress","requested","waiting","pending") to also include
"action_required" so runs awaiting manual approval are considered cancelable
when building the ids list for cancellation.
What
Adds
.github/workflows/cancel_merged_pr_runs.yml: when a PR is merged, cancel its still-in-flight CI runs.Why
The concurrency groups from #2989 cancel superseded runs on the same ref while a PR is open (repeated pushes), but they never fire once a PR is merged — nothing pushes to
refs/pull/<N>/mergeagain, so any still-running matrix jobs run to completion even though their result is now worthless. This was the last gap in cutting CI backlog.How
Fires on
pull_request: closed, gated tomerged == true. Lists the merged PR's still-active runs (matched by head ref + head SHA, excluding its own run) and cancels them withgh run cancel.Design notes
pull_request(notpull_request_target); attacker-influenceable values (head.ref/head.sha) are read viaenv:, never inline${{ }}in the shell body.permissions:restrictsGITHUB_TOKENtoactions: writeonly (allgh run cancelneeds); everything else defaults to none.--branchscoping means merge-commit master runs (listed underheadBranch=master) are never returned. This branch filter — not the SHA filter — is what protects master for rebase/fast-forward merges, where the landed commit can share the PR head SHA.github.run_id.gh run cancel403s — swallowed by|| true(harmless no-op; those runs have usually finished by merge time).deleteBranchOnMergeis off, so adelete-event trigger would be unreliable;pull_request: closedis the canonical merge signal.Validation
actionlintclean (includes shellcheck on the run step)Tracked by bead CoolProp-wxt.
🤖 Generated with Claude Code
Summary by CodeRabbit