Перейти к содержанию

Releasing

The steps a release takes, in order. Written after v0.0.5, where the version bump was missed — every commit for the rest of that day built claiming to be the released version, and that version rides in inter-BBS packets and feeds the League Coordinator's minimum-version gate.

Before tagging

  1. Curate the ChangeLog. The in-progress block accumulates in commit order, which is neither the reader's order nor a finished list. Three passes, in order:

  2. Reorder so the most important, most user-facing changes come first and sysop, command-line and packaging entries come last.

  3. Fold entries that describe one change, and delete any entry that fixes something added in the same cycle — nobody upgrading ever saw the bug.
  4. Shorten to the one-line rule: no entry wraps, no second person, no explanation of how a feature plays, no clause saying what BRE does. If a change will not fit on one line, ask before writing a longer one.

Expect this to be substantial: the v0.0.6 pass cut 71 wrapped entries to 69 single lines.

  1. Regenerate the translations. They are deliberately NOT regenerated on ordinary commits, so a release is where they catch up:
python3 scripts/gen-ui-pot.py && scripts/merge-ui-po.sh
scripts/gen-help-translations.sh

Check what the UI extractor is not seeing. gen-ui-pot.py scans for a fixed list of call forms, so a print helper added since the last release drops out of the catalogs silently — its strings translate at runtime and no translator has ever been shown them. At v0.0.6 that was okNoPause, askYesNoHere and promptSuggestedTight, 28 strings between them. To find the next one, list the helpers that pass a literal through i18n.T and compare them against CALL_PATTERNS.

Never clear a #, fuzzy flag without reading the translation. msgmerge fills a new entry from whichever old one has the most similar English, so a fuzzy msgstr is a guess about spelling, not about meaning. de.po currently renders Specialization as "Regionen" and Relations as "Regionen" — both inherited from Regions on string similarity alone, and ru.po has the same pair. Nobody sees them: internal/i18n skips fuzzy entries by design, so the reader gets English until a human validates one. Un-fuzzying in bulk is what would ship them.

  1. Stamp the ChangeLog. Replace the (in-progress:) heading with YYYY-MM-DD (vX.Y.Z), matching the existing entries.

  2. Update the status line in AGENTS.md — which version is released and which is in development.

  3. Verify. All of these, not a subset:

gofmt -l .            # silence
go vet ./...
go test ./...
GOARCH=386 go test ./internal/game/    # money math on the 32-bit door builds
go test ./internal/play/ -race
  1. Check the Windows 7 toolchain pin. scripts/fetch-win7-go.sh pins a go-legacy-win7 release, which builds the windows/386 asset because a stock-Go binary cannot start on Windows 7 at all (#181). Compare the pin against that project's releases: if it has followed upstream to a newer patch level, bump VERSION and SHA256 together, taking the hash from the release page rather than from the download. Left alone the release still builds, on the older Go — which is the failure worth catching here, since nothing else reports it.

  2. Commit as release: vX.Y.Z.

Tagging

Push trunk, then draft the release on GitHub and let publishing create the tag. Do not tag or push tags from the command line.

The Release workflow builds on publish and attaches the assets — Linux, macOS and Windows archives, the vendored source tarball, and SHA256SUMS. Watch it: a release with no assets is a release nobody can use.

After publishing

  1. Bump Version in internal/game/game.go to the next patch. This is the step that gets forgotten, because the release feels finished once the assets are up.

It is not cosmetic. The constant is the in-development version, so leaving it at the released number means every later build identifies itself as that release — in -version, in the About screen, in the door log, and in the version stamped on every inter-BBS packet, where a Coordinator's minimum version is tested against it.

  1. Decide whether game.Protocol needs bumping, and settle it now rather than when the next format change is half-written.

Bump it only when the packet format actually moved. It is deliberately not the release version: a release that changes menus or balance leaves it alone, so boards on either side of that release go on exchanging packets. Bumping it every release turns it back into the version and forces the whole league to upgrade in lockstep for changes that never touched the wire.

TestPacketWireShapeIsFrozen is what makes this a decision instead of an oversight — it fails the build when a field is added, renamed or reordered, and its comment says which of the three answers applies.

A protocol bump is not the tool for a balance change. Retuning a formula leaves the wire byte-identical, so the protocol number stays put and boards go on exchanging packets while computing different outcomes. That is worse than a format break, not better: an interplanetary attack is resolved on the TARGET board, so the same attack gives different results depending on which end resolves it, and nothing announces it. MinBoardVersion is what covers this — it gates on the game version, which does move for a formula change. Set it when a release changes gameplay a league would notice.

If it did move, the release notes must say the upgrade is coordinated. A league does not roll a protocol change through board by board: it closes the game, drains every board's outbound queue, and switches together (decided 2026-08-31, #229). A held packet comes back only when the READER moves to the number it already carries, so a staggered upgrade strands the board that moves first. Saying this in the notes is the whole mechanism; nothing in the code enforces it.

  1. Delete any renamed or removed asset left behind on the snapshot prerelease by hand. replacesArtifacts only replaces an asset of the same name, so a rename leaves the old file sitting beside the new one.

  2. Bump the Homebrew formula in HomebrewFormula/immortal-barons.rb — the url, the sha256, and the doc list:

gh release download vX.Y.Z -p '*-vendored-source.tar.gz'
sha256sum immortal-barons-vX.Y.Z-vendored-source.tar.gz
tar tzf immortal-barons-vX.Y.Z-vendored-source.tar.gz | grep 'docs/.*\.md$'

The formula installs from that tarball, so every path in doc.install has to exist inside it — the third command is the check. The vendored tarball carries the whole docs/ tree, which is a wider set than the platform archives get from scripts/build-archives.sh.

A doc added on trunk does not go in the formula. It is not in any released tarball yet, so the Homebrew CI job fails the install with Errno::ENOENT — which is how this step came to be written. The list changes only when the pin moves. scripts/build-archives.sh is the opposite: it runs from the tree at release time, so a new doc goes in it straight away.

Missed at v0.0.5, which left the formula installing v0.0.4 until it was noticed.