chore: auto-close stale release PRs after 3 hours of inactivity - #9655
chore: auto-close stale release PRs after 3 hours of inactivity#9655cryptodev-2s wants to merge 21 commits into
Conversation
Abandoned release/* PRs block others from starting new releases; close them, delete the branch, and document the escape-hatch label.
Re-fetch each PR before acting, close before commenting, and continue on per-PR failures so a merge race or failed close cannot leave a misleading comment or abort the whole run.
Wrap per-PR pulls.get and GraphQL merge-queue lookups in try/catch so one transient failure does not abort the rest of the stale-close loop.
Abort if updated_at, head SHA/ref, labels, staleness, or merge-queue state changed after the earlier refresh so a late push is not discarded.
Compare the complete label set on the final pre-close refresh, and re-fetch the branch ref immediately before deleteRef so a late push is not discarded.
Post the stale-close comment only after the delete attempt so it reports whether the branch was removed, skipped due to a tip move, or failed to delete.
There was a problem hiding this comment.
I made some comments below, but they assume that adding this workflow makes sense. That said, maybe there is a preexisting action we can use? This feels like something we shouldn't have to reinvent. For instance if we automatically assign a release label to release PRs then perhaps we can use something like this with the only-pr-labels option: https://github.com/marketplace/actions/close-stale-issues. There are also quite a few actions in the GitHub marketplace that do a similar thing.
| - name: Close inactive release PRs | ||
| uses: actions/github-script@v8 | ||
| env: | ||
| STALE_HOURS: '3' |
There was a problem hiding this comment.
Why are these environment variables? Why not set them as variables inside the script? This way you don't have to parse them as numbers.
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
There was a problem hiding this comment.
Done in 0c750f5ab5: these are now script constants (STALE_HOURS / EXEMPT_LABEL) instead of workflow env vars.
…tants Move the github-script body to .github/scripts and replace env-parsed STALE_HOURS/EXEMPT_LABEL with script constants, per review feedback.
I evaluated actions/stale. It handles general day-based staleness and label filtering, but this workflow needs a three-hour window, release/* branch targeting, merge-queue/auto-merge exemptions, and head-SHA verification before deletion. Supporting those guarantees would still require custom logic, so the extracted script remains the safer fit. |
…c lint Break the script into named functions for eligibility, merge-state, close, delete, and comment, and satisfy jsdoc/require-param-description.
|
@cryptodev-2s Ah I didn't realize that was the official |
Use SKIP_LABEL and a single stale duration, rename helpers/vars, detect forks via head.repo.fork, and simplify the close comment to only call out branch-delete failures.
…ckages Replace the github-script CJS entrypoint with an executable tsx script that uses @actions/github and @actions/core, and fetch PR eligibility via a single GraphQL snapshot query.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Caution MetaMask internal reviewing guidelines:
|
Rename to .mts so TypeScript treats the file as ESM and can import the ESM-only @actions packages, unblocking lint:tsc.
Rename commentOnPull to commentOnPullRequest, and stop describing intentional tip-move skips as failed branch deletions.
One GraphQL eligibility check plus tip verification before delete is enough for this cron job.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 12e7ee4. Configure here.
mcmire
left a comment
There was a problem hiding this comment.
Looks better. Small suggestions for now but I will do another higher-level review a bit later.
| * @returns True when the PR is a candidate for stale close. | ||
| */ | ||
| function isReleasePrCandidate(pullRequest: ListedPullRequest): boolean { | ||
| if (!pullRequest.head.ref.startsWith('release/')) { |
There was a problem hiding this comment.
Do you think it's worth it to reuse the same check we use in update-changelogs.yml to determine whether this is a release branch? I realize that means part of this logic will live in the action and not the script. Alternatively we could not worry about it for now and refactor it in another PR.
There was a problem hiding this comment.
Opened #9685 against this branch with a shared .github/actions/is-release-pr used by both update-changelogs and the stale closer (release/* prefilter, then the same merge-base + action-is-release check). Is that what you had in mind?
There was a problem hiding this comment.
Hmm, that's a bit more complex than I imagined. Maybe we can merge this PR and then we can discuss further in that PR.
There was a problem hiding this comment.
Happy to do a follow up after the merge
| persist-credentials: false | ||
| cache-node-modules: true |
There was a problem hiding this comment.
Do we need these two options? persist-credentials no longer stores the token in the repo in actions/checkout@v6 (we are using v7 in action-checkout-and-setup). And since we aren't running the checkout step in one job and reusing the result in another job we don't need to cache Node modules.
| persist-credentials: false | |
| cache-node-modules: true |
|
|
||
| concurrency: | ||
| group: close-stale-release-prs | ||
| cancel-in-progress: false |
There was a problem hiding this comment.
Nit: I think this is the default: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#concurrency
| cancel-in-progress: false |

Explanation
Abandoned
release/*PRs can sit open and block other engineers from starting a new release. There is no automated cleanup today, so stale release candidates have to be closed by hand.This PR adds a scheduled GitHub Actions workflow (every 30 minutes, plus
workflow_dispatch) that:release/updated_at)Fork heads, PRs in the merge queue, PRs with auto-merge enabled, and PRs labeled
release:keep-openare skipped. The releasing docs are updated to describe this policy.Before destructive actions, the workflow:
deleteRefand skips deletion unless the SHA still matchesFlow
flowchart TD trigger(["cron every 30m / workflow_dispatch"]) --> checkout[Checkout and setup] checkout --> listOpen[List open PRs via REST paginate] listOpen --> candidates[Filter candidates:<br/>same-repo release/*,<br/>no release:keep-open] candidates --> more{"More candidates?"} more -->|no| done([Done]) more -->|yes| snap[GraphQL snapshot] snap --> elig{"Eligible?<br/>OPEN, not fork, not skip label,<br/>updatedAt older than 3h,<br/>not merge-queue / auto-merge"} elig -->|no| more elig -->|yes| closePR[Close PR] closePR --> closeOk{Close succeeded?} closeOk -->|no| more closeOk -->|yes| getRef[git.getRef for head branch] getRef --> tipOk{"Tip SHA still matches<br/>snapshot headRefOid?"} tipOk -->|refresh failed| keepRefresh[Keep branch:<br/>kept-refresh-failed] tipOk -->|moved| keepMoved[Keep branch:<br/>kept-head-moved] tipOk -->|unchanged| deleteRef[git.deleteRef] deleteRef --> delOk{Delete succeeded?} delOk -->|yes| deleted[Branch deleted] delOk -->|no| keepFail[Keep branch:<br/>kept-delete-failed] keepRefresh --> comment[Comment with outcome] keepMoved --> comment deleted --> comment keepFail --> comment comment --> moreReferences
N/A
Checklist
Test plan
workflow_dispatchagainst a throwaway stalerelease/*PR and verify comment, close, and branch deleterelease:keep-openis left aloneNote
Medium Risk
The workflow can close PRs and delete
release/*branches withcontents: write, but scope is narrow (same-repo release branches, merge/auto-merge skips, SHA-checked deletes); mis-timed closes could still disrupt an in-progress release.Overview
Adds automated cleanup for abandoned release PRs so they do not block new releases.
A new scheduled workflow (every 30 minutes, plus manual dispatch) runs
scripts/close-stale-release-prs.mts, which finds open same-repo PRs onrelease/*heads, skips forks, merge-queue/auto-merge, and PRs labeledrelease:keep-open, then closes PRs inactive for 3 hours (updatedAt). It closes before commenting, deletes the branch only if the tip SHA still matches a fresh snapshot, posts an explanatory comment, and continues on per-PR API failures.Releasing docs document the 3-hour policy and the opt-out label. Root devDependencies add
@actions/coreand@actions/githubfor the script.Reviewed by Cursor Bugbot for commit 7a05448. Bugbot is set up for automated code reviews on this repo. Configure here.