Add runtime post-recreate sanity check (peer of smoke-test.sh)
scripts/recreate-sanity-check.sh verifies what is actually live in a recreated container — persisted volumes, pi runtime wiring (keybindings, extensions, mempalace.ts bridge, settings.json, fork/obsmem/studio registrations), /tmp/sshcm, skel defaults, /opt toolkits. smoke-test.sh runs at build time with --entrypoint="" and cannot see any of this. Variant (studio/plain) auto-detected via /opt/pi-studio. pi version is asserted only with --expected-version (built from 'latest', no Dockerfile pin to self-derive). Maintainer tooling, not baked into the image. Documented in README and CHANGELOG.
This commit is contained in:
@@ -37,6 +37,25 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
— which hashes differently than a SHA and triggers one *extra* rebuild,
|
— which hashes differently than a SHA and triggers one *extra* rebuild,
|
||||||
never a *missed* one (fail-toward-rebuild).
|
never a *missed* one (fail-toward-rebuild).
|
||||||
|
|
||||||
|
### Added (maintainer tooling, no image change)
|
||||||
|
|
||||||
|
- **`scripts/recreate-sanity-check.sh`** — runtime post-recreate sanity
|
||||||
|
check; the runtime peer of `smoke-test.sh`. Where `smoke-test.sh` runs at
|
||||||
|
build time with `--entrypoint=""` (and so can never see persisted volumes
|
||||||
|
or the entrypoint's runtime deploy), this verifies what is actually live
|
||||||
|
in the container *after* `docker compose up -d --force-recreate`:
|
||||||
|
persisted named volumes survived, the pi runtime wiring is intact
|
||||||
|
(keybindings symlink, ≥4 extensions, `mempalace.ts` bridge, `settings.json`,
|
||||||
|
and pi-fork / pi-observational-memory / pi-studio registrations),
|
||||||
|
`/tmp/sshcm` is mode 700, shell defaults re-seeded, and `/opt` toolkits
|
||||||
|
intact. Variant (studio/plain) auto-detected via `/opt/pi-studio`. Since
|
||||||
|
pi is built from `latest` (no concrete Dockerfile pin), the version check
|
||||||
|
asserts only when `--expected-version` is passed, else WARNs. Not baked
|
||||||
|
into the image — repo/maintainer tooling, same category as
|
||||||
|
`smoke-test.sh`. A short-name wrapper (`pi-devbox-sanity`) lives in
|
||||||
|
`cli_utils/bin`, kept separate from opencode-devbox's `devbox-sanity` so
|
||||||
|
hosts with only one devbox checked out stay self-contained.
|
||||||
|
|
||||||
### Docs (no image change)
|
### Docs (no image change)
|
||||||
|
|
||||||
- Correct the MemPalace `diary_write` anyOf workaround watch-target in
|
- Correct the MemPalace `diary_write` anyOf workaround watch-target in
|
||||||
|
|||||||
@@ -539,6 +539,26 @@ ssh-jump-via-host configuration. Set `DEVBOX_LAN_ACCESS=jump` and
|
|||||||
./scripts/smoke-test.sh joakimp/pi-devbox:latest
|
./scripts/smoke-test.sh joakimp/pi-devbox:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`smoke-test.sh` is a **build-time** check (runs with `--entrypoint=""`), so
|
||||||
|
it validates image contents and a fresh entrypoint deploy — it never sees a
|
||||||
|
recreated container's persisted volumes.
|
||||||
|
|
||||||
|
### Post-recreate sanity check
|
||||||
|
|
||||||
|
After `docker compose up -d --force-recreate`, run the **runtime** peer of
|
||||||
|
`smoke-test.sh` from *inside* the container to confirm the new image is live,
|
||||||
|
persisted volumes survived, and pi runtime wiring is intact:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/recreate-sanity-check.sh # auto-detects variant
|
||||||
|
./scripts/recreate-sanity-check.sh --expected-version 0.79.3 # assert pi version
|
||||||
|
```
|
||||||
|
|
||||||
|
If `cli_utils` is on your PATH, the `pi-devbox-sanity` wrapper runs the same
|
||||||
|
check by short name and locates the repo automatically (override with
|
||||||
|
`PI_DEVBOX_REPO=/path/to/pi-devbox`). Like `smoke-test.sh`, this script is
|
||||||
|
maintainer tooling and is **not** shipped in the published image.
|
||||||
|
|
||||||
## Versioning and release
|
## Versioning and release
|
||||||
|
|
||||||
pi-devbox follows semver-ish:
|
pi-devbox follows semver-ish:
|
||||||
|
|||||||
Executable
+273
@@ -0,0 +1,273 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runtime post-recreate verification for pi-devbox.
|
||||||
|
#
|
||||||
|
# Verifies that after `docker compose up -d --force-recreate`:
|
||||||
|
# - The new image is actually live (pi version matches, when an expected
|
||||||
|
# version is supplied — see the version note below)
|
||||||
|
# - Persisted named volumes survived (~/.pi config, shell history, zoxide,
|
||||||
|
# nvim data, uv cache, ssh-local)
|
||||||
|
# - pi runtime wiring is intact: keybindings symlink, ≥4 extensions, the
|
||||||
|
# mempalace.ts bridge, settings.json, and the pi-fork /
|
||||||
|
# pi-observational-memory / (studio variant) pi-studio package registrations
|
||||||
|
# - Shell defaults re-seeded from /etc/skel-devbox
|
||||||
|
# - /tmp/sshcm exists with mode 700 (ssh ControlMaster dir)
|
||||||
|
# - /opt toolkits intact
|
||||||
|
# - Known expected-absences don't regress
|
||||||
|
#
|
||||||
|
# This is repo/maintainer tooling — the runtime peer of smoke-test.sh.
|
||||||
|
# smoke-test.sh runs at BUILD time with `--entrypoint=""`, so it can never see
|
||||||
|
# a recreated container's persisted volumes or the entrypoint's runtime
|
||||||
|
# deploy. This script is its runtime counterpart: it inspects what is actually
|
||||||
|
# live in the container you are sitting in after a recreate.
|
||||||
|
#
|
||||||
|
# It is NOT baked into the published Docker Hub image; run it from a checkout of
|
||||||
|
# the pi-devbox repo (which a maintainer already has for CI builds). A plain
|
||||||
|
# `docker pull` consumer is not the audience and will not have this file.
|
||||||
|
#
|
||||||
|
# Version note: pi's version is resolved from `latest` at CI build time and is
|
||||||
|
# NOT pinned to a concrete value in Dockerfile.variant (ARG PI_VERSION=latest).
|
||||||
|
# So unlike opencode-devbox, this script cannot self-derive an expected version
|
||||||
|
# from the Dockerfile. Pass --expected-version to assert a match; without it the
|
||||||
|
# live pi version is reported as an informational WARN, not a failure.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] [--variant studio|plain]
|
||||||
|
#
|
||||||
|
# Exit codes:
|
||||||
|
# 0 all checks passed
|
||||||
|
# 1 one or more checks failed
|
||||||
|
# 2 usage error
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
EXPECTED_VERSION=""
|
||||||
|
VARIANT=""
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--expected-version)
|
||||||
|
EXPECTED_VERSION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--variant)
|
||||||
|
VARIANT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 [--expected-version X.Y.Z] [--variant studio|plain]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
pass() { echo " ✓ $1"; }
|
||||||
|
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
||||||
|
warn() { echo " ⚠ $1" >&2; }
|
||||||
|
|
||||||
|
# Auto-detect variant if not provided. The studio variant vendors pi-studio to
|
||||||
|
# /opt/pi-studio; the plain variant does not.
|
||||||
|
if [ -z "$VARIANT" ]; then
|
||||||
|
if [ -d /opt/pi-studio ]; then
|
||||||
|
VARIANT="studio"
|
||||||
|
else
|
||||||
|
VARIANT="plain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Print header with git context
|
||||||
|
echo "=== Recreate sanity check (variant: $VARIANT) ==="
|
||||||
|
if GIT_TAG=$(git -C "$REPO_DIR" describe --tags 2>/dev/null); then
|
||||||
|
echo " Repo HEAD: $GIT_TAG (version-match only meaningful when image tag matches)"
|
||||||
|
else
|
||||||
|
echo " Repo HEAD: (not a git repo or no tags)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "-- pi version --"
|
||||||
|
if ACTUAL_VERSION=$(pi --version 2>&1 | head -1); then
|
||||||
|
if [ -n "$EXPECTED_VERSION" ]; then
|
||||||
|
if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then
|
||||||
|
pass "pi version $ACTUAL_VERSION"
|
||||||
|
else
|
||||||
|
fail "pi version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "pi version $ACTUAL_VERSION (no --expected-version given; pi is built from 'latest', cannot self-derive — informational only)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "pi --version failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Persisted named volumes (must survive --force-recreate) --"
|
||||||
|
|
||||||
|
# ~/.pi config volume (devbox-pi-config) — holds agent settings, extensions,
|
||||||
|
# keybindings symlink. Must exist and be non-empty after recreate.
|
||||||
|
if [ -d "$HOME/.pi/agent" ] && [ -n "$(ls -A "$HOME/.pi/agent" 2>/dev/null)" ]; then
|
||||||
|
pass "~/.pi/agent exists and is non-empty"
|
||||||
|
else
|
||||||
|
fail "~/.pi/agent missing or empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shell history volume (devbox-shell-history). An empty .bash_history right
|
||||||
|
# after recreate is NORMAL — only the mount point must exist.
|
||||||
|
if [ -d "$HOME/.cache/bash" ]; then
|
||||||
|
pass "~/.cache/bash exists as directory"
|
||||||
|
else
|
||||||
|
fail "~/.cache/bash missing or not a directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# remaining persisted volumes — mount points must exist
|
||||||
|
for vol_path in \
|
||||||
|
"$HOME/.local/share/zoxide" \
|
||||||
|
"$HOME/.local/share/nvim" \
|
||||||
|
"$HOME/.local/share/uv" \
|
||||||
|
"$HOME/.ssh-local"; do
|
||||||
|
if [ -d "$vol_path" ]; then
|
||||||
|
pass "$vol_path exists"
|
||||||
|
else
|
||||||
|
fail "$vol_path missing or not a directory"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# mempalace palace — CONDITIONAL. In this repo's docker-compose.yml the
|
||||||
|
# devbox-palace named volume is commented out; the palace is reached via the
|
||||||
|
# shared /workspace (virtiofs) path instead. So absence of a local palace dir
|
||||||
|
# is NOT a recreate regression here.
|
||||||
|
if [ -f "$HOME/.mempalace/palace/chroma.sqlite3" ]; then
|
||||||
|
SIZE=$(du -h "$HOME/.mempalace/palace/chroma.sqlite3" | cut -f1)
|
||||||
|
if [ -s "$HOME/.mempalace/palace/chroma.sqlite3" ]; then
|
||||||
|
pass "~/.mempalace/palace/chroma.sqlite3 exists ($SIZE)"
|
||||||
|
else
|
||||||
|
fail "~/.mempalace/palace/chroma.sqlite3 exists but is empty"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "~/.mempalace/palace/chroma.sqlite3 absent — expected unless devbox-palace volume is enabled (palace is shared via /workspace by default)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- pi runtime wiring (deployed by entrypoint-user.sh) --"
|
||||||
|
|
||||||
|
# keybindings symlink (pi-toolkit)
|
||||||
|
if [ -L "$HOME/.pi/agent/keybindings.json" ]; then
|
||||||
|
pass "~/.pi/agent/keybindings.json symlink (pi-toolkit)"
|
||||||
|
else
|
||||||
|
fail "~/.pi/agent/keybindings.json missing or not a symlink"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# extensions deployed (pi-extensions) — expect ≥4 *.ts
|
||||||
|
EXT_COUNT=$(ls -1 "$HOME"/.pi/agent/extensions/*.ts 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
if [ "$EXT_COUNT" -ge 4 ]; then
|
||||||
|
pass "$EXT_COUNT extensions deployed (≥4, pi-extensions)"
|
||||||
|
else
|
||||||
|
fail "only $EXT_COUNT extensions deployed (expected ≥4)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# mempalace.ts bridge symlink
|
||||||
|
if [ -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then
|
||||||
|
pass "~/.pi/agent/extensions/mempalace.ts bridge symlink"
|
||||||
|
else
|
||||||
|
fail "~/.pi/agent/extensions/mempalace.ts missing or not a symlink"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# settings.json bootstrapped
|
||||||
|
if [ -f "$HOME/.pi/agent/settings.json" ]; then
|
||||||
|
pass "~/.pi/agent/settings.json bootstrapped"
|
||||||
|
else
|
||||||
|
fail "~/.pi/agent/settings.json missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# pi package registrations (pi install <local-path> → recorded in settings.json)
|
||||||
|
if [ -f "$HOME/.pi/agent/settings.json" ]; then
|
||||||
|
for pkg in pi-fork pi-observational-memory; do
|
||||||
|
if grep -q "$pkg" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||||
|
pass "$pkg registered in settings.json"
|
||||||
|
else
|
||||||
|
fail "$pkg not registered in settings.json"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$VARIANT" = "studio" ]; then
|
||||||
|
if grep -q "pi-studio" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||||
|
pass "pi-studio registered in settings.json"
|
||||||
|
else
|
||||||
|
fail "pi-studio not registered in settings.json (studio variant)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- ssh ControlMaster dir --"
|
||||||
|
if [ -d /tmp/sshcm ] && [ "$(stat -c %a /tmp/sshcm 2>/dev/null)" = "700" ]; then
|
||||||
|
pass "/tmp/sshcm exists with mode 700"
|
||||||
|
else
|
||||||
|
fail "/tmp/sshcm missing or not mode 700"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Shell defaults re-seeded from /etc/skel-devbox --"
|
||||||
|
if [ -f "$HOME/.bash_aliases" ]; then
|
||||||
|
pass "~/.bash_aliases exists"
|
||||||
|
else
|
||||||
|
fail "~/.bash_aliases missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -f "$HOME/.inputrc" ]; then
|
||||||
|
pass "~/.inputrc exists"
|
||||||
|
else
|
||||||
|
fail "~/.inputrc missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- cli_utils bind-mount --"
|
||||||
|
if [ -d /workspace/cli_utils ] && [ -d /workspace/cli_utils/.git ]; then
|
||||||
|
pass "/workspace/cli_utils exists with .git subdir"
|
||||||
|
else
|
||||||
|
warn "/workspace/cli_utils missing or .git subdir absent — expected only if cli_utils is bind-mounted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Baked /opt toolkits --"
|
||||||
|
for opt_path in /opt/pi-toolkit /opt/pi-extensions /opt/pi-fork /opt/pi-observational-memory /opt/mempalace-toolkit; do
|
||||||
|
if [ -d "$opt_path" ]; then
|
||||||
|
pass "$opt_path exists"
|
||||||
|
else
|
||||||
|
fail "$opt_path missing"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$VARIANT" = "studio" ]; then
|
||||||
|
if [ -d /opt/pi-studio ] && [ -f /opt/pi-studio/client/studio-client.js ]; then
|
||||||
|
pass "/opt/pi-studio exists with prebuilt client bundle"
|
||||||
|
else
|
||||||
|
fail "/opt/pi-studio missing or prebuilt client bundle absent (studio variant)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# mempalace MCP entrypoint on PATH
|
||||||
|
if command -v mempalace-mcp >/dev/null 2>&1; then
|
||||||
|
pass "mempalace-mcp on PATH"
|
||||||
|
else
|
||||||
|
fail "mempalace-mcp not on PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Known expected-absences (regressions vs by-design) --"
|
||||||
|
if ! command -v go >/dev/null 2>&1; then
|
||||||
|
warn "go absent — expected unless image built with INSTALL_GO=true"
|
||||||
|
else
|
||||||
|
pass "go is on PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$VARIANT" = "plain" ] && [ ! -d /opt/pi-studio ]; then
|
||||||
|
warn "/opt/pi-studio absent — expected on the plain (non-studio) variant"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "=== FAILED: $FAILED check(s) ===" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "=== PASSED ==="
|
||||||
Reference in New Issue
Block a user