Module-rust phase 18, step 2 of docs/modules/rust/PLAN.md §34.2.7 (D146, D147). The compose job now writes three streams on the bundles branch: current.json schema 1, ServUO, exactly as before v2/servuo/ schema 2, game "servuo" v2/rust/ schema 2, game "rust" (Rust-Link + Rust-Plugins) The gates move into .gitea/scripts/compose-bundles.sh, so they can be run by hand against the real release API or a mock without a runner. Each game composes independently: a Rust failure still lets a ServUO bundle publish, and the run goes red afterwards. A game whose repos have never released composes nothing and does not fail, which is where Rust is until its cutover. A ServUO matrix carries the same tag at both schemas, so the first run publishes v2/servuo/bundle-2026.09.15.json rather than a second name for the pair. Schema 1 stops being composed on 2027-01-01 and is left frozen. Tested in debian:12 against the real releases (v2/servuo published as 2026.09.15, schema 1 unchanged, Rust skipped; rerun is a no-op; past the retirement date schema 1 is frozen) and against a mock Gitea for Rust (document composed, protocol mismatch refused, missing manifest key refused, rerun a no-op, ServUO failing independently). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
8.3 KiB
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 for one game: a sidecar release and a game-side payload release. For ServUO that is link
and a servuo-plugins overlay; for Rust it is Rust-Link and a Rust-Plugins plugin. 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 (the logic is
in .gitea/scripts/compose-bundles.sh), 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, and docs/modules/rust/PLAN.md §34.2.2 for
schema 2.
Where they live: the bundles branch
The JSON documents are not in this directory. They are published to a branch of their own,
bundles. There is one
stream per game and schema, each a directory holding the same two kinds of file:
| Directory | Stream |
|---|---|
| (root) | Schema 1, ServUO. What every installer up to v0.2.x reads. Retires 2027-01-01 |
v2/servuo/ |
Schema 2, "game": "servuo" |
v2/rust/ |
Schema 2, "game": "rust" |
| 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. |
Schema 1 retires on 2027-01-01. Until then it is composed exactly as before, beside schema 2, so
every installer already in the field keeps updating. After that date it stops being composed and is
left frozen at its last bundle, never deleted: an old installer still resolves something, and
every bundle-<tag>.json stays pinnable. A ServUO matrix carries the same tag at both schemas.
A schema-2 document names one game rather than both, because the games release on their own schedules: a document naming both would hand a ServUO host a new bundle every time a Rust plugin shipped. A game none of whose repos has released composes nothing, and that is not a failure.
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.
Why a branch rather than main. main is protected and this job is unattended: the pre-receive
hook declines a push from CI, which is not something a nightly cron can resolve. A branch of its own
keeps everything the original choice was for — a reviewable diff, a git history of the compat
matrix, plain anonymous raw URLs, no credentials on the shard host — and needs no protection
exception. Whitelisting a scheduled job for pushes to the default branch would buy nothing this does
not.
This directory keeps the documentation, because that is what belongs on main: the branch carries
data, and only data.
How the installer fetches these
Plain anonymous GETs 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/bundles/current.json
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/bundle-2026.08.04.json
https://gitea.whitlocktech.com/RunicGateway/installer/raw/branch/bundles/v2/rust/current.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 1 (ServUO, retiring)
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.
{
"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 each
"linux-x86_64": { "name": "…", "url": "…", "sha256": "…" },
"linux-aarch64": { "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.
Schema 2
The same document for either game: a game discriminant, a sidecar, and a payload whose kind
says what it is. A reader switches on game, and refuses a schema it does not know.
{
"schema": 2,
"game": "rust", // or "servuo"
"bundle": "2026.09.27",
"generated": "2026-09-27T…Z",
"protocol": 12,
"sidecar": {
"repo": "RunicGateway/Rust-Link", "tag": "v…", "version": "…", "protocol": 12,
"assets": { // Rust: no linux-aarch64 — RustDedicated has no arm64 build
"linux-x86_64": { "name": "…", "url": "…", "sha256": "…" },
"windows-x86_64": { "name": "…", "url": "…", "sha256": "…" }
},
"launcher": { "name": "with-sidecar.sh", "url": "…", "sha256": "…" } // Rust only: the egg's startup
},
"payload": {
"kind": "plugin", // "overlay" for ServUO
"repo": "RunicGateway/Rust-Plugins", "tag": "v…", "version": "…", "commit": "…",
"protocol": 12,
"compat": { // per game; for ServUO it is schema 1's `servuo` block
"frameworks": { "oxide": { "min_version": "2.0.7585" },
"carbon": { "min_version": "2.0.259" } },
"requires_plugins": ["Kits", "ZoneManager"]
},
"asset": { "name": "runicgateway-rust-plugin-….tar.gz", "url": "…", "sha256": "…" }
}
}
A ServUO schema-2 document is schema 1's content re-shaped: link becomes sidecar (no
launcher), overlay becomes a payload of "kind": "overlay", and its servuo block becomes
compat (min_version, patches_verified_against).
The two gates hold for both games. For Rust, gate 1 reads PROTOCOL_VERSION from Rust-Link's
sidecar/src/main.rs at its release tag and protocol from the manifest.json inside the plugin
tarball (runicgateway-rust-plugin/manifest.json). The stakes are higher there than for ServUO: the
Rust game link has no 409, so a mismatched plugin would mis-parse rather than be refused.