Skip to content

Decide whether merge-and-release's branch cleanup should use compare-and-delete #946

Closed as not planned

Description

@ptr727

A CodeRabbit finding has recurred identically across three PRs touching merge-and-release/SKILL.md's and drive-pr/SKILL.md's branch cleanup steps: #936 (the original promotion review), #940 (the merge-and-release/drive-pr fix chain), and #945 (the next promotion). Declined each time with the same reasoning below, but it keeps resurfacing, so it needs an actual decision rather than another decline.

What the cleanup steps do today

Both skills' cleanup, after confirming (via gh pr list/gh pr view, matched on headRepository.nameWithOwner and headRefOid) that a feature branch's PR genuinely merged, delete the branch in two separate steps:

  1. Verify the local and/or remote branch tip still matches the confirmed headRefOid (git rev-parse --verify, git ls-remote --heads --exit-code).
  2. Delete it (git branch -D, git push origin --delete), as a separate, later command.

The finding

Between steps 1 and 2 there is a TOCTOU (time-of-check-to-time-of-use) window: a concurrent push could move the branch after verification but before deletion, and the plain delete would remove whatever is there at that moment, not what was verified. CodeRabbit's suggested fix (most detailed version, from #945):

Delete the local ref with an expected-OID operation such as git update-ref -d "refs/heads/<branch>" "<headRefOid>", and use an explicit expected-value lease for the remote delete; stop when the expected OID does not match or conditional deletion is unavailable. Before deletion, verify that both the fetch and resolved push URLs for origin refer to the intended repository, since they may point to different destinations.

The remote half of that is functionally a git push --force-with-lease=<ref>:<expected-oid> for a delete.

Why it's been declined each time

git-commit-conventions's "Never force push" section is a blanket, unconditional rule:

Do not run git push --force or git push --force-with-lease under any circumstances. Force pushing rewrites shared history and can cause data loss. This holds regardless of how confident the rewrite looks, a rejected push is recoverable, a force-pushed one is not.

Implementing CodeRabbit's suggestion means carving a narrow, explicit exception into that rule for this one cleanup case. That's a real policy change to a rule that exists because of a specific incident, not a mechanical bug fix an agent should decide unilaterally mid-PR. CodeRabbit's own severity marking on the #945 instance was "Heavy lift", agreeing it's more than a quick fix.

The practical risk is also narrow as things stand: the branch being deleted is one the current task exclusively owns per repo-worktree's isolation model, and deletion happens immediately after GitHub itself confirms the merge, so the race window is small and the actor who could win it would have to be pushing to a branch that just finished its own PR.

The decision needed

  1. Is the current two-step check-then-delete acceptable as-is (declining the finding permanently, with a note explaining why), or does the race genuinely warrant closing?
  2. If closing it: does git-commit-conventions' blanket force-push prohibition get a narrow, explicitly-scoped exception for this one case (an OID-gated delete of a branch this task's own cleanup just verified), or is there a different mechanism that avoids touching that rule at all (e.g., a repository-side branch protection setting, a cleanup lock file, or something else)?
  3. Separately, and lower-stakes: should the cleanup also verify origin's fetch and push URLs resolve to the intended owner/repo before deleting anything, independent of the OID question?

Filed at the maintainer's request after the finding recurred a third time, to review together before deciding.

Activity

  1. added
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Sep 5, 2026
  2. ptr727 commented on Sep 9, 2026

    @ptr727
    OwnerAuthor

    Carried forward from #1309, closed as a duplicate of this issue. It raised the same TOCTOU window (verify the branch tip, then delete it as a separate command) and the same blocked remedy (git-commit-conventions' unconditional force-push ban), from a different reviewer on a different pull request: coderabbitai on the develop -> main promotion #1304, deferred there under the instruction to merge the promotion and take the findings up afterwards. That is a fourth recurrence of this finding, after #936, #940 and #945.

    Two things it adds to the decision framing here:

    A third affected surface. backlog-burndown "Cleanup Is the Orchestrator's" deletes an abandoned or merged group's remote branch on the same verify-then-delete sequence, so whatever is decided binds three skills rather than the two named above: merge-and-release, drive-pr, and backlog-burndown.

    The rule ruling out the remedy is stated in only one of them. drive-pr step 4 says it explicitly: "Never --force-with-lease here, git-commit-conventions forbids it unconditionally, this plain verify-then-delete is the safety gate, not a compare-and-swap at delete time." backlog-burndown states the sequence without naming what it rules out or what residual race remains. So there is a fourth question alongside the three above:

    1. If decision 1 lands on "accept the race deliberately", every skill that states the verify-then-delete sequence should say so where it states it, rather than one skill ruling out the mechanism and the others leaving the reader to infer it.

    #1309 also sharpened the question itself: whether the fleet's unconditional force-push ban is meant to reach a delete-time expected-OID condition on a branch deletion, which pushes no commits and rewrites no history, or whether that ban is about rewriting a shared branch and a guarded delete sits outside it.

  3. ptr727 commented on Sep 10, 2026

    @ptr727
    OwnerAuthor

    Maintainer's decision, 2026-09-09: the finding is declined permanently, with the reason recorded here so it stops recurring.

    The two-step check-then-delete stays. The branch being deleted is one the task exclusively owns per repo-worktree's isolation model, and the deletion follows GitHub's own confirmation of the merge, so the window is small and the actor who could win it would have to be pushing to a branch that has just finished its own pull request.

    git-commit-conventions' force-push prohibition is therefore not narrowed. That rule exists because of a specific incident, and this race does not warrant carving an exception into it.

    This is the third time the finding has been raised. A reviewer raising it a fourth time can be answered with this comment.

    Asked while writing the handoff on #1501. Dropping decision: the question is settled and what remains is recording the decline where a reviewer will meet it.

  4. removed
    decisionA question waiting on the maintainer, alone or beside whatever else the issue carries
    on Sep 10, 2026
  5. ptr727 commented on Oct 17, 2026

    @ptr727
    OwnerAuthor

    Closing as decided. The maintainer's 2026-09-09 decision above declines compare-and-delete permanently. The tree matches it: scripts/pr_review.py cleanup re-reads the remote tip against the pull request's headRefOid and confirms the delete by reading the ref back, and its local half re-reads the branch tip before git branch -D, both verify-then-delete as decided. That landed in #3186 and was wired into the skills by #3188.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions