Skip to content

chore: auto-close stale release PRs after 3 hours of inactivity#9655

Open
cryptodev-2s wants to merge 9 commits into
mainfrom
chore/close-stale-release-prs
Open

chore: auto-close stale release PRs after 3 hours of inactivity#9655
cryptodev-2s wants to merge 9 commits into
mainfrom
chore/close-stale-release-prs

Conversation

@cryptodev-2s

@cryptodev-2s cryptodev-2s commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

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:

  • Finds open same-repo PRs whose head branch starts with release/
  • Closes them after 3 hours of inactivity (updated_at)
  • Leaves a comment explaining why
  • Deletes the release branch

PRs in the merge queue, with auto-merge enabled, or labeled release:keep-open are skipped. The releasing docs are updated to describe this policy.

To avoid racing active work, the workflow:

  • Re-fetches each PR and merge-queue state before closing, and aborts if updated_at, head SHA/ref, or the full label set changed
  • Closes before commenting so a failed close does not reset the stale timer
  • Re-fetches the branch tip immediately before deleteRef and skips deletion unless the SHA still matches
  • Comments only after the delete attempt, with wording that matches whether the branch was deleted or kept
  • Continues past per-PR API failures so one error does not abort the rest of the run

References

N/A

Checklist

  • I've updated the test suite for new or updated code as appropriate
  • I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate
  • I've communicated my changes to consumers by updating changelogs for packages I've changed
  • I've introduced breaking changes in this PR and have prepared draft pull requests for clients and consumer packages to resolve them

Test plan

  • Confirm workflow checks / actionlint pass on this PR
  • Optionally run via workflow_dispatch against a throwaway stale release/* PR and verify comment, close, and branch delete
  • Confirm a recently updated release PR is left alone
  • Confirm a PR with release:keep-open is left alone
  • Confirm a late push (head SHA change) causes branch deletion to be skipped and the comment says the branch was kept
  • Confirm a label change after the initial snapshot causes close to be skipped

Note

Medium Risk
The workflow can close PRs and delete release branches with write permissions; safeguards reduce races but mistaken closure could disrupt an in-progress release if exemptions fail.

Overview
Adds automated cleanup for abandoned release/* pull requests so they no longer block new releases.

A scheduled GitHub Actions workflow (every 30 minutes, plus workflow_dispatch) runs a new close-stale-release-prs.cjs script that finds open same-repo PRs on release/*, closes them after 3 hours without activity (by updated_at), posts an explanatory comment, and deletes the branch when the tip SHA is unchanged. PRs with release:keep-open, merge queue membership, or auto-merge enabled are skipped. The script re-fetches PR and merge state before closing, aborts if labels/head/activity changed, closes before commenting, and only deletes the branch after verifying the SHA still matches.

docs/processes/releasing.md documents the 3-hour policy and the release:keep-open escape hatch.

Reviewed by Cursor Bugbot for commit 7c1b13c. Bugbot is set up for automated code reviews on this repo. Configure here.

Abandoned release/* PRs block others from starting new releases; close them, delete the branch, and document the escape-hatch label.
@cryptodev-2s
cryptodev-2s requested a review from a team as a code owner July 24, 2026 15:39
Comment thread .github/workflows/close-stale-release-prs.yml Outdated
Comment thread .github/workflows/close-stale-release-prs.yml Outdated
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.
Comment thread .github/workflows/close-stale-release-prs.yml Outdated
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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 6495138. Configure here.

Comment thread .github/workflows/close-stale-release-prs.yml Outdated
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.
per_page: 100,
});

const releasePrs = pulls.filter((pr) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe github.rest.pulls.list has options for filtering, so we can use that to search for specific branches, labels, even updated time, AFAIK. That should let us simplify this script a bunch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hmm, not supported in github.rest.pulls.list directly actually, but you can do it using github.request(?): https://docs.github.com/en/rest/search/search?apiVersion=2026-03-10#search-issues-and-pull-requests

@cryptodev-2s cryptodev-2s Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I investigated the search endpoint, but it doesn’t support the required release/* head-prefix and precise three-hour filtering in a way that removes our validation logic. We would still need per-PR refreshes and merge-state checks, so search would add another API shape without materially simplifying the script.

@mcmire mcmire left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0c750f5ab5: these are now script constants (STALE_HOURS / EXEMPT_LABEL) instead of workflow env vars.

EXEMPT_LABEL: 'release:keep-open'
with:
script: |
const staleHours = Number(process.env.STALE_HOURS);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This script is pretty long for being within a workflow file. Perhaps we should extract it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0c750f5ab5: logic moved to .github/scripts/close-stale-release-prs.cjs; the workflow now just checks out and invokes it.

…tants

Move the github-script body to .github/scripts and replace env-parsed STALE_HOURS/EXEMPT_LABEL with script constants, per review feedback.
@cryptodev-2s

cryptodev-2s commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

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.

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
cryptodev-2s requested review from Mrtenz and mcmire July 24, 2026 22:49
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.

3 participants