docs(installer): add release orchestration and the bundle manifest #81

Merged
whitlocktech merged 1 commits from docs/installer-release-orchestration into main 2026-08-01 21:42:38 +00: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`.
--- ---