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