Skip to content

fix(governance): harden automated branch pruning against deletion race (#10035, #9917) - #10112

Merged
mrveiss merged 7 commits into
Dev_new_guifrom
issue-10035
Jun 14, 2026
Merged

mrveiss merged 7 commits into
Dev_new_guifrom
issue-10035

Conversation

@mrveiss

@mrveiss mrveiss commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Thinking Path

Umbrella #9927 (governance) member #10035 reported a freshly-pushed, unmerged branch being deleted within ~2 min of push. Auditing the two branch-pruning code paths (.github/workflows/branch-cleanup.yml and scripts/cleanup-worktrees.sh) surfaced the root cause: the closed-issue cleanup extracted the issue number with grep -oP '\d{4,}' | head -1, which reads the year out of a dated branch name (chore/triage-delta-2026-06-12 → 2026) and the work-item id out of a Paperclip branch (MVA-3847-fix → 3847). A wrong match against a closed GitHub issue with a merged PR deletes the unrelated branch. Neither path had the <24h / open-PR guards #10035 asks for.

What Changed

  • New scripts/lib/branch-guards.sh — single source of truth for safe pruning:
    • extract_issue_number — only issue-NNNN / hotfix-NNNN branches map to a GitHub issue; date tokens and MVA-NNNN work-items never match.
    • branch_recently_pushed — skip branches whose tip commit is younger than BRANCH_MIN_AGE_HOURS (default 24h), closing the push→PR-creation gap.
    • branch_has_open_pr — skip any branch with an open PR.
  • branch-cleanup.yml — both deletion steps source the guards; merged step now skips open-PR branches; closed-issue step uses the safe extractor + recency/open-PR guards. Merged-detection stays ancestor-based (never patch-equivalence), documented inline.
  • scripts/cleanup-worktrees.sh — Phase 1 remote delete + Phase 3 local/remote cleanup all routed through the guards.
  • docs/developer/CLAUDE_GIT.md — new "Automated Branch Pruning" section documenting the policy + guards (governance(repo): enable auto-delete-head-branches + automate merged-branch pruning #9917 doc task).
  • scripts/lib/branch-guards_test.sh — hermetic unit tests (git/gh stubbed), 14 cases.

Implements #10035 and the code half of #9917. The "Automatically delete head branches" repo toggle (#9917) and the #9911 one-time backfill remain owner-driven (tracked on #9927).

Verification

  • bash scripts/lib/branch-guards_test.sh → Passed: 14 Failed: 0.
  • Extraction spot-check: chore/triage-delta-2026-06-12→(empty), MVA-3847-fix→(empty), issue-9943→9943, hotfix-1234→1234.
  • bash scripts/cleanup-worktrees.sh --dry-run --branches-only → fresh in-flight branches (issue-10035, issue-10012, …) correctly SKIPped (<24h); genuinely-merged old branches still flagged; no date/MVA branch mis-matched.
  • bash -n clean on all shell files; workflow YAML parses.

Model Used

claude-opus-4-8

#10035, #9917)

Centralize safe-pruning guards in scripts/lib/branch-guards.sh and wire both
branch-cleanup.yml and cleanup-worktrees.sh to them, closing the #10035 race
where a freshly-pushed unmerged branch could be deleted.

- extract_issue_number: only issue-/hotfix- branches map to a GitHub issue;
  date tokens (...-2026-06-12 -> 2026) and MVA work-items (MVA-3847 -> 3847)
  are no longer mis-read as issue numbers (root cause of the deletion).
- branch_recently_pushed: skip branches with a tip commit younger than
  BRANCH_MIN_AGE_HOURS (default 24h) -- closes the push->PR-creation gap.
- branch_has_open_pr: skip any branch that still has an open PR.
- Merged-detection stays ancestor-based, never patch-equivalence (documented).
- Unit tests (branch-guards_test.sh): 14/14 pass.
- Document the pruning policy + guards in docs/developer/CLAUDE_GIT.md (#9917).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 14, 2026 •

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No hardcoded values detected that have SSOT config equivalents!

mrveiss and others added 2 commits June 14, 2026 15:24
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ecency check (#10035)

- branch_recently_pushed: treat empty/non-numeric git-log output as not-recent
  (HIGH: '[ "" -eq 0 ]' exits rc=2, silently bypassing the guard).
- branch-cleanup.yml merged step: add branch_recently_pushed guard for symmetry
  with the closed-issue step (closes the push->PR gap on the merged path too).
- Test the exit-0-but-empty-stdout path (15/15).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mrveiss

mrveiss commented Jun 14, 2026

Copy link
Copy Markdown
Owner Author

Addressed code review (thanks):

  • HIGH branch_recently_pushed empty-timestamp bypass — fixed: empty/non-numeric git log output is now treated as not-recent via a numeric-regex guard before the -eq comparison ([ "" -eq 0 ] exits rc=2 under set -e, confirmed). Added a test for the exit-0-but-empty-stdout path → 15/15.
  • MEDIUM merged step missing recency guard — fixed: added branch_recently_pushed to the merged-branches step for symmetry with the closed-issue step.
  • MEDIUM broad gh pr list --search match — pre-existing, bounded by the CLOSED + recency/open-PR guards; filed as discovery fix(governance): scope branch-cleanup merged-PR confirmation search (avoid broad --search match) #10114 to keep this diff tight.
  • LOW items (fail-open gh errors, by-design regex conservatism) acknowledged as acceptable per the docstrings/CLAUDE_GIT.md.

Pushed as 2d1ea282c.

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.

1 participant