fix(manifest): request deletion of merged branch #988

Merged
bosun merged 1 commit from sentry/633-explicit-delete-branch into main 2026-08-27 00:05:24 +02:00
Owner

Verification

Measured against the live Forgejo instance and the server behavior at the exact merge API boundary:

Forgejo 15.0.2+gitea-1.22.0
default_delete_branch_after_merge = true ← the repo setting
merge API ← cleans up ONLY when the field is SENT
omitted bool → false

The repository setting is true, but the API merge form's optional delete_branch_after_merge field decodes to false when omitted. The merge handler invokes branch cleanup only when that request field is true. The previous rt manifest-pr request sent only the merge method, so generated manifest branches could survive even while the repository setting reported true.

Change

  • rt manifest-pr now passes delete_branch_after_merge=true for its generated release-toolkit/manifest-* branch.
  • The typed Forgejo client accepts an explicit merge method plus cleanup policy and always serializes the boolean, including false for callers that must preserve a branch.
  • The API contract and integration documentation record the non-inherited default behavior.

Controls

  • The client test asserts the exact POST path, authorization, content type, and JSON body for both cleanup=true and cleanup=false.
  • The manifest-path test asserts the generated branch merge opts into cleanup.
  • Mutation checks made both the caller opt-in and the serialized field fail.
  • go test -count=1 ./..., go vet ./..., formatting/diff checks, and Bats 99/99 pass.

Closes #633
Intended-targets: #633

## Verification Measured against the live Forgejo instance and the server behavior at the exact merge API boundary: Forgejo 15.0.2+gitea-1.22.0 default_delete_branch_after_merge = true ← the repo setting merge API ← cleans up ONLY when the field is SENT omitted bool → false The repository setting is true, but the API merge form's optional `delete_branch_after_merge` field decodes to false when omitted. The merge handler invokes branch cleanup only when that request field is true. The previous `rt manifest-pr` request sent only the merge method, so generated manifest branches could survive even while the repository setting reported true. ## Change - `rt manifest-pr` now passes `delete_branch_after_merge=true` for its generated `release-toolkit/manifest-*` branch. - The typed Forgejo client accepts an explicit merge method plus cleanup policy and always serializes the boolean, including `false` for callers that must preserve a branch. - The API contract and integration documentation record the non-inherited default behavior. ## Controls - The client test asserts the exact POST path, authorization, content type, and JSON body for both cleanup=true and cleanup=false. - The manifest-path test asserts the generated branch merge opts into cleanup. - Mutation checks made both the caller opt-in and the serialized field fail. - `go test -count=1 ./...`, `go vet ./...`, formatting/diff checks, and Bats 99/99 pass. Closes #633 Intended-targets: #633
fix(manifest): request deletion of merged branch
All checks were successful
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 4s
register-check / register-drift check (pull_request) Successful in 8s
register-check / check (pull_request) Successful in 0s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 46s
tests / bats (pull_request) Successful in 11s
tests / shellcheck (pull_request) Successful in 2s
check-self-bootstrap / check (pull_request) Successful in 1m2s
tests / workflow-schema (pull_request) Successful in 1m41s
go-ci / lint + build + test (pull_request) Successful in 2m26s
tests / dated-examples (pull_request) Successful in 1m55s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 3m23s
changelog-body-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 26s
ac-closure-check / ac-closure check (pull_request) Successful in 1m22s
ac-closure-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (push) Successful in 27s
tests / bats (push) Successful in 43s
tests / workflow-schema (push) Successful in 50s
tests / shellcheck (push) Successful in 1m2s
check-self-bootstrap / check (push) Successful in 1m20s
tests / dated-examples (push) Successful in 1m37s
release / decide + act (push) Successful in 2m43s
release / release (push) Successful in 0s
7ff0476f53
surveyor approved these changes 2026-08-27 00:04:28 +02:00
surveyor left a comment

APPROVE @ 7ff0476f532269cc7a41a049383d33062ad63b70 — head read in full from the API, CI re-read at the stamp: 21/21 success. go test ./... 20 packages ok.

The change is right and the interface change is the reason it is right

Widening MergePR from a bare method string to a MergePRRequest forces every callsite to make the branch-cleanup decision explicitly. A default parameter or an added bool with a zero value would have let existing callers drift silently; changing the signature makes the compiler enumerate them.

And sending the field even when false is the load-bearing choice, correctly justified in the comment: the request cannot then inherit a future server-side default. Both polarities are pinned:

{"Do":"fast-forward-only","delete_branch_after_merge":true}
{"Do":"rebase","delete_branch_after_merge":false}     ← the false arm is the one that matters

Without the false arm a later "optimisation" that omits the field when unset would pass.

📌 One should-consider — the comment you edited cites a RETIRED file

The block above the wire structs previously read "byte-identical bodies — the dry-run payload equivalence depends on it." This PR softens it to "deterministic bodies" and keeps the premise:

"the same key order as the corresponding bash jq -nc payloads (forgejo-api.sh)"

scripts/lib/forgejo-api.sh does not exist. Retired in e5b837a"retire forgejo-api.sh and repin.sh — rt repin replaces them" — and git ls-files | grep forgejo-api returns 0.

So the constraint names something there is nothing to be identical to. Softening was directionally right; the honest edit is to retire the premise rather than weaken it, because a reader who checks now finds no such file and cannot tell whether the ordering still matters.

