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
2 changed files with 192 additions and 20 deletions

View File

@@ -83,10 +83,16 @@ Download the installer for your OS, plus `SHA256SUMS`, from the
``` ```
runicgateway-installer-linux-x86_64 runicgateway-installer-linux-x86_64
runicgateway-installer-linux-aarch64
runicgateway-installer-windows-x86_64.exe runicgateway-installer-windows-x86_64.exe
SHA256SUMS SHA256SUMS
``` ```
`linux-aarch64` is for arm64 hosts — Ampere/Graviton instances, Pi-class boxes. `uname -m` says
`aarch64` on those and `x86_64` otherwise. There is no macOS build and no Windows-on-arm build: the
shard dials the sidecar out on loopback, so the two have to share a host, and no ServUO host is
either of those.
**Linux** **Linux**
```bash ```bash
@@ -218,7 +224,8 @@ reports "unchanged" and writes nothing.
| `--host <name>` | `install` | The hostname to print in the website URLs. | | `--host <name>` | `install` | The hostname to print in the website URLs. |
| `--site-url <url>` | `install` | Your site's base URL, for the Admin → Shard link. | | `--site-url <url>` | `install` | Your site's base URL, for the Admin → Shard link. |
| `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. **On `uninstall` it means yes** — that prompt defaults to no, and typing `uninstall --yes` is not an accident. | | `--yes` | all | Assume the default answer to every prompt. Combine with the flags above for an unattended run. **On `uninstall` it means yes** — that prompt defaults to no, and typing `uninstall --yes` is not an accident. |
| `--purge` | `uninstall` | Also delete `sidecar.toml`, `uo-link.db` and the cached patch set, all of which are otherwise kept. | | `--no-backup` | `install`, `update` | Do not copy the files this run is about to overwrite. They are otherwise saved under the state directory — see [§7](#7-day-two). |
| `--purge` | `uninstall` | Also delete `sidecar.toml`, `uo-link.db`, the cached patch set and every backup, all of which are otherwise kept. |
Exit codes are `0` success, `1` the run failed, `2` the arguments were unusable. Two commands also Exit codes are `0` success, `1` the run failed, `2` the arguments were unusable. Two commands also
use `1` for a run that *completed* and found something wrong, so they can be read from a script: use `1` for a run that *completed* and found something wrong, so they can be read from a script:
@@ -238,6 +245,7 @@ else still was).
| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps | | `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps |
| `/etc/runicgateway/patches/` | Copies of the patches the tier evaluated, so `uninstall` can print the exact hunks long after the release tarball is gone, and a refused one is still on hand to apply yourself | | `/etc/runicgateway/patches/` | Copies of the patches the tier evaluated, so `uninstall` can print the exact hunks long after the release tarball is gone, and a refused one is still on hand to apply yourself |
| `/etc/runicgateway/patches/originals/` | Each file the patch tier edited, exactly as it was beforehand — a revert you can verify rather than reconstruct | | `/etc/runicgateway/patches/originals/` | Each file the patch tier edited, exactly as it was beforehand — a revert you can verify rather than reconstruct |
| `/etc/runicgateway/backups/<timestamp>/` | Copies of the files a run replaced, with a `manifest.json` naming each. Newest three kept; skip with `--no-backup` |
| `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) | | `/var/lib/runicgateway/uo-link.db` | The sidecar's SQLite store (event history, cached profiles, link map) |
| `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user | | `/etc/systemd/system/runicgateway-link.service` | The service unit, running as a dedicated user |
@@ -250,6 +258,7 @@ else still was).
| `%ProgramData%\RunicGateway\install.json` | As above | | `%ProgramData%\RunicGateway\install.json` | As above |
| `%ProgramData%\RunicGateway\patches\` | As above | | `%ProgramData%\RunicGateway\patches\` | As above |
| `%ProgramData%\RunicGateway\patches\originals\` | As above | | `%ProgramData%\RunicGateway\patches\originals\` | As above |
| `%ProgramData%\RunicGatewayackups\<timestamp>\` | As above |
| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store | | `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store |
| Service `RunicGatewayLink` | Automatic start, restart on failure, running as `NT SERVICE\RunicGatewayLink` | | Service `RunicGatewayLink` | Automatic start, restart on failure, running as `NT SERVICE\RunicGatewayLink` |
@@ -520,6 +529,8 @@ first thing a maintainer will want.
✓ Protocol sidecar 3 = overlay manifest 3 ✓ Protocol sidecar 3 = overlay manifest 3
✗ Shard connected no — the shard is running (pid 8123) but has not dialed in ✗ Shard connected no — the shard is running (pid 8123) but has not dialed in
✓ Bundle 2026.08.04 — up to date ✓ Bundle 2026.08.04 — up to date
✓ Backups 2026-08-04T09:12:44Z — 3 file(s) replaced by update to bundle 2026.08.04
3 kept in /etc/runicgateway/backups
``` ```
Rows come from asking the installed sidecar (`--version`, `--print-config`) rather than from reading Rows come from asking the installed sidecar (`--version`, `--print-config`) rather than from reading
@@ -557,6 +568,16 @@ together — never to two independently-latest artifacts that may disagree.
Your `sidecar.toml`, your `Bridge.cfg` edits and your database are not touched. `Bridge.cfg` is Your `sidecar.toml`, your `Bridge.cfg` edits and your database are not touched. `Bridge.cfg` is
overwritten only if you have not changed it; a modified copy is reported, not clobbered. overwritten only if you have not changed it; a modified copy is reported, not clobbered.
**Anything it does overwrite is copied first.** Every `.cs` file the overlay owns is replaced
unconditionally — that is deliberate, they are code — so if you have edited one, the run saves your
copy under `backups/<timestamp>/` in the state directory before writing, alongside `sidecar.toml`
and any stock ServUO file the patch tier is about to touch. Each backup carries a `manifest.json`
saying where every file came from. The newest three are kept; `--no-backup` skips taking one.
Putting a file back is yours to do — the installer will not restore an old file over a newer
release, because it cannot know what has changed since. A run that overwrites nothing takes no
backup, so a no-op `update` leaves nothing behind.
It updates the ServUO tree `install.json` names — not a tree it detects — and it needs the shard It updates the ServUO tree `install.json` names — not a tree it detects — and it needs the shard
stopped, exactly as `install` does. There is nothing to update on a host that was never installed; stopped, exactly as `install` does. There is nothing to update on a host that was never installed;
it says so rather than performing a first install under a verb that promises to preserve. it says so rather than performing a first install under a verb that promises to preserve.
@@ -581,7 +602,7 @@ your work.
| | | | | |
|---|---| |---|---|
| **Removed** | The sidecar binary, its service entry, `install.json` | | **Removed** | The sidecar binary, its service entry, `install.json` |
| **Kept** | `sidecar.toml`, `uo-link.db`, and the cached patch set with its pre-patch originals (`--purge` drops all of them) | | **Kept** | `sidecar.toml`, `uo-link.db`, the cached patch set with its pre-patch originals, and every backup an upgrade took (`--purge` drops all of them) |
| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete — with any file you have edited since deployment flagged, so you do not delete your own work by mistake | | **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete — with any file you have edited since deployment flagged, so you do not delete your own work by mistake |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert — with how each landed, since one placed into a file you had already modified is worth a closer look. The pre-patch copy kept under `patches/originals/` is there to diff against. | | **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert — with how each landed, since one placed into a file you had already modified is worth a closer look. The pre-patch copy kept under `patches/originals/` is there to diff against. |
@@ -679,6 +700,10 @@ for the *comparison* while writing back your file's own endings, which is why it
### A3. Install the sidecar ### A3. Install the sidecar
On an arm64 host substitute `uo-link-sidecar-linux-aarch64` for the asset name below (`uname -m`
says `aarch64`); releases from v1.2.0 carry both. Take the version from the bundle you fetched in
A1 rather than the one written here.
```bash ```bash
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/uo-link-sidecar-linux-x86_64 curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/uo-link-sidecar-linux-x86_64
curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/SHA256SUMS curl -LO https://gitea.whitlocktech.com/RunicGateway/link/releases/download/v1.1.0/SHA256SUMS

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,160 @@ 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 | State |
|---|---|---|
| `.deb` for uo-link | **Dropped** (§5.1) | — |
| Windows MSI | **Dropped** (§5.1) | — |
| arm64 cross build | **Build** — Linux `aarch64`, both components (§5.2) | In review: [installer#9](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/9), [link#26](https://gitea.whitlocktech.com/RunicGateway/link/pulls/26), [installer#10](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/10); step 3 waits on link's first arm64 release |
| Backup before upgrade | **Build** — on by default, scoped to what a run overwrites (§5.3) | In review: [installer#11](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/11) |
| Docs the release invalidates | **Do** (§5.4) | Pending |
#### 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.
**What building it found.** Neither crate cross-compiles with the arm64 *compiler* alone.
`gcc-aarch64-linux-gnu` only **recommends** `libc6-dev-arm64-cross`, and both release workflows
install with `--no-install-recommends` — so the C that each crate pulls in (bundled SQLite under
`sqlx` for the sidecar, `ring` under `ureq`'s rustls for the installer) dies on a missing
`bits/libc-header-start.h` while every Rust dependency builds fine. Both halves were reproduced in a
`rust:1-slim-bookworm` container, failure then fix, before either workflow was written.
#### 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: whenever the run is about to overwrite something**, whichever verb was typed. This
section first said "`update`, and `install` over an existing record", justified by a first install
overwriting nothing — **which is wrong**, and building it is what showed that. A tree deployed by
hand per [`INSTALL.md`](INSTALL.md) Appendix A2 — the path this project recommends while the
binary is unreleased — has `.cs` files the first `install` plans as `Change` and overwrites, with
no prior record anywhere to notice. The direct test covers that case, and a genuine first install
onto a clean tree still writes nothing, because there is nothing to copy. `--verify` writes none
either, for the reason it runs no part of the sidecar half (Phase 2): a dry run must not create
state.
- **`sidecar.toml` joins a backup that is already being taken; it is never the reason for one.**
Nothing in the installer rewrites it, so treating it as a trigger would leave a dated directory
behind after every no-op `update` — the empty-backup problem one step along. It is copied so that
a restored set of files arrives with the token that matches it.
- **`--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.
- **The directory is created lazily and the manifest is written last**, so a directory carrying one
is a *complete* backup. Listing and pruning consider only those: a run interrupted mid-copy must
neither be mistaken for a backup nor be able to evict a good one by being newer than it. It is
left on disk for a human to look at rather than silently deleted.
- **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 +1161,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 +1176,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