docs(installer): Phase 5 — scope, the §5.3 correction, and the operator guide #96

Merged
whitlocktech merged 3 commits from docs/installer-phase5-scope into main 2026-08-05 17:52:22 +00:00
Member

What & why

The documentation half of Phase 5. Follows #95, which put the phase before the cutover but left its four sketched items unexamined. Three commits, in the order the work happened: settle the scope, correct what building it disproved, then specify the run.

Code PRs this covers: installer#9, link#26, installer#10 (arm64), installer#11 (backup), installer#12 (repo README).


1 — §5.1: no .deb, no MSI

Both were proposed before Phases 2 and 4 existed, and now collide with what those phases made the installer own.

A .deb under link's release would own /usr/bin/runicgateway-link, the systemd unit and the service user — the three things service.rs writes, hardens and removes, and that install.json records so doctor and uninstall can reason about them. Two owners for one file is not a packaging detail: uninstall deleting a dpkg-owned binary leaves the package installed and broken, an apt upgrade makes doctor report drift no operator caused, and the unit text would live in two repos free to disagree about the account it runs as. The variant that avoids all that — binary-only, no unit, no user — buys apt-managed upgrades of one file, which update already does from a protocol-checked bundle (§7.1). That is the stronger guarantee, so the package would trade correctness for familiarity.

An MSI contradicts a locked decision — the installer does not install itself (Phase 0.4) — and adds a second uninstall path (Add/Remove Programs) beside the uninstall verb, which owns state an MSI cannot see. Unsigned, it raises the same SmartScreen prompt a bare .exe does.

2 — §5.2: Linux aarch64, in a forced order

The step table names repo, branch and reason for each of four steps. The order is dictated by the bundle CI being strict in opposite directions — an unrecognized asset name fails, and a missing required key fails — so the name must be taught before the release that carries it, and required only after. bundle.yml is edited on main only and never on edge, so the cutover merge has nothing to conflict over.

Building it added a paragraph. Neither crate cross-compiles with the arm64 compiler alone: gcc-aarch64-linux-gnu only recommends libc6-dev-arm64-cross, and both release workflows install --no-install-recommends. Every Rust dependency builds fine and then the C dies on a missing bits/libc-header-start.h — bundled SQLite under sqlx for the sidecar, ring under ureq's rustls for the installer. Reproduced both ways in a container before either workflow was written.

3 — §5.3: the trigger was wrong, and building it showed that

The section said a backup is taken for an update and for an install over an existing record, justified by "a first install overwrites nothing." That is not true of a tree deployed by hand per INSTALL.md Appendix A2 — the path this project recommends while the binary is unreleased. There, a first install finds .cs files that differ, plans them as Change, and overwrites them with no prior record anywhere to notice.

The rule is now the direct one: back up whenever the run is about to overwrite something. A genuine first install onto a clean tree still writes nothing, because there is nothing to copy.

Two smaller corrections in the same area: sidecar.toml joins a backup rather than causing one (nothing rewrites it, so triggering on it would leave a dated directory after every no-op update), and the directory is created lazily with the manifest written last — so an interrupted run can neither be mistaken for a backup nor evict a good one by being newer.

4 — §5.4 / INSTALL.md: the operator guide

The guide is the specification of the run, so this is part of building the phase rather than a write-up after it: the aarch64 download lines (uname -m, and why there is no macOS or Windows-on-arm build), the arm64 asset in Appendix A3's by-hand path, what update backs up and when it does not, --no-backup in the flags table, backups in --purge's row, the uninstall table, both paths tables and doctor's sample output.

Also updated in PLAN.md: §3's release layout (loses the .deb, gains the aarch64 assets), §2.6's arm64 bullet, §7.1's example bundle, Phase 4's uninstall "Kept" row, and a state column on the Phase 5 table linking each code PR.

How it was tested

Prose. The claims that are observations rather than decisions were checked against the code they describe: bundle.rs:102, bundle.yml's unrecognized-name failure and required-key assertion, link/release.yml's targets, store.rs's CREATE TABLE IF NOT EXISTS DDL, overlay.rs's Action::Change planning of a differing file with no prior record, and the branch comparison showing edge ⊃ main with identical bundle.yml. The libc6-dev-arm64-cross finding is a reproduced container build, not a recollection.

Checklist

  • I have read CONTRIBUTING.md.
  • The change builds and existing tests/checks pass locally.
  • I have added or updated tests/docs where it makes sense.
  • My commits are reasonably scoped with clear messages.

AI-assisted contributions (required)

  • No AI tools were used to produce this contribution.
  • AI tools were used. Tool(s): Claude Code (Opus 5). I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a Co-Authored-By trailer.

License

  • I agree that my contribution is licensed under this project's license (GNU GPL v3.0 or later), and I have the right to contribute it.
