fix(checks): undated-release fires on a final tag, not on a candidate #20

Merged
mikluko merged 3 commits from mikluko/octant/1738-undated-release into main 2026-09-06 19:44:54 +00:00
mikluko commented 2026-09-06 19:43:38 +00:00 (Migrated from github.com)

The defect

undated-entry and undated-release split on whether a tag already names the
newest entry's version, and read the answer as the release shipped. That is
sound for a final version and false for a pre-release: a candidate ships its tag
and stays undated on purpose, which is what a stabilization branch is for.

Reproduced against the built binary, one git tag apart with the document
untouched:

before v1.3.0-rc.1 exists   valid=true
after  v1.3.0-rc.1 exists   error: version 1.3.0-rc.1 carries no date and tag
                            v1.3.0-rc.1 already names it; the release shipped

The fix

undated-release fires where the tag naming the entry is a final. A
candidate falls through to undated-entry — the check a stabilization branch
already switches off — so that branch goes quiet again and the trunk is
unchanged.

The predicate reads the entry, not the tag. Git.tag now matches by
semver.Compare, and §11 counts the pre-release identifiers, so a tag that
matches this entry carries the same ones it does. e.Semver.Prerelease() is that
question already parsed and in hand; re-parsing the tag to ask it would be a
second spelling of an answer we are holding. That equivalence rests on tag()
comparing by precedence, so it is a comment in the diff rather than left to be
re-derived.

Why nothing caught it

It was unreachable until an hour ago. While examples/release-branch/
composed vX.Y.Z-pre.<run> against an entry naming X.Y.Z, no tag a branch cut
ever named its own entry's version. #18 deleted that composition, so the tag
names the entry's version from the first candidate on — and every push after it
became a finding, on the one branch whose purpose is to cut them.

The suite could not reach it either: the five repository-reading checks are
excluded from examples_test.go by design, undated-release among them, which
is why the example asserts the input in TestReleaseBranchKeepsUndatedReleaseOn
rather than watching the check fire.

Tests

Two new TestGitChecks rows and TestUndatedCandidateIsSilentOnAStabilizationBranch,
which is the branch's own configuration — undated-entry off, everything else
default. Both fail without the predicate, checked by reverting it. A third row
pins the converse — a candidate tag does not date the final it was cut for —
which held before and is relied on now.

The split survives in both directions: the existing case holding a final tag
naming an undated entry to undated-release on those same settings is untouched
and still passes.

Also here

The two places where examples/release-branch/ paraphrases this check said the
old semantics. Corrected in place — the example keeps undated-release on, and
now says why that is safe rather than leaving a reader to find out that the
branch's own tags trip it.

Notes

  • [Unreleased] only, so merging cuts no tag. The entry rides in whatever
    promotes it — #16, presumably.
  • #19 (date-mismatch) is the same switch and the same Git.tag. Whichever
    of us lands second rebases; no preference here.

go test ./..., go vet ./..., gofmt -l clean, go run ./cmd/gen-readme
idempotent (it moved the two check descriptions, which is the README table).

## The defect `undated-entry` and `undated-release` split on whether a tag already names the newest entry's version, and read the answer as **the release shipped**. That is sound for a final version and false for a pre-release: a candidate ships its tag and stays undated on purpose, which is what a stabilization branch is for. Reproduced against the built binary, one `git tag` apart with the document untouched: ``` before v1.3.0-rc.1 exists valid=true after v1.3.0-rc.1 exists error: version 1.3.0-rc.1 carries no date and tag v1.3.0-rc.1 already names it; the release shipped ``` ## The fix `undated-release` fires where the tag naming the entry is a **final**. A candidate falls through to `undated-entry` — the check a stabilization branch already switches off — so that branch goes quiet again and the trunk is unchanged. **The predicate reads the entry, not the tag.** `Git.tag` now matches by `semver.Compare`, and §11 counts the pre-release identifiers, so a tag that matches this entry carries the same ones it does. `e.Semver.Prerelease()` is that question already parsed and in hand; re-parsing the tag to ask it would be a second spelling of an answer we are holding. That equivalence rests on `tag()` comparing by precedence, so it is a comment in the diff rather than left to be re-derived. ## Why nothing caught it It was **unreachable until an hour ago**. While `examples/release-branch/` composed `vX.Y.Z-pre.<run>` against an entry naming `X.Y.Z`, no tag a branch cut ever named its own entry's version. #18 deleted that composition, so the tag names the entry's version from the first candidate on — and every push after it became a finding, on the one branch whose purpose is to cut them. The suite could not reach it either: the five repository-reading checks are excluded from `examples_test.go` by design, `undated-release` among them, which is why the example asserts the *input* in `TestReleaseBranchKeepsUndatedReleaseOn` rather than watching the check fire. ## Tests Two new `TestGitChecks` rows and `TestUndatedCandidateIsSilentOnAStabilizationBranch`, which is the branch's own configuration — `undated-entry` off, everything else default. **Both fail without the predicate**, checked by reverting it. A third row pins the converse — a candidate tag does not date the final it was cut for — which held before and is relied on now. The split survives in both directions: the existing case holding a final tag naming an undated entry to `undated-release` on those same settings is untouched and still passes. ## Also here The two places where `examples/release-branch/` paraphrases this check said the old semantics. Corrected in place — the example keeps `undated-release` on, and now says why that is safe rather than leaving a reader to find out that the branch's own tags trip it. ## Notes - `[Unreleased]` only, so merging cuts no tag. The entry rides in whatever promotes it — #16, presumably. - **#19 (`date-mismatch`) is the same switch and the same `Git.tag`.** Whichever of us lands second rebases; no preference here. `go test ./...`, `go vet ./...`, `gofmt -l` clean, `go run ./cmd/gen-readme` idempotent (it moved the two check descriptions, which is the README table).
Sign in to join this conversation.
No description provided.