Compare commits

1 Commits

Author SHA1 Message Date
152ffef86e docs(installer): add release orchestration and the bundle manifest
The installer needs CI that reacts when a component publishes a release. Adds
that as section 7, folded into version tracking because the bundle IS the compat
matrix -- which closes the "where does the compat matrix live" gap section 7
previously left open.

- 7.1 Bundle manifest: CI publishes an exact, protocol-checked combination of
  component versions; the installer resolves against it at run time and
  --bundle <tag> pins one. A link release regenerates JSON and leaves the
  installer binary untouched, so operators don't re-download the installer for a
  sidecar patch and the repo doesn't accumulate releases with identical code.
  Two compose-time gates: sidecar PROTOCOL_VERSION must equal the overlay
  manifest's declared version, and every asset's SHA256 must match.
- 7.2 Triggers: each component's release job POSTs to the installer's
  workflow-dispatch endpoint (link's release.yml already declares
  workflow_dispatch and already holds a write:repository token), plus a nightly
  cron so a missed dispatch self-heals. repository_dispatch avoided -- support
  is uncertain on this Gitea version.
- 7.3 Stale overlay: dispatch, don't wait. Components self-release on merge to
  their own main, so the release normally already exists. If main is ahead with
  *releasable* commits (docs:/chore: correctly cut nothing), fire that repo's
  workflow, compose from what exists now, warn loudly, and let the nightly fold
  in the result. Dispatching another repo's workflow is fine -- it still runs
  its own gates -- but polling it is not, since Gitea's dispatch endpoint
  returns no run handle.

Bundle CI becomes a Phase 0 deliverable, since Phase 1 resolves what to install
from the bundle. `update` now moves between checked combinations rather than two
independently-latest artifacts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 06:09:58 -05:00
3 changed files with 29 additions and 179 deletions

View File

