test: add runtime recreate-sanity-check script
Runtime peer to the build-time smoke-test.sh: run inside the container after `docker compose up -d --force-recreate` to confirm the new image is live (opencode version matches Dockerfile.variant), persisted named volumes survived, omos skill symlinks resolve, shell defaults re-seeded, and /opt toolkits intact. smoke-test.sh runs with --entrypoint="" and cannot see the running container's volumes/symlinks, hence a separate runtime check. Not run by CI or the entrypoint (it needs the release-time expected version and a running container). Maintainer tooling, not baked into the image. Registered in AGENTS.md File roles. Doc/script-only — no image rebuild.
This commit is contained in:
@@ -22,6 +22,7 @@ Docker image packaging [opencode](https://opencode.ai) into a production-ready d
|
|||||||
- `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. The top `Host *` block also overrides `UserKnownHostsFile` and `ControlPath` into the writable `~/.ssh-local` sidecar (first-value-wins), because the bind-mounted `~/.ssh` is read-only — otherwise multiplexed hosts (`ControlPath ~/.ssh/cm/...`) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
|
- `rootfs/usr/local/lib/opencode-devbox/setup-lan-access.sh` — host-OS-agnostic LAN reachability helper. Detects VM-backed hosts (macOS OrbStack / Docker Desktop, via `host.docker.internal` resolution) and generates a writable `~/.ssh-local/config` using the host as an SSH jump; no-op on native Linux. Controlled by `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` / `DEVBOX_HOST_ALIAS` / `DEVBOX_LAN_AUTOJUMP_PRIVATE`. Ships the mechanism only (generic `host` jump alias); user targets stay host-side — named-peer `ProxyJump host` overrides go in a bind-mounted `~/.config/devbox-shell/ssh-lan.conf` (Included before `~/.ssh/config`), never baked into the image. **Scoping invariant:** every `Include` in the generated config MUST be preceded by a bare `Host *` reset — an `Include` is scoped to the enclosing `Host`/`Match` block, so without the reset the included config only applies when targeting `host`/`mac` and named peers fall back to SSH defaults. The top `Host *` block also overrides `UserKnownHostsFile` and `ControlPath` into the writable `~/.ssh-local` sidecar (first-value-wins), because the bind-mounted `~/.ssh` is read-only — otherwise multiplexed hosts (`ControlPath ~/.ssh/cm/...`) fail to create their master socket. Non-fatal. Counted in the base hash, so editing it advances `base-latest`.
|
||||||
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
|
- `rootfs/usr/local/lib/opencode-devbox/generate-config.py` — generates `~/.config/opencode/opencode.jsonc` from env vars. Never overwrites an existing config (checks both `.json` and `.jsonc`). Auto-registers MCP servers for detected tools (mempalace via `mempalace-mcp`, gitea-mcp, context7 remote endpoint).
|
||||||
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
- `scripts/smoke-test.sh` — post-build image verification. Asserts binary presence, opencode startup, entrypoint correctness, config generation idempotency, and image size thresholds. Used by both CI workflows.
|
||||||
|
- `scripts/recreate-sanity-check.sh` — **runtime** post-recreate verification (counterpart to the build-time `smoke-test.sh`). Run inside the container after `docker compose up -d --force-recreate` to confirm the new image is live (opencode version matches `Dockerfile.variant`'s `OPENCODE_VERSION`), persisted named volumes survived (mempalace palace, opencode.db, bash-history), omos runtime skill symlinks resolve, shell defaults re-seeded, and `/opt` toolkits intact. Not run by CI or the entrypoint — it needs the running container + volumes that smoke-test.sh (which uses `--entrypoint=""`) cannot see.
|
||||||
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
- `scripts/generate-dockerhub-md.py` — generates `DOCKER_HUB.md` from a hand-maintained `HUB_TEMPLATE` constant. `--check` fails if the committed file is out of sync (enforced by the `validate` workflow).
|
||||||
- `DOCKER_HUB.md` — **auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
- `DOCKER_HUB.md` — **auto-generated** from `HUB_TEMPLATE` in `scripts/generate-dockerhub-md.py`. Do not edit directly. Pushed to Docker Hub description via CI API call. Must stay under 25 kB. Short description field must be ≤100 bytes.
|
||||||
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
|
- `README.md` — authoritative source documentation for everything in this repo. Independent of `DOCKER_HUB.md`: the Hub doc is hand-maintained in the generator's `HUB_TEMPLATE` and intentionally slim, linking back to the gitea README for depth.
|
||||||
|
|||||||
Executable
+212
@@ -0,0 +1,212 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Runtime post-recreate verification for opencode-devbox.
|
||||||
|
#
|
||||||
|
# Verifies that after `docker compose up -d --force-recreate`:
|
||||||
|
# - The new image is actually live (opencode version matches Dockerfile.variant)
|
||||||
|
# - Persisted named volumes survived (mempalace palace, opencode.db, bash-history)
|
||||||
|
# - OMOS runtime skill symlinks resolve (omos variant only)
|
||||||
|
# - Shell defaults re-seeded from /etc/skel-devbox
|
||||||
|
# - /opt toolkits intact
|
||||||
|
# - Known expected-absences don't regress
|
||||||
|
#
|
||||||
|
# This is repo/maintainer tooling — the runtime peer of smoke-test.sh. It is
|
||||||
|
# NOT baked into the published Docker Hub image; run it from a checkout of the
|
||||||
|
# opencode-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.
|
||||||
|
#
|
||||||
|
# Usage: ./scripts/recreate-sanity-check.sh [--expected-version X.Y.Z] [--variant base|omos]
|
||||||
|
#
|
||||||
|
# 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 base|omos]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
FAILED=0
|
||||||
|
pass() { echo " ✓ $1"; }
|
||||||
|
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
||||||
|
warn() { echo " ⚠ $1" >&2; }
|
||||||
|
|
||||||
|
# Determine expected opencode version from Dockerfile.variant if not provided
|
||||||
|
if [ -z "$EXPECTED_VERSION" ]; then
|
||||||
|
EXPECTED_VERSION="$(grep -oE 'OPENCODE_VERSION=[0-9.]+' "$REPO_DIR/Dockerfile.variant" | head -1 | cut -d= -f2)"
|
||||||
|
if [ -z "$EXPECTED_VERSION" ]; then
|
||||||
|
echo "error: could not determine OPENCODE_VERSION from $REPO_DIR/Dockerfile.variant" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Auto-detect variant if not provided
|
||||||
|
if [ -z "$VARIANT" ]; then
|
||||||
|
if command -v bun >/dev/null 2>&1 || [ -d /usr/lib/node_modules/oh-my-opencode-slim ] || [ -d /usr/local/lib/node_modules/oh-my-opencode-slim ]; then
|
||||||
|
VARIANT="omos"
|
||||||
|
else
|
||||||
|
VARIANT="base"
|
||||||
|
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 "-- opencode version --"
|
||||||
|
if ACTUAL_VERSION=$(opencode --version 2>&1 | head -1); then
|
||||||
|
if [ "$ACTUAL_VERSION" = "$EXPECTED_VERSION" ]; then
|
||||||
|
pass "opencode version $ACTUAL_VERSION"
|
||||||
|
else
|
||||||
|
fail "opencode version mismatch: expected $EXPECTED_VERSION, got $ACTUAL_VERSION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "opencode --version failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Persisted named volumes (must survive --force-recreate) --"
|
||||||
|
|
||||||
|
# mempalace palace volume
|
||||||
|
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
|
||||||
|
fail "~/.mempalace/palace/chroma.sqlite3 missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# opencode session history volume
|
||||||
|
if [ -f "$HOME/.local/share/opencode/opencode.db" ]; then
|
||||||
|
SIZE=$(du -h "$HOME/.local/share/opencode/opencode.db" | cut -f1)
|
||||||
|
if [ -s "$HOME/.local/share/opencode/opencode.db" ]; then
|
||||||
|
pass "~/.local/share/opencode/opencode.db exists ($SIZE)"
|
||||||
|
else
|
||||||
|
fail "~/.local/share/opencode/opencode.db exists but is empty"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "~/.local/share/opencode/opencode.db missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# bash-history volume mount point (empty .bash_history right after recreate is NORMAL)
|
||||||
|
if [ -d "$HOME/.cache/bash" ]; then
|
||||||
|
pass "~/.cache/bash exists as directory"
|
||||||
|
else
|
||||||
|
fail "~/.cache/bash missing or not a directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- omos runtime skill symlinks (omos variant only; skip on base) --"
|
||||||
|
if [ "$VARIANT" = "omos" ]; then
|
||||||
|
SKILLS_OK=0
|
||||||
|
SKILLS_TOTAL=5
|
||||||
|
for skill in clonedeps codemap deepwork oh-my-opencode-slim simplify; do
|
||||||
|
SKILL_PATH="$HOME/.agents/skills/$skill"
|
||||||
|
if [ -L "$SKILL_PATH" ]; then
|
||||||
|
TARGET=$(readlink -f "$SKILL_PATH")
|
||||||
|
# Check if target resolves to a real directory and contains the expected path
|
||||||
|
if [ -d "$TARGET" ] && echo "$TARGET" | grep -q "node_modules/oh-my-opencode-slim/src/skills/$skill"; then
|
||||||
|
SKILLS_OK=$((SKILLS_OK + 1))
|
||||||
|
else
|
||||||
|
fail "~/.agents/skills/$skill symlink target invalid: $TARGET"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "~/.agents/skills/$skill missing or not a symlink"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$SKILLS_OK" -eq "$SKILLS_TOTAL" ]; then
|
||||||
|
pass "$SKILLS_OK/$SKILLS_TOTAL omos skill symlinks resolve"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Migration marker
|
||||||
|
if [ -f "$HOME/.config/opencode/.omos-skills-migrated" ]; then
|
||||||
|
pass "~/.config/opencode/.omos-skills-migrated exists"
|
||||||
|
else
|
||||||
|
fail "~/.config/opencode/.omos-skills-migrated missing"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " - skipped (base variant)"
|
||||||
|
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
|
||||||
|
fail "/workspace/cli_utils missing or .git subdir absent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Baked /opt toolkits --"
|
||||||
|
if [ -d /opt/mempalace-toolkit ]; then
|
||||||
|
if MEMPALACE_SESSION_PATH=$(command -v mempalace-session 2>/dev/null); then
|
||||||
|
RESOLVED=$(readlink -f "$MEMPALACE_SESSION_PATH")
|
||||||
|
pass "/opt/mempalace-toolkit exists, mempalace-session resolves to $RESOLVED"
|
||||||
|
else
|
||||||
|
fail "/opt/mempalace-toolkit exists but mempalace-session not on PATH"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "/opt/mempalace-toolkit missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "-- Known expected-absences (regressions vs by-design) --"
|
||||||
|
if [ ! -d "$HOME/.local/bin" ]; then
|
||||||
|
warn "~/.local/bin absent — expected; mempalace toolkit relocated to /opt (not a wrapper-loss regression)"
|
||||||
|
else
|
||||||
|
pass "~/.local/bin exists (toolkit may have been installed locally)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$FAILED" -gt 0 ]; then
|
||||||
|
echo "=== FAILED: $FAILED check(s) ===" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "=== PASSED ==="
|
||||||
Reference in New Issue
Block a user