|
|
|
|
@@ -1,8 +1,20 @@
|
|
|
|
|
# Runic Gateway Installer — plan
|
|
|
|
|
|
|
|
|
|
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)).
|
|
|
|
|
Status: **Phase 0 all but complete** — every prerequisite in another repo has landed, and the
|
|
|
|
|
installer repo now publishes the bundle manifest, so *what* the installer will install is already
|
|
|
|
|
released and composed ahead of the binary that installs it. No installer code exists yet; `0.4`
|
|
|
|
|
(`INSTALL.md`) is the remaining item, then Phase 1. 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`) | ✅ Merged — [link#24](https://gitea.whitlocktech.com/RunicGateway/link/pulls/24) (docs half [docs#84](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/84)); released as [`v1.1.0`](https://gitea.whitlocktech.com/RunicGateway/link/releases/tag/v1.1.0) |
|
|
|
|
|
| 0.3 Bundle CI in the installer repo | 🟨 In review — [installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3), plus the dispatch step in each component ([link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25), [servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)). First bundle: `2026.08.04` |
|
|
|
|
|
| 0.4 This file + `INSTALL.md` | 🟦 This file exists; `INSTALL.md` is **next** — the shape has now settled |
|
|
|
|
|
| — 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)) |
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -105,9 +117,13 @@ 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 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:
|
|
|
|
|
**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:
|
|
|
|
|
|
|
|
|
|
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
|
|
|
|
|
service user
|
|
|
|
|
@@ -122,6 +138,12 @@ 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
|
|
|
|
|
@@ -134,15 +156,16 @@ operators.
|
|
|
|
|
|
|
|
|
|
### 2.6 Prerequisites the overview assumed away
|
|
|
|
|
|
|
|
|
|
- **`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).
|
|
|
|
|
- **`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)).
|
|
|
|
|
- **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` 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.
|
|
|
|
|
- **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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -157,7 +180,7 @@ Runic Gateway Installer v1.0.0
|
|
|
|
|
├── runicgateway-installer-linux-x86_64
|
|
|
|
|
└── SHA256SUMS
|
|
|
|
|
|
|
|
|
|
uo-link v3.x.y (existing release, extended)
|
|
|
|
|
uo-link v0.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)
|
|
|
|
|
@@ -222,13 +245,95 @@ 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
|
|
|
|
|
resolves what to install *from* the bundle.
|
|
|
|
|
|
|
|
|
|
As built ([installer#3](https://gitea.whitlocktech.com/RunicGateway/installer/pulls/3),
|
|
|
|
|
[link#25](https://gitea.whitlocktech.com/RunicGateway/link/pulls/25),
|
|
|
|
|
[servuo-plugins#9](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/pulls/9)) —
|
|
|
|
|
`installer/.gitea/workflows/bundle.yml`, with the decisions §7 had left open:
|
|
|
|
|
|
|
|
|
|
- **Bundles are committed to the installer repo, not published as releases** — see §7.1 for
|
|
|
|
|
where and why. That was the one genuinely open question here, and the deciding factor is that
|
|
|
|
|
this repo's *own* releases are the installer binaries.
|
|
|
|
|
- **Gate 1 reads the sidecar's protocol from source at the release tag**, not from the binary.
|
|
|
|
|
`--print-config` (Phase 0.2) would answer authoritatively, but only for releases from `v1.1.0`
|
|
|
|
|
onward, and `--bundle <tag>` has to be able to recompose a bundle from an older pair. Reading
|
|
|
|
|
`sidecar/src/main.rs` at the tag the release was built from works uniformly, needs no execution
|
|
|
|
|
of a downloaded artifact, and does not provision a throwaway config whose auth token would then
|
|
|
|
|
be sitting in a CI log. A constant that has moved or been renamed is a hard failure — treating
|
|
|
|
|
"could not read" as "matches" is exactly how a mismatched pair would ship.
|
|
|
|
|
- **Gate 2 records the hash CI computed itself**, after verifying the download against the
|
|
|
|
|
publishing repo's `SHA256SUMS`. It also asserts the reverse direction — an asset with *no*
|
|
|
|
|
`SHA256SUMS` entry — because `sha256sum -c` silently passes over a file the sums file does not
|
|
|
|
|
mention, which would put an unverified artifact in the bundle.
|
|
|
|
|
- **Release metadata is read anonymously**, on purpose: those are exactly the requests the
|
|
|
|
|
shipped installer makes on a host with no Gitea credentials, so a repo flipped to private
|
|
|
|
|
fails CI here instead of on an operator's machine.
|
|
|
|
|
- **An unrecognized asset name is a hard failure.** link's binaries are mapped onto platform keys
|
|
|
|
|
by suffix; adding a target (aarch64, macOS) to its release workflow therefore reddens this job
|
|
|
|
|
rather than silently omitting the new binary from every bundle.
|
|
|
|
|
- **A run that changes nothing writes nothing** — the comparison excludes `bundle` and
|
|
|
|
|
`generated`, which are metadata about the run. Without that the nightly cron would commit a
|
|
|
|
|
dated duplicate of the same matrix every morning.
|
|
|
|
|
|
|
|
|
|
The workflow's compose steps were run against the live releases before merge, producing the
|
|
|
|
|
first bundle (`2026.08.04`: link `v1.1.0` + overlay `v0.1.1`, protocol 3), which is committed so
|
|
|
|
|
the manifest exists ahead of the binary that reads it.
|
|
|
|
|
4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
|
|
|
|
|
shape is settled.
|
|
|
|
|
|
|
|
|
|
@@ -249,7 +354,10 @@ 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).
|
|
|
|
|
- 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.
|
|
|
|
|
|
|
|
|
|
### Phase 3 — patch tier (opt-in)
|
|
|
|
|
|
|
|
|
|
@@ -262,9 +370,9 @@ degrade loudly rather than silently.
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
✓ ServUO found /opt/ServUO (57.4)
|
|
|
|
|
✓ Overlay in sync 23 files, all hashes match install.json
|
|
|
|
|
✓ Overlay in sync 30 files, all hashes match install.json
|
|
|
|
|
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
|
|
|
|
|
✓ uo-link installed 3.0.1
|
|
|
|
|
✓ uo-link installed 0.3.0
|
|
|
|
|
✓ Service running, enabled
|
|
|
|
|
✓ Sidecar reachable 127.0.0.1:8080 /health ok
|
|
|
|
|
✓ Protocol sidecar 3 = overlay manifest 3
|
|
|
|
|
@@ -274,6 +382,12 @@ 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:
|
|
|
|
|
|
|
|
|
|
@@ -325,11 +439,20 @@ 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
|
|
|
|
|
@@ -342,6 +465,41 @@ 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
|
|
|
|
|
@@ -349,15 +507,33 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
|
|
|
|
"bundle": "2026.08.01",
|
|
|
|
|
"schema": 1,
|
|
|
|
|
"bundle": "2026.08.04",
|
|
|
|
|
"generated": "2026-08-04T16:07:13Z",
|
|
|
|
|
"protocol": 3,
|
|
|
|
|
"link": { "version": "3.0.1", "sha256": "a91f..." },
|
|
|
|
|
"overlay": { "version": "2.4.0", "commit": "a81f42c", "sha256": "7c3e..." }
|
|
|
|
|
"link": {
|
|
|
|
|
"repo": "RunicGateway/link", "tag": "v1.1.0", "version": "1.1.0", "protocol": 3,
|
|
|
|
|
"assets": {
|
|
|
|
|
"linux-x86_64": { "name": "uo-link-sidecar-linux-x86_64", "url": "…", "sha256": "27d491ef…" },
|
|
|
|
|
"windows-x86_64": { "name": "uo-link-sidecar-windows-x86_64.exe", "url": "…", "sha256": "fbefd886…" }
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
"overlay": {
|
|
|
|
|
"repo": "RunicGateway/servuo-plugins", "tag": "v0.1.1", "version": "0.1.1",
|
|
|
|
|
"commit": "3a52abb…", "protocol": 3,
|
|
|
|
|
"servuo": { "min_version": "57.4", "patches_verified_against": "57.4" },
|
|
|
|
|
"asset": { "name": "runicgateway-overlay-0.1.1.tar.gz", "url": "…", "sha256": "75dc6d6c…" }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
hash could only ever have described one of them. `schema` versions this document's shape and is
|
|
|
|
|
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
|
|
|
|
|
reproducible install. Because the bundle is data, **a new `link` release regenerates ~20 lines of
|
|
|
|
|
reproducible install. Because the bundle is data, **a new `link` release regenerates ~30 lines of
|
|
|
|
|
JSON and leaves the installer binary untouched** — operators do not re-download the installer to
|
|
|
|
|
pick up a sidecar patch, and the installer does not accumulate releases whose code is byte-identical.
|
|
|
|
|
|
|
|
|
|
@@ -365,19 +541,53 @@ Two gates run at compose time, both cheap and both worth it:
|
|
|
|
|
|
|
|
|
|
1. The sidecar's `PROTOCOL_VERSION` must equal the overlay manifest's declared protocol version.
|
|
|
|
|
This is the check that catches an `edge`/`main` protocol mismatch before it reaches an operator.
|
|
|
|
|
The two halves are read from different places because they *are* different: the overlay's from
|
|
|
|
|
`manifest.json` inside the tarball (the only statement of it that exists — §7.0), the sidecar's
|
|
|
|
|
from `sidecar/src/main.rs` at the release tag (see Phase 0 item 3 for why not from the binary).
|
|
|
|
|
2. Every referenced asset must exist and its SHA256 must match the publishing repo's `SHA256SUMS`.
|
|
|
|
|
The hash recorded in the bundle is the one CI computed from the asset it downloaded, *after* that
|
|
|
|
|
check — and the installer verifies every download against it. These artifacts are deliberately
|
|
|
|
|
unsigned (§3), so the checksum is the whole trust anchor; a hash copied from a file nobody
|
|
|
|
|
verified would make the chain decorative.
|
|
|
|
|
|
|
|
|
|
#### Where bundles are published
|
|
|
|
|
|
|
|
|
|
Committed to the installer repo under `bundles/`, so the installer's fetch is a plain anonymous
|
|
|
|
|
`GET` against a public repo — the shard host has no Gitea credentials (§1):
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
bundles/current.json → …/RunicGateway/installer/raw/branch/main/bundles/current.json
|
|
|
|
|
bundles/bundle-<tag>.json → …/raw/branch/main/bundles/bundle-2026.08.04.json (--bundle)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Every bundle is kept forever, so `--bundle` stays reproducible. Tags are UTC dates; a second bundle
|
|
|
|
|
on the same day — a sidecar release in the morning and an overlay release in the afternoon is the
|
|
|
|
|
normal way that happens — becomes `2026.08.04.2`, so one tag always names exactly one matrix.
|
|
|
|
|
|
|
|
|
|
**Not one Gitea release per bundle**, which was the obvious alternative. This repo's own releases
|
|
|
|
|
are the installer *binaries*, and `/releases/latest` returns whichever release is newest regardless
|
|
|
|
|
of kind — interleaving bundle releases would make "latest" intermittently resolve to a release
|
|
|
|
|
carrying no installer binary. Committing also yields a reviewable diff and a git history of the
|
|
|
|
|
compat matrix, and needs no new branch-protection exception: `release.yml`'s version-bump commit
|
|
|
|
|
already requires the CI user to be able to push to `main`.
|
|
|
|
|
|
|
|
|
|
### 7.2 What triggers a bundle
|
|
|
|
|
|
|
|
|
|
| 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, once Phase 0 gives it a release workflow |
|
|
|
|
|
| `link` publishes a release | Its release job `POST`s to the installer repo's workflow-dispatch endpoint as its final step |
|
|
|
|
|
| `servuo-plugins` publishes a release | Same. Phase 0 item 1 gave it the release workflow; the dispatch step was left as a marked TODO until there was something to dispatch, and landed with the bundle CI it calls (item 3) — a step that `404`s on every release is worse than no step |
|
|
|
|
|
| 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,
|
|
|
|
|
whereas dispatching an existing `workflow_dispatch` workflow via the API works today.
|
|
|
|
|
|
|
|
|
|
**A failed dispatch is a warning, never a failed release.** By the time that step runs the component
|
|
|
|
|
release is published and correct; failing the job would misreport it. This also keeps the dispatch
|
|
|
|
|
from becoming a new hard credential requirement — `REGISTRY_TOKEN` having write on the installer
|
|
|
|
|
repo is a nicety, and without it the nightly cron picks the release up anyway. A dropped dispatch
|
|
|
|
|
costs latency, not correctness, which is the whole reason the cron exists.
|
|
|
|
|
|
|
|
|
|
### 7.3 Stale-overlay handling: dispatch, don't wait
|
|
|
|
|
|
|
|
|
|
Each component **self-releases on merge to its own `main`**, using the same conventional-commit
|
|
|
|
|
@@ -385,6 +595,12 @@ engine. Note that "updated since the last release" must mean *releasable* commit
|
|
|
|
|
`RELEASE=false` when nothing but `docs:`/`chore:` has landed, so a docs typo correctly does **not**
|
|
|
|
|
cut an overlay release, and the bundle keeps using the existing one.
|
|
|
|
|
|
|
|
|
|
The compose job's copy of that rule additionally **excludes merge commits**, whose subject is
|
|
|
|
|
`Merge pull request '<the real subject>'`. Without that, every squash-free merge of a `feat:` branch
|
|
|
|
|
would be counted twice, and worse, a merge of a `docs:` branch whose *title* happens to quote a
|
|
|
|
|
`fix:` would be read as releasable — re-dispatching, every night, a release workflow that correctly
|
|
|
|
|
declines to run.
|
|
|
|
|
|
|
|
|
|
So by the time the installer's CI looks, the release normally already exists. If it finds
|
|
|
|
|
`servuo-plugins` main ahead of its latest release *with* releasable commits, it:
|
|
|
|
|
|
|
|
|
|
@@ -401,11 +617,12 @@ rather than being silently retriggered every night forever.
|
|
|
|
|
|
|
|
|
|
### 7.4 Open risk
|
|
|
|
|
|
|
|
|
|
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`.
|
|
|
|
|
**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`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
@@ -421,12 +638,14 @@ bundle. See `docs/link/v3.md`.
|
|
|
|
|
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?
|
|
|
|
|
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 and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## 9. Administrator experience
|
|
|
|
|
|