diff --git a/README.md b/README.md index 679624b..6df81ff 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,11 @@ so they live in one place, independent of either codebase. ## Layout ``` -website/ docs from the shard website (Node/Express + MariaDB + React/Vite) -link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS) -android/ docs from the native Android client (Kotlin + Jetpack Compose) -ci/ cross-cutting CI/quality notes +website/ docs from the shard website (Node/Express + MariaDB + React/Vite) +link/ docs from the ServUO bridge (C# plugin + Rust sidecar + Node WS) +android/ docs from the native Android client (Kotlin + Jetpack Compose) +installer/ docs for the installer that deploys a shard's bridge components +ci/ cross-cutting CI/quality notes ``` ### `website/` @@ -50,6 +51,12 @@ ci/ cross-cutting CI/quality notes | [TRUSTED_DEVICES_APP_HANDOFF.md](android/TRUSTED_DEVICES_APP_HANDOFF.md) | Trusted-devices app handoff notes | | [PROJECT_TREE.md](android/PROJECT_TREE.md) | Auto-generated snapshot of the repo's tracked file layout | +### `installer/` +| Doc | What it covers | +|---|---| +| [INSTALL.md](installer/INSTALL.md) | **Operator guide** — installing Runic Gateway on a ServUO shard, connecting it to the website, and diagnosing it. Includes the by-hand path, which works today | +| [PLAN.md](installer/PLAN.md) | Installer design of record — phases, locked decisions, the bundle/compat-matrix model | + ## Provenance - `website/*` was extracted from `RunicGateway/website` via `git filter-repo`. diff --git a/installer/INSTALL.md b/installer/INSTALL.md new file mode 100644 index 0000000..ef4bca6 --- /dev/null +++ b/installer/INSTALL.md @@ -0,0 +1,692 @@ +# Installing Runic Gateway on your shard + +Operator guide for the **Runic Gateway installer** — the tool that takes a working ServUO +installation and connects it to a Runic Gateway website. + +> **Status: the installer binary is not released yet.** +> +> Everything it installs *is* released and published — the sidecar, the plugin overlay, and the +> [bundle manifest](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) +> that names the checked combination of the two. This guide is the operator-facing contract those +> phases build to, and it is written first on purpose: it is the specification of what the run +> looks like, what it asks, where it writes, and what it prints. +> +> **You can install today without it** — [Appendix A](#appendix-a--installing-by-hand) is the same +> deployment done by hand, with the commands verified against the current releases. When the binary +> ships, Appendix A stays as the reference for what it does under the hood. +> +> Design of record: [PLAN.md](PLAN.md). + +--- + +## What this installs + +Three things, on the machine that runs your shard: + +| # | Component | Where it comes from | +|---|---|---| +| 1 | **The plugin overlay** — C# source that ServUO compiles at boot, copied into your server tree | [`RunicGateway/servuo-plugins`](https://gitea.whitlocktech.com/RunicGateway/servuo-plugins) release tarball | +| 2 | **The uo-link sidecar** — a small Rust service that the shard dials out to, and that your website reads from | [`RunicGateway/link`](https://gitea.whitlocktech.com/RunicGateway/link) release binary | +| 3 | **A record of what it did** — `install.json`, plus a cached copy of any patches it applied | Written by the installer | + +``` + ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website + (1) overlay (2) binary + service (yours) +``` + +The shard **dials out**; it never listens for the website and is never reachable from the internet. +Only the sidecar is exposed, and only to your website. + +### What it deliberately does not do + +- **It never restarts or manages ServUO.** Your shard keeps starting the way it always has. The + installer refuses to run while ServUO is up, and tells you when a restart is required. +- **It never deletes anything from your server tree.** The overlay sync only adds and overwrites. +- **It never contacts your website.** It prints four values for you to paste into Admin → Shard. +- **It never edits stock ServUO files without asking.** That is the opt-in + [patch tier](#4-the-patch-tier-optional), and skipping it still leaves you with a working bridge. + +--- + +## Before you begin + +| Requirement | Detail | +|---|---| +| A working ServUO install | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. | +| ServUO **57.4** *(patch tier only)* | The base install works on any reasonably current ServUO. The patch tier is verified against stock 57.4 only, and is skipped with a warning on anything else. | +| ServUO **stopped** | `ServUO.exe` holds a lock on `Scripts.dll` and writes `Saves/` on exit. The installer refuses to deploy under a running shard. | +| Administrator / root | It writes into system directories and registers a service. | +| Outbound HTTPS | To `gitea.whitlocktech.com`, to fetch the bundle and the two artifacts. Nothing inbound is needed, and no Gitea account or git client is required. | +| The sidecar on the **same host** as the shard | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — the loopback socket *is* the trust boundary for inbound commands. | +| Admin access to your Runic Gateway site | The last step is pasting four values into Admin → Shard. | + +**Back up first.** The overlay overwrites `Scripts/Scripts.csproj` (a stock file), and the patch +tier edits stock sources. A copy of `Scripts/` and `Config/` before you start costs nothing. + +--- + +## 1. Download and verify + +Releases are **unsigned**. There is no code-signing certificate and no notarization, so the +`SHA256SUMS` file published beside every artifact is the whole trust anchor — check it. + +Download the installer for your OS, plus `SHA256SUMS`, from the +[installer releases page](https://gitea.whitlocktech.com/RunicGateway/installer/releases): + +``` +runicgateway-installer-linux-x86_64 +runicgateway-installer-windows-x86_64.exe +SHA256SUMS +``` + +**Linux** + +```bash +sha256sum -c SHA256SUMS --ignore-missing +chmod +x runicgateway-installer-linux-x86_64 +``` + +**Windows** (PowerShell) + +```powershell +(Get-FileHash .\runicgateway-installer-windows-x86_64.exe -Algorithm SHA256).Hash +Get-Content .\SHA256SUMS # compare the line for this file, case-insensitively +``` + +Windows will show a **SmartScreen "Windows protected your PC"** prompt on first run, because the +binary is unsigned and unknown. Once you have verified the checksum above: *More info* → +*Run anyway*. If you would rather not, Appendix A's manual path uses no unsigned binary except the +sidecar itself, which you verify the same way. + +The installer applies the same standard to everything **it** downloads: each artifact's SHA256 is +checked against the value recorded in the bundle manifest — which CI computed after verifying it +against the publishing repo's own `SHA256SUMS` — and a mismatch aborts the run. + +### What it installs is a bundle, not "latest" + +The three components version independently but must agree on one wire protocol, so CI publishes a +[**bundle**](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md): +one exact, protocol-checked pair of sidecar + overlay versions. The installer resolves that at run +time rather than hardcoding versions or blindly taking each repo's newest release. + +Consequences worth knowing: + +- A sidecar patch release does **not** mean re-downloading the installer. The bundle is data. +- `--bundle ` (e.g. `--bundle 2026.08.04`) pins an exact past combination, so a reinstall six + months from now reproduces today's install rather than tomorrow's. + +--- + +## 2. Run it + +```bash +sudo ./runicgateway-installer-linux-x86_64 install +``` + +```powershell +# Windows: from an elevated PowerShell +.\runicgateway-installer-windows-x86_64.exe install +``` + +Run `install --verify` first if you want to see exactly what would change and write nothing — the +same idea as `deploy.ps1 -Verify`, which developers of the plugin use. + +The installer **does not install itself.** Keep the binary somewhere sensible on the host (it is +one file); `doctor`, `update` and `uninstall` are run from it later. Examples below shorten it to +`runicgateway`. + +### What it asks + +1. **Your ServUO root** — detected if the installer is run from inside it or from an obvious + sibling, otherwise prompted. A directory qualifies only if it contains `ServUO.exe`, `Scripts/` + and `Config/`. +2. **Whether to apply the patch tier** — off unless you say yes, and not offered at all if your + ServUO is not 57.4. See [§4](#4-the-patch-tier-optional). +3. **The hostname your website should use to reach this machine** — used only to compose the two + URLs it prints at the end. The sidecar's bind address is frequently `127.0.0.1` or `0.0.0.0`, + neither of which is something to hand to a website. +4. **Your site's URL** — used only to print a clickable link to its Admin → Shard page. The + installer never contacts your website. + +### An illustrative run + +``` +Runic Gateway installer — bundle 2026.08.04 (protocol 3) + + ServUO /opt/ServUO (57.4) + Shard process not running + Overlay servuo-plugins v0.1.1 protocol 3 + Sidecar uo-link v1.1.0 protocol 3 + +✓ overlay tarball verified sha256 75dc6d6c… +✓ sidecar binary verified sha256 27d491ef… + +Overlay sync + ADD Config/Bridge.cfg + ADD Scripts/Custom/Bridge/*.cs (22 files) + CHANGE Scripts/Scripts.csproj + deployed. add=23 change=1 unchanged=0 + +Patch tier skipped (not selected) + Without it: no vendor.sale events, no in-game moderation audit forwarding. + +uo-link + binary /usr/bin/runicgateway-link + config /etc/runicgateway/sidecar.toml (created) + database /var/lib/runicgateway/uo-link.db + service runicgateway-link.service enabled, running + +Recorded /etc/runicgateway/install.json + +Scripts.csproj changed — ServUO rebuilds Scripts.dll on next boot. +Start your shard when ready; the installer does not start it for you. +``` + +Then the [token handoff](#5-connect-the-website). + +### Commands and flags + +The surface this guide specifies. Each command is idempotent: a second run with nothing new to do +reports "unchanged" and writes nothing. + +| Command | What it does | +|---|---| +| `install` | The full run above. | +| `doctor` | Diagnoses an existing deployment end to end — see [§7](#7-day-two). | +| `update` | Re-resolves the bundle; updates the sidecar (replace + restart) and the overlay (re-sync + tell you to restart ServUO). | +| `uninstall` | Removes only what the installer exclusively owns; prints — never performs — anything inside your ServUO tree. | + +| Flag | Applies to | Meaning | +|---|---|---| +| `--verify` | `install`, `update` | Dry run. Report every change that would be made; write nothing. | +| `--servuo ` | `install`, `doctor`, `update` | Name the ServUO root instead of detecting or prompting. | +| `--bundle ` | `install`, `update` | Pin an exact published bundle instead of the current one. | +| `--patches` / `--no-patches` | `install` | Decide the patch tier non-interactively. `--patches` still refuses on a non-57.4 tree. | +| `--host ` | `install` | The hostname to print in the website URLs. | +| `--site-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. | +| `--purge` | `uninstall` | Also delete `sidecar.toml` and `uo-link.db`, which are otherwise kept. | + +--- + +## 3. Where everything lands + +**Linux** + +| Path | What | +|---|---| +| `/usr/bin/runicgateway-link` | The sidecar binary | +| `/etc/runicgateway/sidecar.toml` | Sidecar config, including the auth token | +| `/etc/runicgateway/install.json` | What the installer deployed: versions, commit, per-file hashes, applied patches, timestamps | +| `/etc/runicgateway/patches/` | Copies of any patches applied, so `uninstall` can print the exact hunks long after the release tarball is gone | +| `/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 | + +**Windows** + +| Path | What | +|---|---| +| `%ProgramFiles%\RunicGateway\uo-link-sidecar.exe` | The sidecar binary | +| `%ProgramData%\RunicGateway\sidecar.toml` | Sidecar config, including the auth token | +| `%ProgramData%\RunicGateway\install.json` | As above | +| `%ProgramData%\RunicGateway\patches\` | As above | +| `%ProgramData%\RunicGateway\uo-link.db` | The sidecar's SQLite store | +| Service `RunicGatewayLink` | Automatic start, restart on failure | + +**Inside your ServUO tree** (added by the overlay sync — 24 files): + +``` +Config/Bridge.cfg every bridge setting, heavily commented +Scripts/Custom/Bridge/*.cs 22 files: the plugin itself +Scripts/Scripts.csproj OVERWRITES a stock file (see below) +``` + +Both service definitions pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly. The sidecar's own +defaults are relative to its working directory, and a service manager's working directory is not +somewhere you want a database — on Windows it can be `%SystemRoot%\System32` or, under +`C:\Program Files\`, a silently redirected VirtualStore copy. + +> **`Scripts.csproj` is overwritten deliberately.** The stock file omits `Scripts/Custom/`, so the +> plugin would sit in the tree and never compile — and ServUO would not tell you, because it +> ignores the script build's exit code and silently reloads the previous `Scripts.dll`. That +> failure mode is the reason [§6](#6-start-servuo-and-verify) exists. + +--- + +## 4. The patch tier (optional) + +Most of the plugin ships as **added** files, which is why the base install is a safe file copy. Two +features cannot: they need edits to stock ServUO sources, because the events they depend on do not +exist. + +| Patch | Edits | Gives you | Rebuild needed | +|---|---|---|---| +| `playervendor-sale-eventsink.patch` + `playervendor-sale-gump.patch` | `Server/EventSink.cs`, `Scripts/Gumps/PlayerVendorGumps.cs` | `vendor.sale` events — player-vendor purchases with buyer, owner, price and commission, which is what cheat detection needs | **Core solution rebuild** (`dotnet build ServUO.sln`) — the dynamic script build is not enough | +| `commandlogging-event.patch` | `Scripts/Commands/Logging.cs` | In-game moderation actions (`[ban`, `[kick`, `[bcast`) forwarded to the website's moderation log as `admin.audit` | Script build only — a shard restart is enough | + +Each patch has a companion `.cs` file that is copied **only after** its patch applies, because it +references symbols the patch introduces. That is why they are not in the base overlay: shipping them +unconditionally would break the build on every unpatched install. + +How the installer handles it: + +- **Opt-in.** The base install completes without it, and declining is a supported outcome, not a + degraded one. +- **Dry-run first, always.** Every patch is checked (`git apply --check`) before anything is + applied, and reported per patch. Most real shards are hand-modified; a patch that does not apply + is expected, not alarming. +- **All or nothing per feature.** The two vendor-sale patches are one unit and are applied together + or not at all. +- **Skipped entirely on a ServUO that is not 57.4**, with a warning. Unverified diffs are never + applied to an unknown tree. +- **Recorded, and the `.patch` files cached**, so re-runs stay idempotent and `uninstall` can print + the exact hunks to revert. + +If it is skipped or fails, you lose exactly two things — **`vendor.sale` events** and **in-game +moderation audit forwarding**. Everything else works. You can apply the patches later by hand (see +`patches/README.md` in the tarball) and re-run `install` to record it. + +--- + +## 5. Connect the website + +The installer ends a successful run by printing the one manual step it cannot do for you: + +``` +Runic Gateway is installed. + +One manual step remains — connect the website to this sidecar: + + Base URL http://shard.example.com:8080 + WebSocket URL ws://shard.example.com:8080/ws + Protocol version 3 + Auth token 4f9c… (also in /etc/runicgateway/sidecar.toml) + +Paste these into Admin → Shard on your Runic Gateway site: + https://your-site.example/admin/shard + +The token is write-only once saved — the site will never show it back to you. +``` + +Every value there comes from asking the installed sidecar itself (`--print-config`), not from a +log file or a guess, so it cannot drift from what the service actually runs. + +On your site, sign in as an administrator and open **Admin → Shard (uo-link)**: + +| Field on the page | Paste | +|---|---| +| Enable the shard integration | ✔ on | +| Base URL (REST) | the **Base URL** line | +| WebSocket URL (feed) | the **WebSocket URL** line | +| Auth token | the **Auth token** line | +| Protocol | the **Protocol version** line (`3`) | + +Saving restarts the site's ingest client, so the change takes effect immediately. The token is +AES-GCM encrypted at rest and **never returned to any client** — losing it means reading it back +from `sidecar.toml` on the shard host, not from the website. + +### If your website is on a different machine + +The sidecar binds `127.0.0.1:8080` by default, which is reachable only from the shard host. If your +website runs elsewhere, you must widen the bind — and then narrow the access: + +1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` (or a specific LAN address) and restart the + service. +2. **Firewall port 8080 to your website's address only.** The auth token is always on, but it + travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS. +3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse proxy or a + VPN/WireGuard link, and give the website the proxied `https://` / `wss://` URLs. + +The `[shard] bind` line is a different matter: leave it on `127.0.0.1:7788`. That socket accepts +*inbound commands* to the game, and being loopback-only is what makes that safe. + +--- + +## 6. Start ServUO and verify + +Start your shard the way you always do. Then confirm the bridge is actually live — not merely +installed. **A successful file copy is not a working bridge**: ServUO shells out to `dotnet build`, +prints the output, ignores the exit code, and reloads the existing `Scripts.dll`, so a broken script +build looks exactly like a clean boot. + +**a. Watch the boot output.** You want to see the build succeed *and* the bridge announce itself: + +``` +Core: Compiling scripts... +Build succeeded. +[Bridge] enabled=True endpoint=127.0.0.1:7788 queueCap=10000 sweeps(stat=30s decay=60s … +``` + +If you scrolled past it, force the question: + +```bash +cd +dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64 # must be 0 errors +``` + +**b. Ask the shard, in game.** As an Administrator: + +``` +[bridge status +``` + +It reports the config plus `connected=True depth=0 sent=… dropped=0 …`. `connected=False` means the +shard cannot reach the sidecar; `dropped` climbing means the sidecar is wedged and the shard is +shedding events rather than stalling — which it is designed to do. `[bridge reload` re-reads +`Bridge.cfg` without a restart; `[bridge sweepnow` forces one pass of every stream. + +**c. Ask the sidecar.** `/health` needs no auth, so it is safe to curl from a terminal: + +```bash +curl -s http://127.0.0.1:8080/health +{"status":"ok","protocol":3,"plugin_connected":true,"database":"ok","uptime":"2m","last_event":"2026-08-04T18:22:10.412Z"} +``` + +`plugin_connected: true` is the one that matters — it is the only value in this whole guide that +distinguishes "files copied" from "the bridge works". + +**d. Ask the website.** The public site should stop showing the shard as offline, and live events +should appear on the admin dashboard within seconds. + +--- + +## 7. Day two + +### `runicgateway doctor` + +The command that makes this supportable. Run it before asking anyone for help — its output is the +first thing a maintainer will want. + +``` +✓ ServUO found /opt/ServUO (57.4) +✓ Overlay in sync 24 files, all hashes match install.json +⚠ Patch tier 1 of 3 applied — vendor.sale unavailable +✓ uo-link installed 1.1.0 +✓ Service running, enabled +✓ Sidecar reachable 127.0.0.1:8080 /health ok +✓ Protocol sidecar 3 = overlay manifest 3 +✗ Shard connected no shard has dialed in since boot +``` + +Three of those rows come from asking the installed sidecar (`--version`, `--print-config`) rather +than from reading `install.json`, so `doctor` reports what the binary would actually do — including +which config and database file the *service* resolves — rather than what the installer believes it +was told. The overlay row compares live file hashes against both `install.json` and the release +manifest, which is how it tells "you edited a deployed file" from "the overlay moved on". + +### `runicgateway update` + +Re-resolves the bundle and moves both halves to a combination whose protocol versions were checked +together — never to two independently-latest artifacts that may disagree. + +- **Sidecar**: download → verify → replace binary → restart service. No shard downtime. +- **Overlay**: download → verify → re-sync → record the new commit → **tell you to restart ServUO.** + It does not restart your shard. + +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. + +### `runicgateway uninstall` + +Removes what it exclusively owns, and **prints** everything else. The installer cannot know what you +have changed in your own server tree since deployment, so an automatic revert risks silently eating +your work. + +| | | +|---|---| +| **Removed** | The sidecar binary, its service entry, `install.json`, the cached patch set | +| **Kept** | `sidecar.toml` and `uo-link.db` — config and history survive (`--purge` drops them) | +| **Printed, not done** | Every overlay file deployed into your ServUO tree, by path, for you to delete | +| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs` and `Logging.cs`, for you to revert | + +The report is also written to a file, so it survives the scrollback. + +--- + +## Troubleshooting + +| Symptom | Cause and fix | +|---|---| +| **"ServUO is running — stop it before installing"** | Correct, and not overridable. `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit; deploying underneath it corrupts one or both. Stop the shard, install, start it again. | +| Shard boots clean but nothing reaches the site | The classic silent failure: ServUO ignores the script build's exit code and reloaded a **stale `Scripts.dll`**. Run `dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64` and read the errors it prints. | +| `[bridge status` says `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service is running, and that `[shard] bind` in `sidecar.toml` matches `Host`/`Port` in `Bridge.cfg`. | +| `[bridge` is not a command | The plugin did not compile, or `Bridge.cfg` has the bridge disabled. See the row above. | +| Website says the shard is offline; `/health` is fine locally | The website cannot reach port 8080 — bind address or firewall. See [§5](#if-your-website-is-on-a-different-machine). Note that the site is *designed* to render normally with the shard offline, so this fails quietly by design. | +| Website logs `409` from the sidecar | Protocol mismatch: the number in Admin → Shard does not match the sidecar's. The sidecar rejects rather than mis-parsing. Set the field to what `/health` reports (`protocol`). If the *sidecar* and *overlay* disagree, you have a hand-assembled pair — reinstall from a bundle. | +| `401` from the sidecar | Wrong or missing auth token. Read the live one back with `uo-link-sidecar --print-config --config `; do not retype it from a screenshot. | +| A patch will not apply | Expected on a hand-modified shard. The base install is unaffected; you lose only the two features in [§4](#4-the-patch-tier-optional). Apply the hunks by hand if you want them. | +| `vendor.sale` events never arrive despite patching | The `EventSink.cs` patch is a **core** change. A shard restart is not enough — rebuild the solution (`dotnet build ServUO.sln`). | +| Sidecar writes its database somewhere unexpected | A relative `[store] path` resolves against the directory holding `sidecar.toml` — not the working directory. Run `--print-config` to see the absolute path it will actually use. | +| Token leaked into a log or a screenshot | Clear `[web] auth_token` in `sidecar.toml`, restart the service (a new token is generated and saved), read it back with `--print-config`, and re-save it in Admin → Shard. | + +--- + +## Appendix A — installing by hand + +This is what the installer automates. It works today, on the current releases, and is the fallback +whenever you would rather not run an unsigned binary. + +Throughout: `` is your ServUO root, and **the shard is stopped**. + +### A1. Fetch the bundle (so you install a checked pair) + +```bash +curl -s https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json +``` + +It names the sidecar tag, the overlay tag, their agreed `protocol`, and the SHA256 of every asset. +Use those versions together; that pairing is the only thing CI has verified. + +### A2. Deploy the plugin overlay + +```bash +curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/runicgateway-overlay-0.1.1.tar.gz +curl -LO https://gitea.whitlocktech.com/RunicGateway/servuo-plugins/releases/download/v0.1.1/SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing # must say: OK + +tar xzf runicgateway-overlay-0.1.1.tar.gz # → runicgateway-overlay/ +cd runicgateway-overlay +cat manifest.json # version, commit, protocol, per-file hashes + +cp -r overlay/. / # adds files; overwrites Scripts/Scripts.csproj +``` + +On Windows, `Expand-Archive` does not read `.tar.gz`; use `tar.exe` (shipped with Windows 10+) and +`Copy-Item -Recurse -Force`. Plugin developers have `deploy.ps1` in the source repo, which does the +same copy with a hash diff and a `-Verify` dry run — it is not shipped in the tarball. + +The overlay only ever **adds or overwrites**. Nothing in your tree is deleted. + +*Optional — the patch tier* (stock ServUO 57.4 only; see `patches/README.md` in the tarball for the +full explanation): + +```bash +cd +git apply --check patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch +git apply patches/playervendor-sale-eventsink.patch patches/playervendor-sale-gump.patch +cp patches/BridgeVendorSale.cs Scripts/Custom/Bridge/ +dotnet build ServUO.sln # REQUIRED — EventSink.cs is a core file + +git apply --check patches/commandlogging-event.patch +git apply patches/commandlogging-event.patch +cp patches/BridgeModerationAudit.cs Scripts/Custom/Bridge/ +``` + +`git apply` works in a plain directory — the shard does not need to be a git repo. If you use +`patch` instead, note the core files are CRLF: use `patch --binary`. + +### A3. Install the sidecar + +```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/SHA256SUMS +sha256sum -c SHA256SUMS --ignore-missing + +sudo install -m 0755 uo-link-sidecar-linux-x86_64 /usr/bin/runicgateway-link +sudo mkdir -p /etc/runicgateway /var/lib/runicgateway +``` + +Provision the config and read back the token in one step. `--print-config` writes the file if it is +missing, generates the auth token if there is none, and prints the resolved settings as JSON — it is +the supported alternative to scraping the startup log: + +```bash +sudo UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db \ + /usr/bin/runicgateway-link --print-config --config /etc/runicgateway/sidecar.toml +``` + +```json +{ + "component": "uo-link-sidecar", + "version": "1.1.0", + "protocol": 3, + "config_path": "/etc/runicgateway/sidecar.toml", + "config_created": true, + "token_generated": true, + "shard": { "bind": "127.0.0.1:7788" }, + "web": { + "bind": "127.0.0.1:8080", + "ws_path": "/ws", + "auth_required": true, + "auth_token": "4f9c…" + }, + "store": { "path": "/var/lib/runicgateway/uo-link.db" } +} +``` + +`config_created` and `token_generated` tell you whether *this* run provisioned anything — the values +alone cannot distinguish a fresh install from a re-read. **The output contains the auth token in +clear text**: keep it out of shell transcripts, logs and support bundles. + +### A4. Register the service + +**Linux** — `/etc/systemd/system/runicgateway-link.service`: + +```ini +[Unit] +Description=Runic Gateway uo-link sidecar +After=network.target + +[Service] +Type=simple +User=runicgateway +Environment=UOLINK_CONFIG=/etc/runicgateway/sidecar.toml +Environment=UOLINK_DB_PATH=/var/lib/runicgateway/uo-link.db +ExecStart=/usr/bin/runicgateway-link +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo useradd --system --no-create-home runicgateway +sudo chown -R runicgateway /var/lib/runicgateway /etc/runicgateway +sudo systemctl daemon-reload +sudo systemctl enable --now runicgateway-link +systemctl status runicgateway-link +``` + +**Windows** (elevated PowerShell) — binary under `%ProgramFiles%`, data under `%ProgramData%`: + +```powershell +New-Item -ItemType Directory -Force "$env:ProgramFiles\RunicGateway", "$env:ProgramData\RunicGateway" | Out-Null +Copy-Item .\uo-link-sidecar-windows-x86_64.exe "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" + +& "$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe" --print-config --config "$env:ProgramData\RunicGateway\sidecar.toml" + +sc.exe create RunicGatewayLink binPath= "\"$env:ProgramFiles\RunicGateway\uo-link-sidecar.exe\"" start= auto +sc.exe failure RunicGatewayLink reset= 86400 actions= restart/5000 +[Environment]::SetEnvironmentVariable('UOLINK_CONFIG', "$env:ProgramData\RunicGateway\sidecar.toml", 'Machine') +[Environment]::SetEnvironmentVariable('UOLINK_DB_PATH', "$env:ProgramData\RunicGateway\uo-link.db", 'Machine') +sc.exe start RunicGatewayLink +``` + +Machine environment variables are read at service start, so set them before starting — and never +leave the config path to the default, which is relative to the service's working directory. + +### A5. Connect the website, start the shard, verify + +Exactly as in [§5](#5-connect-the-website) and [§6](#6-start-servuo-and-verify): paste the four +values into Admin → Shard, start ServUO, then check `[bridge status` in game and `/health` on the +sidecar. + +### A6. Updating by hand + +Re-read `current.json`, and if either version moved: replace the sidecar binary and restart its +service; re-extract the overlay tarball over your tree and restart ServUO. Keep the two in step — +`current.json` is the only statement that a given pair speaks the same protocol. + +--- + +## Appendix B — `sidecar.toml` reference + +Written on first run with a generated token. Environment variables override the file; the file +overrides these defaults. + +```toml +[shard] +bind = "127.0.0.1:7788" # where the SHARD dials in. Keep this on loopback. + +[web] +bind = "127.0.0.1:8080" # where the WEBSITE connects. Widen only with a firewall in front. +auth_token = "…" # generated if blank; the website's Admin → Shard "Auth token" + +[store] +path = "uo-link.db" # relative paths resolve against this file's directory, not the CWD +``` + +| Environment variable | Overrides | +|---|---| +| `UOLINK_CONFIG` | Which config file to read (`--config ` outranks it) | +| `UOLINK_SHARD_BIND` | `[shard] bind` | +| `UOLINK_WEB_BIND` | `[web] bind` | +| `UOLINK_WEB_TOKEN` | `[web] auth_token` | +| `UOLINK_DB_PATH` | `[store] path` | + +| Sidecar command | Output | +|---|---| +| `uo-link-sidecar --version` | `uo-link-sidecar 1.1.0 (protocol 3)` | +| `uo-link-sidecar --print-config [--config PATH]` | The JSON in [A3](#a3-install-the-sidecar). Provisions on first run. **Contains the token.** | +| `uo-link-sidecar --help` | Usage. An unrecognized argument exits `2` rather than starting a sidecar you did not ask for. | + +Authentication is **always on**: a blank token is generated and written back, so the web surface is +never unauthenticated. `/health` is the one unauthenticated route, so monitoring can reach it. + +--- + +## Appendix C — `Config/Bridge.cfg` settings worth reviewing + +The file is deployed heavily commented and every setting has a working default — you can leave it +entirely alone. These are the ones most shards want to look at once. Run `[bridge reload` after +editing; endpoint changes take effect on the next reconnect. + +| Setting | Default | Why you might change it | +|---|---|---| +| `LinkUrl` | `https://yoursite/link` | Shown in game when a player runs `[link` to connect their account. **Set this to your site.** | +| `PublicConnectAddress` | *(blank)* | The one connection detail the bridge will publish, e.g. `play.myshard.com,2593`. Blank omits it; `Server.cfg`'s address is **never** published automatically. | +| `AdminWriteEnabled` | `false` | Opt-in staff write plane: kick/ban/broadcast from the website. Authorization is enforced on the website; `AdminAccessFloor` is the shard-side floor that even a compromised sidecar cannot cross. | +| `MarketEnabled`, `MarketSweepSeconds`, `MarketSweepBatch` | `true`, `60`, `25` | The player-vendor index. Coverage takes `ceil(vendors / batch) × seconds` — 500 vendors is one full pass every 20 minutes at the defaults. | +| `PointsLeaderboardEnabled`, `PointsTopN`, `PointsSystems` | `true`, `10`, *(all shown on the loyalty gump)* | Standings boards. One frame **per system**, and ServUO carries ~25 of them, so a large `TopN` multiplies. | +| `RulesetEnabled`, `RulesetIncludeSchedule` | `true`, `true` | Publishes your ruleset (expansion, caps, systems on/off) to the site's rules page. Turn the schedule off if you would rather not advertise a predictable restart window. | +| `SignupMode` | `hybrid` | Which side may mint accounts — `website`, `game`, or `hybrid`. Pair `website` with `Accounts.AutoCreateAccounts=false`, or an in-game login still creates accounts. | +| `QueueCap` | `10000` | Outbound queue cap. On overflow the plugin **drops oldest** and counts drops, because a stalled sidecar must never take the shard down with it. | + +Sweep intervals (`StatSweepSeconds`, `DecaySweepSeconds`, `EconomySweepSeconds`, and the rest) trade +freshness against Core-thread time. The measured cost is small — a vitals sweep is 0.0015 ms per +character, so 1000 online players is ~1.5 ms per pass — but there is rarely anything to gain by +hurrying them. + +--- + +## Where to go next + +| Doc | What | +|---|---| +| [PLAN.md](PLAN.md) | The installer's design of record — phases, locked decisions, the bundle model | +| [`bundles/README.md`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/README.md) | The compat matrix: what a bundle is and how it is composed | +| [link/INTEGRATION.md](../link/INTEGRATION.md) | The sidecar's HTTP/WS API — for anyone integrating something other than the website | +| [link/ADMIN_CONTROLS.md](../link/ADMIN_CONTROLS.md) | The staff write plane in detail, before you turn `AdminWriteEnabled` on | +| [website/SHARD_VISIBILITY.md](../website/SHARD_VISIBILITY.md) | Which shard data each audience sees, configured on the website | +| [link/SHARD_PREREQS.md](../link/SHARD_PREREQS.md) | A worked example of diagnosing a shard whose scripts silently stopped compiling | diff --git a/installer/PLAN.md b/installer/PLAN.md index fd10c38..b0f4b91 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -1,16 +1,19 @@ # 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 +Status: **Phase 0 complete.** Every prerequisite in another repo has landed, the installer repo +publishes the bundle manifest, and [`INSTALL.md`](INSTALL.md) now specifies the operator-facing run +— so *what* the installer installs and *what using it looks like* both exist ahead of the binary. +No installer code exists yet; **Phase 1 is next.** 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 | +| 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 | ✅ Merged — [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`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/main/bundles/current.json) | +| 0.4 This file + `INSTALL.md` | 🟨 In review — [docs#87](https://gitea.whitlocktech.com/RunicGateway/docs/pulls/87). [`INSTALL.md`](INSTALL.md) is the operator guide, written before the binary because it *is* the specification of the run | | — 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)) | --- @@ -177,7 +180,7 @@ Runic Gateway Installer v1.0.0 ├── runicgateway-installer-linux-x86_64 └── SHA256SUMS -uo-link v0.x.y (existing release, extended) +uo-link v1.1.0 (existing release, extended) ├── uo-link-sidecar-windows-x86_64.exe ├── uo-link-sidecar-linux-x86_64 ├── runicgateway-link__amd64.deb (Phase 5) @@ -298,15 +301,84 @@ Repo work that must land before an installer can exist. 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 ` 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. + As built ([`INSTALL.md`](INSTALL.md)) — written *before* the binary on purpose. Everything it + installs is already released (items 1–3), so the guide is not speculation about a tool that + might exist; it is the specification of what the run asks, where it writes, what it prints, and + what the operator does next. Phase 1–4 implement it. + + - **It is useful before the installer exists.** Appendix A is the same deployment done by hand — + bundle fetch, tarball verify + overlay copy, the optional patch tier, `--print-config` + provisioning, and a systemd unit / `sc create` service — composed from the released artifacts' + actual contents and the sidecar's config and CLI source rather than from memory. That appendix + doubles as **Phase 1's acceptance test**: walking it end to end on a real shard is what proves + the automated path has nothing left to discover. + - **The installer does not install itself.** §5's `runicgateway doctor` sketch implied a name on + `PATH`; nothing places one there, and adding self-installation would give the tool a second + lifecycle to manage. The guide names the downloaded artifact, says to keep it, and shortens it + in later examples. + - **The flag surface got fixed here**, because a guide cannot describe a run in the abstract: + `--verify`, `--bundle`, `--purge` were already named by §5/§7; `--servuo`, `--patches` / + `--no-patches`, `--host`, `--site-url` and `--yes` are the remainder, chosen so every prompt + in §6's handoff has a non-interactive equivalent and an unattended install is expressible. + - **A modified `Bridge.cfg` must survive an update** — see Phase 1, where this changes the sync + rule inherited from `deploy.ps1`. + - **Remote-website deployments needed an answer.** `[web] bind` defaults to `127.0.0.1`, which + only works when the site runs on the shard host. The guide says to widen it, firewall it to + the website's address, and front it with TLS or a VPN off a trusted network — because the + token is always required but travels as a plain bearer token over HTTP. `[shard] bind` stays + on loopback, since that socket carries inbound commands *into* the game. + ### Phase 1 — installer core - ServUO root detection and validation (`ServUO.exe`, `Scripts/`, `Config/`), with version detection and an explicit refusal when the ServUO process is running. - Overlay sync: fetch tarball → verify SHA256 → hash-compare against the server tree → add/change, **never delete**. Port of `deploy.ps1` semantics including its `-Verify` dry run (`--verify`). +- **One deviation from `deploy.ps1`: an operator-modified `Config/Bridge.cfg` is reported, not + overwritten.** `deploy.ps1` overwrites every file whose hash differs, which is right for a + developer redeploying their own tree and wrong for an operator who has set `LinkUrl`, + `PublicConnectAddress` and sweep intervals — an `update` would silently revert the shard's entire + configuration. `install.json` records the hash deployed, so the installer can distinguish "the + operator edited this" from "the overlay moved on" (§7.0) and act only on the second. The rule is + specific to `Bridge.cfg`: it is the only file in the overlay that is *meant* to be edited in + place, and it carries no code, so a stale copy cannot break the build. Every `.cs` file and + `Scripts.csproj` still overwrite unconditionally. - Write `install.json`: component, version, source commit, per-file hashes, applied patches, timestamp. - Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing. @@ -334,9 +406,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 24 files, all hashes match install.json ⚠ Patch tier 1 of 3 applied — vendor.sale unavailable -✓ uo-link installed 0.3.0 +✓ uo-link installed 1.1.0 ✓ Service running, enabled ✓ Sidecar reachable 127.0.0.1:8080 /health ok ✓ Protocol sidecar 3 = overlay manifest 3 @@ -357,8 +429,9 @@ deliberately: - **uo-link**: compare the bundle's version against what is installed → download → verify checksum → replace binary → restart service. -- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync → record commit → - tell the operator ServUO must restart (the installer does not restart the shard). +- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync (leaving a modified + `Bridge.cfg` alone — Phase 1) → record commit → tell the operator ServUO must restart (the + installer does not restart the shard). Because both come from one bundle, an update always moves to a combination whose protocol versions were checked together, rather than to two independently-latest artifacts that may disagree. @@ -471,15 +544,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": "0.3.0", "sha256": "a91f..." }, - "overlay": { "version": "0.1.0", "commit": "968b526", "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 ` 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. @@ -487,19 +578,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-.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. 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 | +| `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 @@ -507,6 +632,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 ''`. 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: