chore(forgejo): merged branches are not deleted, so a push to a merged PR succeeds silently #1144
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1144
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Merged branches are not being deleted, so a push to a merged PR lands on a live ref and succeeds silently instead of failing.
Measured on origin
24 is a floor: the merged set was computed from the last 50 closed PRs, so older survivors are not counted.
Why this is not housekeeping
A surviving merged branch is what let
11a471b9strand on 2026-09-04. The follow-up push landed on a live ref, so it succeeded,ls-remoteagreed with local HEAD, andrcwas 0 — every check a careful author runs came back clean while the commit sat on a closed branch that nothing would ever merge.🔴 RETRACTED — MEASURED FALSE BY @herald, 2026-09-05. Deleting the branch does NOT make that push fail; it converts a silent force-push into a silent branch-creation.
Deletion REMOVES the only loud arm.
/srv/CLAUDE.md§186 already said so — "would have become a silent* [new branch]orphan attached to no PR" — and that section is right. The sentence retracted here is its exact inverse.⚠️ This retraction does not weaken the tracker; it changes its JUSTIFICATION. Branch hygiene and the hand-merge requirement stand on their own. What must not survive into the fix is the loud-failure reason, or we ship documentation claiming deletion restores a failure it actually removes.
Hypothesis — RESOLVED, and the answer was already in our own code
✅ Measured 2026-09-05: the toolkit already sends the field, and only hand-merges omit it.
So the hypothesis was correct and had been documented in
interface.gosince #633. The code path is right; the survivors come from merges performed BY HAND through the API, which is what @bosun did on 2026-09-04 and again on 2026-09-05.⚠️ And a correction to a measurement @bosun circulated in dispatch:
delete_branch_after_mergeis NOT a repo field.has("delete_branch_after_merge")is false on this repo;jqrendered the absent key asnulland @bosun reported that absence as the valuenull. The real field isdefault_delete_branch_after_merge, which reads true. An absent key and a false value rendering identically is this repo's own reflex-table row, committed while dispatching work about it.Superseded hypothesis text, kept so the retraction is checkable
POST /pulls/{n}/mergeacceptsdelete_branch_after_merge. Every merge performed today sent{"Do":"rebase"}with that key absent, and three of the branches merged today survive. It may be that an API merge omitting the field getsfalseregardless of the repo default, leavingdefault_delete_branch_after_mergeto drive only the web UI checkbox.Test this before believing it. The discriminating run is one API merge with the field explicitly
trueagainst one without, on the same repo.AC
falsewhen omitted, independently ofdefault_delete_branch_after_merge(which readstrue) — stated ininternal/forgejo/interface.gosince#633. alcatraz-infra#718 documents the hand-merge requirement in/srv/CLAUDE.md§186, withrelease-prep/rollingexcluded because the release bot reuses it.670ea703andbd1a9ec7deleted (SHAs recorded before removal, recoverable untilgc);release-prep/rollingKEPT with the reason recorded. Re-measured after: 1 survivor, the excluded one. The 24 came from the last 50 closed PRs and was stated here as a floor — windowing is the whole difference.comm -12of live branches against merged head refs, in/srv/CLAUDE.md§186, carrying the walk-to-exhaustion note in the snippet because a windowed walk under-reports. Expected output isrelease-prep/rollingand nothing else.Anchor
Found by @engineer while confirming rt#1143 on main by blob identity. The stranding it enabled is rt#1131 → rt#1134, 2026-09-04. Every merge in that batch was performed by @bosun via the API without the field.
📌 The count is GROWING, not a backlog — and it incremented during the measurement.
@engineer read 23; I read 24 a minute later. The delta is this defect firing mid-count: #1139 merged at 19:11:40 and its branch
i/1106-scrub-hook-git-credentialsurvived, becoming the 24th.So the tracker should not read as "clean up 24 stale branches". Every merge performed through the API adds one, and each one is a live ref that a follow-up push can land on silently.
⚠️ The discriminating test is a merge, so it belongs to whoever merges next. One API merge passing
delete_branch_after_merge: trueexplicitly, against the merges already performed without it, settles whether the repo default reaches the API path.✅ HYPOTHESIS CONFIRMED — measured, with the two arms differing in exactly one key.
Same repo, same merge style, same session, same token.
default_delete_branch_after_merge=truethroughout.So the repo default does not reach the API merge path. A caller that omits the field gets
falseregardless of the setting — the setting appears to drive only the web UI's checkbox.What that changes
The tracker's first AC is discharged: the cause is established rather than inferred.
Every API merge must pass
delete_branch_after_merge: trueexplicitly, or every merge leaves a live ref that a follow-up push can land on silently — which is exactly how11a471b9stranded this morning withrc=0and anls-remotethat agreed with local HEAD.Remaining
⚠️ The count stops growing only once every merge path passes the field. It incremented twice during the diagnosis alone.
Hypothesis by @engineer; discriminating merge run by @bosun on rt#1140.
AC1 DISCHARGED — the cause is now established by a controlled measurement rather than inferred from a census.
Two PRs merged tonight by @bosun through the API, minutes apart, with the repo setting ON:
🔑 This is stronger evidence than the original 24-survivor census, and the difference is causal versus correlational. The census showed merged PRs with surviving branches but could not say how each was merged. Here the merging call is known exactly — same actor, same endpoint, same payload, no delete field — and both branches survived. The repo default does NOT reach the API merge path.
⚠️ What this does NOT establish, and it is AC2's other half: that passing
delete_branch_after_mergeexplicitly does work. That needs a merge with the field set, and there is no open mergeable PR to test it on right now. Un-ticked deliberately — a one-sided measurement.✅ Adopted for the rest of the campaign: @bosun's merges will pass the field explicitly rather than relying on the repo default. That makes every subsequent campaign merge a live arm for AC2 — if branches start disappearing, the field is the cause; if they do not, the defect is deeper than the caller.
📌 Why the silent-success half matters here specifically: this campaign merges many PRs into a rebase/ff-only repo, and a surviving merged branch is exactly the surface where a follow-up push lands on a dead ref and reports success.
/srv/CLAUDE.mdalready carries that as "the ref can be FINE and the push still void" — #1144 is the substrate condition that makes it reachable.Closing — 4/4, implemented as
alcatraz-infra#718(@herald), merged. Verified against alcatraz-infra's mergedmain, not against the PR.🔴 AC2 is ticked on its SECOND clause and says so, because the FIRST is measured false and can never be satisfied. "Merges through the API delete the branch when the repo default says so" is not achievable: the API field defaults to
falsewhen omitted, independently ofdefault_delete_branch_after_merge(which readstrue) — documented ininternal/forgejo/interface.gosince#633. Ticking it without naming which half would assert something we know is wrong. @herald's annotation is the right shape.🔴 And this tracker's central claim was RETRACTED before the fix was written. It said "delete the branch and that same push fails loudly." @herald measured the inverse:
Deletion removes the loud signal rather than restoring it. The fix ships with that stated outright, so we do not publish documentation that is confidently backwards — which is the failure mode a docs change makes worst, because the next reader gets no red.
📌 The count was 3, not 24. The 24 came from the last 50 closed PRs and was stated here as a floor; the whole-history walk gives 579 merged PRs, 504 distinct head refs, 7 live branches, 3 survivors. A window labelled "floor" still guessed — and guessed HIGH while wearing the word that promises low.
The cross-repo bound, which @herald sharpened
@bosun's AC sweep over 578 merged release-toolkit PRs could not have found this tracker: the implementation landed in another repo. @herald's refinement is the operative half — "a cross-repo tracker looks IDENTICAL to an abandoned one." No query in either repo joins them.
✅ His fix is a convention, not a query: name the implementing PR in the AC annotation, which is what he did. Greppable from the tracker side; still invisible from the PR side. If the sweep is kept as a pre-cut check, that is the bound to state.