ci: add weekly stale PR reminder - #283
Open
lisazacarias wants to merge 1 commit into
Open
Conversation
Lists open PRs with no approving review that are older than five days and posts them to Slack on Monday mornings. Advisory only: it never comments on a PR, requests a review, or closes anything. Drafts and CHANGES_REQUESTED are excluded because neither is blocked on a reviewer. The filtering logic lives in .github/scripts/stale_prs.py rather than a heredoc in the workflow, matching operator_notes.py. CI runs `flake8 .` and `black --check .` from the repo root, so a script is linted where embedded YAML Python is not. Safe to merge before SLACK_STALE_PR_WEBHOOK exists: without the secret the job prints the list to the run log and exits clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🟢 PR size: 294 lines — within target
Target is 400 lines excluding tests, hard stop around 800 — see |
s-aderhold
approved these changes
Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Adds a scheduled workflow that lists open PRs with no approving review older
than five days and posts them to Slack on Monday mornings. Advisory only: it
never comments on a PR, requests a review, or closes anything. Drafts and
CHANGES_REQUESTEDare excluded because neither is waiting on a reviewer.Safe to merge before the webhook exists — without
SLACK_STALE_PR_WEBHOOKthe job prints the list to the run log and exits clean.
Operator-visible
None.
Scope
Single-purpose: one workflow plus the script it calls. 288 added lines, no
existing file touched, nothing to split.
Decisions worth recording
Filtering logic in
.github/scripts/stale_prs.py, not a heredoc in theworkflow. The first draft had ~70 lines of Python embedded in the YAML. CI
runs
flake8 .andblack --check .from the repo root with no.githubexclusion, so a script gets linted where embedded YAML Python does not, and
it can be imported and unit-tested. This mirrors
operator_notes.py, whichalready splits the same way: real logic in a script, the small Slack post
inline. Cost is a
checkoutstep the workflow otherwise would not need.The review filter excludes rather than includes. It skips
APPROVEDandCHANGES_REQUESTEDinstead of testing for a specific "unreviewed" value. Thismatters because
reviewDecisionis not one value: againstmain(protected,required_approving_review_count = 1) unreviewed PRs returnREVIEW_REQUIRED, but a PR opened against an unprotected base returns"",and the field can be absent. Excluding the two known-not-waiting states keeps
all three cases reported, and keeps working if GitHub adds a fourth state.
Kept the
$GITHUB_OUTPUTheredoc guard even though it cannot currentlyfire. Everything interpolated into the note — PR titles, author logins, team
names — is single-line, so no line can equal the delimiter. Unlike
operator_notes.py, where PR bodies make it load-bearing, here it is purelydefensive. Retained because it costs nothing and stops being inert the moment
anyone renders a PR body into this note.
continue-on-error: trueon the Slack step matchesrelease.yml:135. Afailed nudge should not turn the repo's Actions tab red. The tradeoff is that
a broken webhook fails quietly; the
findstep does not share this, so anAPI or permissions failure still fails the job loudly.
Learning reviewer
None specifically. CODEOWNERS will auto-request both reviewers, but there is
no machine behavior here — no PV writes, no hardware assumptions, nothing that
changes what the linac does. Reviewing the
reviewDecisionreasoning above isthe only part that needs a second opinion.
Testing
Verified with a harness that parses this workflow, extracts each step's
run:block, executes it under bash with
ghstubbed to captured API output, andparses
$GITHUB_OUTPUTusing the runner's own delimiter rules — so the Slackstep receives exactly what it would receive in CI.
Covered: real API output from this repo (5 open PRs → 2 correctly listed);
the nothing-stale path;
reviewRequestscontaining bothUser(login) andTeam(name/slug) entries, plus slug-only and unknown-__typenamefallbacks;
reviewDecisionasREVIEW_REQUIRED,"", and absent; a PR titlecontaining
STALE_EOF,$(id), backticks, quotes and; rm -rf /(allinert); the day-count boundary; the query-limit truncation warning; and a
simulated 403 confirming a permissions failure fails the step visibly.
black --check .,flake8 .andactionlintare all clean repo-wide.Not verified, and worth knowing before this is trusted: whether
GITHUB_TOKENwithpull-requests: readis sufficient for--json reviewDecision,reviewRequests.workflow_dispatchrequires the fileon the default branch, so no pre-merge run is possible. The reasoning that it
should suffice is that these are pull-request resources and
GITHUB_TOKENcannot be granted org-level scopes at all. If it is wrong,
ghexitsnon-zero, the step fails red, and the run log shows GitHub's own message —
nothing silently under-reports. Recommend dispatching once by hand after merge
before relying on the Monday cron.
pytestpasses — 2905 passed, 3 skipped in 114s. The three skips arepre-existing and unrelated (
test_tune_cavity,test_backend_cavity,test_fault).addoptsscopes coverage to
--cov=sc_linac_physics, and this adds nothing tothat package.