# Release runbook

> How a `spt-pacer-tool` release ships. The spine is fixed (changelog · bump · land green ·
> build · pack · tag · GitHub Release · verify the asset). This project **does not sign** its
> release assets: `[update]` declares no `signing_key`, so first acquisition and every update
> trust `gh`'s authenticated TLS + GitHub, exactly as `spt adapter add --release` does. If that
> ever changes, sign locally — release keys stay out of CI.

<!-- [doc->REQ-PACER-INSTALL] -->

## One-time setup

- `gh` installed and authenticated with push rights on `BigscreenVR/spt-pacer-tool` (the home
  repo is **private**, which is also why `manifest.toml` pins `[update].transport = "gh"` —
  the unauthenticated HTTPS fetch 404s on it).
- Cross-build toolchain for the Linux assets: `cargo install cargo-zigbuild`, `zig` on PATH, and
  `rustup target add x86_64-unknown-linux-gnu x86_64-unknown-linux-musl`.

## Per release

1. **Land green.** Bump `[adapter].version` in `manifest.toml` — **the version of truth** — and
   keep `Cargo.toml`'s `version` equal to it. `cargo test` and `traceable-reqs check` both green.
2. **Write the user-facing changelog.** Add a `## [<version>] — <date>` section at the top of
   `CHANGELOG.md` with **Added / Changed / Fixed** subsections. This section becomes the GitHub
   Release **body** verbatim, so it is the changelog every user reads. Rules:
   - **User-facing UX only.** What a person using pacer notices or does differently. Name the
     actual commands and assets they touch.
   - **No internal lingo** — no requirement ids, module names, commit hashes, milestone or hazard
     codes. A reader who has never seen the source must understand every line.
   - **"You" is the person reading the notes — never the agent** (operator rule, 2026-07-26).
     pacer's behavior is almost all *about agents*: what they write, when their endpoint goes
     quiet, what a nudge does to their turn. Written in the second person ("a wait code written in
     the turn **you** are finishing lands the instant **your** endpoint goes quiet") it reads as
     instructions to the human installing pacer, who does none of those things. Say **the agent**
     / **that agent's** and keep the second person for what the reader actually does — download the
     binary, run the commands, place it on their PATH.
   - **No manual word-wrap** (operator rule, 2026-07-26) — do NOT hardcode newlines to wrap a bullet
     or paragraph across physical lines. GitHub word-wraps the release body for you, and a
     hand-wrapped line becomes a forced break in the rendered notes. Write each bullet / paragraph as
     **one physical line**; only structural newlines belong in a section (blank lines between
     paragraphs and subsections, the `###` headers, the boundary between `-` bullets). The changelog
     section IS the release body, so this applies to `CHANGELOG.md` itself, not just to what is
     pasted into GitHub.
   - **Fixing an already-published body:** correct `CHANGELOG.md`, then re-push each affected
     release from its section — `gh release edit vX.Y.Z --repo BigscreenVR/spt-pacer-tool
     --notes-file <that version's section>`. The published notes are a live surface; a fix that
     lands only in the repo leaves every reader looking at the old text.
   - **Flag breaking changes** prominently under Changed.
3. **Build all three targets FRESH.** A stale binary silently ships old code; the packer refuses
   one older than `src/`, but rebuild rather than rely on that.
   ```sh
   cargo build --release                                            # x86_64-pc-windows-msvc
   cargo zigbuild --release --target x86_64-unknown-linux-gnu
   cargo zigbuild --release --target x86_64-unknown-linux-musl
   ```
4. **Pack**: `sh ci/publish/package-adapter.sh` → `dist/adapter.spt` (multi-platform: the shared
   `manifest.toml` at the root, THREE per-triple binary roots — msvc, gnu, musl) plus
   `SHA256SUMS`. The archive is the whole release; no standalone binaries ship since 0.3.0.
   The script prints the archive listing — read it.
5. **Validate the manifest** against the node-local published schema before it ships:
   ```sh
   python -c "import json,tomllib,os,jsonschema; jsonschema.validate(tomllib.load(open('manifest.toml','rb')), json.load(open(os.path.expanduser('~')+'/AppData/Local/spt-core/docs/manifest.schema.json')))"
   ```
6. **Tag**: `git tag vX.Y.Z && git push origin vX.Y.Z`.
7. **Release with the assets** — the body is this version's changelog section, nothing else:
   ```sh
   gh release create vX.Y.Z --repo BigscreenVR/spt-pacer-tool \
     --title vX.Y.Z --notes-file <this version's changelog section> \
     dist/adapter.spt dist/SHA256SUMS
   ```
8. **Verify by downloading the asset back.** Publishing is not evidence the change shipped —
   `gh release download`, unpack, and prove the new behavior is in the bytes users get (grep the
   new string in every triple, check the packed `manifest.toml` version). Diff the count against
   the previous release's asset so presence means something.
   - **When the fix adds no new runtime string** (a pure logic change — comments do not compile
     into the binary), the grep degenerates to 0/0 and proves nothing. Verify by DIGEST instead:
     `sha256sum` each downloaded asset against the `dist/` build you actually tested, and
     `sha256sum -c SHA256SUMS` inside the download. That proves the shipped bytes are the tested
     bytes, which is the property the grep was standing in for.

## Notes

- **The binary IS delivered by the adapter update since 0.3.0.** On spt-core ≥ 0.44.0 a
  `[shell].spawn` program token resolves install-dir-first, and the fat archive carries the
  binary per triple — `spt adapter update PACER` re-extracts manifest and binary together
  (the 0.2.x PATH-copy era is over; `PACKAGING-PLAN.md` records how it ended). Restarting the
  instance picks the new binary up.
- **Two numbers, never conflated:** the **adapter semver** (`[adapter].version`, matched by the
  tag and by `Cargo.toml`) and **`min_spt_core_version`** (the enforced compatibility floor).
  The floor moves only when pacer actually starts needing a newer spt-core.
- **musl rides inside the archive.** `x86_64-unknown-linux-musl` triple recognition landed in
  spt-core v0.30.0 (doyle-confirmed 2026-07-26; the docs sentence lags behind in spt-core
  PR #98) — a musl node self-identifies as the musl triple and the extractor selects and
  flattens it like the other two. ALWAYS rebuild musl fresh: a stale musl binary silently
  ships old code to pre-glibc-2.39 hosts.
