Files
opencode-devbox/scripts/smoke-test.sh
T
joakimp 349bb633ff
Validate / docs-check (push) Successful in 1m13s
Validate / validate-omos (push) Successful in 15m30s
Validate / validate-base (push) Successful in 17m11s
Publish Docker Image / build-omos (push) Failing after 14m43s
Publish Docker Image / update-description (push) Has been cancelled
Publish Docker Image / build-base (push) Has been cancelled
Fix OMOS bunx detection
entrypoint-user.sh gated OMOS auto-install on 'command -v bunx', but
neither upstream bun installer nor our Dockerfile creates a bunx
symlink — only the bun binary exists on PATH. The check always failed
on a fresh OMOS image, printing 'ENABLE_OMOS=true but bun is not
installed.' even though bun was right there. Latent until now because
the only exercised path had a persisted oh-my-opencode-slim.json from
a prior install.

Fixes:
- Gate on 'command -v bun' instead of bunx.
- Call 'bun x oh-my-opencode-slim@latest install ...' (bun x is the
  real subcommand that actually works with only the bun binary).
- Add 'ln -sf bun /usr/local/bin/bunx' in the Dockerfile OMOS block
  so interactive users can still type bunx by habit.
- Smoke test asserts the bunx symlink exists on the OMOS variant.

Also verify 'test -L /usr/local/bin/bunx' as a build-time sanity check.
Can't use 'bunx --help' for that — bun exits 1 on help output even
though it prints the usage correctly.
2026-04-29 09:01:23 +02:00

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-server" "test -x /usr/local/bin/mempalace-mcp-server && echo wrapper-present"
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 ==="