bug(prep): a stable rt prep refuses every tag that is not vX.Y.Z, so an adopter with any other tag_format cannot cut #1532

Open
opened 2026-10-03 14:51:03 +02:00 by claude · 1 comment
Member

A stable cut stops in rt prep's doc-version-refs step whenever tag_format renders anything other than vX.Y.Z. The step has written nothing since #1411 and #1415. Its only remaining behaviour is this refusal.

Measured

rt was built from origin/main 8086700 (go build ./cmd/rt, reports rt dev). The scratch adopter repo had tag_format: 'release-{version}', one tag release-0.1.0, one added fragment, and origin set to an unreachable URL.

$ rt prep --dry-run
[rt prep] new tag: release-0.2.0 | branch: release-prep/release-0.2.0 | date: 2026-10-03
Error: doc version refs: prep: UpdateDocVersionRefs: malformed tag (stable vX.Y.Z required): release-0.2.0
rc=1

$ rt prep --dry-run --rolling-mode --target-version 0.2.0     # the argv reusable-release.yml:832 builds
Error: doc version refs: prep: UpdateDocVersionRefs: malformed tag (stable vX.Y.Z required): release-0.2.0
rc=1

$ rt prep --dry-run --target-version 0.2.0-rc.1                # prerelease cut: the step is skipped
[rt prep] [DRY-RUN] would: checkout release-prep/release-0.2.0-rc.1 + commit + push   rc=0

control: the same repo with tag_format 'v{version}' and tag v0.1.0
$ rt prep --dry-run                                             rc=0, new tag v0.2.0

The path, read at origin/main:

  • internal/config/config.go:241-246: RenderTag substitutes {version} into tag_format, so the new tag is release-0.2.0.
  • cmd/rt/prep.go:259 renders newTag. :391-395 passes it to prepkg.UpdateDocVersionRefs on every stable cut and returns its error. There is no toolkit-self guard.
  • internal/prep/doc_version_refs.go:10 is semverTagRE = ^v\d+\.\d+\.\d+$, and :40-41 refuses anything else. The rest of the function returns an empty slice on every call (:87).
  • Two comments contradict the code:
    • cmd/rt/prep.go:386-390 says the step is "toolkit-self only" and "silently no-ops on repos without the toolkit's own README/docs/examples". Neither is true of the call at :391.
    • release-toolkit.yml:24-27 says it "runs unconditionally and re-pins README.md + docs/integration.md + examples/". It re-pins nothing.

