Skip to content

feat(pr-pipeline): delete stale branches daily by the branch-cleanup rule - #8959

Closed
kyle-sexton wants to merge 2 commits into
mainfrom
feat/branch-cleanup-w7
Closed

kyle-sexton wants to merge 2 commits into
mainfrom
feat/branch-cleanup-w7

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 22, 2026 •

Copy link
Copy Markdown
Member

No related issue: owner decision W-7 (2026-10-21)

Summary

No non-default branch lives forever. A daily maintenance-delete-stale-branches.yml sorts every branch of this repository by a new branch-cleanup rule and deletes the finished ones, at most 25 a run, writing a ledger line before each delete. A lane run that stopped (needs a human, blocked, limited, funding, turn-cap) never has its branch deleted.

Fix

  • .github/scripts/branch-cleanup/rules.mjs and cleanup.mjs: per branch, cheapest read first.
    • Never touched: default, release/*, dependabot/*, gh-readonly-queue/*.
    • Kept: protected or ruleset branch; open PR; lane branch (work-resolve-issue/issue-<n>-...) while issue n is open; other branch while an issue its newest PR body links (Closes/Fixes/Resolves/Refs) is open; anything closed under min-age-days.
    • Reported, never deleted: a lane branch whose closed issue carries a lanes-App stop marker for its run (or one naming no run, or a resumed run's marker), reusing parseOutcomeMarker and DIMENSION_STEPS from report-outcome.mjs; a branch that never had a PR; merged then commits ahead after the tip moved; closed unmerged then tip moved; any failed read.
    • Deleted: newest PR merged at the tip, merged with the tip moved and 0 commits ahead (compare), or closed unmerged at the tip, every linked issue closed; or a lane branch with no PR whose issue closed; the later close at least min-age-days ago.
  • It prints will delete N of M deletable branch(es) (cap C) first, deletes oldest-closed first up to the cap, and right before each delete re-reads the tip (skips when it moved) and open PRs from and into the branch. The ledger {repo, branch, sha, rule, issue, pr, deletedAt} goes to the step summary and the branch-cleanup-ledger-<run>-<attempt> artifact (uploaded under always()). Delete is DELETE git/refs/heads/<branch> with the job's GITHUB_TOKEN (contents: write, issues/pull-requests read).
  • Dry-run: schedule deletes; workflow_dispatch input dry-run defaults to true. In dry-run the HTTP client throws on any non-GET. The lanes kill switch stops the job before it reads.
  • Config: branch-cleanup.min-age-days (14) and max-deletes-per-run (25) in docs/conventions/pr-pipeline.yaml, the schema, branchCleanupFromBase in resolve-config, and a "Branch cleanup" section in the pr-pipeline README. Changelog fragment under .changes/pr-pipeline/.
  • repo-fleet-hygiene's audit-fleet.sh is unchanged; it stays the per-branch prompt path for the reported branches.

Verification

Related

  • Follow-ups: #8957 (pr-pipeline-sandbox adoption), #8958 (atomic compare-and-delete, plan/delete job split, stopped-run branches of closed issues).

🤖 Generated with Claude Code

kyle-sexton and others added 2 commits October 21, 2026 20:42
…rule

Owner decision W-7: no non-default branch lives forever. A daily
maintenance-delete-stale-branches.yml sorts every branch: closed or merged
PR and closed linked issue at least min-age-days (14) ago deletes, at most
max-deletes-per-run (25) a run; an open issue, an open PR, a protected or
ruleset branch, and a stopped lane run's branch are never deleted; anything
unreadable or without a PR is listed for a person. Each delete re-reads the
tip and open PRs and writes a ledger line first. Manual runs default to
dry-run, which refuses every non-GET request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p suite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-22T01:23:41.182489Z e8cc272 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

claude-security-review reviewed e8cc27244880c42bedaf9aebf6cd2f056807382e: 0 finding(s), 0 as inline comments.

Security review: PR #8959 @ e8cc272

Result: no security findings. Nothing was posted inline.

Scope read: cleanup.mjs, rules.mjs, maintenance-delete-stale-branches.yml, the branchCleanupFromBase addition in resolve-config.mjs, and the check-kill-switch action. I did not read the test files, the pr-pipeline convention docs or schema, or the changelog fragment. The convention docs and schema have additions only, with no deleted or softened instruction, so the instruction-surface lens does not apply. I did not read docs/conventions/instruction-exception-register/README.md, so the protected-class list was not consulted.

Checked, and why each held:

  • Trigger and ref: the job runs only on schedule or workflow_dispatch, with if: github.ref == 'refs/heads/main'. No PR or fork-controlled input reaches it.
  • Code that runs: the checkout is github.sha on main with persist-credentials: false. npm ci uses --ignore-scripts --omit=dev. No branch code is checked out or run.
  • Live versus dry-run: the schedule runs live and workflow_dispatch defaults to dry-run. DRY_RUN is read as !== "false", so an unset or odd value means dry-run. The client throws on any non-GET request in dry-run.
  • Kill switch: check-kill-switch proceeds only when both variables read exactly false. An unset variable stops the run.
  • Token scope: contents: write is job-scoped and needed for the ref delete. The top-level permissions is {}. issues and pull-requests are read-only.
  • Injection: the REPOSITORY format is validated. Every branch-name path segment goes through encodeURIComponent. The head= filter also uses encodeURIComponent and is pinned to the repo owner, so fork branches do not match. No shell interpolates branch names, and the workflow's only ${{ }} in run: bodies goes through env:.
  • Trust boundary: the lane's stopped-run detection trusts outcome markers only from comments by the lanes bot login (LANES_BOT_LOGIN). [bot] logins cannot be registered by users, so a user cannot forge them. Linked issues come from the PR body, which only the PR author or a maintainer can edit. Anyone who can create these branches already has write access.
  • Fail-safe direction: every unreadable read (issue, compare, rules, close time) reports the branch to a person instead of deleting it. min-age-days falls back to 14 and max-deletes-per-run to 25 when the config value is invalid. Excluded prefixes, protected branches, ruleset-covered branches, open PRs into or out of the branch, and moved tips are all kept or reported.
  • Delete safety: each delete is re-checked against the tip SHA and open PRs just before it runs. The ledger line is appended before the DELETE, so a restore SHA exists. The remaining gap between the re-check and the DELETE is a small time-of-check window. The ledger SHA recovers a branch through git push.
  • Step summary: branch names in the markdown summary are cosmetic only and carry no secret.

Manifest items: none. The PR touches no plugin.json or marketplace.json.

Actions hardening: the pinned actions, token permissions and triggers are zizmor's lane. I found nothing that needs trust-boundary reasoning beyond what is listed above.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e8cc272448

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +164 to +168
try {
results.push({ name, sha, ...(await evaluate(call, context, { name, sha, protected: branch.protected })) });
} catch (error) {
results.push({ name, sha, verdict: "report", reason: `a read failed: ${error.message}` });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reserve API calls for the deletion phase

When branch evaluation consumes the shared 600-call budget, every planned deletion fails its first re-read because deletion starts only after all branches have been evaluated. I reproduced this with 300 old merged branches: 297 were classified as deletable, 25 were planned, and zero were deleted because every re-read reported call budget of 600 spent. Subsequent scheduled runs repeat the same scan, so cleanup cannot reduce the backlog without manual intervention. Reserve calls for the revalidation and deletion phase, or process a bounded batch that leaves enough budget to delete its candidates.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

claude-review reviewed e8cc27244880c42bedaf9aebf6cd2f056807382e: 0 finding(s), 0 as inline comments.

Code review: PR #8959 (branch cleanup)

I found nothing to block. I read the whole diff and checked the report-outcome.mjs exports it reuses (parseOutcomeMarker, DIMENSION_STEPS, the marker regex). I did not run the tests; the test counts in the PR body are the author's claims and I did not confirm them.

Closing issue: none. The PR states No related issue: owner decision W-7, so there are no acceptance criteria to check.

Manifests: the diff changes no plugin.json or marketplace.json. The bump: minor fragment fits a new, additive leaf and workflow.

Path-scoped rules: none of the repo's .claude/rules globs matches a changed file, apart from the cost-claims rule's docs/**/*.md. The README additions state no prices.

