diff --git a/.gitea/workflows/docker-publish.yml b/.gitea/workflows/docker-publish.yml index b37541c..060cbab 100644 --- a/.gitea/workflows/docker-publish.yml +++ b/.gitea/workflows/docker-publish.yml @@ -33,6 +33,23 @@ jobs: - uses: docker/setup-buildx-action@v4 with: {driver-opts: network=host} + # Derive PI_VERSION from the tag (e.g. v0.75.5 -> 0.75.5; v0.75.5b -> 0.75.5). + # MUST be passed as a build-arg so Docker's layer cache invalidates when pi + # is bumped. Without this, the bare `npm install -g ` in the Dockerfile + # produces an identical layer-hash across builds and the registry buildcache + # silently reuses the layer from whatever pi version was current when the + # cache was first populated. Discovered 2026-05-23 — every pi-devbox release + # since v0.74.0 had been shipping the same image bytes (manifest digests + # identical across v0.74.0..v0.75.5 on both arches). + - name: Resolve PI_VERSION from tag + id: resolve + run: | + TAG="${{ github.ref_name }}" + PI_VERSION="${TAG#v}" + PI_VERSION=$(echo "$PI_VERSION" | sed 's/[a-z]*$//') + echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT" + echo "Resolved PI_VERSION=${PI_VERSION} from tag ${TAG}" + - name: Build (amd64, load to local daemon) uses: docker/build-push-action@v7 with: @@ -41,8 +58,12 @@ jobs: push: false load: true tags: pi-devbox:smoke + build-args: | + PI_VERSION=${{ steps.resolve.outputs.pi_version }} - name: Smoke test + env: + EXPECTED_PI_VERSION: ${{ steps.resolve.outputs.pi_version }} run: bash scripts/smoke-test.sh pi-devbox:smoke publish: @@ -81,6 +102,16 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" + # See the smoke job for why this is required (cache-hit silent regression). + - name: Resolve PI_VERSION from tag + id: resolve + run: | + TAG="${{ github.ref_name }}" + PI_VERSION="${TAG#v}" + PI_VERSION=$(echo "$PI_VERSION" | sed 's/[a-z]*$//') + echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT" + echo "Resolved PI_VERSION=${PI_VERSION} from tag ${TAG}" + - name: Build and push (amd64 + arm64) uses: docker/build-push-action@v7 with: @@ -88,6 +119,8 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.tags.outputs.tags }} + build-args: | + PI_VERSION=${{ steps.resolve.outputs.pi_version }} cache-from: type=registry,ref=${{ env.IMAGE }}:buildcache cache-to: type=registry,ref=${{ env.IMAGE }}:buildcache,mode=max diff --git a/AGENTS.md b/AGENTS.md index 311932a..70b3789 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,7 @@ per version. Don't try to derive notes from the npm registry metadata - Do NOT call `mempalace-toolkit/install.sh` in the Dockerfile — the base entrypoint handles it - `NPM_CONFIG_PREFIX=/usr` must be set per-RUN for any build-time `npm install -g` to keep baked binaries off the volume-shadowed path - The smoke test threshold is 2200 MB — update if the image legitimately grows past it +- **PI_VERSION must be passed explicitly by CI as a concrete version** (derived from the git tag), not left as the `latest` default. The Dockerfile's bare `npm install -g @earendil-works/pi-coding-agent` (without `@${PI_VERSION}`) produces an identical layer-hash across builds; combined with registry buildcache (`cache-from`/`cache-to`) the layer gets reused even when `latest` would have resolved to a newer pi version. **All releases v0.74.0 → v0.75.5 silently shipped the same image bytes** because of this (verified via `docker manifest inspect` — identical digests across both arches and all four tags). Fixed in v0.75.5b: workflow now derives `PI_VERSION` from `${{ github.ref_name }}` and passes it as a build-arg; smoke-test asserts the resulting `pi --version` matches via `EXPECTED_PI_VERSION` env var. Same latent bug exists in opencode-devbox's `with-pi` variants but is masked there because `OPENCODE_VERSION` bumps invalidate downstream layers — will only manifest when cutting a `vN.N.Nb`-style opencode-version-unchanged release that only bumps pi. ## Documentation drift sweep diff --git a/CHANGELOG.md b/CHANGELOG.md index d9eff04..bda0f55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,19 @@ Tags follow the pi npm version: `v{pi_version}[letter]` — bare tag for the fir ## Unreleased +## v0.75.5b — 2026-05-23 + +Recovery release fixing a **silent cache-hit regression** discovered in the v0.75.5 image. All four releases v0.74.0 through v0.75.5 had been shipping the same image bytes because the Dockerfile's `npm install -g @earendil-works/pi-coding-agent` (bare, when `PI_VERSION=latest`) produces an identical layer-hash across builds. Combined with the registry buildcache, Docker reused the layer from whatever pi version was current when the cache was first populated. + +Verification: `docker manifest inspect joakimp/pi-devbox:vX.Y.Z` showed identical SHA256 digests on both `linux/amd64` and `linux/arm64` for v0.74.0, v0.75.3, v0.75.4, v0.75.5. Users on `:latest` were getting whatever pi version was baked into the v0.74.0 build (probably 0.74.0 itself). + +- **Workflow fix:** Both `smoke` and `publish` jobs now derive `PI_VERSION` from `github.ref_name` (e.g. `v0.75.5b` → `0.75.5`) and pass it as a build-arg. The Dockerfile's existing `if PI_VERSION=latest` branch never fires in CI now — always takes the `@${PI_VERSION}` branch — so the layer-hash includes the version and cache invalidates correctly. +- **Smoke test:** New `run_expect` helper asserts `pi --version` output contains `EXPECTED_PI_VERSION` (passed from the resolve step). Would have caught this regression on v0.75.3 if it had existed. +- **Dockerfile:** Comment added above `ARG PI_VERSION=latest` documenting the cache-hit footgun and pointing at the workflow's resolve step + AGENTS.md gotcha. +- **AGENTS.md:** New convention bullet explaining the cache-hit class of bug and noting the latent same-bug in opencode-devbox's `with-pi` variants (currently masked by OPENCODE_VERSION bumps). + +No image-side changes vs v0.75.5 *intent* — this build will produce the actual pi 0.75.5 image content that v0.75.5 was supposed to ship. + ## v0.75.5 — 2026-05-23 pi `0.75.4` → `0.75.5` bump (one upstream patch release, two days after v0.75.4). diff --git a/Dockerfile b/Dockerfile index 746e58d..bf14a03 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,13 @@ ARG BASE_IMAGE=joakimp/opencode-devbox:base-latest FROM ${BASE_IMAGE} +# PI_VERSION should be passed explicitly by CI as a concrete version +# (e.g. PI_VERSION=0.75.5, derived from the git tag). The default `latest` +# is for local dev convenience only — it has a known cache-hit footgun +# when used in registry-cached CI builds. See .gitea/workflows/docker- +# publish.yml § "Resolve PI_VERSION from tag" and AGENTS.md gotcha for +# the full story (silent same-bytes-across-releases regression discovered +# 2026-05-23 affecting all builds v0.74.0..v0.75.5). ARG PI_VERSION=latest ARG PI_TOOLKIT_REF=main ARG PI_EXTENSIONS_REF=main diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 54119d3..38eb8aa 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -28,12 +28,33 @@ run() { fi } +# Stricter version of `run` that also asserts an expected substring in stdout. +# Used for catching the "image bytes silently identical to previous release" +# class of regression (Docker layer cache hit on `npm install -g ` because +# the bare command string is identical across builds, even when `latest` would +# resolve differently). Discovered 2026-05-23 — every pi-devbox release v0.74.0 +# through v0.75.5 had been shipping the same image bytes. +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 + printf " ✅ %s (got %s)\n" "$label" "$expect"; PASS=$((PASS+1)) + else + printf " ❌ %s — expected substring %q, got: %s\n" "$label" "$expect" "$out"; FAIL=$((FAIL+1)) + fi +} + echo "=== pi-devbox smoke test: $IMAGE ===" echo "" # ── Basic binary checks ─────────────────────────────────────────────── echo "── Binaries ──" -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 "node" "node --version" run "git" "git --version" run "aws" "aws --version"