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
Showing only changes of commit 38f83ad2e0 - Show all commits

View File

@@ -17,7 +17,10 @@ release layout is exactly what Phase 5 changes. So the `edge → main` cutover i
things: things:
1. **Phase 5, packaging polish** (§5) — the last work before a first release, rather than the first 1. **Phase 5, packaging polish** (§5) — the last work before a first release, rather than the first
work after it. work after it. Its scope is now settled: **no `.deb` and no MSI** (§5.1 — both would give the
service, its unit and its user a second owner), **Linux `aarch64` for both components** (§5.2),
**a backup of what an upgrade overwrites** (§5.3), and the docs a first release invalidates
(§5.4).
2. **The Windows SCM half being verified on a real host.** systemd registration has now been run for 2. **The Windows SCM half being verified on a real host.** systemd registration has now been run for
real (see [Phase 2 as built](#phase-2--uo-link-install-and-service)), and doing it found a bug no real (see [Phase 2 as built](#phase-2--uo-link-install-and-service)), and doing it found a bug no
amount of unit testing had. `sc create`, the virtual service account, the failure actions and the amount of unit testing had. `sc create`, the virtual service account, the failure actions and the
@@ -277,9 +280,11 @@ operators.
- **`servuo-plugins` had no release workflow.** Only `link` did. "Pull latest repository" is replaced - **`servuo-plugins` had no release workflow.** Only `link` did. "Pull latest repository" is replaced
by a release tarball, which had to be built first — Phase 0 item 1, now in review by a release tarball, which had to be built first — Phase 0 item 1, now in review
([servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7)). ([servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7)).
- **arm64 is not buildable today.** `link/release.yml` cross-compiles only - **arm64 was not buildable today.** `link/release.yml` cross-compiled only
`x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`. An arm64 `.deb` needs another cross `x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`, so `platform_key()` refused every other
toolchain. host by name. That was a property of the workflows, not of Rust — Phase 5 adds
`aarch64-unknown-linux-gnu` to both components in the order §5.2 sets out. There is no `.deb`, so
the cross toolchain is the whole cost.
- **The compat matrix has no home.** `PROTOCOL_VERSION` lives in `link/sidecar/src/main.rs`. The - **The compat matrix has no home.** `PROTOCOL_VERSION` lives in `link/sidecar/src/main.rs`. The
sidecar publishes it via `X-UOLink-Version` and `/health`, and the website stores an expected sidecar publishes it via `X-UOLink-Version` and `/health`, and the website stores an expected
value — but the *plugin's* protocol version is not queryable before boot. Phase 0 item 1 gives it value — but the *plugin's* protocol version is not queryable before boot. Phase 0 item 1 gives it
@@ -296,12 +301,13 @@ Components are published as Gitea release artifacts. Operators download from the
Runic Gateway Installer v1.0.0 Runic Gateway Installer v1.0.0
├── runicgateway-installer-windows-x86_64.exe ├── runicgateway-installer-windows-x86_64.exe
├── runicgateway-installer-linux-x86_64 ├── runicgateway-installer-linux-x86_64
├── runicgateway-installer-linux-aarch64 (Phase 5)
└── SHA256SUMS └── SHA256SUMS
uo-link v1.1.0 (existing release, extended) uo-link v1.1.0 (existing release, extended)
├── uo-link-sidecar-windows-x86_64.exe ├── uo-link-sidecar-windows-x86_64.exe
├── uo-link-sidecar-linux-x86_64 ├── uo-link-sidecar-linux-x86_64
├── runicgateway-link_<ver>_amd64.deb (Phase 5) ├── uo-link-sidecar-linux-aarch64 (Phase 5)
└── SHA256SUMS └── SHA256SUMS
servuo-plugins v<ver> (new release, Phase 0) servuo-plugins v<ver> (new release, Phase 0)
@@ -794,7 +800,7 @@ a clever automatic revert risks silently eating their work. It removes and it re
| Action | Scope | | Action | Scope |
|---|---| |---|---|
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` | | Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` |
| Kept | `sidecar.toml`, `uo-link.db`, and the cached patch set with its pre-patch originals (`--purge` to drop them) | | Kept | `sidecar.toml`, `uo-link.db`, the cached patch set with its pre-patch originals, and (from Phase 5) the backups an upgrade took — `--purge` to drop them |
| **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete | | **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files — with the rung that applied each one (§2.2.1), since a `region-match` apply means the surrounding file was already the operator's — for them to revert by hand | | **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files — with the rung that applied each one (§2.2.1), since a `region-match` apply means the surrounding file was already the operator's — for them to revert by hand |
@@ -876,21 +882,138 @@ Windows host, since only half of `service.rs` compiles on either.
### Phase 5 — packaging polish ### Phase 5 — packaging polish
`.deb` packaging, Windows MSI, arm64 cross build, and optional automated backup before upgrade. The last work before the first release. It was sketched as four items — `.deb` packaging, Windows
Deliberately last *of the build phases*: v1 can register services directly (`sc create` / a written MSI, arm64 cross build, optional automated backup before upgrade — and the org lead settled its scope
systemd unit) and ship plain binaries. Nothing in Phases 14 should have to change to add these. on 2026-08-05: **two of the four are dropped rather than deferred**, because what stops them is an
ownership conflict that does not improve with time, and two are built.
**It now runs before the `edge → main` cutover rather than after it** (org lead, 2026-08-05). The **It runs before the `edge → main` cutover rather than after it.** The original order assumed the
original order assumed the cutover would cut a v1 and packaging would follow as a v1.x — but this cutover would cut a v1 and packaging would follow as a v1.x — but this phase changes the *release
phase changes the *release layout* (§3), so shipping first would mean a first release that is layout* (§3), so shipping first would mean a first release that is superseded by the next one, and
superseded by the next one, and operators who downloaded a bare binary being told to re-download a operators who downloaded a bare binary being told to re-download a package. Deferring the cutover
package. Deferring the cutover costs nothing: nothing is published from `edge`, and the guide's costs nothing: nothing is published from `edge`, and the guide's Appendix A is the supported path
Appendix A is the supported path meanwhile. meanwhile.
The two entry criteria for the cutover are therefore this phase and the outstanding **Windows SCM | Item | Decision |
|---|---|
| `.deb` for uo-link | **Dropped** (§5.1) |
| Windows MSI | **Dropped** (§5.1) |
| arm64 cross build | **Build** — Linux `aarch64`, both components (§5.2) |
| Backup before upgrade | **Build** — on by default, scoped to what a run overwrites (§5.3) |
#### 5.1 What ships is plain binaries: no `.deb`, no MSI
Both were proposed before Phases 2 and 4 existed. They now collide with the components those phases
made the installer own, and the collision is the deciding argument in each case.
- **A `.deb` under link's release would own `/usr/bin/runicgateway-link`, the systemd unit and the
service user** — the same 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` replacing that binary 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 of that — a binary-only `.deb`, 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 of the two guarantees, so the package would be trading correctness for
familiarity.
- **An MSI contradicts a decision already locked**: the installer does not install itself (Phase 0.4
as built). It would also add a second uninstall path — Add/Remove Programs — beside the
`uninstall` verb, which owns state an MSI cannot see: `install.json`, the cached patch set and its
pre-images, and the ServUO-tree report that is printed rather than done (Phase 4). Unsigned, it
raises the same SmartScreen prompt a bare `.exe` does (§3), so it does not even buy the dialog it
looks like it should.
So §3's release layout loses its `.deb` line and gains no installer package. Nothing in Phases 14
changes — this item is a removal, which is why settling it is cheap and shipping it first would not
have been.
**What would reopen it.** If operators ask for `apt`- or `winget`-managed installs, the thing to
package is the sidecar as a standalone service *with the installer taught to detect and defer to a
package-managed one*. That is a design change to who owns the service, not packaging polish, and it
is out of scope here.
#### 5.2 arm64 — Linux `aarch64`, both components
§2.6 recorded arm64 as "not buildable today", which was true of the *workflows* rather than of Rust:
`link/release.yml` and `installer/release.yml` cross-compile `x86_64` Linux and Windows only, so
`platform_key()` (`src/bundle.rs`) refuses every other host by name. Ampere/Graviton instances and
Pi-class boxes are a realistic ServUO home, and the installer's dependency set was already chosen to
avoid OpenSSL and any C toolchain of its own (Phase 1 as built) — which is what makes this two build
steps per workflow rather than a toolchain project.
**Both components or neither.** An installer that runs on `aarch64` but resolves a bundle carrying no
`aarch64` sidecar has moved the failure later, not fixed it. Windows-on-arm and macOS stay unbuilt:
no supported MinGW target here for the first, and no shard host is the second.
**The order is forced by the bundle CI, which is strict on purpose** (Phase 0.3 as built): an
unrecognized `link` asset name is a hard failure, and `linux-x86_64` + `windows-x86_64` are asserted
present. So a link release carrying a new binary reddens the compose job unless it is taught the name
first — and requiring the name before link publishes it fails *every* bundle for as long as the gap
lasts. Hence:
| Step | Repo / branch | Change | Why it is this one first |
|---|---|---|---|
| 1 | `installer` `main` | `bundle.yml` maps `*-linux-aarch64``linux-aarch64`, and **does not** add it to the required list | The compose job runs from `main`; teaching it the name first means link's next release composes instead of failing |
| 2 | `link` `main` | `release.yml` adds `aarch64-unknown-linux-gnu`, cross-linked with `gcc-aarch64-linux-gnu` | Publishes the first `aarch64` sidecar; the nightly cron folds it into a bundle |
| 3 | `installer` `main` | `linux-aarch64` joins the required list | Only safe once a release actually carries it — from here a dropped target reddens CI instead of silently vanishing from every bundle |
| 4 | `installer` `edge` | `release.yml` adds the same target; `platform_key()` learns `("linux", "aarch64")` | The installer binary itself, on the branch that carries the crate |
`bundle.yml` is therefore edited on `main` only and never on `edge`, so the cutover merge has nothing
to conflict over.
#### 5.3 Backup before overwrite
**Scoped by what cannot be fetched again.** Not the sidecar binary or the overlay files — both are
re-downloadable and hash-named in the bundle. Not the database either: `link/sidecar/src/store.rs`
creates every table `IF NOT EXISTS` and every one of them holds shard state the sweeps repopulate, so
it is a cache with a schema rather than a record. What an `update` can destroy irrecoverably is:
1. **An operator's own edits to a deployed `.cs` file.** Phase 1 overwrites those unconditionally and
by design — `Bridge.cfg` is the single exception — so the one place the tool knowingly discards
work is the one place it should keep a copy.
2. **`sidecar.toml`**, whose token the website already holds. Mint a new one and the site's saved
config starts answering `409`/`401` with nothing on the sidecar to explain why (§2.4).
So the rule is **copy what this run is about to overwrite, plus `sidecar.toml`** — not a snapshot of
everything. A snapshot of all 24 overlay files would be mostly byte-identical to a release tarball
that is still downloadable, and the noise would bury the two files that matter.
- **Where:** `<state>/backups/<utc-timestamp>/`, each file mirroring its path in the tree it came
from, beside a `manifest.json` naming the source path, its SHA256, and the bundle moved from and
to. In the state directory, not the ServUO tree — the same rule the tier's `patches/originals/`
follows, and for the same reason: `uninstall` has promised never to clean up in there.
- **When:** on by default for `update`, and for an `install` over an existing record. A first
install overwrites nothing and writes no backup. `--verify` writes none either, for the reason it
runs no part of the sidecar half (Phase 2): a dry run must not create state.
- **`--no-backup`** opts out, for an operator with their own snapshotting.
- **Retention is three.** Older ones are pruned as new ones are written; an unbounded directory of
ServUO source copies on a shard host is its own support problem. `uninstall` keeps them and
`--purge` drops them, alongside the config, the database and the cached patch set — same rule as
Phase 4, and the same reason: they are the only offline record of what was there before.
- **Restore is printed, not done.** Consistent with the uninstall report, and for the identical
reason: the installer cannot know what has changed since, and a clever automatic restore over a
newer overlay eats work rather than saving it. `doctor` names the most recent backup and its
timestamp, so an operator asking "can I go back" does not have to know the layout to answer.
#### 5.4 Documentation the release layout invalidates
Not optional, and grouped here because a first release is the moment these are read for the first
time by someone who was not in the room:
- **`installer/README.md`'s status table still says Phase 1 is built and Phase 2 is next**, four
phases later. It is the first thing a visitor to the repo reads.
- **`INSTALL.md`** gains the `aarch64` download lines, the backup behaviour and `--no-backup`.
- **This file:** §3 loses the `.deb` and gains the `aarch64` assets, §2.6's arm64 bullet is now
historical, and §7.1's example bundle grows the third asset key.
#### Cutover entry criteria
The `edge → main` cutover is gated on this phase **and** on the outstanding **Windows SCM
verification** (see the status header and [Phase 2 as built](#phase-2--uo-link-install-and-service)). verification** (see the status header and [Phase 2 as built](#phase-2--uo-link-install-and-service)).
The second is not busywork: running the systemd half for real found a bug that unit tests could not, The second is not busywork: running the systemd half for real found a bug that unit tests could not,
and `sc create` remains the only code in the crate that has never executed. and `sc create` remains the only code in the crate that has never executed. The smoke script at the
workspace root was run on 2026-08-05 and stopped at its first check — an unelevated shell — so
nothing in that half has executed yet; it needs one run from an elevated PowerShell.
--- ---
@@ -1016,6 +1139,7 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi
"repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3, "repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3,
"assets": { "assets": {
"linux-x86_64": { "name": "uo-link-sidecar-linux-x86_64", "url": "…", "sha256": "27d491ef…" }, "linux-x86_64": { "name": "uo-link-sidecar-linux-x86_64", "url": "…", "sha256": "27d491ef…" },
"linux-aarch64": { "name": "uo-link-sidecar-linux-aarch64", "url": "…", "sha256": "…" },
"windows-x86_64": { "name": "uo-link-sidecar-windows-x86_64.exe", "url": "…", "sha256": "fbefd886…" } "windows-x86_64": { "name": "uo-link-sidecar-windows-x86_64.exe", "url": "…", "sha256": "fbefd886…" }
} }
}, },
@@ -1030,7 +1154,8 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi
Note `link.assets` is a **map keyed by platform**, not the single `sha256` this section originally Note `link.assets` is a **map keyed by platform**, not the single `sha256` this section originally
sketched: link publishes a Linux binary and a Windows `.exe`, and the installer runs on both, so one sketched: link publishes a Linux binary and a Windows `.exe`, and the installer runs on both, so one
hash could only ever have described one of them. `schema` versions this document's shape and is hash could only ever have described one of them. Phase 5's `linux-aarch64` (§5.2) is the first
addition that map was shaped for, and it costs the bundle nothing but a key. `schema` versions this document's shape and is
independent of `protocol` and of either component's release version — all three move separately. independent of `protocol` and of either component's release version — all three move separately.
The installer fetches the current bundle at run time; `--bundle <tag>` pins an older one for a The installer fetches the current bundle at run time; `--bundle <tag>` pins an older one for a