Files
codebase-memory-mcp/scripts/smoke-wrappers.sh
T
Martin Vogel 6382fad7b3 fix(ci): give the wrapper smoke a Go toolchain and a deletable work tree
The v0.12.1 release smoke failed on every leg in Phase 13w, the
package-wrapper install gate added in #2664:

- macOS: the runner images have no Go on PATH, and
  SMOKE_REQUIRE_WRAPPERS=1 rightly fails without one. pr-smoke and the
  three release smoke jobs now set up Go from pkg/go/go.mod, with the
  same pinned step the wrapper test job uses.
- Windows: `mkdir -m 700` changes the mode in a second step, which MSYS2
  cannot do under the smoke profile's owner-only DACL. mktemp -d sets
  the mode at creation.
- Linux: the runner's older Go downloaded the 1.26.1 toolchain into the
  smoke profile's HOME. Go writes its module cache read-only, so the
  harness could not delete its profile afterwards. All Go state now stays
  in the wrapper's own work tree, and its cleanup restores write
  permission before removing it.

npm and PyPI passed on every leg that reached them; the gate itself is
unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-16 22:12:26 +02:00

157 lines
6.0 KiB
Bash

#!/usr/bin/env bash
# smoke-wrappers.sh — install the npm, PyPI and Go wrappers from this checkout
# against a release fixture and run the binary each one installed.
#
# The wrappers download the release archive themselves. This runs each one the
# way a user's package manager does, against CBM_DOWNLOAD_URL (the test
# override install.sh honours too) serving the real release archives, so a
# wrapper that cannot install what we ship fails here instead of on users
# (#2657: a 256 MiB archive-member cap against 331 MB binaries).
#
# Usage: scripts/smoke-wrappers.sh DOWNLOAD_URL [npm] [pypi] [go]
# DOWNLOAD_URL base URL serving the release archives and checksums.txt:
# https, or plain http to a loopback authority
# (no wrapper names = all three)
# Environment:
# SMOKE_REQUIRE_WRAPPERS=1 a missing toolchain fails instead of skipping (CI)
# Exit: 0 = every wrapper installed and ran its binary (or was skipped) ·
# 1 = a wrapper failed · 2 = usage error.
set -euo pipefail
case "${BASH_SOURCE[0]}" in */*) cd "${BASH_SOURCE[0]%/*}/.." ;; *) cd .. ;; esac
ROOT="$PWD"
URL="${1:-}"
[ -n "$URL" ] || { sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2; }
shift
WRAPPERS=("$@")
[ ${#WRAPPERS[@]} -gt 0 ] || WRAPPERS=(npm pypi go)
REQUIRE="${SMOKE_REQUIRE_WRAPPERS:-0}"
EXE=""
case "${OSTYPE:-}" in msys* | cygwin* | win32*) EXE=".exe" ;; esac
# mktemp -d sets 0700 at creation. `mkdir -m 700` changes the mode in a second
# step, which MSYS2 cannot do under the smoke profile's owner-only DACL.
WORK="$(mktemp -d "${TMPDIR:-/tmp}/cbm-smoke-wrappers.XXXXXX")"
# Go writes its module cache read-only; restore write permission so it can be removed.
trap 'chmod -R u+w "$WORK" 2>/dev/null; rm -rf "$WORK"' EXIT
FAILED=0
missing() { # wrapper, what
if [ "$REQUIRE" = "1" ]; then
echo "FAIL $1: SMOKE_REQUIRE_WRAPPERS=1 but $2 is not available"
FAILED=1
else
echo "SKIP $1: $2 not available on this host"
fi
}
# A fresh HOME, cache and profile per wrapper: nothing a wrapper writes leaks
# into the next one, and a binary cached by an earlier run cannot stand in.
isolated() { # wrapper dir, command...
local dir="$1"
shift
mkdir -p "$dir/home" "$dir/cache" "$dir/localappdata"
HOME="$dir/home" USERPROFILE="$dir/home" XDG_CACHE_HOME="$dir/cache" \
LOCALAPPDATA="$dir/localappdata" CBM_CACHE_DIR="$dir/cbm-cache" \
CBM_DOWNLOAD_URL="$URL" "$@"
}
ran() { # wrapper, output file, rc
# The binary's own answer ("codebase-memory-mcp 0.12.0"), not a wrapper
# message ("codebase-memory-mcp: downloading ..."), proves it ran.
if [ "$3" -eq 0 ] && grep -E -q '^codebase-memory-mcp [^:]+$' "$2"; then
echo "OK $1: installed and ran the release binary: $(grep -E '^codebase-memory-mcp [^:]+$' "$2" | tail -1)"
else
echo "FAIL $1: the installed wrapper did not run the release binary (rc=$3):"
sed 's/^/ /' "$2" | tail -20
FAILED=1
fi
}
smoke_npm() {
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
missing npm "node/npm"
return
fi
local dir="$WORK/npm" tgz="" line modules rc=0
mkdir -p "$dir/prefix"
(cd "$ROOT/pkg/npm" && npm pack --silent --pack-destination "$dir") >"$dir/pack.out"
while IFS= read -r line; do [ -n "$line" ] && tgz="$line"; done <"$dir/pack.out"
# A global install from the packed tarball runs postinstall (install.js)
# exactly as `npm i -g codebase-memory-mcp` does.
isolated "$dir" npm install --global --prefix "$dir/prefix" --cache "$dir/cache/npm" \
--foreground-scripts --no-audit --no-fund "$dir/$tgz" >"$dir/install.log" 2>&1 || rc=$?
if [ "$rc" -ne 0 ]; then
ran npm "$dir/install.log" "$rc"
return
fi
local modules
npm root --global --prefix "$dir/prefix" >"$dir/root.out"
IFS= read -r modules <"$dir/root.out"
isolated "$dir" node "$modules/codebase-memory-mcp/bin.js" --version >"$dir/run.log" 2>&1 ||
rc=$?
ran npm "$dir/run.log" "$rc"
}
smoke_pypi() {
local py=""
if command -v python3 >/dev/null 2>&1; then
py=python3
elif command -v python >/dev/null 2>&1; then
py=python
fi
if [ -z "$py" ]; then
missing pypi "python3"
return
fi
local dir="$WORK/pypi" rc=0
mkdir -p "$dir"
"$py" -m venv "$dir/venv"
local vpy="$dir/venv/bin/python"
[ -x "$vpy" ] || vpy="$dir/venv/Scripts/python.exe"
"$vpy" -m pip install --quiet --disable-pip-version-check "$ROOT/pkg/pypi" \
>"$dir/install.log" 2>&1 || rc=$?
if [ "$rc" -ne 0 ]; then
ran pypi "$dir/install.log" "$rc"
return
fi
# The console script's entry point; the first run downloads the binary.
isolated "$dir" "$vpy" -c \
'import sys, codebase_memory_mcp; sys.argv = ["codebase-memory-mcp", "--version"]; codebase_memory_mcp.main()' \
>"$dir/run.log" 2>&1 || rc=$?
ran pypi "$dir/run.log" "$rc"
}
smoke_go() {
if ! command -v go >/dev/null 2>&1; then
missing go "go"
return
fi
local dir="$WORK/go" rc=0
mkdir -p "$dir"
# All Go state stays under $WORK: a toolchain that GOTOOLCHAIN=auto downloads
# lands read-only in the module cache, which must not end up in the caller's
# HOME (the smoke harness could not delete its profile afterwards).
(cd "$ROOT/pkg/go" && GOTOOLCHAIN="${GOTOOLCHAIN:-auto}" \
GOPATH="$dir/gopath" GOMODCACHE="$dir/gomodcache" GOCACHE="$dir/gocache" \
go build -o "$dir/codebase-memory-mcp$EXE" ./cmd/codebase-memory-mcp) \
>"$dir/install.log" 2>&1 || rc=$?
if [ "$rc" -ne 0 ]; then
ran go "$dir/install.log" "$rc"
return
fi
# The first run downloads the binary into the wrapper's cache.
isolated "$dir" "$dir/codebase-memory-mcp$EXE" --version >"$dir/run.log" 2>&1 || rc=$?
ran go "$dir/run.log" "$rc"
}
for wrapper in "${WRAPPERS[@]}"; do
case "$wrapper" in
npm) smoke_npm ;;
pypi) smoke_pypi ;;
go) smoke_go ;;
*) echo "unknown wrapper: $wrapper" >&2; exit 2 ;;
esac
done
exit "$FAILED"