Files
installer/bundles/README.md
wtclaude 4f268f7ffb
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m12s
ci(bundle): compose schema 2 for ServUO and Rust beside schema 1
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
2026-09-25 22:49:47 -05:00

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.