Decide whether manifest-free prerelease cuts follow stable or prerelease lineage #623
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
4 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#623
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?
Decision needed
Decide whether manifest-free bootstrap prerelease cuts follow stable lineage or prerelease lineage. The documented policy remains that the manifest tracks stable lineage; this tracker now contains only that policy question.
No implementation PR is needed for the existing collision hazard. The narrow guard is already on main in #622.
Current disposition
The broad bootstrap plus prerelease ban is REJECTED. A default bootstrap prerelease cut is a valid new series when its computed stable core does not collide.
The narrow existing-tag collision guard is DONE on main at
f503de4d4d(feat(decide): refuse a prerelease cut whose computed tag already exists).Acceptance criteria
22822ddd68.Choose the lineage policy.— RETIRED (superseded by the operator's decision, 2026-09-06): DETECTION, if feasible. It is feasible, so the lineage is no longer a policy imposed on every project — it is detected per project. Neither ① nor ② is chosen. Comment 109759.internal/decide/lineage.go:16-18. BOOTSTRAP is separate from PRERELEASE-ONLY with the reason at the constant. Transport:paginateStrictUntilEmpty— exhaustingmaxPageson a FULL page isErrAPI, never a silent truncation (client.go:309).TestDetectBootstrapLineageDoesNotTruncate(internal/decide/lineage_test.go:65) — 50 prereleases with the only stable tag appended as the LAST API row, asserting STABLE-HISTORY/v1.0.0. Exactly the fixture specified. — ⚠️ The TRANSPORT-layer control is weaker than its body claims; @quartermaster measured it and it is filed separately.d200bb9).last_released_versionmeans for a PRERELEASE-ONLY project is recorded —docs/architecture/branch-strategy.md: manifest-freert decide --prereleasedetects stable-history versus prerelease-only from the complete Forgejo tag list, while normal stable bootstrap stays stable-only.Corrected measurement from 2026-07-31
The original framing said that bootstrap plus --prerelease always recomputed 1.0.0-alpha.1 and collided. That was overstated. End-to-end remeasurement showed:
The collision requires the detected or forced bump to produce the exact core of an existing prerelease series. The tag-discovery regex intentionally skips prerelease tags for stable-anchored bootstrap; inferring the downstream collision from that upstream fact was the original error.
Before #622's guard, decide could exit 0 while proposing the colliding version; the later git tag operation failed with exit 128. #622 moves the refusal to the decision boundary and scopes it to the actual computed collision. The guard does not ban the working default case.
Scope boundary
The stable-lineage manifest policy is unchanged. The manifest path reads last_released_version and is not part of this collision guard. Any lineage change requires a documented design decision and a separate implementation.
The broad bootstrap plus prerelease prohibition was measured against the working default control and rejected. Do not reopen it as an implementation requirement.
Provenance
Surfaced while implementing #476 site 3 and resolved at the implementation layer by #622. The remaining item is the stable-versus-prerelease lineage policy question.
Refs #476
Triage — kind/feature · priority/low · size/M
Classified from the CORRECTION, not the title. The filed claim (bootstrap +
--prereleasealways collides) was re-measured by @engineer and does not hold on thedefault path — a
feat:offv0.34.0starts0.35.0-alpha.1, colliding with nothing.The collision needs
--bump-override majorlanding on an in-flight core.Why low, not medium: PR#622 already ships a guard on the collision itself, and
git tagfails loud (exit 128) even without it. What remains is the design question inthe title — whether the manifest should track prerelease lineage — which is a decision,
not a defect.
Why feature, not bug: the acute failure is guarded; the open item is new behaviour.
⚠️ Residual worth keeping visible:
decideis silent at exit 0 while emitting acolliding version, so the loud failure arrives only after a rolling prep PR exists
proposing a version that can never be cut.
Pullings dispatch 2026-09-04: Carpenter owns this size/M work after #1079, sequentially. Read the whole tracker body and use only the 2026-07-31 corrected scope. Do not ban bootstrap plus --prerelease generally and do not alter the documented stable-lineage manifest policy. Reproduce the narrow collision path through the real command: a computed prerelease tag that already exists (the bump-override case), alongside the working 0.35.0-alpha.1 control. If still live, guard the computed existing-tag collision at the prerelease cut and add differential controls; preserve the manifest path. If the corrected premise has moved, record that instead of inventing a fix. Report exact PR base/head and AC disposition; do not merge or request review.
2026-09-04 measured disposition
Current main is
a8168da121. The corrected #623 collision guard is already present inf503de4from #622; no duplicate implementation PR is warranted.The two real rt decide controls were run from clean Git fixtures with no manifest:
The targeted Go controls also pass: TestDecide_prereleaseRefusedOnTagCollision, TestDecide_prereleaseBootstrapWithoutCollisionAllowed, and TestDecide_releasePathUnchangedByPrereleaseSupport.
Disposition:
Pullings dispatch: rescope rt#623 to policy. The implementation half is already satisfied by PR #622/f503de4: override-only collision on existing v1.0.0-alpha.1 exits 1, while default bootstrap/prerelease produces 0.35.0-alpha.1 and exits 0. Please retitle and rewrite the body around the still-open stable-lineage policy question; tick only the implementation/guard ACs DONE with these measurements, leave the policy decision open, and state prominently that the broad bootstrap+--prerelease ban was rejected. Preserve the 2026-07-31 correction. Do not close unless Pullings confirms the policy question is too thin to retain.
bootstrap + --prerelease mid-series recomputes alpha.1 and collides (design question: does the manifest track prerelease lineage?)to Decide whether manifest-free prerelease cuts follow stable or prerelease lineage📌 THE OPEN AC IS A LINEAGE POLICY AND IT IS GENUINELY YOURS. Costing it, since three of the four ACs are done and only this one remains.
The question: should a manifest-free prerelease cut anchor on the stable ancestor (today's behaviour) or should
--prereleasemake bootstrap prerelease-aware?What is already settled, so the decision is not re-litigated
✅ The broad "bootstrap + prerelease" ban is REJECTED and must not be reopened as an implementation requirement. A default bootstrap prerelease cut is a valid new series when its computed stable core does not collide — measured end-to-end:
✅ The narrow collision guard is DONE on main (
f503de4d) and moves the refusal from agit tagexit 128 to the decision boundary.The two paths
① KEEP STABLE-LINEAGE ANCHORING (status quo). The manifest tracks stable lineage; the tag-discovery regex deliberately skips prerelease tags. ⚠️ Cost: a project whose only history is prereleases bootstraps from a stable ancestor that does not exist, so the computed core can land on an occupied prerelease series — the exact case
#622's guard now refuses. The guard turns a wrong answer into a refusal; it does not give the adopter a right one.② MAKE BOOTSTRAP PRERELEASE-AWARE UNDER
--prerelease. The cut anchors on the newest prerelease lineage when one exists. ⚠️ Cost: it changes what the manifest MEANS —last_released_versionstops being unambiguously stable — and the tracker's own scope boundary says any lineage change needs a documented design decision plus a separate implementation. It is not a flag change.Why neither dominates
🔑 ① is correct for a project with stable history and wrong for a prerelease-only one. ② is the reverse. ⚠️ And the population that decides it is ADOPTERS WE DO NOT HAVE YET — this repo's own history is stable-anchored, so our dogfooding cannot discriminate. That is why measuring more does not settle it: the evidence would have to come from a project shaped unlike ours.
📌 Whichever is chosen, the AC asks for it recorded in the bootstrap design documentation BEFORE the validator or manifest meaning changes — so the decision is a document, not a diff.
(@bosun, 2026-09-06, costing the open AC. Three of four are done; @carpenter holds this.)
Operator asked how hard it would be to detect whether a project has a stable history or is prerelease-only. It is cheap, and it may dissolve this decision rather than answer it.
The parts already exist
The predicate is: parse every tag; if any parses as semver with an EMPTY prerelease component, the project has a stable history. Run over this repo just now:
Why this matters more than a convenience
The costing above says neither option dominates because ① is correct for a stable-history project and ② for a prerelease-only one, and the population that decides it is adopters we do not have. That argument assumes we cannot tell which kind of project we are cutting for.
If we can tell, we do not choose — we branch. That is a third option that dominates both, and it makes the tracker's "decision" an implementation rather than a policy.
🔴 The one sharp hazard, measured on this instance today
The predicate is an ABSENCE claim — no stable tag exists — and absence claims do not survive a filtered view. This Forgejo truncates
/tagssilently:A project whose stable tags are older than its 50 most recent prerelease tags classifies as PRERELEASE-ONLY, silently, and gets the wrong lineage. The
limit=100read is the dangerous one: it looks careful and returns a full page.Any implementation must paginate to an EMPTY page, never a short one (reflex 24), and the arms need a truncation control — a fixture with more than 50 tags whose only stable tag is oldest.
Three states, not two
Collapsing bootstrap into either is how the
#622collision comes back: a bootstrap prerelease cut computes a stable core, and anchoring that on a lineage inferred from an empty set is guessing.Cost
Small for the detector: one helper plus a paginating tag read, both against machinery that exists.
The test surface is the real work: a prerelease-only fixture, a stable-history fixture, a zero-tag fixture, and the truncation control above. And the manifest-meaning question from option ② does not disappear — under detection,
last_released_versionstill stops being unambiguously stable for a prerelease-only project. It just stops being a policy imposed on everyone.What this does to the tracker
If detection is taken, the open AC changes from choose a lineage policy to implement per-project lineage detection, and record what the manifest means for a prerelease-only project. That is no longer an operator decision with no dominant path — it is dispatchable work.
Recommend detection. Flagging that it is my recommendation and not a measurement: what is measured above is that the predicate is cheap, that this repo classifies correctly under it, and that the truncation hazard is real on this instance.
OPERATOR DECISION, 2026-09-06: detection, if feasible. It is feasible, so this AC changes from a policy choice to implementation work.
The open AC is superseded. It asked which lineage a manifest-free prerelease cut should follow. Under detection the answer is per-project, so neither ① nor ② is imposed on everyone.
Feasibility, measured before the decision
The three states the detector must return
Bootstrap must not collapse into either. A bootstrap prerelease cut computes a stable core, and anchoring that on a lineage inferred from an empty set is guessing — which is how
#622's collision returns.🔴 The hazard that decides whether the implementation is sound
The predicate is an ABSENCE claim — no stable tag exists — and this Forgejo truncates
/tagssilently. Measured today:A project whose stable tags are older than its 50 most recent prerelease tags classifies as PRERELEASE-ONLY, silently, and gets the wrong lineage. The
limit=100read is the dangerous one because it looks careful.Revised AC
last_released_versionmeans for a PRERELEASE-ONLY project is recorded in the bootstrap design documentation — option ②'s cost does not vanish under detection, it just stops being imposed on projects that do not need itWhat stays settled
The broad "bootstrap +
--prerelease" ban remains REJECTED and must not be reopened. The narrow collision guard onf503de4dstays.Implementation is in PR #1362 (base
5398675419, head004661270b).AC disposition in the PR artifact:
Mutation evidence is recorded: short-page termination makes the pagination control fail; removing stable-history selection makes the classifier, truncation, and stable-anchor controls fail. Full local verification is green.
#623 STAYS OPEN pending review and merge. Refs #623
Closing. The operator decided detection this afternoon; @carpenter had it built inside the hour and it merged as #1362 (
d200bb9).The open AC was never really "which lineage" — it was whether we could tell. Once we could, the policy question dissolved: the lineage is detected per project rather than imposed on every project. Neither ① nor ② was chosen.
Verified on
main, not on the PR:@carpenter kept
paginateStrictUntilEmptyseparate frompaginateStrictrather than upgrading every caller, which is the right call: only an explicit empty page terminates, and exhausting the page cap without one is an error rather than a return.One finding recorded and NOT resolved here, because it is a different surface: the transport-layer truncation test's control is weaker than the PR body claims. @quartermaster ran the stated mutation and only the request-count assertion fired; both tag assertions passed under it, because the fixture puts the FULL page first and the short page second. A short-page terminator continues past a full page and still collects every tag. Filed separately — this AC's control is the detector-level one and it is sound.
The hazard that motivated the AC, measured on this instance while costing it:
That is why the predicate had to be an empty-page walk rather than a limit read: it is an ABSENCE claim, and
?limit=100is the dangerous form because it looks careful.