ci(bundle): compose and publish the bundle manifest
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
Phase 0 item 3 of docs/installer/PLAN.md (§7.1-§7.3). The installer resolves what to install *from* the bundle, so this has to exist before Phase 1 code is useful. Both components it composes now have releases, which is what unblocked it. Adds .gitea/workflows/bundle.yml — resolve both components' latest releases, run the two compose-time gates, and publish bundles/current.json — plus the first real bundle (2026.08.04: link v1.1.0 + overlay v0.1.1, protocol 3). Bundles are COMMITTED under bundles/, not published as releases. This repo's own releases are the installer binaries, and /releases/latest returns whichever release is newest regardless of kind, so interleaving bundle releases would make "latest" intermittently resolve to a release carrying no installer binary. The push to main needs no new branch-protection exception: release.yml's version-bump commit already requires it. Gate 1 (protocol agreement) reads the sidecar's PROTOCOL_VERSION from sidecar/src/main.rs at the release tag, not from the binary. --print-config would answer, but only for releases from v1.1.0 on, and --bundle <tag> has to be able to recompose an older bundle. It also avoids executing a downloaded artifact and provisioning a throwaway config whose auth token would land in a CI log. The overlay half comes from manifest.json inside the tarball, which is the only statement of that version that exists. Gate 2 (assets) downloads every asset and verifies it against the SHA256SUMS its publishing repo shipped, then records the hash it computed itself. These artifacts are deliberately unsigned, so a hash copied from a file nobody checked would make the whole chain decorative. An asset with no SHA256SUMS entry is caught separately, since `sha256sum -c` passes right over it. Release reads are ANONYMOUS on purpose: they are exactly the requests the shipped installer makes on a host with no Gitea credentials, so a repo flipped to private fails here rather than on an operator's machine. Stale components (§7.3) are dispatched, never awaited — Gitea's dispatch endpoint returns no run handle. "Ahead of its release" counts only releasable commits and excludes merge commits, whose subject quotes the feat/fix title and would otherwise re-dispatch a workflow that correctly declines to run, every night. A run that finds nothing changed writes nothing, comparing everything except `bundle` and `generated` — that is what makes the nightly cron free rather than a dated duplicate every morning. Verified by running the workflow's exact compose steps in a Linux container against the live releases: both gates pass, the composed bundle is the file committed here, a re-run reports changed=false, and the stale-detection filter scores 1 releasable commit for link v1.0.0..main (excluding the merge that quotes it) and 0 for a docs-only range. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
92
bundles/README.md
Normal file
92
bundles/README.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Bundles
|
||||
|
||||
**These files are generated. Do not edit them by hand.**
|
||||
|
||||
A *bundle* names one exact, protocol-checked combination of the two components the installer
|
||||
deploys — a `uo-link` sidecar release and a `servuo-plugins` overlay release. The installer does not
|
||||
hardcode versions and does not resolve "latest" at run time; it fetches one of these documents and
|
||||
installs what it names. **The bundle is the compat matrix.**
|
||||
|
||||
They are written by [`.gitea/workflows/bundle.yml`](../.gitea/workflows/bundle.yml), which composes
|
||||
one whenever a component publishes a release (dispatched by that release's own workflow) and
|
||||
nightly, so a missed dispatch self-heals. A run that finds nothing changed writes nothing.
|
||||
|
||||
See `docs/installer/PLAN.md` §7 for the design.
|
||||
|
||||
## Layout
|
||||
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `current.json` | The bundle the installer uses by default. Always a copy of the newest `bundle-*.json`. |
|
||||
| `bundle-<tag>.json` | Every bundle ever published, kept forever so `--bundle <tag>` stays reproducible. |
|
||||
|
||||
Tags are UTC dates — `2026.08.04`. A second bundle on the same day (a sidecar release in the
|
||||
morning, an overlay release in the afternoon) becomes `2026.08.04.2`, so one tag always names
|
||||
exactly one matrix.
|
||||
|
||||
## How the installer fetches these
|
||||
|
||||
Plain anonymous `GET`s against a public repo. The shard host gets no git and no Gitea credentials
|
||||
(`PLAN.md` §1), so nothing here may require auth:
|
||||
|
||||
```
|
||||
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/current.json
|
||||
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/main/bundles/bundle-2026.08.04.json
|
||||
```
|
||||
|
||||
Bundles are committed rather than published as Gitea releases because this repo's *own* releases are
|
||||
the installer binaries, and `/releases/latest` returns whichever release is newest regardless of
|
||||
kind — interleaving the two would make "latest" intermittently resolve to a release containing no
|
||||
installer binary.
|
||||
|
||||
## Schema
|
||||
|
||||
`schema` is the version of *this document's* shape, and is unrelated to `protocol` (the uo-link wire
|
||||
protocol) or to either component's release version. All three move independently.
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"schema": 1,
|
||||
"bundle": "2026.08.04", // this bundle's tag; what --bundle takes
|
||||
"generated": "2026-08-04T16:07:13Z",
|
||||
"protocol": 3, // the wire protocol both halves speak (gate 1 proved it)
|
||||
|
||||
"link": {
|
||||
"repo": "RunicGateway/link",
|
||||
"tag": "v1.1.0",
|
||||
"version": "1.1.0",
|
||||
"protocol": 3,
|
||||
"assets": { // per-platform: the installer runs on both
|
||||
"linux-x86_64": { "name": "…", "url": "…", "sha256": "…" },
|
||||
"windows-x86_64": { "name": "…", "url": "…", "sha256": "…" }
|
||||
}
|
||||
},
|
||||
|
||||
"overlay": {
|
||||
"repo": "RunicGateway/servuo-plugins",
|
||||
"tag": "v0.1.1",
|
||||
"version": "0.1.1",
|
||||
"commit": "3a52abb…", // recorded into install.json at deploy time
|
||||
"protocol": 3,
|
||||
"servuo": {
|
||||
"min_version": "57.4", // base overlay: only adds files
|
||||
"patches_verified_against": "57.4" // patch tier: skipped with a warning elsewhere
|
||||
},
|
||||
"asset": { "name": "runicgateway-overlay-0.1.1.tar.gz", "url": "…", "sha256": "…" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The two things worth knowing
|
||||
|
||||
**`sha256` is load-bearing, not decorative.** Every artifact Runic Gateway publishes is deliberately
|
||||
**unsigned** (`PLAN.md` §3) — the checksum is the entire trust anchor. Each hash here was computed by
|
||||
CI from the asset it actually downloaded, *after* verifying it against the `SHA256SUMS` the
|
||||
publishing repo shipped beside it. The installer must verify every download against these values and
|
||||
refuse on a mismatch. A bundle whose hashes are trusted but never checked buys nothing.
|
||||
|
||||
**`link.protocol` and `overlay.protocol` are always equal, and that is the point.** The sidecar
|
||||
rejects a protocol mismatch with `409` rather than mis-parsing, so a mismatched pair is a shard
|
||||
emitting into a void. CI refuses to publish one: it reads `PROTOCOL_VERSION` from the sidecar's
|
||||
source at its release tag and the declared `protocol` from the overlay tarball's `manifest.json`, and
|
||||
fails if they differ. The top-level `protocol` is that agreed value.
|
||||
Reference in New Issue
Block a user