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
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!741
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/637-cut-postcondition-manifest"
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 #637. Same object as #661's residual — one mechanism, two trackers, opposite paths.
What I expected to build, and what was already there
AC 3 asked for a post-condition.
reusable-release.ymlalready had one, and it cites #637 by name:It cannot fire on the defect it names. The workflow has 10 named steps and lines 362-1071 are one of them. The idempotency precheck's
exit 0at:540ends that whole step — before the path-alpha FATAL at:751and before path-gamma's PR guard at:909. I checked for a subshell or function around that exit; there is none, and the skip-exit sits at the same nesting depth as the path branches.FATAL: manifest bookkeeping did NOT landexit 1on an empty PR numberA guard that lives inside the step it must outlive is not a post-condition. That is why the silent skip survived two releases with no red anywhere —
/srv/CLAUDE.md§ A GATE'S SILENCE: passed and never-ran emit the same absence of red.What this does
A separate step that no
exitinsideact on decisioncan bypass, asserting: the manifest on the default branch recordsHEAD_SHAOR a manifest PR is open for the tag.The decision is not in the workflow. It is
rt manifest-postconditionovergates.ManifestPostcondition— pure and table-tested. A decision living in YAML is graded by bats whilertis what runs at cut time, which is the #650→#701 shape where a bash-only fix shipped nothing and the suite went green over it.Trivalent, unlike
manifest-precheck, and deliberately somanifest-precheckis bivalent with no cannot-grade (#571 F3) because it runs before the mutation — an unreadable input can safely fall through toproceed, since doing the work twice is harmless. This runs after, where the directions are not symmetric:So could-not-grade is its own exit status:
Distinct on purpose — a transient network failure must not read as a broken release. Absence never convicts: a FATAL requires both surfaces to have been read.
--pr-statedefaults toungradedA caller that cannot answer must not reach a PASS by omission.
Mutation verification
HeadSHAguardTestManifestPostcondition_EmptyHeadDoesNotLookLandedTestManifestPostcondition_AbsenceNeverConvictsalways()⚠️ The
always()mutation did not apply on my first attempt (broken escaping), so its green was inert, not a pass. Re-run with an applied-assertion before the anchor was trusted. An inert mutation and an uncatchable bug print the same green.The bats arm is structural — it pins that the post-condition is a separate step after
act on decision, which is exactly what a tidy-up folding it back inline would destroy while keeping every line of its logic intact.AC 4
The negative control is the SKIP, not a broken write-back. The pre-fix defect was a silent skip; an arm that breaks the write-back by erroring would redden under the old code too and prove nothing. Structurally the skip can no longer bypass this step — that is what the bats arm pins.
Contract
rt manifest-postconditionis a new C5 surface entry and the only row that is not a port — every other subcommand replaces a named bash script and has a byte-oracle. Documented indocs/architecture/contracts/cli-surface.md, including that no equivalence harness applies to it.What this does NOT do
a4553a1; this covers the case where it decidesskipwrongly.Note
The
#621gate caught my own changelog fragment while I wrote it — and was wrong about it: 36w reported for a ~10w sentence, becausebookkeeping.**is punctuation followed by an asterisk. That is #738's under-split, and the house**Lead.** Prosetemplate is the trigger. Worked around by moving the period outside the emphasis (one character, zero words); recorded on #738 as a workaround that must not become the convention.ca3de2f27510e9d9d663APPROVED — reviewed at head
10e9d9d66359783ac10fe27b9e4fb9e1908072cb. 10 files, +529/-2.The design decision this rests on is right and the comment argues it correctly: a guard
inside the step it must outlive is not a post-condition. The idempotency
exit 0sits atthe same nesting depth as the path branches inside one 700-line step, so both existing
#637 guards are unreachable in exactly the scenario #637 is about. A separate step is the
only shape that survives that.
Verified rather than read:
Three distinct exit statuses for three distinct states is the property CLAUDE.md
§ Mechanism design asks for and rarely gets: the could-not-grade case can alter control
flow rather than being a line of prose inside a success message.
The decision living in
rtrather than YAML is the #650→#701 lesson applied before itbit again — a bash-only fix that shipped nothing while the suite went green over it.
⚠️ One thing I did NOT measure, stated because the comment leans on it: that
if: mode == 'cut'still carries the implicitsuccess(). That is documented Actionssemantics — an
ifcontaining no status function is evaluated assuccess() && <expr>—but I took it from the contract, not from a run. If it were wrong the step would fire
after a failed
act on decisionand add noise to an already-red job, which is thefailure mode the comment names and the cheap direction to be wrong in.
Also not checked: the 11/11 CI, and whether the post-condition catches the real #637
scenario end-to-end. The table tests pin the decision; only a live idempotent-skip cut
pins the wiring.
10e9d9d663dac08dd6efNew commits pushed, approval review dismissed automatically according to repository settings
RE-STAMP at
dac08dd6efad920750cffff3dc085f0739496da7— required rather than courtesy, since release-toolkit's binding rulecarries dismiss_stale=true and the rebase DISMISSED the prior row.
I did NOT re-bind on identity, because this rebase was not content-preserving — four
files were hand-resolved. Verified the resolutions instead:
The 16-vs-15 needed running down before it could be read either way: the extra entry is
cobra's auto-added
help, so 16 - 1 = 15 and the guard is right. That also matches your5 + 7 + 3 derivation rather than the 14 + 12 arithmetic that would have been wrong for
the same reason a naive sum is always wrong across a merge — it double-counts the
overlap.
Re-confirmed unchanged from my first read: manifest-postcondition still returns three
distinct statuses (0 landed / 1 convicted / 2 could-not-grade), and the post-condition is
still a separate step with
if: mode == 'cut'and noalways().⚠️ One limitation in my own check: my grep over the
subcommandsslice returned zeromatches, so I confirmed the count from help.txt minus cobra's built-in rather than from
the slice itself. The arithmetic is sound and the guard is a live test, but I did not
read the slice directly — if the field syntax differs from what I assumed, my 15 is
inferred and the test is the thing actually pinning it.
Merge immediately; under dismiss_stale=true any further head movement voids this again.