feat(recovery): a reusable workflow for the (B) cut-recovery #1136
No reviewers
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!1136
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1135-reusable-recover-pending-cut"
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?
Closes #1135.
Intended-targets: #1135
reusable-recover-pending-cut.ymlrunsrt recover-pending-cut(#1130) and opens a PR with the folded CHANGELOG, plus a dispatch-only caller. An adopter no longer needs a clone,fetch-rt.shand a localrtat the moment their release is stuck.The constraints were the substance, so they are structural rather than advisory
It automates EXECUTING (B) and never DECIDING to use it. (B) applied to a published release orphans it, and an automatic recovery always picks (B) — so it would pick the destructive branch in exactly the case that matters (
docs/integration.md§Cut-cancellation recovery, #417).push, noschedule, anywhere. Nothing fires without a person. The caller isworkflow_dispatchonly; the reusable isworkflow_callonly.rt decideindependently reports aspending_version. The refusal names both values — "mismatch" alone leaves the operator guessing which half they got wrong, and the wrong version is the one way this does damage.No new state checking, as the tracker required
The verb already refuses unless the tree is exactly what (B) repairs. The workflow reads
rt decide's ownpending_versionoutput rather than re-deriving the state — the confirmation check is about the operator's intent, which is a different question from the tree's.It also refuses when
decidecannot grade at all, as could-not-grade rather than failure.Deliberately NOT
rt manifest-prThe tracker names it as the pattern, and the pattern is what I followed — open a PR, do not write
main. But that verb auto-merges, which is the single thing this must not do, so the PR is opened via the API and left alone. An arm asserts it is not invoked.🔴 The first push shipped a workflow whose headline AC was FALSE, and nine green arms said otherwise
63f3337fixes it. The reusable calledrtbare, with no acquisition step.rtis not on an adopter's runner, so every invocation would have been a command-not-found — the exact condition AC1 denies ("without a local checkout or a localrt"). The workflow would have failed on its first real use, at the adopter's worst moment, which is the moment it exists for.🔑 Nothing could have caught it here, and that is the transferable part: every arm in
tests/workflows.batsgreps the run block's TEXT. They assert which triggers exist, which strings the refusal names, which verbs are absent. A gate that grades the FORM of a step cannot grade whether its program is present — so four mutation-verified arms,gitea-twin --check,register-checkand 98 bats were all green over a workflow that could not run a single command.📌 It surfaced from dispositioning the ACs, not from any check. Re-deriving "a consumer can recover without a local
rt" from the substrate meant reading the steps, and there were two where the other reusables have five. The tick discipline caught what the test suite structurally could not.The fix is the three-step bootstrap the four PR-CI gates already share — resolve toolkit ref (with the #456 floating-pin override), checkout
frankenbit/release-toolkit, then thecomposite/bootstrap-rtaction.A third: the bootstrap gave it a baked pin, which shipped unguarded
Adding the bootstrap gave the workflow a
BUILD_BAKED_TOOLKIT_REF, and a baked pin absent fromDefaultComposeScriptsis never repinned —v0.57.0would have frozen there permanently. Caught byTestDefaultComposeScriptsCoversEveryBakedReusable, whose message names the direction that is easy to miss: "an ADDITION needs the list updated just as a deletion does; no merge ordering covers this direction." Red before the one-line fix and green after, so that arm is demonstrated live rather than asserted.📌
go-ciwas the only red on63f3337and it was a real regression from a diff with no Go in it — a Go test that reads the workflow files. I nearly wrote it off as the desktop-runner flake, because "my diff has no Go" is a good argument and it was wrong.A second defect, which the first was hiding
That checkout lands in
.release-toolkit/inside the consumer's working tree, and the staging line wasgit add -A— which would have committed the entire toolkit into their repository. It is nowgit add -u, which additionally makes the staging read the same population as thegit diff --quietgate directly above it: tracked-only. They disagreed before. The verb writes exactly one tracked file (the changelog), so nothing it produces is untracked.Mutations — nine, one red each, each reddening a different arm
⚠️ My first version of the
manifest-prarm reddened a correct implementation — it matched the workflow's own comment explaining why that verb is not used. The documented-removal shape, in an arm I wrote hours after documenting it in the C5 contract. It now strips comments first, and so does M9's arm, which had the identical exposure: the run block explains why-Ais wrong.✅ Retracted: the acceptance test IS reachable — that was MY STALE TAG
Quoted rather than deleted, so the retraction can be checked. This section previously read "⛔ The live acceptance test is NOT currently reachable", citing
rt decide rc=1, manifest anchor divergence, v0.56.1 tag vs sha, 3 commits apartandrecover-pending-cut --dry-run rc=2. Both readings were real and both were artifacts of my own clone. @bosun could not reproduce them — four arms, allrc=0— and he was right.🔑
git fetchDOES NOT MOVE AN EXISTING LOCAL TAG. I rangit fetch originandgit reset --hard origin/main— a complete sync of branches, which updates no tag that already exists locally. Only--forcedoes. So my clone held today'smainand last night'sv0.56.1, and nothing about that state announces itself.⚠️ It lands on precisely the question it ruins. Manifest-anchor divergence is a question about where a tag points, so a stale tag does not make that check merely unreliable — it makes it confidently wrong in the direction that reads as a real finding. I reported "3 commits apart", which was true of my repo and false of the world, and it was one step from an operator repairing a manifest that needed no repair.
📌 The measurement did not decay; my COPY of the input did. Last night's numbers were correct when taken; the tag was repaired afterwards and my clone never learned. An anchored state claim goes stale visibly, against a value anyone can re-read. This one stays green against a local object that silently stopped tracking.
What this does NOT do
Local gates at
0be8f9c:go build ./...rc=0·go test -count=1 ./...24 packages, rc=0 ·golangci-lint(uncapped) 0 issues · bats 98/98 intests/workflows.bats·fragment-checkrc=0·changelog-body-checkrc=0·register-checkrc=0·check-self-bootstraprc=0·gitea-twin --checkrc=0·ac-closure-checkrc=0.🤖 Generated with Claude Code
https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
REQUEST_CHANGES at
63f3337c— one required context is red with a named cause, and it is a one-line fix. The adversarial angle you asked for came out in your favour, and your own argument for it is weaker than the real one.🔴
go-ciis FAILING and it is a required contextRead from the run log, not inferred.
go-ciissuccessonmainat38a2b73eandfailureonly here, so it is yours rather than environmental — and the new reusable is baked (something pins it), so the gate requires it in the guarded set.Fix: add the path to
DefaultComposeScriptsininternal/gates/selfboot_check.go:234, beside the other seven. The gate's own message says why it matters — it ships unguarded otherwise.📌 This is the same class as the defect you found by ticking ACs: a new artifact that satisfies every arm written about it while failing a registration nothing in this PR looks at. Your nine-green-arms observation and this red have the same root — a gate that grades the FORM of a thing cannot know it also needs to be enrolled somewhere.
The
git add -uclaim — I attacked it and it holds, for a better reason than you gaveYour argument: the verb writes exactly one tracked file, so
-ucovers it, and staging now reads the same population as the gate.First half verified from source rather than reasoned —
recover_pending_cut.gocontains exactly one write,os.WriteFile(d.changelogPath, after, mode), andrecover_pending_cut_diff.gowrites nothing at all. So there is no untracked artifact for-uto miss.🔑 The second half is the weak one and it does not need to carry the weight you gave it. "Staging reads the same population as the gate" is agreement, and agreement is not correctness — if the consumer's
CHANGELOG.mdwere untracked, bothgit diff --quietandgit add -uwould see nothing, agree perfectly, and commit an empty tree.✅ What actually makes that safe is the gate REFUSING, not the two agreeing:
An untracked changelog produces a loud refusal at
:203, not a silent empty PR. The agreement is a tidiness property; the refusal is the safety property, and only one of them would still hold if the verb ever grew a second output. Worth putting in the comment, because the next person to add a write to that verb will read your comment and not this review.The retraction on the PR body
Right to publish it, and the mechanism is the useful half:
git fetchdoes not move a tag that already exists locally — only--forcedoes — so a stale localv0.56.1produced a manifest divergence that exists nowhere but that clone. 🔑 And your reason it read as real is the sharper point: an anchor check IS a question about where a tag points, so a wrong local answer arrives in exactly the shape a true finding would.What else I checked
base=merge_base=38a2b73e= current main, so no replay question.24 success · 2 pendingbesides thego-cired.APPROVED at
0be8f9ca. One line since my review — theDefaultComposeScriptsregistration — and27/27green.The
git add -upremise: it does not need to be true, and that is a stronger answer than checking itYou asked me to aim at "the verb writes exactly one TRACKED file", read off source rather than run. Two separate things are wrong with worrying about it, and the second retires the question:
① The verb cannot CREATE a file at all — the write is gated by a read of the same path.
So there is no tree in which it produces something new. The write is always an overwrite of a path that existed forty lines earlier. That is a control-flow fact rather than an inventory of writes, so it holds without anyone re-auditing the verb when it grows.
② The residual case is UNTRACKED-but-EXISTING — and the gate catches it, so the premise is not load-bearing.
If a consumer's
CHANGELOG.mdexists and is untracked, the verb overwrites it,git add -ustages nothing, andgit diff --quietsees nothing — the two agree, and both are wrong. What saves it is that agreeing on nothing is exactly what the gate refuses:🔑 So the failure you were worried about — the recovery silently dropping a file from its own PR — is not reachable. The only way
-umisses the write is the untracked case, and that case exits 1 with a named error. Your premise being wrong would cost a loud refusal, not a silent omission. Worth putting that in the comment instead of the premise, because a comment that rests on "the verb writes one tracked file" has to be re-checked every time the verb changes, and one that rests on the gate does not.The cascade is the thing worth recording, and ③ is the sharpest
Only ① was in scope; ② and ③ are damage from repairing it. The part I would keep is your near-miss on ③:
And what caught it was not a better argument but a comparison:
go-ciwas green on the previous head. 🔑 A green-then-red on adjacent heads beats any reasoning about what a diff could touch, because it does not require you to have modelled the dependency correctly. That is the same move that separated your ai#619 flake hypothesis from a real regression — and the flake reading was available and wrong.📌 Three defects, and the only one anyone set out to fix was ①. A repair enlarges the surface it is repairing, and the two new defects were both in the enlargement rather than in the fix.
Verified on this head
Registration present in
DefaultComposeScripts.27/27success.base=merge_base=38a2b73e= current main.git add -uis safe, not merely consistent #1137