ci: audit MEMPALACE_VERSION the way PI_VERSION is audited
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 17s

Closes the item v1.8.6 (and v1.8.5 before it) listed as "Still open": the
palace pin was a literal string in Dockerfile.base with zero references in
docker-publish.yml, while PI_VERSION had a concreteness gate, a
published-on-registry check and a never-silently-adopt drift warning.

resolve-versions now applies all of those to MEMPALACE_VERSION, read from
Dockerfile.base so a local `docker build` and CI install the same version by
construction, plus one gate pi does not need: a YANKED release is refused,
because an exact pin installs one silently under PEP 592 and would have
shipped a withdrawn palace client to the whole fleet.

smoke gains `installed mempalace matches CI's audited pin` via a new
EXPECTED_MEMPALACE_VERSION threaded into both smoke jobs. It is not redundant
with `manifest mempalace_version matches the installed core`: that compares two
properties of one image and cannot notice that both are the wrong version. The
case this covers is a variant built FROM a cached base carrying an older pin —
internally consistent, silently stale.

Mutation-tested by extracting the shipped block out of the YAML and stubbing
curl: 9 cases covering every gate, then once end-to-end against live PyPI. That
found a real defect in the first draft — the yank message inlined a jq program
inside $(...) inside a double-quoted string, where the escaping broke the
filter (jq compile error) while the surrounding `exit 1` still fired: a gate
that looked correct and reported garbage.

Note: correcting Dockerfile.base's now-false "known gap, carried forward"
comment forces a base rebuild (~67 min) on the next tag. Leaving a comment
asserting the audit does not exist was the worse option.
This commit is contained in:
2026-08-25 20:33:19 +02:00
parent cb7b8ad2ae
commit 2b8c3a4db4
4 changed files with 129 additions and 6 deletions
+51
View File
@@ -13,6 +13,57 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
## Unreleased
### Added
- **`MEMPALACE_VERSION` now gets the same CI audit as `PI_VERSION`** — closing
the item v1.8.6 (and v1.8.5 before it) listed as "Still open". The pin was a
literal string in `Dockerfile.base` with **zero** references anywhere in
`.gitea/workflows/docker-publish.yml`, while `PI_VERSION` had ~20: a
concreteness gate, a published-on-registry check, and a never-silently-adopt
drift warning. `resolve-versions` now applies all of them to the palace pin,
read from `Dockerfile.base` (not duplicated in the workflow, so a local
`docker build` and CI install the same version by construction):
| Gate | Behaviour |
|---|---|
| not a concrete `X.Y.Z` | **error** — no floating palace version, same policy as pi |
| not published on PyPI | **error** at resolve time, instead of a `uv tool install` failure mid-build |
| **yanked** on PyPI | **error** — an exact pin installs a yanked release silently under PEP 592, so `mempalace==X` would have shipped a withdrawn client to the whole fleet |
| newer release exists | **warning** naming what to audit before adopting (MCP tool-schema = the agent-facing contract; client/server skew against the central palace) |
Plus one smoke assertion, `installed mempalace matches CI's audited pin`,
gated on a new `EXPECTED_MEMPALACE_VERSION` env threaded into both the `smoke`
and `smoke-studio` jobs. It is **not** redundant with the existing `manifest
mempalace_version matches the installed core`: that one compares two
properties of a single image and therefore cannot notice that *both* are the
wrong version. The failure mode this one covers is a variant built `FROM` a
cached base whose `MEMPALACE_VERSION` pin was older — internally consistent,
silently stale, invisible to every other assertion (the risk
`scripts/check-base-hash.sh` exists to reduce but cannot eliminate).
**Mutation-tested rather than reasoned about**, by extracting the shipped
block out of the YAML and running it with a stubbed `curl`: 9 cases —
`latest` / `3.8` / absent ARG refused; 404 and a registry echoing a different
version refused; a yanked release refused *with its reason*; a newer release
warning without failing; a transient PyPI outage not failing a build whose pin
is already verified; happy path silent and emitting the job output. Then once
more end-to-end against live PyPI with the real `Dockerfile.base`. **This
found a genuine defect in the first draft**: the yank message inlined a jq
program inside a `$(...)` inside a double-quoted string, where the escaping
broke the *filter* (jq compile error) while the surrounding `exit 1` still
fired — a gate that looked correct and reported garbage. The reason is now
hoisted into its own variable. The new smoke assertion was checked the same
way, through the real `run` helper's `sh -c` quoting path: passes on `3.8.0`,
fails on `3.7.1` *and* on `3.8.01` (exact equality, not the substring match
the pi assertion uses), and skips cleanly when the env is unset so a local
`smoke-test.sh` run is unaffected.
⚠️ **Costs a base rebuild on the next tag**: `Dockerfile.base` is hashed
wholesale into `base_tag`, and its now-false "Known gap, carried forward"
comment had to be corrected in place (leaving a comment that says the audit
does not exist would repeat the shipped-false-claim mistake corrected below).
Expect ~67 min, as for v1.8.5/v1.8.6.
### Fixed
- **Four build-provenance smoke assertions verified the presence of a manifest