Skip to content

branch-sweep: the "merged tips are ancestors of main" assumption is unchecked, and currently false #167

Description

@bdelanghe

What

branch-sweep.yml's header reassures the operator that deletion is safe because merged history is preserved on the default branch:

Merged-branch tips are ancestors of the default branch, so a merged branch that auto-delete somehow missed is also swept — its history is on main.

Nothing in the workflow checks that. The candidate rules are: not default, not protected, no open PR, and either tip older than stale_days or a prefix match. Reachability from the default branch is never consulted.

The assumption is false in this repo today — see #166, where three claude/* branches share no merge base at all with main and hold the only reachable copy of everything before 2026-08-01.

Why the prose is the risky part

The header is what an operator reads before flipping dry_run to false. It states a safety property the code does not enforce, which is precisely the failure mode docs/agentic-code-hygiene.md rule 3 names — a gate's own claim about itself is not evidence.

Suggested fix

Add a reachability check to the candidate loop and surface it in the printed table, so the dry run distinguishes "already on main" from "would discard commits":

if git merge-base --is-ancestor "$tip_sha" "origin/$default"; then
  reason="$reason, merged"
else
  unique=$(git rev-list --count "origin/$default..$tip_sha")
  reason="$reason, ${unique} commit(s) NOT on $default"
fi

Then either refuse non-ancestor branches unless a second input explicitly opts in, or at minimum make the dry-run output say plainly which branches carry unreachable commits. The current output — WOULD DELETE <name> (prefix claude/) — reads identically whether the branch is redundant or is the last ref holding two months of history.

The workflow runs gh api rather than a full checkout, so this needs either a fetch of the candidate tips or the compare API; the cheap version is GET /repos/{repo}/compare/{default}...{branch} and reading behind_by / ahead_by.

Also worth reconsidering

prefix waives the age rule entirely. Combined with the missing reachability check, prefix: claude/ on this repo is a single dispatch away from deleting history. Age is a weak proxy for "over", but it is currently the only thing standing between a routine sweep and #166.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions