Release and changelog — how the cut reaches main
Floors, mirrors, refused cuts and the changelog pipeline, for the change that ships or reviews one (merge and gate).
Release and changelog
- Floor pins — a syntax floor (
bundle.SyntaxFloors:parser.ProfileSince,ImportSince,ContractSince,bundle.ToolAliasesSince) is pinned by the lot that ships the syntax to"next"while its release is uncut. It stays strictly above every numeric release, including when main advances while a branch waits. A numeric pin at exactly the next minor above the newest release still works; therelease-floorsreport warns C168, pointing to"next". The cut writes the real number: release-it'sbefore:git:beforeReleasehook runsgo run ./cmd/release-floors --apply(internal/floorsalign) in the one slot wherepackage.jsonis bumped andCHANGELOG.mdis rendered while nothing is staged, so every pin the cut claims — strictly above the previous release, measured there and not against the version being cut, since a major bump leaves the pin below it — is rewritten to the version being released inside thechore: releasecommit itself. A pin already at a released version is left alone. Everyrequires: {iterion: ">= next"}in the shipped bot and example manifests joins the same staged rewrite; numeric manifest floors keep their own contract. Two symbolic pins in one cut receive the same version. Every other shape aborts the release before that commit: a numeric pin that is not the next minor above the previous release (an over-pin, or one a release overtook), a pin naming a release never cut, an unorderable pin, a floor with no entry inreleaseNotesMarkers, and — the #1154 shape — a floor whose word the notes just rendered do not carry, which is what afeatmerged under a non-conventional subject leaves behind. The rule isbundle.HoldSyntaxFloor, the same predicateTestSyntaxFloorsNameReleasesThatExistruns on every tree — the cut runs the test's predicate plus the resolution its rewrite needs (the pin's identifier declared in exactly one non-test file underpkg/, bound to a plain release literal or"next"), so it can refuse what the test lets pass, never the reverse. Either refusal is loud, anddevbox run -- task release:floorspreviews the whole verdict on any checkout and writes nothing (--applybelongs to the hook and refuses to run unlesspackage.json's version differs from the one HEAD carries). The hatch is the marker table's exemption (an empty marker with its reason beside it,parser.ProfileSince[2]the precedent): an exempted floor realigns through notes that say nothing about it. - After a realigned cut, the mirrors. The cut moves the CONSTANT, and whatever spells the number out has to follow. Two guards hold that, and both are required checks: the floor tests derive their expectations from the constants rather than repeating them — a probe that needs a release below a floor derives it from that floor (
internal/floortest.Below), never from a lower pin, which names the floor itself the day two floors are cut in one release;floortest.EachPinOrderreplays such a case with two pins on one release — keyed onparser.MaxProfile, the pin the cut moves, and run once where the live pins already carry that state — andfloortest's own sweep (TestNoTestSpellsABelowFloorAsAnotherPin) fails any_test.go, the files routed through the helper included, that builds a floor from one pin while naming another, or that keys a collapse case on a profile pin read by a literal index. The hatch is afloortest:exemptcomment NAMING the pin it covers, with the reason: it reaches that pin and no other, so one case cannot vouch for a sibling pin spliced in beside it. The sweep rejects literal symbolic test floors too ("next",">= next"), unlessfloortest:exempt nextexplains a test of the symbolic contract itself.Below(t, "next")derives a numeric probe below the uncut floor. Also,TestEveryShippedBundleDeclaresAFloorThatReachesWhatItNeeds(pkg/bundle) walks the 41 shippedbots/*/andexamples/*/manifests and refuses one whoserequires.iterionno longer reaches what its own sources need — whereiterion validateonly warns (C252 is a warning, exit 0) while the push admission refuses the same bundle with a 409. What neither guard covers is a manifest left one release too HIGH after a downward realignment: that over-declares (the bundle refuses runners that could read it), and no arithmetic can tell an intended floor from a stale one. The cut names it instead of deciding: after realigning a pin DOWN (3.180.0 → 3.179.1), its report warns on every shippedbots/*/andexamples/*/manifest whoserequires.iterionstill equals the pin's old value — path and value, right under the realignment line — and exits 0; a manifest already lowered, or carrying a deliberate floor, warns nothing, and whether to lower a warned one stays the operator's call. - A refused cut leaves the bump behind. The hook fails before release-it arms its rollback, so
package.json,CHANGELOG.md,charts/iterion/Chart.yamlandinternal/mdcode/testdata/committed-reading.txtstay modified and a second local run reports "Working dir must be clean". Restore them (git checkout -- package.json CHANGELOG.md charts/iterion/Chart.yaml internal/mdcode/testdata/committed-reading.txt) before retrying. CI is unaffected:version.ymlruns on a fresh checkout and nothing was committed or pushed. - The cut pins its own corpus edits. internal/mdcode's committed-reading golden pins every tracked
.md,.yamland.ymlgit does not marklinguist-generated, and the cut editsCHANGELOG.mdand the chart, so the same slot then runsgo run ./cmd/corpus-reading --cut(internal/corpusreading): it re-pins exactly the files the cut edited, in the release commit. Any other drift — a pinned file main changed without its golden line, even one the cut also edits — refuses the cut and names the paths. Land that golden through a reviewed change (task corpus:reading -- --write, then review the moved lines) and cut again. A change that edits a pinned file carries its golden update in its own diff the same way. - tests.yml — on push/PR: gofmt, go vet, unit tests, e2e tests
- release.yml — on git tags (v*): multi-platform builds (linux/darwin/windows × amd64/arm64), GitHub release
- version.yml — conventional changelog via release-it, version from
package.json. A merged PR releases only while no merge group is in flight — the release commit is a direct push tomain, which rebuilds every group the queue is building — so the merge that drains the queue releases the whole burst; the nightly and aworkflow_dispatchrelease regardless (releases and the queue). release-it writes the new section into CHANGELOG.md as part of the release commit itself (infile+git add . --update), so the file cannot drift from the tags — never hand-edit it. The same hook slot regenerates internal/mdcode's committed-corpus golden FIRST, before the aligner moves any pin, because the cut rewrites two files that golden pins (CHANGELOG.md here,charts/iterion/Chart.yamlinafter:bump) — the regeneration runs with-vso its stderr announcement reaches the release log (cmd/go swallows a passing package's output without it), and agit diff --statline right after logs the golden's move: without it every release would reopen the corpus red. It holds the current major only; earlier ones are archived under docs/changelog/ because GitHub stops rendering markdown past 512 KB. Each entry carries a collapsedwhyexcerpt taken from the commit body — the rendering lives in scripts/changelog-writer.mjs, shared by release-it (.release-it.mjs) and the regenerator (task changelog:gen), so a rebuilt section is byte-identical to a released one. Re-runtask changelog:genafter a major bump, or when it warns the file is nearing the ceiling.
