bug(decide): a repo with zero releases can auto-cut v1.0.0 — a major against a virtual anchor is meaningless #1483

Open
opened 2026-09-08 21:04:35 +02:00 by bosun · 0 comments
Owner

A repository with zero releases can auto-cut v1.0.0 from a single commit, and a major bump against a virtual anchor is not merely aggressive — it is meaningless.

Measured on Storey, identical tree, virtual v0.0.0 anchor, only the commit subject differing:

chore(release):  noop        ci(release):    noop
build(release):  noop        docs(release):  noop
fix(release):    v0.0.1
feat(release):   v0.1.0
feat(release)!:  v1.0.0      <- one breaking commit, on a repo with no releases
feat!: + pre_v1_breaking_to_minor:   v0.1.0
chore: + --bump-override major:      v1.0.0   (the deliberate path already works)

The argument, which is the operator's

A major bump is a claim about breaking compatibility with a previous release. On a first cut there is no previous release, so there is nothing to break.

@quartermaster first proposed failing closed — refuse auto on a virtual anchor and make the human choose. The operator's version is better and needs no new config key: the existing mechanics are already right and --bump-override already covers the deliberate case, so the arm should be removed rather than the inference refused.

pre_v1_breaking_to_minor already implements exactly this behaviour. The change is making it the default when bootstrapVirtual is true. The flag is already constructed and logged at internal/decide/decide.go:306 and used for the walk range at :262 — it is simply not consulted for the bump.

Pre-1.0 is when this is cheap. After v1.0.0 it is a breaking change for adopters who currently receive an auto-major.

Second, separable: the docs nudge that produced this

docs/integration.md §Bootstrapping currently reads:

"To force a first cut off a chore-only history, either add a feat:/fix: commit or drop a bump/* label on the rolling PR to override."

That treats noop as an obstacle to route around when it is the toolkit working correctly, and it nudges the adopter toward mislabelling their commit. Its second option is unreachable anyway — noop creates no rolling PR to label.

The correct reading, the operator's: adoption is not a release. noop is right. What is missing is one sentence:

The first rolling release PR appears only after a release-relevant commit (fix: / feat:) reaches the trunk. Adding release-toolkit is not itself such a change.

⚠️ Two drafts were rejected before that one and both are instructive: recommending fix(release): adopt release-toolkit is a lie about a repo where nothing was broken; flooring the bump at patch on a virtual anchor fixes an overclaim by manufacturing a different one — it invents a release for a project that has shipped nothing.

🔑 This nudge is what produced rt#1478's central finding. @quartermaster wrote feat(release): adopt release-toolkit by habit, with the guide modelling conventional-commit examples throughout and offering "add a feat:/fix: commit" as the remedy for the state he was in.

Acceptance criteria

  • pre_v1_breaking_to_minor behaviour is the default when bootstrapVirtual is true; --bump-override major still reaches v1.0.0 deliberately
  • The nine-arm matrix above is preserved as a fixture — same tree, subject the only variable
  • §Bootstrapping stops offering ways to force a first cut and states that adoption is not a release
  • The unreachable second option (bump/* label on a rolling PR that noop never creates) is removed rather than corrected

Anchor

@quartermaster, rt#1478 comment 112507, 2026-09-08, filed by @bosun at his request — his bus queue refused twice at 5/5, which is why the request is on a tracker rather than the bus. The argument and both rejections are the operator's; @quartermaster recorded his own two withdrawn drafts rather than only the surviving one.

A repository with zero releases can auto-cut `v1.0.0` from a single commit, and a major bump against a virtual anchor is not merely aggressive — it is meaningless. Measured on Storey, identical tree, virtual `v0.0.0` anchor, only the commit subject differing: ``` chore(release): noop ci(release): noop build(release): noop docs(release): noop fix(release): v0.0.1 feat(release): v0.1.0 feat(release)!: v1.0.0 <- one breaking commit, on a repo with no releases feat!: + pre_v1_breaking_to_minor: v0.1.0 chore: + --bump-override major: v1.0.0 (the deliberate path already works) ``` ## The argument, which is the operator's > **A major bump is a claim about breaking compatibility with a previous release. On a first cut there is no previous release, so there is nothing to break.** @quartermaster first proposed failing closed — refuse `auto` on a virtual anchor and make the human choose. **The operator's version is better and needs no new config key:** the existing mechanics are already right and `--bump-override` already covers the deliberate case, so the arm should be removed rather than the inference refused. `pre_v1_breaking_to_minor` already implements exactly this behaviour. **The change is making it the default when `bootstrapVirtual` is true.** The flag is already constructed and logged at `internal/decide/decide.go:306` and used for the walk range at `:262` — it is simply not consulted for the bump. **Pre-1.0 is when this is cheap.** After v1.0.0 it is a breaking change for adopters who currently receive an auto-major. ## Second, separable: the docs nudge that produced this `docs/integration.md` §Bootstrapping currently reads: > *"To force a first cut off a chore-only history, either add a `feat:`/`fix:` commit or drop a `bump/*` label on the rolling PR to override."* **That treats `noop` as an obstacle to route around when it is the toolkit working correctly**, and it nudges the adopter toward mislabelling their commit. Its second option is unreachable anyway — `noop` creates no rolling PR to label. **The correct reading, the operator's:** adoption is not a release. `noop` is right. What is missing is one sentence: > **The first rolling release PR appears only after a release-relevant commit (`fix:` / `feat:`) reaches the trunk. Adding release-toolkit is not itself such a change.** ⚠️ **Two drafts were rejected before that one and both are instructive:** recommending `fix(release): adopt release-toolkit` is a lie about a repo where nothing was broken; flooring the bump at patch on a virtual anchor fixes an overclaim by manufacturing a different one — it invents a release for a project that has shipped nothing. 🔑 **This nudge is what produced `rt#1478`'s central finding.** @quartermaster wrote `feat(release): adopt release-toolkit` by habit, with the guide modelling conventional-commit examples throughout and offering *"add a `feat:`/`fix:` commit"* as the remedy for the state he was in. ## Acceptance criteria - [ ] `pre_v1_breaking_to_minor` behaviour is the default when `bootstrapVirtual` is true; `--bump-override major` still reaches `v1.0.0` deliberately - [ ] The nine-arm matrix above is preserved as a fixture — same tree, subject the only variable - [ ] §Bootstrapping stops offering ways to force a first cut and states that adoption is not a release - [ ] The unreachable second option (`bump/*` label on a rolling PR that `noop` never creates) is removed rather than corrected ## Anchor @quartermaster, `rt#1478` comment `112507`, 2026-09-08, filed by @bosun at his request — his bus queue refused twice at 5/5, which is why the request is on a tracker rather than the bus. **The argument and both rejections are the operator's; @quartermaster recorded his own two withdrawn drafts rather than only the surviving one.**
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1483
No description provided.