## What & why The documentation half of Phase 5. Follows [#95](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/95), which put the phase before the cutover but left its four sketched items unexamined. Three commits, in the order the work happened: settle the scope, correct what building it disproved, then specify the run. Code PRs this covers: installer#9, link#26, installer#10 (arm64), installer#11 (backup), installer#12 (repo README). --- ### 1 — §5.1: no `.deb`, no MSI Both were proposed before Phases 2 and 4 existed, and now collide with what those phases made the installer own. A `.deb` under link's release would own `/usr/bin/runicgateway-link`, the systemd unit **and** the service user — the three things `service.rs` writes, hardens and removes, and that `install.json` records so `doctor` and `uninstall` can reason about them. Two owners for one file is not a packaging detail: `uninstall` deleting a dpkg-owned binary leaves the package *installed and broken*, an `apt upgrade` makes `doctor` report drift no operator caused, and the unit text would live in two repos free to disagree about the account it runs as. The variant that avoids all that — binary-only, no unit, no user — buys apt-managed upgrades of one file, which `update` already does from a **protocol-checked** bundle (§7.1). That is the stronger guarantee, so the package would trade correctness for familiarity. An MSI contradicts a locked decision — *the installer does not install itself* (Phase 0.4) — and adds a second uninstall path (Add/Remove Programs) beside the `uninstall` verb, which owns state an MSI cannot see. Unsigned, it raises the same SmartScreen prompt a bare `.exe` does. ### 2 — §5.2: Linux `aarch64`, in a forced order The step table names repo, branch and reason for each of four steps. The order is dictated by the bundle CI being strict in *opposite* directions — an unrecognized asset name fails, and a missing required key fails — so the name must be taught before the release that carries it, and required only after. `bundle.yml` is edited on `main` only and never on `edge`, so the cutover merge has nothing to conflict over. **Building it added a paragraph.** Neither crate cross-compiles with the arm64 *compiler* alone: `gcc-aarch64-linux-gnu` only **recommends** `libc6-dev-arm64-cross`, and both release workflows install `--no-install-recommends`. Every Rust dependency builds fine and then the C dies on a missing `bits/libc-header-start.h` — bundled SQLite under `sqlx` for the sidecar, `ring` under `ureq`'s rustls for the installer. Reproduced both ways in a container before either workflow was written. ### 3 — §5.3: the trigger was wrong, and building it showed that The section said a backup is taken for an `update` and for an `install` over an existing record, justified by *"a first install overwrites nothing."* **That is not true of a tree deployed by hand per `INSTALL.md` Appendix A2** — the path this project recommends while the binary is unreleased. There, a first `install` finds `.cs` files that differ, plans them as `Change`, and overwrites them with no prior record anywhere to notice. The rule is now the direct one: **back up whenever the run is about to overwrite something.** A genuine first install onto a clean tree still writes nothing, because there is nothing to copy. Two smaller corrections in the same area: `sidecar.toml` joins a backup rather than causing one (nothing rewrites it, so triggering on it would leave a dated directory after every no-op `update`), and the directory is created lazily with the manifest written last — so an interrupted run can neither be mistaken for a backup nor evict a good one by being newer. ### 4 — §5.4 / `INSTALL.md`: the operator guide The guide *is* the specification of the run, so this is part of building the phase rather than a write-up after it: the `aarch64` download lines (`uname -m`, and why there is no macOS or Windows-on-arm build), the arm64 asset in Appendix A3's by-hand path, what `update` backs up and when it does not, `--no-backup` in the flags table, backups in `--purge`'s row, the uninstall table, both paths tables and `doctor`'s sample output. Also updated in `PLAN.md`: §3's release layout (loses the `.deb`, gains the `aarch64` assets), §2.6's arm64 bullet, §7.1's example bundle, Phase 4's uninstall "Kept" row, and a state column on the Phase 5 table linking each code PR. ## How it was tested Prose. The claims that are observations rather than decisions were checked against the code they describe: `bundle.rs:102`, `bundle.yml`'s unrecognized-name failure and required-key assertion, `link/release.yml`'s targets, `store.rs`'s `CREATE TABLE IF NOT EXISTS` DDL, `overlay.rs`'s `Action::Change` planning of a differing file with no prior record, and the branch comparison showing `edge ⊃ main` with identical `bundle.yml`. The `libc6-dev-arm64-cross` finding is a reproduced container build, not a recollection. ## Checklist - [x] I have read [CONTRIBUTING.md](CONTRIBUTING.md). - [x] The change builds and existing tests/checks pass locally. - [x] I have added or updated tests/docs where it makes sense. - [x] My commits are reasonably scoped with clear messages. ## AI-assisted contributions (required) - [ ] No AI tools were used to produce this contribution. - [x] AI tools were used. Tool(s): `Claude Code (Opus 5)`. I have reviewed and understand every change, and take responsibility for it. AI-authored commits are marked with a `Co-Authored-By` trailer. ## License - [x] I agree that my contribution is licensed under this project's license (**GNU GPL v3.0 or later**), and I have the right to contribute it.
wtclaude added 1 commit 2026-08-05 10:21:10 +00:00
Phase 5 was sketched as four items; two of them are dropped rather than
deferred, because what stops them is an ownership conflict that does not
improve with time (org lead, 2026-08-05).

