Releasing¶
How spec versions are tagged and published. This document is the
operational counterpart to GOVERNANCE.md — governance
covers the proposal lifecycle and SemVer policy; this doc covers the
mechanics of cutting a release.
Standard release flow¶
Every spec version follows the same sequence:
- Acceptance PR merges to
main. The PR flips a proposal'sStatus: Draft → Accepted, lands the spec text changes, adds new conformance fixtures, and updatesCHANGELOG.md/README.md/docs/proposals.md. - Tag the merge commit. An annotated tag is created on the acceptance PR's merge commit:
- Publish the GitHub Release. Notes come from the new
CHANGELOG.mdentry; the standard release is marked--latest:
No CI workflow triggers off the tag — releases are manual, by design. The release exists primarily so downstream implementations can pin their submodule reference to a stable spec version.
Versioning¶
Pre-1.0 SemVer at the whole-spec level (not per-capability):
- MINOR (
v0.N.0) — new behavior, new conformance rules, additions any implementation could fail. - PATCH (
v0.N.P) — textual clarification with no behavior change, fixture data fix, or backport that aligns existing impls without introducing new failure modes.
Pre-1.0 MINOR may carry breaking changes per GOVERNANCE.md. PATCH is
reserved for changes implementations can adopt without code work
(though they may need to adjust to a more-explicit reading of an
existing rule — see the v0.16.1 / v0.17.1 / v0.21.1 precedents).
Backporting fixes — cherry-pick ordering¶
The rule: when a published tag carries a typo or fixture-data bug
that needs a PATCH-bump fix, the fix lands on main first (or
simultaneously with any maintenance tag), so subsequent MINOR releases
off main don't bake the typo into further tags.
The pattern that fails this rule:
- A bug is found on a published tag, say
vA.B.0. - A maintenance tag
vA.B.1is cut from a side branch offvA.B.0with the fix isolated. Tag and release published. - The cherry-pick back to
mainis scheduled separately and lags. - Before the cherry-pick lands, a new MINOR
vA.(B+1).0is tagged offmain— inheriting the original typo. - Same again at
vA.(B+2).0,vA.(B+2).1, … - The cherry-pick eventually lands on main, fixing future tags from that point forward — but every interleaved tag between the maintenance bump and the cherry-pick is permanently broken (without a force-tag, see below).
The right sequence:
- Fix on
mainfirst. Apply the fix onmainand tag the resulting commit as the PATCH (e.g.,vA.B.1). Both the main line and the canonical PATCH tag are fixed in a single commit. No maintenance branch needed; no cherry-pick window. - Maintenance side branch SECOND (only if needed). If a separate
side-branch tag is required (e.g., the bug originated at
vA.B.0and downstream impls explicitly pin there), cut the side-branch tag AFTERmaincarries the fix. The side-branch tag points at a commit whose only difference frommainis the lack of not-yet-cherry-picked work, so subsequent main tags continue to carry the fix. - No interleaved MINOR releases between maintenance and cherry-pick. If a maintenance bump and a follow-on MINOR both need to ship in close succession, sequence them: maintenance tag, cherry-pick to main, MINOR tag. Never interleave.
When the rule is violated (e.g., it was violated for the v0.18.1 maintenance tag landing on a side branch before the cherry-pick to main), the recovery is a series of force-tag retags (next section).
Force-tag retags (rare, pre-1.0 only)¶
When interleaved tags carry a typo that should have been backported, the cleanest recovery is to retag each affected version with the fix cherry-picked onto the original tagged commit:
- From the broken tag's SHA, create a side branch:
- Apply the focused fix (one commit, just the fix — no CHANGELOG churn, no version-line bumps). The retag must not change the spec surface beyond what the fix touches.
- Force-update the tag in place, preserving the original tag message:
- Force-push the tag:
- Drop the local side branch (the tag preserves the new commit).
After retagging, any GitHub Release pinned to the tag follows the new SHA automatically; the release notes don't auto-update, but a note added to the release body acknowledging the retag is useful for implementations that previously fetched the broken version.
Force-tagging published releases is a destructive operation:
- Pre-1.0 only. Once the spec hits
v1.0.0, tags are immutable — the cost of breaking implementations that have pinned to a tag and cached its SHA outweighs the benefit of an in-place fix. Use a new PATCH tag instead. - Time-window sensitive. Force-tagging within days of the original tag is low-risk (downstream implementations are still catching up; few have cached the SHA). Force-tagging tags that are weeks or months old is higher-risk — the next PATCH on the line is usually the better path.
- Limit to one fix per retag pass. Bundle multiple fixes into a new tag instead.
- Document in the release. Add a "Retagged on YYYY-MM-DD with <fix>" note to the corresponding GitHub Release body so any implementation that cached the prior SHA can detect the change.
Maintenance tags¶
A maintenance tag (vX.Y.(Z+1) cut from a side branch off vX.Y.Z)
is appropriate when:
- The fix is purely textual or fixture-data.
- An implementation needs to pin to
vX.Yand an in-place force-tag onvX.Y.Zis undesirable (e.g., the tag is older than the force-tag time window above).
For the maintenance tag itself:
- Cut the side branch from the tagged commit.
- Apply the fix +
CHANGELOG.mdentry naming the maintenance bump. - Bump README
Current spec versionon the side branch ONLY if the side branch is being published as the new "canonical" version of thevX.Y.*line (uncommon; usually the main line is canonical). - Tag the side-branch commit, push the tag, publish the GitHub
Release with
--latest=false(so it doesn't supersede the main line's latest tag).
After cutting the maintenance tag, immediately cherry-pick the fix
to main to preserve the cherry-pick ordering rule above. The
cherry-pick is the canonical "the fix is also on main" landing; it
doesn't need its own tag (the fix flows forward into the next MINOR's
content).
Pre-release validators¶
Before tagging a release, the same validators that run in CI MUST pass locally on the merge commit (or HEAD of the maintenance side branch):
python3 scripts/validate_markdown_links.py— every internal link resolves.mkdocs build --strict— every documentation page builds; no warnings or errors. (Pre-flight with--strictonly; do not pair with--quiet—--quietsuppresses warnings that--strictwould otherwise catch, so a build CI rejects can pass locally.)
CI runs these validators on every PR; the local check is a safety net.
Closing out¶
After the tag is pushed and the release is published, the acceptance work is closed:
- Local cleanup: delete the merged accept branch
(
git branch -d accept/NNNN-*),git fetch --prune. (Remote branch auto-deletes on PR merge per the repo's settings; skipgit push origin --delete.) - Task tracker: mark the accept task complete.
- Coord thread (if applicable): notify downstream impl agents that the new spec version is available and the pin can advance.
Quick reference¶
| Operation | Command |
|---|---|
| Tag a merge commit | git tag -a vX.Y.Z <sha> -m "spec vX.Y.Z — <desc> (proposal NNNN)" |
| Push tag | git push origin vX.Y.Z |
| Publish release | gh release create vX.Y.Z --target main --latest --title "vX.Y.Z" --notes "..." |
| Force-retag | git tag -af vX.Y.Z HEAD -m "<original>" && git push origin --force vX.Y.Z |
| Local validators | python3 scripts/validate_markdown_links.py && mkdocs build --strict |