Files
installer/bundles
wtclaude c100d4a2d7
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
ci(bundle): compose and publish the bundle manifest
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>
2026-08-04 11:10:51 -05:00
..

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, 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 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/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.

{
  "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.