23bae2ab7d
Validate / docs-check (push) Successful in 20s
Validate / validate-base (push) Successful in 11m32s
Validate / validate-omos (push) Successful in 15m18s
Publish Docker Image / build-base (push) Successful in 53m5s
Publish Docker Image / build-omos (push) Successful in 1h11m3s
Publish Docker Image / update-description (push) Successful in 15s
The mempalace Python package ships a 'mempalace-mcp' console entry
point; 'uv tool install' places it on PATH as a shim whose shebang
points at the isolated venv's Python. Our hand-rolled wrapper at
/usr/local/bin/mempalace-mcp-server was duplicating what uv installs
for free — one less file to maintain.
Fixes the MCP error users saw after the v1.14.28b → v1.14.29 upgrade
path: custom opencode.json files typically had the pre-v1.14.29
command ['python3', '-m', 'mempalace.mcp_server'] which worked with
the old pip install but fails silently after the uv-tool migration
because system python3 cannot import from the venv. Opencode surfaced
this as 'MCP error -32000: connection closed'.
- generate-config.py now emits ['mempalace-mcp'] and keys its detect
on shutil.which('mempalace-mcp').
- Dockerfile drops 'COPY rootfs/usr/local/bin/' and the chmod of the
wrapper. Build shrinks from 30 to 29 stages.
- rootfs/usr/local/bin/ removed entirely.
- Smoke test asserts /usr/local/bin/mempalace-mcp is executable and
prints its symlink target.
- README's MemPalace section shows ['mempalace-mcp'] and explicitly
warns against the old pattern with the observed failure mode.
- CHANGELOG adds a v1.14.29c entry.
219 lines
7.8 KiB
Bash
Executable File
219 lines
7.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Smoke-test a freshly-built opencode-devbox image.
|
|
#
|
|
# Verifies:
|
|
# - Core binaries are on PATH and runnable
|
|
# - opencode itself starts and prints a version
|
|
# - Entrypoint runs cleanly as non-root after UID adjustment
|
|
# - Generated opencode.json has the expected shape
|
|
# - MCP wrapper works (when mempalace is installed)
|
|
#
|
|
# Usage: ./scripts/smoke-test.sh <image> [--variant base|omos]
|
|
#
|
|
# Exit codes:
|
|
# 0 all checks passed
|
|
# 1 one or more checks failed
|
|
|
|
set -euo pipefail
|
|
|
|
IMAGE="${1:-}"
|
|
VARIANT="base"
|
|
if [ "${2:-}" = "--variant" ]; then
|
|
VARIANT="${3:-base}"
|
|
fi
|
|
|
|
if [ -z "$IMAGE" ]; then
|
|
echo "usage: $0 <image> [--variant base|omos]" >&2
|
|
exit 2
|
|
fi
|
|
|
|
FAILED=0
|
|
pass() { echo " ✓ $1"; }
|
|
fail() { echo " ✗ $1" >&2; FAILED=$((FAILED + 1)); }
|
|
|
|
run() {
|
|
# Run a command inside the image and capture its output.
|
|
# First arg is a label, rest is the shell command.
|
|
local label="$1"; shift
|
|
local out
|
|
if out=$(docker run --rm --entrypoint="" "$IMAGE" sh -c "$*" 2>&1); then
|
|
pass "$label ($(echo "$out" | head -1))"
|
|
else
|
|
fail "$label: $out"
|
|
fi
|
|
}
|
|
|
|
echo "=== Smoke test: $IMAGE (variant: $VARIANT) ==="
|
|
echo
|
|
echo "-- Resolved component versions --"
|
|
# Prints the actual version of every floating component so CI logs
|
|
# always record what got baked into this image, even when Dockerfile
|
|
# ARGs default to "latest".
|
|
docker run --rm --entrypoint="" "$IMAGE" sh -c '
|
|
printf " %-15s %s\n" "opencode" "$(opencode --version 2>&1 | head -1)"
|
|
printf " %-15s %s\n" "node" "$(node --version)"
|
|
printf " %-15s %s\n" "npm" "$(npm --version)"
|
|
printf " %-15s %s\n" "nvim" "$(nvim --version | head -1)"
|
|
printf " %-15s %s\n" "bat" "$(bat --version)"
|
|
printf " %-15s %s\n" "eza" "$(eza --version | head -2 | tail -1)"
|
|
printf " %-15s %s\n" "zoxide" "$(zoxide --version)"
|
|
printf " %-15s %s\n" "uv" "$(uv --version)"
|
|
printf " %-15s %s\n" "fzf" "$(fzf --version)"
|
|
printf " %-15s %s\n" "fd" "$(fd --version)"
|
|
printf " %-15s %s\n" "rg" "$(rg --version | head -1)"
|
|
printf " %-15s %s\n" "gosu" "$(gosu --version)"
|
|
printf " %-15s %s\n" "git-lfs" "$(git-lfs --version)"
|
|
printf " %-15s %s\n" "gitea-mcp" "$(gitea-mcp --version 2>&1 | head -1)"
|
|
printf " %-15s %s\n" "aws" "$(aws --version 2>&1)"
|
|
if command -v bun >/dev/null 2>&1; then
|
|
printf " %-15s %s\n" "bun" "$(bun --version)"
|
|
fi
|
|
if command -v mempalace >/dev/null 2>&1; then
|
|
printf " %-15s %s\n" "mempalace" "$(mempalace --version 2>&1 | head -1 || echo installed)"
|
|
fi
|
|
'
|
|
echo
|
|
echo "-- Core binaries --"
|
|
run "opencode" "opencode --version"
|
|
run "node" "node --version"
|
|
run "npm" "npm --version"
|
|
run "git" "git --version"
|
|
run "nvim" "nvim --version | head -1"
|
|
run "bat" "bat --version"
|
|
run "eza" "eza --version | head -1"
|
|
run "zoxide" "zoxide --version"
|
|
run "uv" "uv --version"
|
|
run "uvx" "uvx --version"
|
|
run "rustup-init" "rustup-init --version"
|
|
run "fzf" "fzf --version"
|
|
run "fd" "fd --version"
|
|
run "rg" "rg --version | head -1"
|
|
run "jq" "jq --version"
|
|
run "aws" "aws --version"
|
|
run "gitea-mcp" "gitea-mcp --version"
|
|
run "gosu" "gosu --version"
|
|
run "tmux" "tmux -V"
|
|
|
|
echo
|
|
echo "-- Optional / variant-gated --"
|
|
# mempalace: present unless built with INSTALL_MEMPALACE=false
|
|
if docker run --rm --entrypoint="" "$IMAGE" sh -c "command -v mempalace" >/dev/null 2>&1; then
|
|
run "mempalace" "mempalace --help | head -1"
|
|
run "mempalace-mcp" "test -x /usr/local/bin/mempalace-mcp && readlink /usr/local/bin/mempalace-mcp"
|
|
else
|
|
echo " - mempalace not installed (INSTALL_MEMPALACE=false)"
|
|
fi
|
|
|
|
# bun: only in the omos variant
|
|
if [ "$VARIANT" = "omos" ]; then
|
|
run "bun (omos)" "bun --version"
|
|
run "bunx symlink (omos)" "test -L /usr/local/bin/bunx && readlink /usr/local/bin/bunx"
|
|
# oh-my-opencode-slim is npm-installed globally (not a bun install);
|
|
# verify it shows up in the global module list.
|
|
run "oh-my-opencode-slim" "npm ls -g --depth=0 2>/dev/null | grep oh-my-opencode-slim"
|
|
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"
|
|
else
|
|
pass "bun correctly absent from base image"
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
echo "-- Entrypoint behaviour --"
|
|
|
|
# Generate-config script exists and has valid syntax.
|
|
run "generate-config.py exists" \
|
|
"test -x /usr/local/lib/opencode-devbox/generate-config.py && python3 -m py_compile /usr/local/lib/opencode-devbox/generate-config.py && echo ok"
|
|
|
|
# Entrypoint drops to developer user and runs a trivial command.
|
|
# Writes the result to a file inside the container so we don't have to
|
|
# disentangle entrypoint log output from command stdout on the host.
|
|
label="entrypoint drops to developer"
|
|
tmpout=$(mktemp)
|
|
if docker run --rm -e OPENCODE_PROVIDER= "$IMAGE" \
|
|
sh -c 'whoami > /tmp/who && cat /tmp/who' > "$tmpout" 2>/dev/null; then
|
|
# The last line of stdout is the whoami output. Entrypoint log lines
|
|
# (MemPalace init, "Adjusted developer UID", etc.) go to stderr or
|
|
# get printed before our sh command runs.
|
|
actual=$(tail -1 "$tmpout" | tr -d '[:space:]')
|
|
if [ "$actual" = "developer" ]; then
|
|
pass "$label"
|
|
else
|
|
fail "$label: expected 'developer', got '$actual' (full output: $(cat "$tmpout"))"
|
|
fi
|
|
else
|
|
fail "$label: container failed"
|
|
fi
|
|
rm -f "$tmpout"
|
|
|
|
# Config generation with anthropic provider writes valid JSON with the
|
|
# expected shape. The script's log message goes to stderr (line 1 of
|
|
# generate-config.py uses file=sys.stderr) so capturing only stdout
|
|
# gives us clean JSON.
|
|
label="generate-config produces valid opencode.json"
|
|
tmp=$(mktemp -d)
|
|
if docker run --rm \
|
|
-e OPENCODE_PROVIDER=anthropic \
|
|
-e HOME=/tmp/home \
|
|
--entrypoint="" \
|
|
"$IMAGE" sh -c '
|
|
mkdir -p /tmp/home
|
|
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
|
cat /tmp/home/.config/opencode/opencode.json
|
|
' > "$tmp/out.json" 2>/dev/null; then
|
|
if python3 -c "
|
|
import json, sys
|
|
c = json.load(open('$tmp/out.json'))
|
|
assert c['model'].startswith('anthropic/'), c
|
|
assert c['autoupdate'] is False
|
|
assert c['share'] == 'disabled'
|
|
" 2>&1; then
|
|
pass "$label"
|
|
else
|
|
fail "$label: output doesn't match expected shape: $(cat "$tmp/out.json")"
|
|
fi
|
|
else
|
|
fail "$label: container failed: $(cat "$tmp/out.json")"
|
|
fi
|
|
|
|
# Config generation is idempotent — running twice must not overwrite.
|
|
label="generate-config never overwrites existing config"
|
|
if docker run --rm \
|
|
-e OPENCODE_PROVIDER=anthropic \
|
|
-e HOME=/tmp/home \
|
|
--entrypoint="" \
|
|
"$IMAGE" sh -c '
|
|
mkdir -p /tmp/home/.config/opencode
|
|
echo "{\"sentinel\": \"user-config\"}" > /tmp/home/.config/opencode/opencode.json
|
|
python3 /usr/local/lib/opencode-devbox/generate-config.py 2>/dev/null
|
|
cat /tmp/home/.config/opencode/opencode.json
|
|
' 2>/dev/null | grep -q '"sentinel": "user-config"'; then
|
|
pass "$label"
|
|
else
|
|
fail "$label: existing config was modified!"
|
|
fi
|
|
rm -rf "$tmp"
|
|
|
|
echo
|
|
echo "-- Image size --"
|
|
SIZE_BYTES=$(docker image inspect --format='{{.Size}}' "$IMAGE")
|
|
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
|
|
echo " Uncompressed size: ${SIZE_MB} MB"
|
|
|
|
# Thresholds (uncompressed): base 2500 MB, omos 3000 MB. Adjust as image content evolves.
|
|
THRESHOLD=2500
|
|
[ "$VARIANT" = "omos" ] && THRESHOLD=3000
|
|
if [ "$SIZE_MB" -gt "$THRESHOLD" ]; then
|
|
fail "image size ${SIZE_MB} MB exceeds threshold ${THRESHOLD} MB for variant=$VARIANT"
|
|
else
|
|
pass "image size ${SIZE_MB} MB within threshold ${THRESHOLD} MB"
|
|
fi
|
|
|
|
echo
|
|
if [ "$FAILED" -gt 0 ]; then
|
|
echo "=== FAILED: $FAILED check(s) ===" >&2
|
|
exit 1
|
|
fi
|
|
echo "=== PASSED ==="
|