What I checked:

  • Delete path: the order is evaluate, then re-read of the tip and of open PRs from and into the branch, then ledger append, then DELETE. The ledger line is written before the delete. A failed re-read skips the branch, and a failed delete exits non-zero.
  • Dry-run: the client throws on any non-GET, the loop also skips deletes, and the workflow default is true on manual runs. The schedule resolves to 'false'.
  • Stopped-run protection: stoppedRun matches a marker that names this run, names no run ("" or 0), or comes from a resumed run, and it ignores dimension-skip outcomes. Markers are read only from the lanes-App login, and a test covers a marker from anyone else.
  • Workflow hardening:
    • Permissions are {} at the top and contents: write plus issues/pull-requests: read on the job.
    • The checkout has no persisted credentials and uses the run's main SHA.
    • The job is gated to refs/heads/main.
    • The kill switch gates every step that reads or deletes.
    • The ledger artifact uploads under always().
  • Ref paths: refPath encodes each segment of a branch name before the name goes into an API path.

Notes, not findings:

  • CALL_BUDGET (600) is shared between the evaluation pass and the delete-time re-reads (about 4 calls per deleted branch). If the repo grows well past roughly 200 branches, the budget runs out. Evaluation then reports the rest as read failures, and later re-reads throw into "skipped at delete time". That fails safe, nothing is wrongly deleted, but a run could delete nothing without saying why.
  • No test covers the stoppedRun branches for a marker with no run, a run=0 marker, or branch=resumed. The README describes all three.
  • No test covers branchCleanupFromBase or the dynamic import in main(). cleanup() is tested only with a stubbed loadConfig.
  • The PR body says the owner ran the live dry-run "from this branch". The job is gated to github.ref == 'refs/heads/main', so a dispatch from a feature branch would skip it. The diff cannot show how that run happened.

@kyle-sexton kyle-sexton added the do-not-merge Hard merge gate: do not merge while applied. label Oct 22, 2026
kyle-sexton added a commit that referenced this pull request Oct 22, 2026
Evaluation spent the shared 600-call budget, so with about 300 deletable
branches every planned delete failed its re-read and nothing was ever
deleted (Codex review on #8959). cleanup.mjs now reserves 4 calls per
planned delete from the client's budget, evaluates with the rest, and
stops evaluating once it has found max-deletes-per-run candidates or
spent its share; the remaining branches wait for the next run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton

Copy link
Copy Markdown
Member Author

Superseded by #8976, which carries all of W-7 plus the owner's no-dates rule (#8958) and the fix for the Codex P2 here (delete calls are reserved before evaluation).

auto-merge was automatically disabled October 22, 2026 01:50

Pull request was closed

@github-actions
github-actions Bot deleted the feat/branch-cleanup-w7 branch October 22, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge Hard merge gate: do not merge while applied.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant