1 Commits

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 06:09:58 -05:00

View File

@@ -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 | | 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` | | 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 | | 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 <tag>` pins one. Component releases regenerate JSON, not the installer binary |
| Token handoff | **Print token + prefilled admin URL** at the end of the run | | 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 | | 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 | | 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<ver> (new release, Phase 0)
└── SHA256SUMS └── 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 ```bash
scp runicgateway-installer-linux-x86_64 user@server:/tmp/ scp runicgateway-installer-linux-x86_64 user@server:/tmp/
chmod +x runicgateway-installer-linux-x86_64 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 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, 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. 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 4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
shape is settled. 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 The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge
actually works" (§2.1). 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. replace binary → restart service.
- **plugin overlay**: download the newer overlay tarball → verify → re-sync → record commit → tell - **plugin overlay**: download the bundle's overlay tarball → verify → re-sync → record commit →
the operator ServUO must restart (the installer does not restart the shard). 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 `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 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: 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`. `X-UOLink-Version` on every response; a mismatch is rejected `409`.
- **website** — stores an expected protocol version in `uoLinkConfig` (admin-managed). - **website** — stores an expected protocol version in `uoLinkConfig` (admin-managed).
- **plugin overlay** — has no queryable version before ServUO boots. The overlay release - **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 `manifest.json` declares it, and `install.json` records what was deployed.
recorded overlay protocol version against the sidecar's live one.
**Open risk:** the v3 cutover is mid-flight — protocol work landed on `edge` branches with the ### 7.1 The bundle manifest
`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 **The bundle is the compat matrix.** Rather than the installer hardcoding versions or blindly
artifacts declare. See `docs/link/v3.md`. 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 <tag>` 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`.
--- ---