Home / How-to / Make a release
How-to, for maintainers
Make a release
A release is a directory that is never changed after it is on dev, the default branch. The tree is always exactly the release the version file names, or the check fails.
The rules, while the files are frozen
Until phase 4 of the brief is finished (the refactoring with every test passing, and the adversarial exercise), the served files change only as a new release, each change with a test that fails without it and a reason in the pull request. A fix to a finding is a deliberate change to the test that pinned it. A new reader feature is built here, as a new versioned file, never in the newsroom's tree.
1. Change, and test
Edit under the served paths (assets/, for/, wire/, passkey/); if the file is one of the meter's, it is a new component version at a new path, never an edit of v1.4.0. Run the tests in the three engines against the newsroom checkout (running the tests).
2. Version and cut
tools/version.json holds two numbers and a date, all from the file, never the clock:
version, the repository's version. Every change that lands ondevis a new one, in small increments: a patch for a small change (v0.2.1, v0.2.2, ...), a minor for a new capability. The deploy tags it, and refuses a version that is already a tag or not above the latest. CI on a branch fails when the version is not abovedev's.release, the latest cut of the reader code:releases/<release>/, what the consuming sites pin. It changes only when a served file changes; then it is set to the new version and cut.
$EDITOR tools/version.json # {"version": "v0.2.3", "release": "v0.2.3", "date": "2026-10-20"} for a change to a served file;
# {"version": "v0.2.3", "release": "v0.2.0", ...} for a change to the site, the tests or the tools
python3 tools/release.py cut # (served files changed) releases/v0.2.3/<every served file> and manifest.json
python3 tools/release.py check # every release matches its manifest; the tree is the release; the two numbers obey their rule
python3 site/build.py # the site: the version in the bar, the release on /releases/
python3 site/check.py # the site's gate: references, canonical, chrome, sitemap, llms, no key shapes
Add the version's line to site/data/versions.json (newest first); the build refuses a version without one, and /versions/ shows them all. A new page needs its link-preview card: python3 site/og.py draws one into site/og/ for every page that has none (commit it; the gate fails a page without one).
cut refuses to write over an existing release directory, refuses a release that is not the repository's version, and refuses a file that differs from the newsroom's bytes unless CHANGED_FROM_NEWSROOM records why. Write a release's note at site/releases/v0.2.3.md.
3. The pull request
CI runs the syntax check, the manifest check, the build twice, the site check and the tests in three engines. A pull request needs one person and one agent reviewer; the agent's review is a required check. The description says what changed, which test fails without it, and why now.
4. Merge, deploy, tag, check
A push to dev (the default branch, and the release branch) runs deploy-pages.yml: the checks again, a tag of the version, the Pages deploy of www/ and releases/, and then tools/live_check.py, which asks the live host for every file of the release and compares the hashes, retrying while Pages propagates. The release is live when that job is green, not before.
5. The consumers
Tell the newsroom (and sgit.ai, once it pins): taking a release. The newsroom's release note names the API release it moved to.
What never happens
A file under releases/ is never edited. A tag is never moved. A release is never cut from a tree that differs from it. A served file is never loaded unversioned.