Files
installer/bundles/README.md
wtclaude 7db58031c7
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 6s
fix(bundle): publish to a bundles branch, and unbreak the stale check
Three things, all found by the first compose run that ever had a bundle
to write.

1. `main` is protected, so the push was declined by the pre-receive
   hook -- twice, since the retry rebases and pushes to the same place.
   Every bundle since v1.1.1 has been composed correctly and thrown
   away. Bundles now go to a `bundles` branch of their own, at its
   root, which needs no protection exception and 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. The header's claim that this push "needs no new
   branch-protection exception" was simply false.

2. A `${{ }}` written literally in a shell comment silently disabled
   the entire stale-component check. The runner scans a step's script
   for template expressions before running it, fails to parse the empty
   one, and skips the step WITHOUT failing the job -- so the dispatch
   that is supposed to fire a component's release workflow has never
   run once. Reworded, with a warning not to write that token in a
   comment again. (link/release.yml and this repo's release.yml carry
   the same bug in their bump-and-tag step; handled separately.)

3. linux-aarch64 is now a REQUIRED platform key, which was step 3 of
   PLAN.md §5.2 and was waiting on link publishing one. v1.1.1 does, so
   from here a dropped target reddens this job instead of vanishing
   from every bundle.

The published bundles are materialized into a worktree at `published/`,
so the ".2 suffix" scan and the idempotence check read what is actually
published rather than a stale copy on main. The branch is created from
an empty-tree root commit on first use, so it carries no history that
has nothing to do with the compat matrix; it has been seeded already
with bundle 2026.08.04, because every bundle is kept forever and the
move must not lose the one that exists.

bundles/*.json is deleted from main -- it is now a stale copy of data
that lives elsewhere, and a wrong "current" is worse than none. The
README stays and documents the branch.

Verified by running the whole job in a container against a bare repo
standing in for the remote: first run creates the branch and publishes
both files with all three asset keys, second and third runs report
"identical to the published current.json -- nothing to publish" and
push nothing, and the stale check now runs and reports both components
as having nothing releasable.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-05 17:14:57 -05:00

5.0 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 — 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.

Where they live: the bundles branch

The JSON documents are not in this directory. They are published to a branch of their own, bundles, at its root:

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.

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

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