@@ -1,17 +1,8 @@
# Runic Gateway Installer — plan
Status: **Phase 0 in progress.** No installer code exists yet. This document is the design of record;
it supersedes the informal overview it grew out of, which described a ServUO integration that does
not match how `servuo-plugins` actually ships (see
[Corrections](#corrections-to-the-original-overview)).
| Phase 0 item | State |
|---|---|
| 0.1 `servuo-plugins` release workflow | ✅ Merged — [servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7) + [#8](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/8); first overlay release is [`v0.1.1`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/tag/v0.1.1) |
| 0.2 `link` installable (data paths + `--print-config`) | 🟨 In review — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) |
| 0.3 Bundle CI in the installer repo | ⬜ Not started — **next**; both components it composes now exist |
| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` waits on the shape settling |
| — Repo bootstrap (governance + CI) | ✅ [`RunicGateway/installer`](https://gitea.whitlocktech.com/RunicGateway/installer) created; workflows merged ([installer#1](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/1), [#2](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/2)) |
Status: **planning**. No installer code exists yet. This document is the design of record; it
supersedes the informal overview it grew out of, which described a ServUO integration that does not
match how `servuo-plugins` actually ships (see [Corrections](#corrections-to-the-original-overview)).
---
@@ -114,13 +105,9 @@ Two files, two owners:
| `/etc/runicgateway/sidecar.toml` | uo-link | The sidecar's own schema, unchanged. Service sets `UOLINK_CONFIG` to this path |
| `/etc/runicgateway/install.json` | installer | Deployed versions, file hashes, applied patches, ServUO path, timestamps |
**Working-directory trap:** the sidecar wrote both `sidecar.toml` and `uo-link.db` relative to CWD.
Under `C:\Program Files\` that fails or silently lands in VirtualStore. Phase 0.2 fixed the second
half in the sidecar — a relative `[store].path` now resolves against the directory holding
`sidecar.toml`, so pinning the config alone is enough to put the database somewhere deterministic —
but the config path itself is still CWD-relative by default, and "deterministic" is not the same as
"where this install wants it". The service definitions therefore still pin `UOLINK_CONFIG` and
`UOLINK_DB_PATH` explicitly:
**Working-directory trap:** the sidecar writes both `sidecar.toml` and `uo-link.db` relative to CWD.
Under `C:\Program Files\` that fails or silently lands in VirtualStore. The service definitions must
pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly:
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
service user
@@ -135,12 +122,6 @@ becomes write-only. This is the largest "I installed it and nothing happened" fa
The installer closes it by printing a copy-paste block at the end of a successful run — see §6.
Phase 0.2 supplied the missing half of that: `uo-link-sidecar --print-config` provisions the config
if absent and prints the resolved settings — token, both binds, `ws_path`, protocol version, db
path — as JSON. The installer reads the block it prints out of that one call. **It never parses the
log**, which was the alternative and would have made the handoff depend on a log format that is not
a contract.
### 2.5 `deploy.ps1` cannot be the cross-platform deployer
It is PowerShell-only; a Linux ServUO host running .NET typically has no `pwsh`. It also hard-throws
@@ -153,16 +134,15 @@ operators.
### 2.6 Prerequisites the overview assumed away
- **`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
([servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7)).
- **`servuo-plugins` has no release workflow.** Only `link` does. "Pull latest repository" is
replaced by a release tarball, which has to be built first (Phase 0).
- **arm64 is not buildable today.** `link/release.yml` cross-compiles only
`x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`. An arm64 `.deb` needs another cross
toolchain.
- **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
value — but the *plugin's* protocol version is not queryable before boot. Phase 0 item 1 gives it
a home: `servuo-plugins/overlay.toml`, declared into the overlay manifest. See §7.0 / §7.1.
- **The compat matrix has no home.** `PROTOCOL_VERSION` currently lives only in
`link/sidecar/src/main.rs`. The 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. See §7.
---
@@ -177,7 +157,7 @@ Runic Gateway Installer v1.0.0
├── runicgateway-installer-linux-x86_64
└── SHA256SUMS
uo-link v0.x.y (existing release, extended)
uo-link v3.x.y (existing release, extended)
├── uo-link-sidecar-windows-x86_64.exe
├── uo-link-sidecar-linux-x86_64
├── runicgateway-link_<ver>_amd64.deb (Phase 5)
@@ -242,58 +222,9 @@ Repo work that must land before an installer can exist.
consume only `{version, changelog, artifacts}`). The adapter half produces
`runicgateway-overlay-<ver>.tar.gz` containing `overlay/`, `patches/`, and a `manifest.json`
(version, commit, per-file SHA256, declared protocol version, minimum ServUO version).
As built ([servuo-plugins#7](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/7)),
with three deviations from `link`'s copy that each fell out of the repo rather than being chosen:
- **No build gates, structural gates instead.** Nothing in that repo can be compiled without
ServUO reference assemblies, so CI asserts what it honestly can: `Bridge.cfg` and the Bridge
scripts present, `Scripts.csproj` present (its absence ships code that never compiles while
ServUO reports success — §2.1), every `.patch` parseable via `git apply --stat`, and each
patch's companion `.cs` present.
- **No bump commit, so no push to `main`.** `link` writes the version into `Cargo.toml` because
the binary embeds it; the tarball embeds nothing but the generated manifest, so the tag *is*
the version. That workflow needs no branch-protection exception.
- **`overlay.toml` at the repo root** holds the declared `protocol` and the ServUO compatibility
values, read by CI into the manifest. It exists because the number needs one maintained home —
see §7 for why the plugin cannot simply be asked.
The tarball uses a **fixed** top-level directory, `runicgateway-overlay/`, not a versioned one:
the installer looks for `overlay/`, `patches/` and `manifest.json` at known paths rather than
parsing the version it is trying to read. Member order, mtime and ownership are pinned, so a
given tree yields a byte-identical tarball and its checksum moves only when its contents do.
2. **`link`: make the sidecar installable.** Confirm/settle default data paths, and add a way to
read back config non-interactively (e.g. `--print-config` emitting JSON: bind addresses, token,
protocol version, db path) so the installer does not have to scrape logs for the token.
As built ([link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24)) — the sidecar
had **no CLI at all** before this, so the shape was chosen rather than inherited:
- **Four flags, hand-rolled:** `--print-config`, `--config <PATH>`, `--version`, `--help`. No
argument-parsing crate — it would be larger than the code it replaced — and deliberately no
flags that duplicate a config key, so `sidecar.toml` stays the single place settings live.
An unrecognized argument exits `2`; silently ignoring a typo'd flag would start a sidecar that
is not the one the installer asked for.
- **`--print-config` performs first-run setup rather than only reporting.** It runs the same
load path a normal start does, so a missing config file is written and a blank token is
generated and saved. That collapses "provision the sidecar" and "find out its token" into one
non-interactive call — which is exactly the sequence §6 needs. `config_created` and
`token_generated` say whether *this* run did either, because the values alone cannot
distinguish a fresh install from a re-read of an existing one, and a re-run must not report a
token as newly minted.
- **The document is the whole of stdout.** The log subscriber writes to stdout, so it is not
started in this mode. `ws_path` is emitted from the same constant the route is registered
with, so the installer's WebSocket URL cannot drift from the server's.
- **Relative `[store].path` now anchors to the config file's directory, not the CWD** — see
§2.3, which this half-closes on the sidecar side. Absolute paths are used as written; parent
directories are created; `:memory:` and `file:` URIs are left alone.
- **The db path is handed to sqlx as a path, not a `sqlite://` URL.** The URL spelling is
parsed as one: it percent-decodes the path and splits it on `?`, so an installed path
containing `%20` opened a different file than the operator named.
- **No platform data directories are compiled in.** That is the "settle" half of this item, and
the answer is that the *installer* owns layout (§2.3) and pins `UOLINK_CONFIG` /
`UOLINK_DB_PATH` in the service definition. Baking `/etc` and `%ProgramData%` defaults into
the binary would give the same paths two owners and break `cargo run` in a working tree.
3. **Bundle CI in the installer repo** (§7). Compose job (read both repos' latest releases → run the
two gates → publish `bundle.json`), the nightly cron, and the dispatch step appended to each
component's release workflow. This must exist before Phase 1 is useful, since the installer
@@ -318,10 +249,7 @@ Repo work that must land before an installer can exist.
- Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service
registration with automatic start and restart-on-failure.
- Both: `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned in the service definition (§2.3).
- Token surfacing (§6): run the installed binary once as
`uo-link-sidecar --print-config --config <the pinned path>` **before** registering the service.
That both writes the config the service will read and returns the token to print, so the service
never starts against a config that does not exist yet.
- Token surfacing (§6).
### Phase 3 — patch tier (opt-in)
@@ -334,9 +262,9 @@ degrade loudly rather than silently.
```
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 30 files, all hashes match install.json
✓ Overlay in sync 23 files, all hashes match install.json
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
✓ uo-link installed 0.3.0
✓ uo-link installed 3.0.1
✓ Service running, enabled
✓ Sidecar reachable 127.0.0.1:8080 /health ok
✓ Protocol sidecar 3 = overlay manifest 3
@@ -346,12 +274,6 @@ degrade loudly rather than silently.
The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge
actually works" (§2.1).
Three of those rows are answered by the sidecar's own CLI rather than by inspecting the filesystem:
`--version` prints `uo-link-sidecar <ver> (protocol <n>)`, and `--print-config` gives the config and
db paths the *installed service* resolves — so `doctor` reports what the binary would actually do,
not what `install.json` believes it was told to do. The protocol row compares that number against
the overlay manifest's declared one (§7.0).
`runicgateway update` — resolves the current bundle (§7.1), then acts asymmetrically by component,
deliberately:
@@ -403,20 +325,11 @@ Paste these into Admin → Shard on your Runic Gateway site:
The token is write-only once saved — the site will never show it back to you.
```
Every value in that block except the host and the site URL comes from one
`uo-link-sidecar --print-config` call (§2.4): `web.auth_token`, `protocol`, and `web.bind` +
`web.ws_path` for the two URLs. Only the **host** is substituted — `web.bind` is frequently
`0.0.0.0`, which is not something to hand a website — so the installer composes the URLs from the
host it detects or prompts for, rather than echoing the bind address.
The installer prompts for the site URL only to build that link; it never contacts the website. A
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
later without changing anything here.
The printed token is a secret in transit: `--print-config` output must go to the operator's
terminal and the config file, never into an installer log file or a support bundle.
---
## 7. Version tracking, the bundle, and release orchestration
@@ -429,41 +342,6 @@ Three components version independently, bound by a protocol contract:
- **plugin overlay** — has no queryable version before ServUO boots. The overlay release
`manifest.json` declares it, and `install.json` records what was deployed.
### 7.0 The overlay manifest
Shipped inside every `runicgateway-overlay-<ver>.tar.gz`, generated by that repo's release workflow:
```json
{
"component": "servuo-plugins-overlay",
"version": "0.1.0",
"commit": "968b526…",
"repo": "RunicGateway/servuo-plugins",
"protocol": 3,
"servuo": { "min_version": "57.4", "patches_verified_against": "57.4" },
"files": { "overlay/Config/Bridge.cfg": "32718424…", "patches/…": "…" }
}
```
`version` and `commit` come from the release engine; `protocol` and the `servuo` block are read from
`servuo-plugins/overlay.toml`; `files` is a SHA256 per shipped file.
Two of these carry weight beyond documentation:
- **`protocol` is a hand-maintained declaration, and has to be.** The plugin announces no version on
the wire and none is queryable before ServUO boots, so nothing in CI can derive it — which makes
this line the only thing §7.1's gate 1 has to compare the sidecar against. The duty is stated in
`overlay.toml` and in that repo's README: **bump it in the same PR that changes the emitters**, the
way `link` bumps `PROTOCOL_VERSION`.
- **`files` is what makes `doctor` able to tell "the operator edited a deployed file" from "the
overlay moved on"** (§5, Phase 4). The installer copies these hashes into `install.json` at deploy
time; a later mismatch against *both* the manifest and `install.json` means upstream changed, a
mismatch against `install.json` alone means local edits.
`min_version` and `patches_verified_against` are separate on purpose. The base overlay only *adds*
files and is expected to work broadly; the patch tier diffs stock ServUO files and is verified
against exactly one version (§2.2).
### 7.1 The bundle manifest
**The bundle is the compat matrix.** Rather than the installer hardcoding versions or blindly
@@ -473,8 +351,8 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi
{
"bundle": "2026.08.01",
"protocol": 3,
"link": { "version": "0.3.0", "sha256": "a91f..." },
"overlay": { "version": "0.1.0", "commit": "968b526", "sha256": "7c3e..." }
"link": { "version": "3.0.1", "sha256": "a91f..." },
"overlay": { "version": "2.4.0", "commit": "a81f42c", "sha256": "7c3e..." }
}
```
@@ -494,7 +372,7 @@ Two gates run at compose time, both cheap and both worth it:
| Trigger | Why |
|---|---|
| `link` publishes a release | Its release job `POST`s to the installer repo's workflow-dispatch endpoint as its final step. `link/.gitea/workflows/release.yml` already declares `workflow_dispatch: {}` and already holds a `write:repository` token |
| `servuo-plugins` publishes a release | Same. Phase 0 item 1 gave it the release workflow; the dispatch step is marked as a TODO in that workflow's header and lands with the bundle CI it would call (item 3) — a step that `404`s on every release is worse than no step |
| `servuo-plugins` publishes a release | Same, once Phase 0 gives it a release workflow |
| Nightly cron on the installer repo | Recomputes from whatever the latest releases actually are, so a missed or failed dispatch self-heals instead of silently pinning operators to a stale sidecar |
`repository_dispatch` is deliberately avoided — support for it is uncertain on this Gitea version,
@@ -523,12 +401,11 @@ rather than being silently retriggered every night forever.
### 7.4 Open risk
**Settled as of the v3 cutover.** Protocol work landed on `edge` branches and the `edge → main`
cutover has now merged, so `main` speaks protocol 3 consistently across the repos. The rule it
motivated stands regardless and is not a temporary measure: **the installer hardcodes no protocol
version anywhere.** It reads what the artifacts declare, and §7.1's gate 1 is what stops a
mismatched pair from being published as a bundle — which is the mechanism that will matter at the
*next* protocol bump, not just this one. See `docs/link/v3.md`.
The v3 cutover is mid-flight — protocol work landed on `edge` branches with the `edge → main`
cutover still open across four repos. Until that lands, `main` and `edge` disagree about
`PROTOCOL_VERSION`, so the installer must not hardcode a version anywhere; it reads what the
artifacts declare, and §7.1's gate 1 is what stops a mismatched pair from being published as a
bundle. See `docs/link/v3.md`.
---
@@ -544,13 +421,11 @@ mismatched pair from being published as a bundle — which is the mechanism that
3. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
different host, or hard-assume co-location?
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
4. **Branch targeting for the new repo** — `link`, `website`, `servuo-plugins` and `docs` are
mid-cutover between `edge` and `main`. The installer repo starts clean on `main`; the Phase 0
`servuo-plugins` release workflow needs a target branch decision.
**Resolved — branch targeting for the new repo** (was question 4). The v3 cutover landed:
`servuo-plugins#6` merged, so that repo's `main` and `edge` agree at protocol 3. The release
workflow targets `main`, and the installer repo starts clean on `main`. §7.4's caution still applies
in principle — the installer hardcodes no protocol version, it reads what the artifacts declare —
but the specific `edge`/`main` disagreement that motivated it is gone.
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
---

View File

@@ -23,31 +23,7 @@ Every route **except `GET /health`** requires the shared token from `sidecar.tom
| REST | `X-Api-Key: <token>` |
| WebSocket | `?token=<token>` in the connect URL (browsers can't set headers on a WS handshake) |
Missing or wrong token → **401** `{"error":"missing or invalid auth token"}`. The token is compared in constant time. It is generated automatically on first run; rotate by editing `sidecar.toml` and restarting.
To read it back afterwards, ask the sidecar rather than hunting through the startup log or the TOML:
```console
$ uo-link-sidecar --print-config --config /etc/runicgateway/sidecar.toml
{
"component": "uo-link-sidecar",
"config_created": false,
"config_path": "/etc/runicgateway/sidecar.toml",
"protocol": 3,
"shard": { "bind": "127.0.0.1:7788" },
"store": { "path": "/var/lib/runicgateway/uo-link.db" },
"token_generated": false,
"version": "0.1.0",
"web": {
"auth_required": true,
"auth_token": "c0f04ace…",
"bind": "127.0.0.1:8080",
"ws_path": "/ws"
}
}
```
That is the same set of values Admin → Shard asks for — base URL and WS URL are `web.bind` (substituting a reachable host if it is `0.0.0.0`) plus `web.ws_path`. The output **contains the token in clear text**, so treat it as a secret: it belongs in a terminal, not in a log or a CI artifact. `--print-config` also performs first-run setup, writing the config file and generating a token if there is none, and reports whether it did via `config_created` / `token_generated`.
Missing or wrong token → **401** `{"error":"missing or invalid auth token"}`. The token is compared in constant time. It is generated automatically on first run (the sidecar logs it); rotate by editing `sidecar.toml` and restarting.
---

View File

@@ -25,7 +25,6 @@ link/
│ └── PULL_REQUEST_TEMPLATE.md
├── sidecar/
│ ├── src/
│ │ ├── cli.rs
│ │ ├── config.rs
│ │ ├── main.rs
│ │ ├── rpc.rs