docs(installer): settle Phase 5's scope
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)
This commit is contained in:
@@ -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 1–4 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 1–4
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user