Files
felhom-agent/REPORT.md
T
2026-08-03 12:37:09 +02:00

4.5 KiB

REPORT — releasing publishes, and an unreleasable version fails CI (R-115, R-183)

Date: 2026-08-03 · Repo: felhom-agent · NO VERSION BUMP — the agent stays v0.120.0, no Go code changed, nothing was built or deployed.

What changed

File
scripts/release-agent.sh new — THE release path: build → tag → publish → verify by independent download
scripts/check-published-versions.py new — the R-115 gate
scripts/agent_gates.py registers the gate as not --fast (it needs network)
.gitea/workflows/gates.yml CI now runs the full gate set, not --fast
CLAUDE.md the raw go build line is replaced by the release script; a Vouch row replaces the old Publish row

Why

Publishing was a step someone had to remember and was forgotten three times in five days — R-111's seventeen stranded releases, 0.114.0, and 0.120.0, which sat deployed on both demo hosts and undownloadable, so a documented-path reinstall would have silently downgraded them to the pre-merge agent while reporting success. R-111's own closing line named this leg and closed SHIPPED without it; it recurred the same afternoon. A note is not a mechanism.

The script also tags, because felhom-host-install.sh now fetches the agent's sixteen 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. Tag and package are two halves of one release.

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, and it is the one that matters.

It does not vouch — that points machines at a version and stays the operator's act.

The gate's invariant — not the one specified, and the reason was measured

The task's §8.4 asked for "the version the hub tells machines to install must be downloadable". CI cannot see that, measured rather than assumed (P-C):

Endpoint Anonymous
Gitea package download 200 (and 404 for a fake version — it discriminates)
Gitea tags api 200
Gitea package listing api 401 — token required
Hub /api/v1/artifacts/<customer> 401 — per-customer passphrase required

So a credential-free gate can ask "is this version installable" but not "which version is vouched". Adding an operator credential to CI to close that is the operator's call, not a gate author's. The implemented invariant — every v<semver> tag must have a downloadable package and a tag tree that serves the agent's configs — needs no credential and catches all three recorded instances, because the release script creates the tag and publishes in one act.

What it does not catch, stated rather than assumed away: the hub vouching a version that was never released at all. Nothing here can see that; it belongs at vouch time in the hub. → R-184.

Proof

Check Result
go build ./... && go vet ./... OK
go test ./... 29 packages ok, rc=0 (read separately from any commit)
agent_gates.py --fast published correctly SKIPPED — the pre-push hook must not fail because Gitea blinked
agent_gates.py (full) reuse-refs OK, published OK
release script: re-release guard ERROR: tag v0.120.0 already exists — releasing over it would make one version name two binaries, rc=1
release script: clean-tree guard ERROR: working tree is dirty — commit and push first, rc=1

Red-proof F — both directions

  • A tagged-but-unpublished version (v9.9.9 created for the purpose): gate rc=1, binary NOT downloadable (HTTP 404 …). This is the R-115 shape exactly.
  • The gate deregistered from the entry point, same bad state: agent_gates.pyrc=0, "all agent gates OK". Restored → rc=1, CONVICTED: published. The guard is what catches it, not something else.

v9.9.9 was deleted afterwards; git ls-remote --tags shows only v0.120.0.

Tag convention

v<semver>, at the commit the binary was built from. v0.120.0 was created retroactively at cd6e267 — the commit that produced the published binary (sha a7763d31b55b5ce7…). configs/ is byte-identical between that commit and main, so nothing about the sixteen fetched files depends on the choice; cd6e267 is tagged because it is the honest one.