Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
156 lines
7.9 KiB
Bash
Executable File
156 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# e2e-smoke.sh - run the Playwright e2e suite against an ISOLATED, disposable Supabase
|
|
# instance so multiple harness sessions can run it in parallel without colliding.
|
|
# Guaranteed teardown via `trap ... EXIT`.
|
|
#
|
|
# Isolation per run: a leased SLOT (0..K-1) gives offset = slot*20, applied to the
|
|
# project_id, every Supabase port, and the app port + auth URLs, in a throwaway workdir.
|
|
# The flock lease also CAPS concurrency (K stacks max) so we never OOM the host.
|
|
#
|
|
# Exit codes: 0 = suite passed OR gracefully skipped (no slot / low RAM / cannot start);
|
|
# 1 = the e2e suite ran and FAILED. Skips are exit 0 by design (the caller
|
|
# treats a skip as "not run here, run it later", not as a failure).
|
|
#
|
|
# Env: E2E_SMOKE_SLOTS (default 5), E2E_SMOKE_MIN_MB (default 2500), E2E_SMOKE_DRY=1
|
|
# (resolve slot/ports/config and print them, then exit WITHOUT Docker - for tests).
|
|
set -uo pipefail
|
|
|
|
REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
# Where to read the app + specs + schema FROM. A feature-smoke must run the INTEGRATED
|
|
# session code, not the base-branch checkout ($REPO) - so the orchestrator passes
|
|
# E2E_SMOKE_SRC=<WORKTREE_BASE>/_session (the session worktree, on session/<short>, with
|
|
# node_modules already provisioned). Default $REPO = a base-branch baseline check.
|
|
SRC="${E2E_SMOKE_SRC:-$REPO}"
|
|
SLOTS="${E2E_SMOKE_SLOTS:-5}"
|
|
MIN_MB="${E2E_SMOKE_MIN_MB:-2500}"
|
|
DRY="${E2E_SMOKE_DRY:-0}"
|
|
SLOT_LOCK_DIR="/tmp/atomic-crm-e2e-slots"
|
|
CONFIG_SRC="$SRC/supabase/config.e2e.toml"
|
|
mkdir -p "$SLOT_LOCK_DIR"
|
|
|
|
skip() { echo "SKIP: $*"; exit 0; } # graceful: not run here, not a failure
|
|
|
|
# --- memory preflight -------------------------------------------------------
|
|
# Each Supabase stack is ~2-3 GB. Don't attempt a boot the host can't hold; skip
|
|
# gracefully so the caller defers to a human `make test-e2e` instead of OOM-ing.
|
|
avail_mb="$(free -m 2>/dev/null | awk '/^Mem:/{print $7}')"
|
|
if [ "$DRY" != "1" ] && [ -n "$avail_mb" ] && [ "$avail_mb" -lt "$MIN_MB" ]; then
|
|
skip "only ${avail_mb}MB free, need ~${MIN_MB}MB for a Supabase e2e stack; run 'make test-e2e' locally."
|
|
fi
|
|
|
|
# --- lease a slot (flock; also the concurrency cap) -------------------------
|
|
slot=""; SLOT_FD=""
|
|
for i in $(seq 0 $((SLOTS - 1))); do
|
|
exec {fd}>"$SLOT_LOCK_DIR/slot-$i.lock"
|
|
if flock -n "$fd"; then slot="$i"; SLOT_FD="$fd"; break; fi
|
|
exec {fd}>&-
|
|
done
|
|
[ -z "$slot" ] && skip "all $SLOTS e2e slots busy; try again later."
|
|
|
|
offset=$((slot * 20))
|
|
project="atomic-crm-e2e-$slot"
|
|
api_port=$((54341 + offset))
|
|
app_port=$((5175 + slot))
|
|
workroot="$(mktemp -d)"
|
|
workdir="$workroot/e2e" # supabase --workdir
|
|
mkdir -p "$workdir/supabase"
|
|
|
|
# --- guaranteed teardown ----------------------------------------------------
|
|
APP_PID=""
|
|
cleanup() {
|
|
local code=$?
|
|
[ -n "$APP_PID" ] && kill "$APP_PID" 2>/dev/null || true
|
|
[ "$DRY" != "1" ] && npx supabase stop --workdir "$workdir" --no-backup >/dev/null 2>&1 || true
|
|
[ -n "$SLOT_FD" ] && flock -u "$SLOT_FD" 2>/dev/null || true
|
|
rm -rf "$workroot" 2>/dev/null || true
|
|
return $code
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
# --- render the per-slot config ---------------------------------------------
|
|
# Offset every Supabase port (`port =`, `shadow_port =`, `vector_port =`) by `offset`,
|
|
# set a unique project_id (distinct Docker containers), and point the auth URLs at the
|
|
# per-slot app port. perl `/e` evaluates the numeric replacement.
|
|
[ -f "$CONFIG_SRC" ] || skip "no $CONFIG_SRC"
|
|
perl -pe "s/^(project_id\s*=\s*).*/\$1\"$project\"/;
|
|
s/((?:^|_)port\s*=\s*)(\d+)/\$1.(\$2+$offset)/e;
|
|
s/5175/$app_port/g" "$CONFIG_SRC" > "$workdir/supabase/config.toml"
|
|
|
|
# Supabase needs the schema/migrations/seed to build the DB on first start.
|
|
for d in migrations schemas functions templates; do
|
|
[ -d "$SRC/supabase/$d" ] && cp -r "$SRC/supabase/$d" "$workdir/supabase/$d"
|
|
done
|
|
for f in seed.sql signing_keys.json; do
|
|
[ -f "$SRC/supabase/$f" ] && cp "$SRC/supabase/$f" "$workdir/supabase/$f"
|
|
done
|
|
|
|
if [ "$DRY" = "1" ]; then
|
|
echo "DRY-RUN slot=$slot offset=$offset project=$project api_port=$api_port app_port=$app_port workdir=$workdir"
|
|
echo "config.project_id=$(grep -E '^project_id' "$workdir/supabase/config.toml")"
|
|
echo "config.api_port=$(awk '/^\[api\]/{a=1} a&&/^port =/{print $3; exit}' "$workdir/supabase/config.toml")"
|
|
echo "config.site_url=$(grep -E '^site_url' "$workdir/supabase/config.toml" | head -1)"
|
|
exit 0
|
|
fi
|
|
|
|
# --- reap a previously killed run of this slot -----------------------------
|
|
# The trap cleans up normal exits (EXIT/INT/TERM); a SIGKILL would orphan this slot's
|
|
# containers. Before starting fresh, remove any lingering ones for this project_id.
|
|
docker ps -aq --filter "name=${project}" 2>/dev/null | xargs -r docker rm -f >/dev/null 2>&1 || true
|
|
|
|
# --- start the isolated stack ----------------------------------------------
|
|
echo "e2e-smoke: starting isolated Supabase (slot $slot, api :$api_port, project $project)..."
|
|
if ! npx supabase start --workdir "$workdir" >/"$workroot/supabase.log" 2>&1; then
|
|
cat "$workroot/supabase.log" >&2 || true
|
|
skip "isolated Supabase failed to start (see log); deferring e2e to a human run."
|
|
fi
|
|
SERVICE_ROLE_KEY="$(npx supabase status --workdir "$workdir" -o env 2>/dev/null | sed -nE 's/^SERVICE_ROLE_KEY="?([^"]+)"?/\1/p')"
|
|
ANON_KEY="$(npx supabase status --workdir "$workdir" -o env 2>/dev/null | sed -nE 's/^ANON_KEY="?([^"]+)"?/\1/p')"
|
|
export VITE_SUPABASE_URL="http://127.0.0.1:$api_port"
|
|
export VITE_SUPABASE_ANON_KEY="$ANON_KEY"
|
|
export SERVICE_ROLE_KEY
|
|
|
|
# --- materialize the session schema the deploy-time migration will carry --------
|
|
# Ticket developers never write migrations, so a schema-touching session has
|
|
# supabase/schemas/ ahead of supabase/migrations/. The CLI builds the DB from
|
|
# migrations/ only, so without this the isolated stack lacks the new columns and
|
|
# schema-exercising specs 400 against PostgREST - a false FAIL. When the
|
|
# session changed supabase/schemas/, generate a THROWAWAY migration from the delta
|
|
# INTO THE WORKDIR (never $SRC) and apply it. Best-effort: on any failure SKIP the
|
|
# leg rather than emit a false FAIL (the real migration lands in the deploy round).
|
|
schema_changed=0
|
|
branch="$(git -C "$SRC" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")"
|
|
case "$branch" in
|
|
session/*)
|
|
base="session-base/${branch#session/}"
|
|
if git -C "$SRC" rev-parse --verify -q "$base" >/dev/null 2>&1 \
|
|
&& git -C "$SRC" diff --name-only "$base"...HEAD 2>/dev/null | grep -q '^supabase/schemas/'; then
|
|
schema_changed=1
|
|
fi
|
|
;;
|
|
esac
|
|
if [ "$schema_changed" = "1" ] && [ -d "$workdir/supabase/schemas" ]; then
|
|
echo "e2e-smoke: session changed supabase/schemas/, materializing a throwaway migration..."
|
|
if npx supabase db diff --workdir "$workdir" --local -f _e2e_throwaway >"$workroot/dbdiff.log" 2>&1; then
|
|
if ls "$workdir"/supabase/migrations/*_e2e_throwaway.sql >/dev/null 2>&1; then
|
|
npx supabase migration up --workdir "$workdir" --local >"$workroot/migup.log" 2>&1 \
|
|
|| { cat "$workroot/migup.log" >&2; skip "schema pending migration round (throwaway apply failed); deferring the Supabase e2e leg to the deploy-time migration."; }
|
|
fi
|
|
else
|
|
cat "$workroot/dbdiff.log" >&2
|
|
skip "schema pending migration round (throwaway diff failed); deferring the Supabase e2e leg to the deploy-time migration."
|
|
fi
|
|
fi
|
|
|
|
# --- serve the app (dev server reads env at start, so no per-slot build) ----
|
|
( cd "$SRC" && VITE_SUPABASE_URL="$VITE_SUPABASE_URL" npx vite --port "$app_port" --strictPort --mode e2e >"$workroot/app.log" 2>&1 ) &
|
|
APP_PID=$!
|
|
npx wait-on -t 120000 "http-get://127.0.0.1:$api_port/auth/v1/health" "http-get://127.0.0.1:$app_port" >/dev/null 2>&1 \
|
|
|| skip "isolated stack did not become ready in time."
|
|
|
|
# --- run the suite ----------------------------------------------------------
|
|
echo "e2e-smoke: running Playwright against the isolated stack..."
|
|
( cd "$SRC" && CI=true PLAYWRIGHT_BASE_URL="http://127.0.0.1:$app_port" npx playwright test )
|
|
result=$?
|
|
echo "e2e-smoke: suite exit=$result (slot $slot)"
|
|
exit $result
|