82 lines
4.5 KiB
Markdown
82 lines
4.5 KiB
Markdown
# 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.py` → **rc=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.
|