From 152ffef86e38d8a7c6ace9f26de07c6e19023de4 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Sat, 1 Aug 2026 06:09:58 -0500 Subject: [PATCH] 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 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 --- installer/PLAN.md | 94 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 82 insertions(+), 12 deletions(-) diff --git a/installer/PLAN.md b/installer/PLAN.md index ad89727..a096cfe 100644 --- a/installer/PLAN.md +++ b/installer/PLAN.md @@ -29,6 +29,7 @@ release/start scripts. The installer never writes a launcher. | Code signing | **Unsigned.** `SHA256SUMS` is the trust anchor; SmartScreen/Gatekeeper warnings are expected and documented, as with most self-hosted tooling | | Language | **Rust** — single static binary per OS, reuses the cross-compile pattern already proven in `link/.gitea/workflows/release.yml` | | Plugin source | **Release tarball artifact** — no git and no Gitea credentials on the shard host | +| Composition | **Published bundle manifest** (§7.1). CI names an exact, protocol-checked combination of component versions; the installer fetches it at run time and `--bundle ` pins one. Component releases regenerate JSON, not the installer binary | | Token handoff | **Print token + prefilled admin URL** at the end of the run | | Repo | **New repo**, `RunicGateway/installer`. It deploys *both* other components, so living inside `link/` would invert the dependency | | ServUO version | **Warn and skip.** Patches are verified against stock 57.4 only; on anything else the base install proceeds and the patch tier is skipped with a warning. Forks are the norm in a public audience — refusing outright would block most operators | @@ -167,6 +168,9 @@ servuo-plugins v (new release, Phase 0) └── SHA256SUMS ``` +Binding those together is the **bundle manifest** (§7.1) — published by the installer repo's CI, not +by any component, and the thing the installer actually resolves against. + ```bash scp runicgateway-installer-linux-x86_64 user@server:/tmp/ chmod +x runicgateway-installer-linux-x86_64 @@ -221,7 +225,10 @@ Repo work that must land before an installer can exist. 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. -3. **Decide and document the compat matrix format** (§7). +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. 4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the shape is settled. @@ -267,12 +274,16 @@ 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). -`runicgateway update` — asymmetric by component, deliberately: +`runicgateway update` — resolves the current bundle (§7.1), then acts asymmetrically by component, +deliberately: -- **uo-link**: query the Gitea releases API → compare versions → download → verify checksum → +- **uo-link**: compare the bundle's version against what is installed → download → verify checksum → replace binary → restart service. -- **plugin overlay**: download the newer 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 → 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. `runicgateway uninstall` — **removes only what it exclusively owns, and never edits the ServUO tree.** The installer cannot know what the operator has changed in those files since deployment, so @@ -321,7 +332,7 @@ later without changing anything here. --- -## 7. Version tracking and the compat matrix +## 7. Version tracking, the bundle, and release orchestration Three components version independently, bound by a protocol contract: @@ -329,13 +340,72 @@ Three components version independently, bound by a protocol contract: `X-UOLink-Version` on every response; a mismatch is rejected `409`. - **website** — stores an expected protocol version in `uoLinkConfig` (admin-managed). - **plugin overlay** — has no queryable version before ServUO boots. The overlay release - `manifest.json` declares it, and `install.json` records what was deployed. `doctor` compares the - recorded overlay protocol version against the sidecar's live one. + `manifest.json` declares it, and `install.json` records what was deployed. -**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. See `docs/link/v3.md`. +### 7.1 The bundle manifest + +**The bundle is the compat matrix.** Rather than the installer hardcoding versions or blindly +resolving "latest", CI publishes a small manifest naming an exact, checked combination: + +```json +{ + "bundle": "2026.08.01", + "protocol": 3, + "link": { "version": "3.0.1", "sha256": "a91f..." }, + "overlay": { "version": "2.4.0", "commit": "a81f42c", "sha256": "7c3e..." } +} +``` + +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 +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. + +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. +2. Every referenced asset must exist and its SHA256 must match the publishing repo's `SHA256SUMS`. + +### 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 | +| 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. + +### 7.3 Stale-overlay handling: dispatch, don't wait + +Each component **self-releases on merge to its own `main`**, using the same conventional-commit +engine. Note that "updated since the last release" must mean *releasable* commits — the engine sets +`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. + +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: + +1. fires that repo's release workflow via workflow-dispatch and **does not wait for it**, +2. composes this bundle from the assets that exist right now, +3. writes a loud warning into the job summary. + +The new overlay release lands minutes later on its own and the nightly cron folds it into the next +bundle. This gets the automation without the flaky part: dispatching another repo's workflow is +fine — that workflow still runs its own gates — but *polling* it is not, because Gitea's dispatch +endpoint returns no run handle, so the job would have to guess which run is its own and hold a +runner idle meanwhile. The warning exists so a genuinely broken release workflow surfaces once +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`. ---