fix(ci): unblock the release — npm 11 esbuild bloat + two self-inflicted assertions
Lint / hadolint (push) Successful in 8s
Lint / skill-floor (push) Successful in 9s
Lint / doc-drift (push) Successful in 10s
Lint / actionlint (push) Successful in 19s
Publish Docker Image / lint-gate (push) Successful in 15s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 14s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke (push) Successful in 5m4s
Publish Docker Image / smoke-studio (push) Successful in 13m17s
Publish Docker Image / build-variant (push) Successful in 33m49s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 14s
Publish Docker Image / build-variant-studio (push) Successful in 30m58s

v1.9.0 was tagged but never published: smoke failed 90-passed/3-failed and
build-variant needs smoke, so nothing reached the registry. All three are fixed.

1. Size, 431 MB over threshold. Node 24 brings npm 11, which installs EVERY
   @esbuild/<platform> optional binary instead of the matching one: 26 dirs,
   284 MB per pi-coding-agent copy. Measured on pi-fork: npm 10.9.8 -> 165 MB
   (exactly what v1.8.14 shipped), npm 11.19.0 -> 449 MB. npm 11 ignores the
   os/cpu constraints AND --os/--cpu AND an npmrc carrying them, all measured,
   so Dockerfile.variant prunes explicitly, keeping linux-$(node -p
   process.arch) so one line is right on both arches. Pruned in the SAME layer
   as each install, or the bytes survive in the earlier layer. Three sites:
   global pi, pi-fork, pi-studio. Verified esbuild still transforms TS after.

2. om's node_modules assertion tested an npm artefact. om has zero runtime deps;
   its node_modules held ONE file (.package-lock.json) and 20 empty scope dirs.
   npm 11 stopped creating it. Now asserts the entry point pi actually loads,
   read from package.json -> pi.extensions.

3. The skill-source annotation added in v1.9.0 ("baked (package copy)") broke
   the assertion matching "baked$". Pattern now allows an optional suffix; which
   copy shipped stays authoritatively asserted against the manifest + tree hash.

Also: a failed size check now prints the largest layers, largest directories and
an @esbuild sentinel, so this class attributes itself next time instead of
costing a CI dig plus a local npm bisect.

Threshold stays 3800 MB: it caught a real regression and raising it would have
thrown the signal away. No Dockerfile.base/rootfs change, so base-0fb1256c7f99
is reused and build-base is skipped.
This commit is contained in:
Joakim Persson
2026-09-10 23:39:40 +02:00
parent 3a44e81cad
commit 42bd29d654
3 changed files with 157 additions and 5 deletions
+79 -2
View File
@@ -11,7 +11,78 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## Unreleased
## v1.9.1 — 2026-09-10
**v1.9.0 was tagged but never published: its own smoke gate stopped it, and it
was right to.** `build-base` succeeded, then `smoke` failed 90-passed/3-failed,
and because `build-variant` needs `smoke`, both variants, `promote-base-latest`
and `update-description` were skipped. No image reached the registry, so
`latest` still pointed at v1.8.14. v1.9.1 carries everything listed under v1.9.0
below, plus the three fixes here. Two of the three failures were self-inflicted
by v1.9.0's own changes, and the third was a real regression that the Node bump
dragged in — which is the case for keeping the gate strict.
**Failure 1 — the image was 431 MB over its size threshold, and npm 11 was the
cause.** Node 22 → 24 brings npm 10 → 11, and npm 11 installs **every**
`@esbuild/<platform>` optional binary rather than only the one matching the host:
26 platform directories covering aix-ppc64, android, darwin, freebsd, netbsd,
openbsd, win32, s390x, riscv64 and more, none of which this image can execute.
Measured on pi-fork's dependency tree, same repo and same command:
| npm | packages | `node_modules` |
| --- | --- | --- |
| 10.9.8 | 136 | **165 MB** |
| 11.19.0 | 169 | **449 MB** |
The 165 MB figure reproduces exactly what v1.8.14 shipped, which is what
identified npm rather than the image as the variable. esbuild declares those
binaries with `os`/`cpu` constraints, but npm 11 ignores them — and also ignores
`--os`/`--cpu` flags and an `.npmrc` carrying `os=`/`cpu=` (all three measured,
all three still produced 26 directories). So `Dockerfile.variant` now prunes
explicitly, keeping only `linux-$(node -p process.arch)` so one line is correct
on amd64 and arm64. Verified this removes dead weight and not function: after
pruning, `esbuild.transformSync` still compiles TypeScript. The prune runs in the
**same layer** as each `npm install` — deleting in a later `RUN` would leave the
bytes in the earlier layer and shrink the image by nothing. Three sites are
covered: the global pi install, pi-fork, and pi-studio (which pulls its own
pi-coding-agent copy), for roughly 548 MB recovered in the non-studio variant and
822 MB in studio. The threshold stays at 3800 MB deliberately: it caught a real
regression, and raising it to accommodate one would have discarded the signal.
**Failure 2 — the om `node_modules` assertion was checking an npm artefact, not
the software.** `pi-observational-memory` declares **zero** runtime dependencies:
8 devDependencies (omitted by `--omit=dev`) and 4 peerDependencies, which pi
itself provides. npm 10 still materialised a `node_modules` for it, but that
directory contained exactly **one file** (`.package-lock.json`, 4 KB) and no
nested `package.json` — 20 empty scope directories. npm 11 stopped creating it,
so `test -d node_modules` went red while nothing about om had changed or broken.
The assertion now checks what must actually hold — that the entry point pi loads
exists — read out of the manifest pi itself reads (`package.json` →
`pi.extensions`) rather than a hardcoded path that could drift. pi-fork keeps its
`node_modules` check, because pi-fork has real dependencies where the directory's
absence would mean something.
**Failure 3 — the skill-source annotation broke the assertion that reads it.**
v1.9.0 taught `pi-devbox-version` to say *which* pi-extensions copy shipped
(`baked (package copy)`, or a loud FALLBACK/MIXED marker). The smoke assertion
matched `^ $s +baked$`, anchored at the end, so the annotation failed it even
though the state reported was correct. The pattern now allows an optional
` (...)` suffix, matched loosely on purpose: *which* copy shipped is already
asserted authoritatively against the manifest field and its measured tree hash,
and re-encoding that wording in a second regex would just add a second place to
update. The lesson recorded rather than the fix alone: the display branches were
tested in an isolated harness that passed, but the assertion **consuming** them
was never run — harness-passes-therefore-consumer-passes was an assumption.
**A red size assertion now carries its own diagnostic.** Attributing the 431 MB
took a full CI-log dig plus a local npm bisect, while the container knew where
its bytes were the whole time. On failure the check now prints the largest
layers, the largest directories, and a count of `@esbuild` platform directories
as a sentinel for this exact regression recurring — the same principle the `run()`
helper already applies to every other assertion.
No `Dockerfile.base` or `rootfs/` change, so the base fingerprint is untouched
and `base-decide` reuses `base-0fb1256c7f99` built during the v1.9.0 attempt.
**A gate for documentation drift, because five claims rotted in one release and
one of them was published.** Preparing v1.9.0 turned up a cluster of stale
@@ -94,7 +165,13 @@ is stale (a live client shows 45) but left alone rather than corrected on a
guess, since that count cannot be attributed to the baked 3.9.0 server without
measuring it.
## v1.9.0 — 2026-09-10
## v1.9.0 — 2026-09-10 (tagged, never published — superseded by v1.9.1)
> This tag exists in git but no image was ever pushed for it: `smoke` failed
> three assertions and skipped every downstream job. Everything below ships in
> **v1.9.1**, whose entry explains the three failures and their fixes. Kept as
> its own section rather than folded away, because the tag is real and someone
> will eventually find it and wonder why Docker Hub has no v1.9.0.
**`shellcheck` is now in the image, because the release gate it depends on could
not be run by anyone.** v1.8.14 made shell lint a release gate: `scripts/lint-shell.sh`