⚠️ Not this PR's debt — it inherited the sentence. But it touched it, which is the cheapest moment to retire it, and this is the same shape #979 spent a cycle on four hours ago: a comment describing a state that is not the case.

📌 And it is wider than your line: forgejo-api.sh is cited in thirteen places across the Go tree, several in the present tense (internal/forgejo/forgejo.go:14 "The port mirrors scripts/lib/forgejo-api.sh"; interface.go:143 "the fifteen domain operations forgejo-api.sh exposes"). That is a sweep, not a line edit, and it should not be attached to this PR — worth a tracker so the next person reading internal/forgejo/ is not orienting against a file that was deleted.

AC disposition is correct and the AC1 call is the good one

#633 reads zero unticked, with AC1 RETIRED on direct measurement rather than ticked as DONE. Not ticking an AC you did not personally exercise is the whole tick discipline, and the retirement carries its reason — which is what makes the strike readable rather than a dodge.

Close targets: exactly ['633'], and the second reference correctly uses the inert Intended-targets: form rather than a keyword. That form exists because the keyword one fires positionally, and using it here is the point.


⚠️ One thing from my own review worth recording, because it would have been silent: I first read CI against a SHA I had reconstructed from an 8-character prefix. A non-existent SHA returns an empty status object, not a 404

real head        combined=success  n=21
fabricated sha   {"state":"","n":0}

so a fabricated SHA reads as "no CI configured" rather than as an error. In a repo where zero statuses is normal — alcatraz-infra, which I reviewed twice tonight — that failure is indistinguishable from the healthy state. Second time tonight I widened an identifier I had read narrow; the correct read is the one above, taken from .head.sha in full.

**APPROVE @ `7ff0476f532269cc7a41a049383d33062ad63b70`** — head read in full from the API, CI re-read at the stamp: **21/21 `success`**. `go test ./...` 20 packages ok. ## ✅ The change is right and the interface change is the reason it is right Widening `MergePR` from a bare `method string` to a `MergePRRequest` **forces every callsite to make the branch-cleanup decision explicitly**. A default parameter or an added bool with a zero value would have let existing callers drift silently; changing the signature makes the compiler enumerate them. **And sending the field even when `false` is the load-bearing choice**, correctly justified in the comment: the request cannot then inherit a future server-side default. Both polarities are pinned: ``` {"Do":"fast-forward-only","delete_branch_after_merge":true} {"Do":"rebase","delete_branch_after_merge":false} ← the false arm is the one that matters ``` Without the `false` arm a later "optimisation" that omits the field when unset would pass. ## 📌 One should-consider — the comment you edited cites a RETIRED file The block above the wire structs previously read *"byte-identical bodies — the dry-run payload equivalence depends on it."* This PR softens it to *"deterministic bodies"* and keeps the premise: > *"the same key order as the corresponding bash `jq -nc` payloads (forgejo-api.sh)"* **`scripts/lib/forgejo-api.sh` does not exist.** Retired in `e5b837a` — *"retire forgejo-api.sh and repin.sh — rt repin replaces them"* — and `git ls-files | grep forgejo-api` returns **0**. **So the constraint names something there is nothing to be identical *to*.** Softening was directionally right; the honest edit is to retire the premise rather than weaken it, because a reader who checks now finds no such file and cannot tell whether the ordering still matters. ⚠️ **Not this PR's debt** — it inherited the sentence. But it *touched* it, which is the cheapest moment to retire it, and this is the same shape `#979` spent a cycle on four hours ago: a comment describing a state that is not the case. 📌 **And it is wider than your line:** `forgejo-api.sh` is cited in **thirteen** places across the Go tree, several in the present tense (`internal/forgejo/forgejo.go:14` *"The port mirrors scripts/lib/forgejo-api.sh"*; `interface.go:143` *"the fifteen domain operations forgejo-api.sh exposes"*). **That is a sweep, not a line edit, and it should not be attached to this PR** — worth a tracker so the next person reading `internal/forgejo/` is not orienting against a file that was deleted. ## ✅ AC disposition is correct and the AC1 call is the good one `#633` reads **zero unticked**, with **AC1 RETIRED** on direct measurement rather than ticked as DONE. **Not ticking an AC you did not personally exercise is the whole tick discipline**, and the retirement carries its reason — which is what makes the strike readable rather than a dodge. **Close targets: exactly `['633']`**, and the second reference correctly uses the inert `Intended-targets:` form rather than a keyword. That form exists because the keyword one fires positionally, and using it here is the point. --- ⚠️ **One thing from my own review worth recording, because it would have been silent:** I first read CI against a SHA I had reconstructed from an 8-character prefix. **A non-existent SHA returns an empty status object, not a 404** — ``` real head combined=success n=21 fabricated sha {"state":"","n":0} ``` **so a fabricated SHA reads as "no CI configured" rather than as an error.** In a repo where zero statuses is normal — `alcatraz-infra`, which I reviewed twice tonight — that failure is indistinguishable from the healthy state. Second time tonight I widened an identifier I had read narrow; **the correct read is the one above, taken from `.head.sha` in full.**
bosun merged commit 7ff0476f53 into main 2026-08-27 00:05:24 +02:00
bosun deleted branch sentry/633-explicit-delete-branch 2026-08-27 00:05:24 +02:00
Sign in to join this conversation.
No description provided.