Repository navigation
ci: stale-PR automation — bestaxbot closer + generic stale (#275) #279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
acb6549
f49942a
973ce0e
4f095a3
492bd4e
7f7b60a
b7a9446
d4ffd23
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,124 @@ | ||||||||||||||||||
| name: Close Stale Claude-Assisted PRs | ||||||||||||||||||
|
|
||||||||||||||||||
| # Closes Claude-assisted PRs (bestaxbot-authored or labeled | ||||||||||||||||||
| # `claude-assisted`) untouched for 90 days. Mirrors oven-sh/bun's | ||||||||||||||||||
| # close-stale-robobun-prs.yml. Zero Claude usage — plain shell + gh, so it | ||||||||||||||||||
| # can never spend AI budget or act on untrusted content. | ||||||||||||||||||
| # | ||||||||||||||||||
| # This is the ONLY closer for these PRs: stale.yml exempts the | ||||||||||||||||||
| # `claude-assisted` label from its generic 30/14-day sweep, so AI-assisted | ||||||||||||||||||
| # PRs get the longer leash (they routinely sit waiting on human review). | ||||||||||||||||||
| # The sweep is the union of author:bestaxbot and label:claude-assisted — | ||||||||||||||||||
| # the label (auto-applied by auto-label-claude-prs.yml) also covers | ||||||||||||||||||
| # human-authored PRs with Claude attribution, and the author query covers | ||||||||||||||||||
| # bot PRs opened before the labeler existed. A first step backfills the | ||||||||||||||||||
| # claude-assisted label onto any unlabeled bestaxbot PR so the generic | ||||||||||||||||||
| # sweep can't close pre-labeler bot PRs at day 44: this job's 03:41 cron | ||||||||||||||||||
| # fires before stale.yml's 04:17 sweep. | ||||||||||||||||||
| # | ||||||||||||||||||
| # PRs labeled `neverstale` are exempt from this closer too — the label means | ||||||||||||||||||
| # "exempt from stale automation", and that covers the 90-day sweep as well | ||||||||||||||||||
| # as the generic 30/14-day stale.yml layer. | ||||||||||||||||||
| # | ||||||||||||||||||
| # Loop labels (ai-loop / ai-loop-paused / needs-human-review) are stripped | ||||||||||||||||||
| # BEFORE the close (same pattern as on-slop.yml) so the PR exits the AI | ||||||||||||||||||
| # loop's state machine cleanly instead of lingering as a closed-but-labeled | ||||||||||||||||||
| # zombie the watchdog might still try to sweep. | ||||||||||||||||||
|
|
||||||||||||||||||
| on: | ||||||||||||||||||
| schedule: | ||||||||||||||||||
| # 03:41 UTC daily — deliberately offset from the loop watchdog's | ||||||||||||||||||
| # `23 */2 * * *` so the closer never races a sweep re-dispatch. | ||||||||||||||||||
| - cron: '41 3 * * *' | ||||||||||||||||||
| workflow_dispatch: | ||||||||||||||||||
|
Comment on lines
+28
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## File list\n'
git ls-files '.github/workflows/*' | sed 's#^`#-` #'
printf '\n## Target workflow\n'
wc -l .github/workflows/close-stale-bestaxbot-prs.yml
cat -n .github/workflows/close-stale-bestaxbot-prs.yml
printf '\n## Search for concurrency and watchdog references\n'
rg -n "concurrency|23 \\*/2 \\* \\* \\*|close-stale|bestaxbot|watchdog|re-dispatch" .github/workflowsRepository: allxsmith/bestax Length of output: 9279 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n## claude-pr-loop relevant sections\n'
sed -n '1,120p' .github/workflows/claude-pr-loop.yml
printf '\n## on-slop relevant sections\n'
sed -n '1,90p' .github/workflows/on-slop.ymlRepository: allxsmith/bestax Length of output: 8538 Add a 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| # No workflow-level grants; the single job takes exactly what it needs. | ||||||||||||||||||
| permissions: {} | ||||||||||||||||||
|
|
||||||||||||||||||
| # One sweep at a time: a manual workflow_dispatch overlapping the daily cron | ||||||||||||||||||
| # must not race a second closer over the same PRs (double comments, or a | ||||||||||||||||||
| # second `gh pr close` aborting under set -e). | ||||||||||||||||||
| concurrency: | ||||||||||||||||||
| group: close-stale-bestaxbot-prs | ||||||||||||||||||
| cancel-in-progress: false | ||||||||||||||||||
|
|
||||||||||||||||||
| jobs: | ||||||||||||||||||
| close: | ||||||||||||||||||
| runs-on: ubuntu-latest | ||||||||||||||||||
| timeout-minutes: 10 | ||||||||||||||||||
| permissions: | ||||||||||||||||||
| pull-requests: write # comment, edit labels, close | ||||||||||||||||||
| issues: write # issue-comment API for the PR conversation | ||||||||||||||||||
| steps: | ||||||||||||||||||
| # Self-heal: bestaxbot PRs opened before auto-label-claude-prs.yml | ||||||||||||||||||
| # existed (or where it misfired) lack the claude-assisted label, so | ||||||||||||||||||
| # stale.yml's generic 30/14-day sweep would close them at day 44 before | ||||||||||||||||||
| # this 90-day closer ever saw them. Label them here; the REST call | ||||||||||||||||||
| # auto-creates the label on first use, same as the labeler. | ||||||||||||||||||
| - name: Backfill claude-assisted onto unlabeled bestaxbot PRs | ||||||||||||||||||
| env: | ||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||||
| run: | | ||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||
| gh pr list --repo "$REPO" --author bestaxbot --state open \ | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| --limit 1000 --json number,labels \ | ||||||||||||||||||
| | jq -r '.[] | select(([.labels[].name] | index("claude-assisted")) | not) | .number' \ | ||||||||||||||||||
| | while read -r PR; do | ||||||||||||||||||
| echo "Labeling PR #$PR claude-assisted" | ||||||||||||||||||
| gh api -X POST "repos/$REPO/issues/$PR/labels" \ | ||||||||||||||||||
| -f "labels[]=claude-assisted" || true | ||||||||||||||||||
| done | ||||||||||||||||||
|
|
||||||||||||||||||
| - name: Close Claude-assisted PRs inactive for 90+ days | ||||||||||||||||||
| env: | ||||||||||||||||||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||||||||||||||||
| REPO: ${{ github.repository }} | ||||||||||||||||||
| run: | | ||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||
| CUTOFF=$(date -u -d '90 days ago' +%Y-%m-%dT%H:%M:%SZ) | ||||||||||||||||||
| echo "Closing Claude-assisted PRs not updated since $CUTOFF" | ||||||||||||||||||
|
|
||||||||||||||||||
| # Union of the two selectors, deduped by PR number. A label with | ||||||||||||||||||
| # no matches (or that doesn't exist yet) yields an empty list from | ||||||||||||||||||
| # `gh pr list`, not an error. <= makes the 90-day cutoff inclusive. | ||||||||||||||||||
| { | ||||||||||||||||||
| gh pr list --repo "$REPO" --author bestaxbot --state open \ | ||||||||||||||||||
| --limit 1000 --json number,updatedAt,labels | ||||||||||||||||||
| gh pr list --repo "$REPO" --label claude-assisted --state open \ | ||||||||||||||||||
| --limit 1000 --json number,updatedAt,labels | ||||||||||||||||||
| } \ | ||||||||||||||||||
| | jq -rs --arg cutoff "$CUTOFF" \ | ||||||||||||||||||
| 'add | unique_by(.number) | .[] | select(.updatedAt <= $cutoff) | select(([.labels[].name] | index("neverstale")) | not) | .number' \ | ||||||||||||||||||
| | while read -r PR; do | ||||||||||||||||||
| # Re-check eligibility immediately before acting: the listing | ||||||||||||||||||
| # snapshot ages while earlier PRs in the loop are processed, | ||||||||||||||||||
| # and a maintainer may have pushed activity or applied | ||||||||||||||||||
| # neverstale in the meantime. Any fetch failure counts as | ||||||||||||||||||
| # not-eligible — closing on stale data is worse than retrying | ||||||||||||||||||
| # tomorrow. | ||||||||||||||||||
| ELIGIBLE=$(gh pr view "$PR" --repo "$REPO" \ | ||||||||||||||||||
| --json state,updatedAt,labels \ | ||||||||||||||||||
| | jq -r --arg cutoff "$CUTOFF" \ | ||||||||||||||||||
| 'if .state == "OPEN" and .updatedAt <= $cutoff and (([.labels[].name] | index("neverstale")) | not) then "yes" else "no" end' \ | ||||||||||||||||||
| || echo no) | ||||||||||||||||||
| if [ "$ELIGIBLE" != "yes" ]; then | ||||||||||||||||||
| echo "Skipping PR #$PR — no longer eligible" | ||||||||||||||||||
| continue | ||||||||||||||||||
| fi | ||||||||||||||||||
|
|
||||||||||||||||||
| echo "Closing stale PR #$PR" | ||||||||||||||||||
| gh pr comment "$PR" --repo "$REPO" --body "Closing this PR after 90 days of inactivity. If it's still wanted, reopen it — for AI-loop PRs, you can instead restart the loop per the instructions in the PR's \`ai-loop-state\` status comment." | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
|
|
||||||||||||||||||
| # Strip loop labels FIRST so the PR leaves the loop's state | ||||||||||||||||||
| # machine cleanly. gh succeeds when a label is absent, so a | ||||||||||||||||||
| # failure here is a real API/auth error — let it abort (set -e) | ||||||||||||||||||
| # rather than close the PR with loop labels still attached; | ||||||||||||||||||
| # the daily cron retries tomorrow. | ||||||||||||||||||
| gh pr edit "$PR" --repo "$REPO" \ | ||||||||||||||||||
| --remove-label ai-loop \ | ||||||||||||||||||
| --remove-label ai-loop-paused \ | ||||||||||||||||||
| --remove-label needs-human-review | ||||||||||||||||||
|
Comment on lines
+118
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Un-guarded label strip can halt the whole closer — 🟡 Minor · Correctness What: This diverges from the Why it matters: The comment justifies dropping the guard as "a failure here is a real API/auth error" — but a label removed from the repo is neither, yet it aborts the step on the first PR in the loop, so no PRs get closed that day (and every subsequent day until the label is restored). Most 90-day-stale PRs carry none of these loop labels, making them the common case that trips it. Fix: restore the tolerant form so a stray label can't wedge the sweep, while still surfacing genuine auth failures via the daily retry:
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| gh pr close "$PR" --repo "$REPO" | ||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||
| done | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| name: Stale | ||
|
|
||
| # Marks PRs with no activity for 30 days as `stale`, then closes them 14 | ||
| # days later if the inactivity continues. Mirrors oven-sh/bun's stale.yaml. | ||
| # Zero Claude usage — just actions/stale. | ||
| # | ||
| # Escape hatch: a maintainer applies the `neverstale` label to exempt a PR | ||
| # permanently. Any activity (commit, comment, review) also removes `stale` | ||
| # and resets the clock. | ||
| # | ||
| # Claude-assisted PRs (the `claude-assisted` label, auto-applied by | ||
| # auto-label-claude-prs.yml) are exempt from this sweep: they get the longer | ||
| # 90-day leash enforced by close-stale-bestaxbot-prs.yml instead, since they | ||
| # routinely sit waiting on human review. Without this exemption the 30/14 | ||
| # sweep would close them at day 44 and the 90-day closer could never fire. | ||
| # | ||
| # Labels: `stale` is auto-created by actions/stale on first application | ||
| # (suggested color #ededed). `neverstale` (#c2e0c6) is human-applied and | ||
| # created at rollout. | ||
| # | ||
| # Issues are deliberately NOT processed (a deviation from bun, whose issue | ||
| # side keys off a needs-repro/waiting-for-author triage process this repo | ||
| # doesn't run). | ||
|
|
||
| on: | ||
| schedule: | ||
| # 04:17 UTC daily — a distinct minute from both the loop watchdog | ||
| # (`23 */2 * * *`) and the bestaxbot closer (`41 3 * * *`). | ||
| - cron: '17 4 * * *' | ||
| workflow_dispatch: | ||
|
|
||
| # No workflow-level grants; the single job takes exactly what it needs. | ||
| permissions: {} | ||
|
|
||
| jobs: | ||
| stale: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 10 | ||
| permissions: | ||
| pull-requests: write # label, comment, close PRs | ||
| issues: write # actions/stale needs both even with issues disabled | ||
| steps: | ||
| - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 | ||
| with: | ||
| days-before-pr-stale: 30 | ||
| days-before-pr-close: 14 | ||
| stale-pr-label: stale | ||
| exempt-pr-labels: neverstale,claude-assisted | ||
| remove-stale-when-updated: true | ||
| stale-pr-message: > | ||
| This PR has had no activity for 30 days and has been marked | ||
| `stale`. It will close in 14 days unless activity resumes — any | ||
| commit, comment, or review removes the label and resets the | ||
| clock. If this PR should never be swept, ask a maintainer to | ||
| apply the `neverstale` label. | ||
| close-pr-message: > | ||
| Closing after 14 further days of inactivity. If this is still | ||
| wanted, reopen it or open a fresh PR — and consider asking a | ||
| maintainer for the `neverstale` label to exempt it from stale | ||
| automation. | ||
| # Issues disabled — see the header comment. | ||
| days-before-issue-stale: -1 | ||
| days-before-issue-close: -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
auto-label-claude-prs.ymldoesn't exist — human-authored Claude PRs are never protected — 🟠 Major · CorrectnessWhat: Both new workflows repeatedly cite
auto-label-claude-prs.ymlas the source of theclaude-assistedlabel (here, and instale.yml:11). That workflow does not exist anywhere in the repo — the only thing that ever appliesclaude-assistedis the backfill step below, and it is scoped to--author bestaxbot.Why it matters: The stated design is a union:
author:bestaxbotpluslabel:claude-assistedso the label leg "also covers human-authored PRs with Claude attribution." In reality no human-authored PR ever receives the label, so:stale.yml'sexempt-pr-labels: …,claude-assistednever matches a human PR → it gets marked stale at 30 days and closed at 44.--label claude-assistedleg only ever returns bestaxbot PRs (which the author leg already covers), so it's currently redundant.Net: the entire "longer leash for human Claude-assisted PRs" story is inoperative. The bot path works (author query + backfill); the human path silently does not.
Fix: Either ship the
auto-label-claude-prs.ymllabeler in this PR (so the label is actually applied on human PRs with Claude attribution), or drop the claims/leg that depend on it and document that only bestaxbot-authored PRs get the 90-day leash.Verification