smoke: assert the pi stage $HOME-relative, and add a smoke_only dispatch
Lint / actionlint (push) Successful in 16s
Lint / hadolint (push) Successful in 13s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 16s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-studio (push) Successful in 5m20s
Publish Docker Image / smoke (push) Successful in 14m40s
Publish Docker Image / build-variant-studio (push) Successful in 21m51s
Publish Docker Image / build-variant (push) Successful in 15m59s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 17s
Lint / actionlint (push) Successful in 16s
Lint / hadolint (push) Successful in 13s
Publish Docker Image / resolve-versions (push) Successful in 10s
Publish Docker Image / base-decide (push) Successful in 16s
Publish Docker Image / build-base (push) Has been skipped
Publish Docker Image / smoke-studio (push) Successful in 5m20s
Publish Docker Image / smoke (push) Successful in 14m40s
Publish Docker Image / build-variant-studio (push) Successful in 21m51s
Publish Docker Image / build-variant (push) Successful in 15m59s
Publish Docker Image / update-description (push) Successful in 6s
Publish Docker Image / promote-base-latest (push) Successful in 17s
v1.8.0 never shipped: smoke (67/68) and smoke-studio (70/71) each failed the
same single assertion, so build-variant and everything downstream skipped and
latest stayed on v1.7.0.
The assertion was wrong, not the product. It grepped for a literal
stage=/home/developer/.mempalace/pi-stage/, but run() invokes
docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd"
and neither Dockerfile sets USER or ENV HOME — the published base image config
has no HOME at all; it is normally set by entrypoint-user.sh, which
--entrypoint="" skips on purpose. So the assertion executed as root with
HOME=/root, mempalace-pi-session correctly resolved
stage=/root/.mempalace/pi-stage/... (the stage is $HOME-relative by design), and
the literal grep could never match under any circumstances.
The tell was one line below in the log: the sibling assertion "pi stage follows
MEMPALACE_PALACE_PATH" PASSED, because it sets the variable explicitly and never
consults HOME. Default fails + explicit passes = wrong HOME, not broken staging.
Now asserts the invariant actually intended — the stage sits beside the resolved
palace, sharing its lifetime — which is user-independent:
case "$stage" in "stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;; *) exit 1 ;; esac
$HOME is expanded by the container's own shell, so it holds as root, as
developer, or under any future user. Verified all four cases against the real
bin/mempalace-pi-session by extracting the committed assertion bodies and
running them under sh -c: virgin HOME -> exit 0; HOME=/home/developer -> exit 0;
MEMPALACE_PI_STAGE pinned to a .cache path -> exit 1 (the regression this
assertion exists to catch still fails it); developer-identity companion -> 0.
Added that companion assertion, "pi stage is palace-adjacent for the developer
user", which covers the deployment-specific path properly by SUPPLYING
HOME=/home/developer rather than assuming it.
Why this took a release to surface: docker-publish.yml triggers on push tags v*
only. The assertion was added on a push to main (7c00dd6), where only lint.yml
runs, so v1.8.0 was its first execution ever. Any smoke assertion written
outside a release was unvalidated until a release consumed it.
New workflow_dispatch input smoke_only probes/builds the base, runs both smoke
jobs against HEAD, and stops before publishing. Implemented as
`if: inputs.smoke_only != 'true'` on build-variant and build-variant-studio,
deliberately WITHOUT always() so the implicit needs-succeeded gate survives and
a red smoke still blocks a release. promote-base-latest and update-description
already require build-variant success, so they skip on their own. On a tag push
inputs is unset and null != 'true' is true, so releases are unaffected.
Finally, run() no longer discards output. A red ❌ carried zero diagnostic
weight: explaining this one-line failure needed a CI-log dig plus a registry
image-config inspection, when the container had already printed the answer.
Failures now show the last lines of output (guarded with `if`, not a trailing
`&&`, which would abort under set -e), and the stage assertions echo the
resolved stage and the HOME they saw to stderr — invisible while they pass.
This commit is contained in:
@@ -18,6 +18,14 @@ name: Publish Docker Image
|
|||||||
# 5. build-variant multi-arch push of latest + vX.Y.Z tags.
|
# 5. build-variant multi-arch push of latest + vX.Y.Z tags.
|
||||||
# 6. promote-base-latest re-tag base-<hash> → base-latest with `crane copy`.
|
# 6. promote-base-latest re-tag base-<hash> → base-latest with `crane copy`.
|
||||||
# 7. update-description patch Docker Hub description.
|
# 7. update-description patch Docker Hub description.
|
||||||
|
#
|
||||||
|
# Note the trigger: `push: tags: v*` (plus workflow_dispatch). Nothing here runs
|
||||||
|
# on a push to main, so a smoke assertion added outside a release is UNVALIDATED
|
||||||
|
# until the next tag — which is exactly how v1.8.0 shipped a broken assertion
|
||||||
|
# written three days earlier (it asserted a literal /home/developer stage path,
|
||||||
|
# while `run` executes `docker run --entrypoint=""` as root with HOME=/root).
|
||||||
|
# The `smoke_only` dispatch input exists to close that gap: it runs steps 1-4
|
||||||
|
# against HEAD and stops before anything is published.
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -33,6 +41,10 @@ on:
|
|||||||
description: 'Update latest aliases (default true for tag-push, false for manual test runs)'
|
description: 'Update latest aliases (default true for tag-push, false for manual test runs)'
|
||||||
required: false
|
required: false
|
||||||
default: 'false'
|
default: 'false'
|
||||||
|
smoke_only:
|
||||||
|
description: 'Build base + run both smoke jobs against HEAD, then stop. Publishes nothing. Use to validate smoke assertions without cutting a tag.'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
@@ -482,6 +494,14 @@ jobs:
|
|||||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||||
build-variant:
|
build-variant:
|
||||||
needs: [base-decide, smoke, resolve-versions]
|
needs: [base-decide, smoke, resolve-versions]
|
||||||
|
# A `smoke_only` dispatch stops the pipeline here: base is probed/built and
|
||||||
|
# both smoke jobs run, but nothing is published. Deliberately NOT wrapped in
|
||||||
|
# always() — specifying `if:` keeps the implicit "all needs succeeded" gate,
|
||||||
|
# so a failing smoke still blocks the release. On a tag push `inputs` is
|
||||||
|
# unset, and `null != 'true'` is true, so releases are unaffected.
|
||||||
|
# promote-base-latest and update-description need build-variant to have
|
||||||
|
# succeeded, so they skip on their own — no extra guard required.
|
||||||
|
if: inputs.smoke_only != 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
@@ -574,6 +594,7 @@ jobs:
|
|||||||
# or fail independently of the core release.
|
# or fail independently of the core release.
|
||||||
build-variant-studio:
|
build-variant-studio:
|
||||||
needs: [base-decide, smoke-studio, resolve-versions]
|
needs: [base-decide, smoke-studio, resolve-versions]
|
||||||
|
if: inputs.smoke_only != 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
|
|||||||
@@ -11,6 +11,84 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v1.8.1 — 2026-08-15
|
||||||
|
|
||||||
|
Patch release. **Unblocks v1.8.0, which never shipped.** Its `smoke` and
|
||||||
|
`smoke-studio` jobs each failed exactly one assertion (67/68 and 70/71 passed),
|
||||||
|
so `build-variant` and everything downstream skipped: no `v1.8.0` tag reached
|
||||||
|
Docker Hub and `latest` stayed on v1.7.0 from 2026-08-07. Image content is
|
||||||
|
unchanged from what v1.8.0 intended — the pins here are identical (pi `0.84.2`,
|
||||||
|
pi-atelier `v0.8.1`).
|
||||||
|
|
||||||
|
The failing assertion was `pi stage defaults next to the palace (not a cache
|
||||||
|
dir)`, added three days earlier in 7c00dd6. **It was a test bug, not a product
|
||||||
|
regression.** It asserted a literal path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||||
|
```
|
||||||
|
|
||||||
|
but the `run` helper invokes `docker run --rm --entrypoint="" $IMAGE sh -c …`,
|
||||||
|
and neither `Dockerfile.base` nor `Dockerfile.variant` sets `USER` or `ENV HOME`
|
||||||
|
(the published base image config carries no `HOME` at all — `HOME` is normally
|
||||||
|
set by `entrypoint-user.sh`, which `--entrypoint=""` deliberately skips). So the
|
||||||
|
assertion ran as **root with `HOME=/root`**, `mempalace-pi-session` correctly
|
||||||
|
resolved `stage=/root/.mempalace/pi-stage/…` (it is `$HOME`-relative by design:
|
||||||
|
`$MEMPALACE_PALACE_PATH` → `$MEMPAL_PALACE_PATH` → `~/.mempalace/config.json` →
|
||||||
|
`~/.mempalace/palace`), and the literal grep could never match under any
|
||||||
|
circumstances. The tell was one line below it in the log: the sibling assertion
|
||||||
|
`pi stage follows MEMPALACE_PALACE_PATH` **passed**, because it sets the variable
|
||||||
|
explicitly and so never consults `HOME`. Default fails while explicit passes is
|
||||||
|
the signature of a wrong `HOME`, not of broken staging.
|
||||||
|
|
||||||
|
Fixed by asserting the invariant that was actually meant — the stage sits beside
|
||||||
|
the resolved palace, sharing its lifetime — which is user-independent:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
case "$stage" in
|
||||||
|
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
`$HOME` is expanded by the container's own shell, so this holds as root, as
|
||||||
|
`developer`, or under any future user, while a cache-dir default — the
|
||||||
|
regression the assertion exists to catch — still fails it (verified against all
|
||||||
|
three cases plus a simulated `MEMPALACE_PI_STAGE` cache pin). A second
|
||||||
|
assertion, `pi stage is palace-adjacent for the developer user`, now covers the
|
||||||
|
deployment-specific path properly, by *supplying* `HOME=/home/developer` instead
|
||||||
|
of assuming it.
|
||||||
|
|
||||||
|
### Why it took a release to notice — and the `smoke_only` input
|
||||||
|
|
||||||
|
`docker-publish.yml` triggers on `push: tags: v*` only. 7c00dd6 was a push to
|
||||||
|
**main**, so only `lint.yml` ran; v1.8.0 was the first tag afterwards and
|
||||||
|
therefore the assertion's **first execution ever**. Any smoke assertion written
|
||||||
|
outside a release was unvalidated until the next release consumed it — the
|
||||||
|
worst possible moment to discover it.
|
||||||
|
|
||||||
|
New `workflow_dispatch` input **`smoke_only`** closes that: it probes/builds the
|
||||||
|
base and runs both smoke jobs against HEAD, then stops before publishing
|
||||||
|
anything. Implemented as `if: inputs.smoke_only != 'true'` on `build-variant`
|
||||||
|
and `build-variant-studio`, deliberately *without* `always()` so the implicit
|
||||||
|
"needs succeeded" gate survives and a red smoke still blocks a release;
|
||||||
|
`promote-base-latest` and `update-description` already require `build-variant`
|
||||||
|
success and so skip on their own. On a tag push `inputs` is unset and
|
||||||
|
`null != 'true'` is true, so releases behave exactly as before. This release was
|
||||||
|
validated with a `smoke_only` dispatch before the tag was cut.
|
||||||
|
|
||||||
|
### Smoke failures now explain themselves
|
||||||
|
|
||||||
|
`run` discarded all output (`>/dev/null 2>&1`), so a red ❌ carried zero
|
||||||
|
diagnostic weight — explaining this one-line failure took a CI-log dig plus a
|
||||||
|
registry image-config inspection, when the container had already printed the
|
||||||
|
answer and thrown it away. It now captures output and prints the last few lines
|
||||||
|
under a failed assertion only. Assertions that want a diagnostic echo it to
|
||||||
|
stderr (the stage checks now report the resolved stage and the `HOME` they saw),
|
||||||
|
which stays invisible while they pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v1.8.0 — 2026-08-15
|
## v1.8.0 — 2026-08-15
|
||||||
|
|
||||||
Minor release. Headline: **pi sessions now feed MemPalace by themselves.** The
|
Minor release. Headline: **pi sessions now feed MemPalace by themselves.** The
|
||||||
|
|||||||
+41
-1
@@ -43,12 +43,23 @@ PASS=0; FAIL=0
|
|||||||
# catching an unexpected +GB regression.
|
# catching an unexpected +GB regression.
|
||||||
SIZE_THRESHOLD_MB=3800
|
SIZE_THRESHOLD_MB=3800
|
||||||
|
|
||||||
|
# On failure, surface the last few lines the command produced. This used to
|
||||||
|
# discard output entirely (`>/dev/null 2>&1`), which made a red ❌ carry zero
|
||||||
|
# diagnostic weight: explaining the single v1.8.0 stage-default failure took a
|
||||||
|
# full CI-log dig plus a registry-config inspection, when the container had
|
||||||
|
# already printed the answer and thrown it away. Assertions that want a
|
||||||
|
# diagnostic just echo it to stderr — it stays hidden while they pass.
|
||||||
run() {
|
run() {
|
||||||
local label="$1"; local cmd="$2"
|
local label="$1"; local cmd="$2"
|
||||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" >/dev/null 2>&1; then
|
local out
|
||||||
|
if out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$cmd" 2>&1); then
|
||||||
printf " ✅ %s\n" "$label"; PASS=$((PASS+1))
|
printf " ✅ %s\n" "$label"; PASS=$((PASS+1))
|
||||||
else
|
else
|
||||||
printf " ❌ %s\n" "$label"; FAIL=$((FAIL+1))
|
printf " ❌ %s\n" "$label"; FAIL=$((FAIL+1))
|
||||||
|
# `if`, not `&&` — a trailing false under `set -e` would abort the script.
|
||||||
|
if [ -n "$out" ]; then
|
||||||
|
printf " └─ %s\n" "$(printf '%s' "$out" | tail -3 | tr '\n' ' ' | cut -c1-300)"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,8 +113,37 @@ run "mempalace-pi-session on PATH" "mempalace-pi-session --help"
|
|||||||
# default-staged run at a populated dir would export whatever transcripts it
|
# default-staged run at a populated dir would export whatever transcripts it
|
||||||
# finds into the real stage, which is how a synthetic test session ends up
|
# finds into the real stage, which is how a synthetic test session ends up
|
||||||
# staged for mining as if it were a real conversation.
|
# staged for mining as if it were a real conversation.
|
||||||
|
#
|
||||||
|
# Asserted $HOME-RELATIVE, not against a literal /home/developer. `run` invokes
|
||||||
|
# `docker run --entrypoint=""`, and neither Dockerfile sets USER or ENV HOME
|
||||||
|
# (HOME is set by entrypoint-user.sh, which --entrypoint="" deliberately skips),
|
||||||
|
# so these assertions execute as root with HOME=/root. The original literal
|
||||||
|
# /home/developer form could therefore never match and failed the v1.8.0
|
||||||
|
# release — a test bug, not a product one: the stage resolution was correct all
|
||||||
|
# along, it just follows $HOME. The invariant under test ("the stage sits beside
|
||||||
|
# the palace, sharing its lifetime") is user-independent, so pinning the user
|
||||||
|
# was never part of it. A cache-dir default still fails the pattern below, which
|
||||||
|
# is the regression this guards.
|
||||||
|
#
|
||||||
|
# It went unnoticed for three days because this workflow only triggers on
|
||||||
|
# `push: tags: v*` — the assertion was added on a main push, so v1.8.0 was its
|
||||||
|
# first execution ever. Use the `smoke_only` workflow_dispatch input to run
|
||||||
|
# smoke against HEAD without cutting a tag.
|
||||||
run "pi stage defaults next to the palace (not a cache dir)" '
|
run "pi stage defaults next to the palace (not a cache dir)" '
|
||||||
out=$(mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
out=$(mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||||
|
stage=$(echo "$out" | grep -oE "stage=[^ ]+" | head -1)
|
||||||
|
echo "resolved ${stage:-<no stage= line>} with HOME=$HOME" >&2
|
||||||
|
case "$stage" in
|
||||||
|
"stage=$HOME/.mempalace/pi-stage/"*) exit 0 ;;
|
||||||
|
*) exit 1 ;;
|
||||||
|
esac
|
||||||
|
'
|
||||||
|
# Companion to the above: the deployment-specific case the literal assertion was
|
||||||
|
# reaching for, done properly by supplying the HOME the container actually runs
|
||||||
|
# with instead of assuming it.
|
||||||
|
run "pi stage is palace-adjacent for the developer user" '
|
||||||
|
out=$(HOME=/home/developer mempalace-pi-session --dry-run --reason smoke --sessions-dir "$(mktemp -d)" 2>&1) || true
|
||||||
|
echo "$out" | grep -oE "stage=[^ ]+" | head -1 >&2
|
||||||
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
echo "$out" | grep -q "stage=/home/developer/.mempalace/pi-stage/"
|
||||||
'
|
'
|
||||||
run "pi stage follows MEMPALACE_PALACE_PATH" '
|
run "pi stage follows MEMPALACE_PALACE_PATH" '
|
||||||
|
|||||||
Reference in New Issue
Block a user