release path publishes, and an unreleasable version fails CI (R-115, R-183)
gates / gates (push) Failing after 7s

NO VERSION BUMP and nothing built: no Go code changed. The agent stays v0.120.0.

scripts/release-agent.sh — THE way to release. build -> tag -> publish -> verify
by INDEPENDENT download. Publishing was a separate remembered step and was
forgotten three times in five days (R-111's 17 stranded releases, 0.114.0, and
0.120.0 — deployed to both demo hosts and undownloadable, so a documented-path
reinstall would have silently downgraded them WHILE REPORTING SUCCESS). R-111's
own closing line named this leg and closed SHIPPED without it; it recurred the
same afternoon, which is the evidence that a note is not a mechanism.

It tags because felhom-host-install.sh now fetches the sixteen agent config
files from raw/tag/v<version>/ (R-183): a released version with no tag 404s a
box mid-install, as root, on a virgin machine. It verifies by downloading what
it just published and comparing the sha to what it built — the publish step's
own success is a report on its own write; a fetch returning the right bytes is
a different claim. It refuses a dirty/unpushed tree and refuses to re-release an
existing version. It does NOT vouch: that points machines at a version and stays
the operator's act.

scripts/check-published-versions.py — the gate. Every v<semver> tag must have a
downloadable package AND a tag tree serving the agent's configs. Registered as
NOT --fast (needs network; a push must not fail because Gitea blinked), and the
CI workflow now runs the FULL gate set instead of --fast — otherwise the gate
would have been registered and never run, the built-but-never-wired failure this
project has shipped four times.

The invariant is not the one specified, and the reason was measured, not assumed:
the hub artifact manifest is 401 without a per-customer passphrase and Gitea's
package LISTING api is 401 without a token, while the package DOWNLOAD url and
the git TAGS api are anonymous. So CI cannot ask "what is vouched" without an
operator credential — whose addition is the operator's call. The tag-based
invariant needs none and catches all three recorded instances. What it does not
catch (the hub vouching a version never released at all) is filed as R-184.
This commit is contained in:
2026-08-03 12:34:20 +02:00
parent 9dfd89cb94
commit dd2d1feb6e
6 changed files with 363 additions and 3 deletions
+7
View File
@@ -9,6 +9,12 @@
Gates (all must pass; **non-zero exit on any failure**):
1. reuse-refs every path cited by this repo's REUSE.md still resolves
2. published every `v<semver>` tag has a downloadable package AND a tag tree that serves
the agent's configs (R-115). NEEDS NETWORK, so it is **not** in `--fast` and
the pre-push hook does not run it — a push must not fail because Gitea blinked
or because someone is offline on a train. CI runs the FULL set for exactly this
reason: it is the machine that can afford a network check, and it is the half
that emails when something is wrong.
WHY THIS FILE EXISTS, WITH ONE GATE (2026-08-02, R-29 leg (b)).
@@ -39,6 +45,7 @@ SHARED_REUSE = os.path.join(os.path.dirname(ROOT), "felhom.eu", "scripts", "reus
# (label, absolute script path, args, fast)
GATES = [
("reuse-refs", SHARED_REUSE, [ROOT], True),
("published", os.path.join(ROOT, "scripts", "check-published-versions.py"), [], False),
]
VERDICT = {0: "OK", 1: "FAILED", 2: "INCONCLUSIVE"}