Merge pull request 'docs(installer): record Phase 0.3 — the bundle CI and where bundles live' (#85) from docs/installer-phase-0.3 into main
Reviewed-on: #85
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
# Runic Gateway Installer — plan
|
# Runic Gateway Installer — plan
|
||||||
|
|
||||||
Status: **Phase 0 in progress.** No installer code exists yet. This document is the design of record;
|
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
|
it supersedes the informal overview it grew out of, which described a ServUO integration that does
|
||||||
not match how `servuo-plugins` actually ships (see
|
not match how `servuo-plugins` actually ships (see
|
||||||
[Corrections](#corrections-to-the-original-overview)).
|
[Corrections](#corrections-to-the-original-overview)).
|
||||||
@@ -8,9 +11,9 @@ not match how `servuo-plugins` actually ships (see
|
|||||||
| Phase 0 item | State |
|
| 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.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.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 | ⬜ Not started — **next**; both components it composes now exist |
|
| 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` waits on the shape settling |
|
| 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)) |
|
| — 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)) |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -298,6 +301,39 @@ 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
|
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
|
component's release workflow. This must exist before Phase 1 is useful, since the installer
|
||||||
resolves what to install *from* the bundle.
|
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
|
4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
|
||||||
shape is settled.
|
shape is settled.
|
||||||
|
|
||||||
@@ -471,15 +507,33 @@ resolving "latest", CI publishes a small manifest naming an exact, checked combi
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"bundle": "2026.08.01",
|
"schema": 1,
|
||||||
|
"bundle": "2026.08.04",
|
||||||
|
"generated": "2026-08-04T16:07:13Z",
|
||||||
"protocol": 3,
|
"protocol": 3,
|
||||||
"link": { "version": "0.3.0", "sha256": "a91f..." },
|
"link": {
|
||||||
"overlay": { "version": "0.1.0", "commit": "968b526", "sha256": "7c3e..." }
|
"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
|
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
|
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.
|
pick up a sidecar patch, and the installer does not accumulate releases whose code is byte-identical.
|
||||||
|
|
||||||
@@ -487,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.
|
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.
|
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`.
|
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
|
### 7.2 What triggers a bundle
|
||||||
|
|
||||||
| Trigger | Why |
|
| 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 |
|
| `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 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 |
|
| `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 |
|
| 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,
|
`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.
|
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
|
### 7.3 Stale-overlay handling: dispatch, don't wait
|
||||||
|
|
||||||
Each component **self-releases on merge to its own `main`**, using the same conventional-commit
|
Each component **self-releases on merge to its own `main`**, using the same conventional-commit
|
||||||
@@ -507,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**
|
`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.
|
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
|
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:
|
`servuo-plugins` main ahead of its latest release *with* releasable commits, it:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user