ci: delete superseded pending-approval runs on fork PRs - #9531
Conversation
Fork PRs park one jobless "action required" run per workflow per push until a maintainer approves. Concurrency never cancels them because they never start, and when the PR closes GitHub flips every parked run to failure and emails the author once per run (68 emails for pingdotgg#7434). Add a pull_request_target workflow that, on each push to a fork PR, deletes the parked runs whose commit a newer push has replaced. Only the current head keeps its pending runs. Fixes pingdotgg#9529 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
| }); | ||
|
|
||
| const superseded = runs.filter( | ||
| (run) => run.head_repository?.id === pr.head.repo.id && run.head_sha !== pr.head.sha, |
There was a problem hiding this comment.
🟠 High workflows/pr-superseded-runs.yml:47
An older synchronize run can delete the current commit's action_required runs, leaving the PR's actual head without a run to execute. The filter compares against the stale event payload's pr.head.sha, so after commit B replaces A, A's cleanup selects B as superseded; cancel-in-progress is asynchronous and does not prevent this race. Re-fetch the PR head and revalidate it immediately before each deletion, skipping cleanup when the head has changed.
🤖 Copy this AI Prompt to have your agent fix this:
In file @.github/workflows/pr-superseded-runs.yml around line 47:
An older `synchronize` run can delete the current commit's `action_required` runs, leaving the PR's actual head without a run to execute. The filter compares against the stale event payload's `pr.head.sha`, so after commit B replaces A, A's cleanup selects B as superseded; `cancel-in-progress` is asynchronous and does not prevent this race. Re-fetch the PR head and revalidate it immediately before each deletion, skipping cleanup when the head has changed.
| script: | | ||
| const pr = context.payload.pull_request; | ||
|
|
||
| const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { |
There was a problem hiding this comment.
🟡 Medium workflows/pr-superseded-runs.yml:37
This cleanup leaves superseded runs undeleted once the PR has more than 1,000 matching action_required runs, so closing the PR still generates failure notifications for those retained runs. github.paginate follows pages, but listWorkflowRunsForRepo caps filtered results at 1,000; partition the queries by created ranges (or use another bounded enumeration) so every parked run is considered.
🤖 Copy this AI Prompt to have your agent fix this:
In file @.github/workflows/pr-superseded-runs.yml around line 37:
This cleanup leaves superseded runs undeleted once the PR has more than 1,000 matching `action_required` runs, so closing the PR still generates failure notifications for those retained runs. `github.paginate` follows pages, but `listWorkflowRunsForRepo` caps filtered results at 1,000; partition the queries by `created` ranges (or use another bounded enumeration) so every parked run is considered.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9335fb4. Configure here.
|
|
||
| const superseded = runs.filter( | ||
| (run) => run.head_repository?.id === pr.head.repo.id && run.head_sha !== pr.head.sha, | ||
| ); |
There was a problem hiding this comment.
Stale SHA deletes current pending runs
Low Severity
The keep-filter uses the synchronize event’s frozen head.sha, so a re-run of an older job—or a job that listed after a later push—treats the newer commit’s action_required runs as superseded and deletes them. The PR is then left with no pending approval runs for the actual head.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 9335fb4. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This adds a privileged pull_request_target workflow that automatically deletes pending CI runs for fork PRs, changing GitHub Actions behavior and run retention. Unresolved findings identify a race that can delete current-head runs and an enumeration limit that can leave stale runs behind. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


What Changed
Adds
.github/workflows/pr-superseded-runs.yml, apull_request_targetworkflow that runs onsynchronizefor fork PRs only. It lists the repo'spull_requestruns for the PR's head branch that are parked inaction_required, keeps the ones for the current head commit, and deletes the rest. It never checks out PR code and needs onlyactions: write.Why
Fixes #9529.
Fork PRs need maintainer approval before
pull_requestworkflows may run. Until then every push leaves one jobless run per workflow parked in "action required". The existingconcurrencygroups never reach them because a run that never started cannot be cancelled by a newer one, so they pile up (PR #7434 accumulated 68 across 17 commits). When the PR closes, GitHub flips every parked run tofailurewithin seconds and emails the author once per run with "No jobs were run".Cleaning up on
closedcannot win that race, so this cleans up on each push instead. After a push, only the current head's runs stay pending, so a close produces one email per workflow instead of one per workflow per commit. The current head's runs still flip on close; that part is GitHub's behavior and not addressable from the repo.Delete rather than cancel: the API reports these runs as
status: completed,conclusion: action_required, so the cancel endpoint would reject them. Delete is the endpoint GitHub itself uses when it purges runs awaiting approval after 30 days, and nothing is lost because these runs never executed a job.Verified against open PR #6191 by dry-running the same list-and-filter logic with
gh api: 17 runs for the four replaced commits selected, 5 runs for the current head kept. The delete call itself needs repository write access, so it could only be exercised once merged.Checklist
Written with Claude Fable 5.1 in Claude Code.
🤖 Generated with Claude Code
Note
Low Risk
CI-only automation using pull_request_target with no PR checkout; writes only via GitHub Actions API to delete superseded pending runs on fork PRs.
Overview
Adds a
pull_request_targetworkflow that runs on every fork PRsynchronizeevent to trim stale workflow runs stuck inaction_required(waiting for maintainer approval).On each push it lists
pull_requestruns for the PR head branch inaction_required, keeps runs for the current head SHA, and deletes runs tied to older commits on the same fork head repo. It is fork-only, usesactions/github-script(no checkout of PR code), and scopes job permissions toactions: writewith per-PR concurrency so cleanup does not pile up.This reduces how many parked runs flip to failure and email the author when the PR closes—addressing the pile-up that
concurrencycannot cancel because those runs never start.Reviewed by Cursor Bugbot for commit 9335fb4. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add CI workflow to delete superseded pending runs on fork PRs
pull_request_targetsynchronization events to remove staleaction_requiredruns on fork pull requests.actions/github-scriptto find runs matching the source branch,pull_requestevent, andaction_requiredstatus. It deletes runs from the same fork repository with an older head SHA.concurrency, canceling older cleanup jobs.actions: writeandcontents: readpermissions to the workflow to delete runs via the Actions API.📊 Macroscope summarized 9335fb4. 1 file reviewed, 2 issues evaluated, 0 issues filtered, 2 comments posted
🗂️ Filtered Issues