ci: add a report-only merged-branch reaper sweep - #13500
Merged
os-project-manager merged 2 commits intoAug 30, 2026
Merged
Conversation
Adds .github/workflows/merged-branch-reaper.yml: a weekly (plus workflow_dispatch) sweep that classifies every claude/* remote branch by the state of the pull request whose head ref it is, and reports the would-delete list. It deletes nothing. The criterion is PR state MERGED. The is-ancestor probe is not used anywhere, including as a secondary check: this repo squash-merges through a queue that rewrites commits, so a fully merged branch's tip is normally not an ancestor of main. Report-only is enforced by the token, not only by the code -- the job runs with contents: read, which cannot delete a ref. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
The reaper is paths-filtered on itself, and dispatch-gates requires every paths-filtered workflow to either discover a check:* family or declare why it has none. This one genuinely has none: its single step is an API sweep run through actions/github-script, not a named local verification. Uses the marker the gate itself provides rather than an exemption list, per that mechanism's own docblock. The pull_request filter is kept because it is load-bearing: it produced the first real dry-run list (run 33318728567, "111 of 335 ... would be deleted"), which is the human look the ruling requires before deletion is enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
os-project-manager
marked this pull request as ready for review
August 30, 2026 15:44
os-project-manager
enabled auto-merge
August 30, 2026 15:44
This was referenced Aug 30, 2026
This was referenced Aug 30, 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.
Part of #12771
Adds
.github/workflows/merged-branch-reaper.yml— a report-only sweep that classifies everyclaude/*remote branch by the state of the PR whose head ref it is, and prints the would-delete list. It deletes nothing.Part of, notFixes, on purpose: this delivers the dry-run half the ruling asked for. Enabling deletion is a separate, reviewed diff and the maintainer's call, and the measurement below raises a question the card should stay open for.The two fences
is-ancestorprobe appears nowhere — not as the criterion, not as a secondary check. The only mentions of the phrase in the diff are prose forbidding it. Verified:grep -n "is-ancestor\|merge-base"returns 5 hits, all comments or the rendered report caption; reverse-checked with a term known present (merged_at, 10 hits).permissions: contents: read, which is structurally incapable of deleting a ref. A defect in the classification still cannot delete a branch.Measurements — these changed the design
All figures measured 2026-08-30 against the live API, not relayed.
The population, re-derived: 335
claude/*branchesAlso measured, and worth a separate look:
copilot/carries 678 branches — twice theclaude/accumulation. Out of scope here (the ruling namesclaude/*), reported so it is not mistaken for covered.⭐
delete_branch_on_mergeis already enabled — so the event-driven shape is the wrong onedelete_branch_on_mergeclaude/*PRs merged 2026-08-20T13:11Z .. 2026-08-30An
on: pull_requestreaper would fire ~140x/day to find nothing 99.93% of the time, racing the platform setting on the same ref. So this is a weekly scheduled sweep +workflow_dispatchinstead — one mechanism that covers both the legacy backlog and the rare leak. The 111 merged survivors are a one-time debt, not a flow: 62 merged in 2026-04, 33 in 2026-06, 8 in 2026-08 (newest 2026-08-20).⭐ H3 confirmed, and it dominates: half the population has no PR at all
The no-PR bucket is not a stale tail, it is the live growth: 145 of the 170 carry a tip commit dated 2026-08, 123 within the trailing 14 days. Branches pushed by a session that died or never opened a PR.
⇒ This workflow is correct as ruled and clears a real 111-branch debt, but it reaches ~a third of the population and close to none of the ongoing accumulation. Widening the criterion to cover abandoned no-PR branches is a new ruling (what proves such a branch is abandoned rather than in flight?), deliberately not taken here.
Option B — granting seats delete rights — is not touched here; it stays with the #12728 credential pass.
Proving the filter, not just running it
Every bucket above is a demonstrated exclusion on real data, and the report renders each with named examples:
merged_atkeeps the sweep from racingdelete_branch_on_merge. On today's data this bucket is 0 (newest survivor merged 2026-08-20), so the first real run should show 111 would-delete / 0 held.One trap found and guarded in the code:
commits/{sha}/pullslooks like it answers "does this branch have a PR" and does not — it returns PRs that merely contain the commit. Measured, it claimed a PR for 4 of 5 branches that had none of their own. The workflow therefore filters onpr.head.ref === branch.namerather than trusting the endpoint shape.The
pull_requesttrigger on this file means this PR's own CI run produces the first real would-delete list on a real runner — the human look the ruling asks for, rather than a claim about one.Verification
Gate union re-run at final commit
c014a236— 17 derived families, all exit 0, exit codes captured before any pipe:Gate list derived by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, not recalled. The workflow-scanning gates report 29 workflow files where 28 existed before, which is how I know they actually read the new file rather than passing vacuously.github-scriptbody extracted andnode --checkclean.check-test-completeness.mjsexits 3 / PREREQUISITE NOT MET by design — recorded NOT MEASURED, neither red nor green.grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'clean..github/workflows/file and publishes nothing from any package, soskip-changesetapplies.Patch round —
Lint & Repo Gatesred atc014a236, green ate1c856b7dispatch-gates' own self-test refused this file:Reproduced locally at
c014a236(exit 1, same assertion, line 636) before changing anything.The assertion names two legitimate exits: discover a
check:*family, or declare why there is none. This workflow genuinely has none — its single step is an API sweep throughactions/github-script, not a named local verification — so it now carries the marker the gate itself provides:That is the mechanism's intended use, not a way of making the gate stop asking: its own docblock argues against a hardcoded exemption list in the script and for a marker the workflow carries, "read fresh every run". No assertion was weakened, skipped or special-cased.
The
pull_requestfilter was kept, and that was measured rather than assumed. The alternative fix — drop the paths filter so the gate skips the file — would have been smaller but wrong: that trigger is what produced this workflow's first real dry-run list, on a real runner, which is the one human look the ruling requires. Run33318728567completed success with the annotation:That is the deliverable, generated by the workflow itself rather than predicted: 111 would-delete matches the local classification exactly, and the no-PR share (168 live vs 170 measured ~40 min earlier) drifts because this repo merges ~140 PRs/day and branches acquire PRs continuously.
After the fix, at
e1c856b7:Both legs run: the self-test leg (
pnpm check:pm-dispatch-gates, exit 0) and the work leg (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, exit 0, tree clean). Derived family union re-run ate1c856b7— all 17 exit 0.check:pm-dispatch-gatesis not in the family list thatdispatch-gatesderives for a.github/workflows/**surface, even though its live assertion reads every workflow file in the tree. That is exactly why the local run passed and CI did not. Reported rather than patched — it is outside this PR's declared surface.Session: https://claude.ai/code/session_01Pk26oZ12t5N1hwGW1m1MgC
Generated by Claude Code