Skip to content

ci: cancel a merged PR's in-flight CI runs#2996

Merged
ibell merged 1 commit into
masterfrom
ihb/ci-cancel-merged-pr-runs
May 26, 2026
Merged

ci: cancel a merged PR's in-flight CI runs#2996
ibell merged 1 commit into
masterfrom
ihb/ci-cancel-merged-pr-runs

Conversation

@ibell
Copy link
Copy Markdown
Contributor

@ibell ibell commented May 25, 2026

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>/merge again, 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 to merged == true. Lists the merged PR's still-active runs (matched by head ref + head SHA, excluding its own run) and cancels them with gh run cancel.

Design notes

  • Merged-only by request — a PR closed without merging is left alone.
  • No injection: uses pull_request (not pull_request_target); attacker-influenceable values (head.ref/head.sha) are read via env:, never inline ${{ }} in the shell body.
  • Least privilege: top-level permissions: restricts GITHUB_TOKEN to actions: write only (all gh run cancel needs); everything else defaults to none.
  • Mainline-safe: --branch scoping means merge-commit master runs (listed under headBranch=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.
  • Self-safe: the canceller's own run shares the PR head branch+SHA, so it's excluded by github.run_id.
  • Forks: the token is downscoped read-only on forked PRs, so gh run cancel 403s — swallowed by || true (harmless no-op; those runs have usually finished by merge time).
  • deleteBranchOnMerge is off, so a delete-event trigger would be unreliable; pull_request: closed is the canonical merge signal.

Validation

  • actionlint clean (includes shellcheck on the run step)
  • jq filter unit-tested against representative run lists (selects in-flight siblings; excludes self, completed, and different-SHA runs)
  • YAML-only; no C/C++ compile impact

Tracked by bead CoolProp-wxt.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Improved CI/CD efficiency by automating cancellation of redundant workflow runs on merged pull requests.

Review Change Stack

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

A 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.

Changes

CI Run Cancellation on Merge

Layer / File(s) Summary
Cancel merged PR in-flight runs workflow
.github/workflows/cancel_merged_pr_runs.yml
Workflow definition that triggers only for merged PRs, uses gh run list and gh run cancel to clean up pending/queued/in-progress runs on the merged branch, excludes the current workflow execution, and swallows token permission errors from forks.

Poem

🐰 A rabbit's workflow, swift and true,
Cancels runs when PRs are through,
Merged branches now won't linger long,
CI cleanup—a helpful song!
No more ghosts of jobs that ought to die,
They vanish quick as clouds pass by. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'ci: cancel a merged PR's in-flight CI runs' clearly and concisely summarizes the main change: adding a GitHub Actions workflow to cancel in-flight CI runs when a PR is merged.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ihb/ci-cancel-merged-pr-runs

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6db4c19 and 96ec08c.

📒 Files selected for processing (1)
  • .github/workflows/cancel_merged_pr_runs.yml

Comment on lines +52 to +60
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'
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


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.

@ibell ibell merged commit d04d9a2 into master May 26, 2026
29 of 30 checks passed
@ibell ibell deleted the ihb/ci-cancel-merged-pr-runs branch May 26, 2026 01:38
@ibell ibell added this to the v8.0.0 milestone May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant