CI: preventative fix for PI_VERSION/OMOS_VERSION cache-hit silent regression

Mirrors the pi-devbox v0.75.5b fix (2026-05-23) onto the four-variant
pipeline here. The with-pi, omos, and omos-with-pi variants install
upstream npm packages whose *_VERSION build-args defaulted to 'latest'.
When the build-arg string is byte-identical across builds, the layer
hash is identical and the registry buildcache silently reuses the layer
from whatever upstream version was current when the cache was first
populated — same mechanism that shipped pi-devbox v0.74.0..v0.75.5 with
identical image bytes.

Currently masked here because OPENCODE_VERSION is a hard-coded ARG that
bumps every release; parent-chain cache invalidation flushes the
downstream pi/omos layers. Masking would fail on any vN.N.Nb opencode-
version-unchanged release that only bumps pi or omos. Filed last night
as parked followup; fixing preventatively now that #5 (AWS SSO inside
tor-ms22 container) cleared.

CHANGES

.gitea/workflows/docker-publish-split.yml — new resolve-versions job
running 'npm view @earendil-works/pi-coding-agent version' and
'npm view oh-my-opencode-slim version', exposing concrete strings as
job outputs. All six affected jobs (smoke-omos, smoke-with-pi,
smoke-omos-with-pi, build-variant-omos, build-variant-with-pi,
build-variant-omos-with-pi) now consume them as PI_VERSION /
OMOS_VERSION build-args. smoke-base / build-variant-base unaffected.

scripts/smoke-test.sh — new run_expect helper asserting an expected
substring in command output. The pi check uses EXPECTED_PI_VERSION;
the omos check uses EXPECTED_OMOS_VERSION against npm ls -g. Both env
vars are wired from resolve-versions outputs in the smoke jobs. Catches
this regression class on the next release, not four releases later.

Dockerfile.variant — comment blocks above OPENCODE_VERSION (source-
pinned, not subject to the bug), PI_VERSION (CI-resolved), and
OMOS_VERSION (CI-resolved) explaining the cache-hit footgun.

AGENTS.md — new convention bullet under 'Critical conventions' naming
the resolve-versions job + EXPECTED_*_VERSION wiring as the contract
to keep in lockstep when modifying variant build-args.

.gitea/README.md — Step 1 expanded to cover the parallel resolve-
versions job alongside base-decide; pipeline diagram updated.

CHANGELOG.md — Unreleased entry describing the fix, masking mechanism,
and audit footprint.

No image-content change expected on the next release vs what 'latest'
would have resolved to anyway. Purely makes the cache invalidate
correctly going forward.
This commit is contained in:
2026-05-24 15:38:36 +00:00
parent 4cce39d167
commit f7c34091b1
6 changed files with 151 additions and 17 deletions
+29 -1
View File
@@ -43,6 +43,25 @@ run() {
fi
}
# Stricter version of `run` that also asserts an expected substring in
# the command's stdout. Used to catch the "image bytes silently identical
# to previous release" class of regression — Docker layer-cache hit on
# a bare `npm install -g <pkg>` (or @latest) because the build-arg
# string is identical across builds, even when 'latest' would have
# resolved differently. Discovered in pi-devbox 2026-05-23 (every
# release v0.74.0..v0.75.5 shipped the same image bytes); preventatively
# applied here for PI_VERSION + OMOS_VERSION.
run_expect() {
local label="$1"; local cmd="$2"; local expect="$3"
local out
out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" 2>&1) || true
if echo "$out" | grep -Fq "$expect"; then
pass "$label (got $expect)"
else
fail "$label — expected substring '$expect', got: $out"
fi
}
echo "=== Smoke test: $IMAGE (variant: $VARIANT) ==="
echo
echo "-- Resolved component versions --"
@@ -134,7 +153,11 @@ fi
# entrypoint-user.sh on first start, so we test by running the entry
# point chain (not just `docker run --entrypoint=""`).
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v pi" >/dev/null 2>&1; then
run "pi" "pi --version"
if [ -n "${EXPECTED_PI_VERSION:-}" ]; then
run_expect "pi version matches build-arg" "pi --version" "$EXPECTED_PI_VERSION"
else
run "pi" "pi --version"
fi
run "pi-toolkit clone" "test -d /opt/pi-toolkit && git -C /opt/pi-toolkit rev-parse --short HEAD"
run "pi-extensions clone" "test -d /opt/pi-extensions && git -C /opt/pi-extensions rev-parse --short HEAD"
@@ -192,6 +215,11 @@ if [ "$VARIANT" = "omos" ] || [ "$VARIANT" = "omos-with-pi" ]; then
# queries the user prefix and would miss the baked binaries even though
# they're correctly on PATH at /usr/bin.
run "oh-my-opencode-slim" "NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
if [ -n "${EXPECTED_OMOS_VERSION:-}" ]; then
run_expect "omos version matches build-arg" \
"NPM_CONFIG_PREFIX=/usr npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim" \
"$EXPECTED_OMOS_VERSION"
fi
else
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v bun" >/dev/null 2>&1; then
fail "bun should NOT be in base image but was found"