- No .deb and no MSI (5.1). A .deb under link's release would own the
  binary, the systemd unit and the service user -- the three things
  service.rs writes, hardens and removes and install.json records, so
  uninstall would leave a dpkg-installed-but-broken package and an
  apt upgrade would make doctor report drift nobody caused. The
  binary-only variant buys apt-managed upgrades of one file, which
  update already does from a protocol-checked bundle. An MSI
  contradicts "the installer does not install itself" and adds a second
  uninstall path beside the verb that owns install.json, the cached
  patch set and the ServUO-tree report.
- Linux aarch64 for both components (5.2), in the order the bundle CI
  forces: it hard-fails on an unrecognized link asset name and asserts
  the platform keys present, so the name is taught on main first, link
  publishes, the key becomes required, and only then does the crate on
  edge learn it. bundle.yml is never edited on edge, so the cutover
  merge has nothing to conflict over.
- Backup before overwrite (5.3), scoped by what cannot be fetched
  again: not the binary or the overlay files, and not the database
  (store.rs is CREATE TABLE IF NOT EXISTS over shard state the sweeps
  repopulate -- a cache with a schema), but an operator's edits to a
  deployed .cs file, which Phase 1 overwrites by design, and
  sidecar.toml, whose token the website already holds.
- The docs a first release invalidates (5.4), including the repo README
  still announcing Phase 1 four phases later.

Also records that the Windows SCM smoke was attempted on 2026-08-05 and
stopped at its first check on an unelevated shell, so that half remains
entirely unexecuted.

Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 8818c06f1d)
wtclaude added 1 commit 2026-08-05 10:50:22 +00:00
Two things this section got wrong, both found by implementing it.

§5.3 said a backup is taken for an update and for an install over an
existing record, because "a first install overwrites nothing". That is
not true of a tree deployed by hand per INSTALL.md Appendix A2 -- the
path this project recommends while the binary is unreleased. There the
first install finds .cs files that differ, plans them as Change, and
overwrites them with no prior record anywhere to notice. The rule is
now the direct one: back up whenever the run is about to overwrite
something. A genuine first install onto a clean tree still writes
nothing, because there is nothing to copy.

Also records that sidecar.toml joins a backup rather than causing one
(nothing rewrites it, so triggering on it would leave a dated directory
after every no-op update), and that the directory is created lazily
with the manifest written last, so an interrupted run can neither be
mistaken for a backup nor evict a good one.

§5.2 gains what its cross-builds turned up: neither crate builds with
the arm64 compiler alone. gcc-aarch64-linux-gnu only recommends
libc6-dev-arm64-cross while both release workflows install with
--no-install-recommends, so the C in each crate -- bundled SQLite under
sqlx, ring under ureq's rustls -- fails on a missing libc header while
every Rust dependency compiles fine.

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude added 1 commit 2026-08-05 10:53:18 +00:00
PLAN.md §5.4. The operator guide is the specification of the run, so
these are part of building the phase rather than a write-up after it.

- The download list gains runicgateway-installer-linux-aarch64, with
  `uname -m` as the way to tell, and says plainly that there is no macOS
  and no Windows-on-arm build: the shard dials the sidecar out on
  loopback, so the two share a host, and no ServUO host is either.
- Appendix A3 names the arm64 sidecar asset for the by-hand path, and
  points at the bundle from A1 for the version rather than the one
  written in the example.
- §7's `update` says what a backup is, when one is taken and when one is
  not, and that restoring is the operator's to do -- the guide already
  promised their Bridge.cfg edits survive, and this is the same promise
  for the .cs file they edited that gets overwritten by design.
- --no-backup joins the flags table; --purge's row and the uninstall
  table now name backups alongside the config, the database and the
  cached patch set.
- The paths tables and doctor's sample output gain the backups
  directory and its row.

Co-Authored-By: Claude <noreply@anthropic.com>
wtclaude changed title from docs(installer): settle Phase 5's scope — no .deb, no MSI, aarch64 and backups to docs(installer): Phase 5 — scope, the §5.3 correction, and the operator guide 2026-08-05 10:53:49 +00:00
whitlocktech merged commit 00d476c00b into main 2026-08-05 17:52:22 +00:00
whitlocktech deleted branch docs/installer-phase5-scope 2026-08-05 17:52:23 +00:00
Sign in to join this conversation.
No description provided.