Skip to content
Like what we’re building? Star on GitHub

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; the release-floors report warns C168, pointing to "next". The cut writes the real number: release-it's before:git:beforeRelease hook runs go run ./cmd/release-floors --apply (internal/floorsalign) in the one slot where package.json is bumped and CHANGELOG.md is 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 the chore: release commit itself. A pin already at a released version is left alone. Every requires: {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 in releaseNotesMarkers, and — the #1154 shape — a floor whose word the notes just rendered do not carry, which is what a feat merged under a non-conventional subject leaves behind. The rule is bundle.HoldSyntaxFloor, the same predicate TestSyntaxFloorsNameReleasesThatExist runs 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 under pkg/, bound to a plain release literal or "next"), so it can refuse what the test lets pass, never the reverse. Either refusal is loud, and devbox run -- task release:floors previews the whole verdict on any checkout and writes nothing (--apply belongs to the hook and refuses to run unless package.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.EachPinOrder replays such a case with two pins on one release — keyed on parser.MaxProfile, the pin the cut moves, and run once where the live pins already carry that state — and floortest'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 a floortest:exempt comment 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"), unless floortest:exempt next explains 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 shipped bots/*/ and examples/*/ manifests and refuses one whose requires.iterion no longer reaches what its own sources need — where iterion validate only 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 shipped bots/*/ and examples/*/ manifest whose requires.iterion still 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.yaml and internal/mdcode/testdata/committed-reading.txt stay 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.yml runs 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, .yaml and .yml git does not mark linguist-generated, and the cut edits CHANGELOG.md and the chart, so the same slot then runs go 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 to main, which rebuilds every group the queue is building — so the merge that drains the queue releases the whole burst; the nightly and a workflow_dispatch release 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.yaml in after:bump) — the regeneration runs with -v so its stderr announcement reaches the release log (cmd/go swallows a passing package's output without it), and a git diff --stat line 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 collapsed why excerpt 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-run task changelog:gen after a major bump, or when it warns the file is nearing the ceiling.