Where it came from. Before bc30f90 (#754, release-toolkit#720, first released in v0.41.0), the same regex lived in scripts/hooks/update-doc-version-refs.sh. That script ran only as a post_bump_hooks entry in the toolkit's own release-toolkit.yml (git show bc30f90^:release-toolkit.yml, lines 30-31). The Go port made it a built-in step. So since v0.41.0, every adopter's stable cut runs the toolkit's own doc-pin tag check.

A test pins the refusal as if it were the contract. cmd/rt/prep_doc_staging_test.go:51-73 (TestPrep_docStagingStepIsReached, #1438) runs a stable prep with tag_format: release-{version} and requires it to fail inside "doc version refs". Because the function writes nothing, #1438 used the refusal as the only observable proof that site 8d runs.

The documented contract says the opposite.

  • docs/architecture/contracts/config.schema.json:35-39: tag_format is a template whose only placeholder is {version}. Nothing restricts the prefix.
  • docs/integration.md:727: "default; only {version} placeholder supported".
  • docs/adr/0003-multi-stage-operator-gate.md:105: a configurable tag format "gives consumers room to use non-standard tag schemes".

Census, 2026-10-03. Storey, ember, tmux-tell, cellblock and purser all set tag_format: 'v{version}', read from each repo's release-toolkit.yml on main. No adopter on this instance is affected today, which is why this is priority/low. Raise it when an adopter wants another prefix.

Why it matters

  • An adopter that sets any other prefix gets a red release run on every push to main. The run fails at rt prep in the update arm (reusable-release.yml:841), after rt decide has already proposed the version, and no rolling PR is ever opened.
  • The refusal names a doc-pinning rule the adopter has no documentation for, and never mentions tag_format.
  • Lifting this refusal alone does not make such an adopter work. Measured on the same scratch repo: once a chore(release): prepare release-0.2.0 commit lands, rt decide returns mode=blocked (pending cut) instead of mode=cut, because its prepare-subject regex only admits v?X.Y.Z. That is the sibling issue filed alongside this one (rt decide and non-v tags). Fixing this one alone moves the failure from prep to decide.

Decision tree

  • (a) Delete site 8d and UpdateDocVersionRefs.
    • Right when: no doc setter is coming back. #1411 and #1415 removed both of its subjects, and rt readme-pin-check --fix now sets the pins.
    • Why now: #1425 kept the function because a removal could not be verified. #1438's two arms now make it verifiable.
    • Cost: arm ① must be inverted, not deleted (#1382 precedent). It should then pin that a stable prep with tag_format: release-{version} gets past this point.
  • (b) Keep it, but run it only for toolkit-self, which is what prep.go:386-390 already claims.
    • How: gate it on .forgejo/workflows/reusable-release.yml existing, the same condition the bake step uses.
    • Right when: a doc setter is expected back.
    • Cost: arm ① needs a toolkit-self fixture to stay a probe.
  • (c) Refuse a tag_format other than v{version} at config load.
    • How: the refusal names the limitation, and config.schema.json, docs/integration.md and ADR-0003 are narrowed to match.
    • Right when: the decide-side sibling is not going to be done. A late red run becomes an early, explicit refusal, and the docs stop promising what the pipeline cannot do.

(a) or (b) belongs with the decide-side sibling. (c) is the honest stopgap if that sibling is deferred.

Acceptance criteria

  • A decision among (a), (b) and (c) is recorded on this issue.
  • Under (a) or (b):
    • a stable rt prep --dry-run with tag_format: 'release-{version}' gets past the doc-refs step;
    • a test shows it, red on origin/main 8086700 and green after (mutation-verified, AGENTS.md section 8 Axis B);
    • TestPrep_docStagingStepIsReached is inverted or re-anchored, not deleted.
  • Under (c): rt refuses the config before any compose work, the message names tag_format, and config.schema.json, docs/integration.md and ADR-0003 state what is supported.
  • The comments at cmd/rt/prep.go:386-390 and release-toolkit.yml:24-27 describe what the code does.
  • A changelog fragment.

Out of scope

  • rt decide's handling of non-v tags. That is the sibling issue.
  • The v* describes elsewhere, which the sibling issue lists:
    • rt prep's own bump and commit walk (internal/prep/git.go:70,80, internal/prep/gitsource.go:71);
    • rt manifest-check (cmd/rt/manifest_check.go:185-188).
  • ADR-0003:105 also calls tag_format a regex and names the retired release-prep.sh and draft-release.sh. That wording is stale whichever option is chosen.

Anchor

Found while reviewing PR #1531 / issue #1529 on 2026-10-03. #1531's body lists this under "Not in this PR (each needs its own issue)" as pre-existing. None of the 22 open issues covers it. Related: #1425, #1438, #1411, #1415, #754.

A stable cut stops in `rt prep`'s doc-version-refs step whenever `tag_format` renders anything other than `vX.Y.Z`. The step has written nothing since #1411 and #1415. Its only remaining behaviour is this refusal. ## Measured `rt` was built from origin/main `8086700` (`go build ./cmd/rt`, reports `rt dev`). The scratch adopter repo had `tag_format: 'release-{version}'`, one tag `release-0.1.0`, one `added` fragment, and origin set to an unreachable URL. ``` $ rt prep --dry-run [rt prep] new tag: release-0.2.0 | branch: release-prep/release-0.2.0 | date: 2026-10-03 Error: doc version refs: prep: UpdateDocVersionRefs: malformed tag (stable vX.Y.Z required): release-0.2.0 rc=1 $ rt prep --dry-run --rolling-mode --target-version 0.2.0 # the argv reusable-release.yml:832 builds Error: doc version refs: prep: UpdateDocVersionRefs: malformed tag (stable vX.Y.Z required): release-0.2.0 rc=1 $ rt prep --dry-run --target-version 0.2.0-rc.1 # prerelease cut: the step is skipped [rt prep] [DRY-RUN] would: checkout release-prep/release-0.2.0-rc.1 + commit + push rc=0 control: the same repo with tag_format 'v{version}' and tag v0.1.0 $ rt prep --dry-run rc=0, new tag v0.2.0 ``` The path, read at origin/main: - `internal/config/config.go:241-246`: `RenderTag` substitutes `{version}` into `tag_format`, so the new tag is `release-0.2.0`. - `cmd/rt/prep.go:259` renders `newTag`. `:391-395` passes it to `prepkg.UpdateDocVersionRefs` on every stable cut and returns its error. There is no toolkit-self guard. - `internal/prep/doc_version_refs.go:10` is `semverTagRE = ^v\d+\.\d+\.\d+$`, and `:40-41` refuses anything else. The rest of the function returns an empty slice on every call (`:87`). - Two comments contradict the code: - `cmd/rt/prep.go:386-390` says the step is "toolkit-self only" and "silently no-ops on repos without the toolkit's own README/docs/examples". Neither is true of the call at `:391`. - `release-toolkit.yml:24-27` says it "runs unconditionally and re-pins README.md + docs/integration.md + examples/". It re-pins nothing. **Where it came from.** Before `bc30f90` (#754, release-toolkit#720, first released in v0.41.0), the same regex lived in `scripts/hooks/update-doc-version-refs.sh`. That script ran only as a `post_bump_hooks` entry in the toolkit's own `release-toolkit.yml` (`git show bc30f90^:release-toolkit.yml`, lines 30-31). The Go port made it a built-in step. So since v0.41.0, every adopter's stable cut runs the toolkit's own doc-pin tag check. **A test pins the refusal as if it were the contract.** `cmd/rt/prep_doc_staging_test.go:51-73` (`TestPrep_docStagingStepIsReached`, #1438) runs a stable prep with `tag_format: release-{version}` and requires it to fail inside "doc version refs". Because the function writes nothing, #1438 used the refusal as the only observable proof that site 8d runs. **The documented contract says the opposite.** - `docs/architecture/contracts/config.schema.json:35-39`: `tag_format` is a template whose only placeholder is `{version}`. Nothing restricts the prefix. - `docs/integration.md:727`: "default; only {version} placeholder supported". - `docs/adr/0003-multi-stage-operator-gate.md:105`: a configurable tag format "gives consumers room to use non-standard tag schemes". **Census, 2026-10-03.** Storey, ember, tmux-tell, cellblock and purser all set `tag_format: 'v{version}'`, read from each repo's `release-toolkit.yml` on main. No adopter on this instance is affected today, which is why this is priority/low. Raise it when an adopter wants another prefix. ## Why it matters - An adopter that sets any other prefix gets a red release run on every push to main. The run fails at `rt prep` in the update arm (`reusable-release.yml:841`), after `rt decide` has already proposed the version, and no rolling PR is ever opened. - The refusal names a doc-pinning rule the adopter has no documentation for, and never mentions `tag_format`. - **Lifting this refusal alone does not make such an adopter work.** Measured on the same scratch repo: once a `chore(release): prepare release-0.2.0` commit lands, `rt decide` returns `mode=blocked` (pending cut) instead of `mode=cut`, because its prepare-subject regex only admits `v?X.Y.Z`. That is the sibling issue filed alongside this one (`rt decide` and non-`v` tags). Fixing this one alone moves the failure from prep to decide. ## Decision tree - **(a) Delete site 8d and `UpdateDocVersionRefs`.** - Right when: no doc setter is coming back. #1411 and #1415 removed both of its subjects, and `rt readme-pin-check --fix` now sets the pins. - Why now: #1425 kept the function because a removal could not be verified. #1438's two arms now make it verifiable. - Cost: arm ① must be inverted, not deleted (#1382 precedent). It should then pin that a stable prep with `tag_format: release-{version}` gets past this point. - **(b) Keep it, but run it only for toolkit-self**, which is what `prep.go:386-390` already claims. - How: gate it on `.forgejo/workflows/reusable-release.yml` existing, the same condition the bake step uses. - Right when: a doc setter is expected back. - Cost: arm ① needs a toolkit-self fixture to stay a probe. - **(c) Refuse a `tag_format` other than `v{version}` at config load.** - How: the refusal names the limitation, and `config.schema.json`, `docs/integration.md` and ADR-0003 are narrowed to match. - Right when: the decide-side sibling is not going to be done. A late red run becomes an early, explicit refusal, and the docs stop promising what the pipeline cannot do. (a) or (b) belongs with the decide-side sibling. (c) is the honest stopgap if that sibling is deferred. ## Acceptance criteria - [ ] A decision among (a), (b) and (c) is recorded on this issue. - [ ] Under (a) or (b): - a stable `rt prep --dry-run` with `tag_format: 'release-{version}'` gets past the doc-refs step; - a test shows it, red on origin/main `8086700` and green after (mutation-verified, AGENTS.md section 8 Axis B); - `TestPrep_docStagingStepIsReached` is inverted or re-anchored, not deleted. - [ ] Under (c): `rt` refuses the config before any compose work, the message names `tag_format`, and `config.schema.json`, `docs/integration.md` and ADR-0003 state what is supported. - [ ] The comments at `cmd/rt/prep.go:386-390` and `release-toolkit.yml:24-27` describe what the code does. - [ ] A changelog fragment. ## Out of scope - `rt decide`'s handling of non-`v` tags. That is the sibling issue. - The `v*` describes elsewhere, which the sibling issue lists: - `rt prep`'s own bump and commit walk (`internal/prep/git.go:70,80`, `internal/prep/gitsource.go:71`); - `rt manifest-check` (`cmd/rt/manifest_check.go:185-188`). - ADR-0003:105 also calls `tag_format` a regex and names the retired `release-prep.sh` and `draft-release.sh`. That wording is stale whichever option is chosen. ## Anchor Found while reviewing PR #1531 / issue #1529 on 2026-10-03. #1531's body lists this under "Not in this PR (each needs its own issue)" as pre-existing. None of the 22 open issues covers it. Related: #1425, #1438, #1411, #1415, #754.
Author
Member

The sibling issue this body refers to is #1533 (rt decide reads tags and prepare commits as v-prefixed). Both were found while reviewing PR #1531.

The sibling issue this body refers to is #1533 (`rt decide` reads tags and prepare commits as `v`-prefixed). Both were found while reviewing PR #1531.
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#1532
No description provided.