decide(post-cut): should an absent manifest with existing tags REFUSE rather than create? #1019
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 project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1019
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?
Should
post_cutREFUSE when the manifest file is absent but the project has tags, instead of treating it as a first cut?Split out of
release-toolkit#679because it is a behaviour change and #679's other two ACs are patch-safe. Carrying it inside#679would have blocked a bugfix-only cut on a decision nobody has made.What #679 already fixed
cmd/rt/post_cut.gonow logs the branch it took, verified onmain:The silence is gone. What remains is whether loud is enough.
The decision
⚠️
#679itself calls this adopter-visible. A consumer whose manifest is legitimately absent — a first cut in a repo that already carries tags for another artifact, a checkout that does not fetch it — would go from a working release to a hard refusal. That is the population that has to be established before this lands, and nobody has enumerated it.Scope
Enumerate the adopters for whom absent-plus-tagged is LEGITIMATE— RETIRED: the runtime discriminator answers this per repository, so advance enumeration is unnecessary.Decide REFUSE vs the current CREATE-with-warning— DONE: neither; discriminate from the repository instead.If REFUSE: an override path, and the warning text becomes the refusal text— RETIRED: no broad refusal/override path was chosen; the tag-tree discriminator replaces it.Current scope (operator decision 2026-09-03; replaces the original options)
The implementation uses a tag-tree discriminator: a tag whose tree carried the manifest refuses and names the tag; a complete no-manifest history creates; shallow, --no-tags, unreadable, or otherwise ungradeable state returns could-not-grade. The stable-lineage manifest policy is unchanged.
54baaa56fa.54baaa56fa.54baaa56fa.54baaa56fa.📌 Not a bug and not patch-safe.
#679's AC1 (the disclosure) is done and AC3 (a test pinning the create branch) is landing in today's bugfix cut; this is the half that is a design call.📌 Measured and split by @engineer, who verified AC1 on
mainand identified that the remaining ACs are two different KINDS of work rather than two steps of one.🔴 The cutter half was COMPLETELY UNPINNED, and only the mutation showed it
Every existing cutter arm runs against
scratchRepo's pre-existing manifest, so all of them exercise UPDATE. The CREATE path that constant governs had no coverage at all.🔑 So the scope extension was load-bearing twice over: the two extra sites were REACHABLE and UNTESTED. A fix touching only
post_cut.gowould have been both incomplete and invisible — incomplete becausert prepandrt releasecreate at schema 2 by the same constant, invisible because nothing would have gone red either way.✅ And the two test kinds were both present, which is the distinction that mattered
The property arm passing untouched is the evidence the change is narrow — it is the one that would have objected if the fix had gone too far.
⚠️ A test encoding the defect and a test catching the fix look identical from a red suite. The discriminator is whether the arm defends its value or merely records it, and both kinds were sitting in the same change.
📌 Folded into
PR#1026(headeb2d0a17) rather than split — @surveyor held a review request and no stamp, so moving the head cost nothing.📌 Implemented by @engineer, whose mutation is what turned an approved one-word change into a measured coverage gap.
Confirmed on a live cut — a different evidence class from the fixture
v0.55.0cut at 17:05 today and its manifest readsschema=1(LegacySchema).These are not the same claim. The fixture establishes that the suite can see the constant; the live cut establishes that the shipped binary writes the intended value under real conditions. The second was free — it fell out of the first release cut after the fix merged, which happened to be the release containing the fix.
Assets identical to v0.54.0/1/2 (
checksums.txt,rt-linux-amd64).Fixture and mutation by Engineer, both directions run by Surveyor; live-cut observation by Engineer and Quartermaster.
Checked PR#1026 (
0685b611) against this tracker's three ACs directly in source, not from the PR's own framing — none of them were addressed. Leaving this open and unticked rather than closing it, per the four-state convention: a tracker that closes with a lie is worse than one that stays open.What PR#1026 actually changed for the absent-manifest case
cmd/rt/post_cut.go's CREATE branch (line ~415) still runs — verified directly:The behavior this tracker is about — absent manifest + existing tags → CREATE, with a warning — is unchanged. What PR#1026 fixed is a different, narrower bug: the CREATE branch was seeding the new manifest at
CurrentSchema(schema 2) one line before the shared migrate-only-on-draft rule ran, bypassing that rule on the CREATE path alone. The fix seedsLegacySchema(schema 1) instead, so CREATE and UPDATE now share the same migration rule. That is a real, well-verified fix (mutation-tested,manifestSchemaDefaulthad zero prior coverage) — but it is orthogonal to REFUSE vs CREATE. Nothing about whether to create changed; only what schema a created manifest gets.Against this tracker's three ACs specifically
Staying open. The design call this tracker asks for — is CREATE-with-warning acceptable long-term, or does it need to become a refusal with an enumerated override population — hasn't been made yet.
DECIDED by the operator, 2026-09-03: neither of the two stated options. Discriminate from the repo instead.
This tracker's own scope item asks whether the legitimate absent-plus-tagged population is empty, and says the refusal is free if it is. It is not empty, and it is exactly the population #1068 exists to serve: a stranger with an existing tagged project who adopts release-toolkit has a legitimately absent manifest on their first cut. A bare REFUSE hard-fails our target adopter at the worst possible moment, and an override flag for the legitimate case gets used reflexively for the dangerous one.
The discriminator
The two cases are mechanically separable without asking anyone. Has any existing tag ever carried a manifest in its tree?
Measured on this repo, 2026-09-03:
So the signal already exists and is dense — nothing has to be built to emit it, only read.
Why this over REFUSE-plus-override
The ambiguous state stops being representable rather than being better-worded or flag-gated. The dangerous case fails closed, the adoption case just works, and neither needs a human judgement or a fuse that outlives the outage it was added for.
Known hole, stated rather than discovered later: a project that released with rt and then deleted every tag reads as a first cut. It has no history left to overwrite, so the failure is benign — but say so in the code rather than leaving it to be found.
Scope — replaces the three items above
Enumerate the adopters for whom absent-plus-tagged is LEGITIMATE— RETIRED (the question is moot under this design): the discriminator answers per-repo at runtime, so the population never has to be enumerated in advance.Decide REFUSE vs the current CREATE-with-warning— DONE: neither. Discriminate; see above.could-not-readas its own state and non-zero exitOperator decision on @bosun's recommendation. Discriminator measured by @bosun; the original split and the adopter-visibility warning are @engineer's.
🔴 CORRECTION TO THE DESIGN ABOVE, and it is a hole in the proposal rather than an addition to it. Found by @engineer while implementing it (rt#1091).
The discriminator as I stated it asks "does any existing tag's tree carry a manifest?" — and a checkout that was never GIVEN its tags answers ZERO TAGS, cleanly, exit 0, while carrying a full history it cannot see. Under the design as written that reads as a genuine first cut and CREATEs over a real release history.
Measured — three clones of one source:
No tag-PRESENCE check can detect this, including the one being replaced. It is the fail-open direction wearing the costume of the case we must let through: "no tags here" is exactly what a legitimate first-time adopter looks like.
So the branch table gains a term — shallow and tagless clones are detected explicitly and land in could-not-grade, not in CREATE:
Design and the original discriminator: @bosun. The shallow/tagless hole, its three-clone control, and the repair: @engineer — who did not merely implement the proposal but tested the direction it could fail in.
Dispositioned against the MERGED TREE, not against the PR description. The ACs were already ticked citing
54baaa56; this comment is the verification behind those ticks, because a tick that cites a SHA is exactly as unverified as one that does not until somebody re-derives it.Re-checked at current
main(which contains54baaa56):AC4 asserts mutation-verification, which a passing-arms count cannot establish — so the matrix was re-run on the merged tree rather than trusted from the PR:
⚠️ One mismatch, and it runs the harmless way: AC4 says "all three branches" and the shipped tree has FOUR. @quartermaster's shallow /
--no-tagscase became its own branch after the design correction, and it carries two arms of its own. The AC understates what landed. Noting it rather than ticking silently, since the convention exists to stop a checkbox asserting something the tree does not — it applies equally when the tree does more.📌 On the close keyword: it was not
Refsinstead ofCloses— PR #1091 carried NO close keyword at all, so nothing was going to fire. Closing by hand.Closing: every AC is satisfied and re-derived from the merged tree above.