mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat: diff-aware E2E test selector Adds scripts/select-e2e.ts: reads git diff vs origin/master, classifies the change set (EMPTY/DOC_ONLY/SRC), and emits the relevant E2E test files on stdout. Fail-closed by design: any unmapped src/ change runs all E2E. - scripts/e2e-test-map.ts: hand-tuned path-glob -> test files map - scripts/select-e2e.ts: pure-function selector with three explicit cases - scripts/run-e2e.sh: accepts optional file list from argv + --dry-run-list - test/select-e2e.test.ts: 24 cases including 3 codex regression guards (skills/, untracked files, unmapped src/) * feat: local CI gate via docker compose Adds bun run ci:local — runs every check GH Actions runs (gitleaks + unit + 29 E2E files) inside a Docker container that bind-mounts the repo. Pure bind-mount + named volumes (gbrain-ci-node-modules, gbrain-ci-bun-cache, gbrain-ci-pg-data) for fast warm restarts. - docker-compose.ci.yml: pgvector/pgvector:pg16 + oven/bun:1 - scripts/ci-local.sh: orchestrator with --diff, --no-pull, --clean - gitleaks runs on host (scoped to working dir + branch commits) - DATABASE_URL unset for unit phase (matches GH Actions split) - git installed in container at startup (oven/bun:1 omits it) - Postgres host port via GBRAIN_CI_PG_PORT env (default 5434) Stronger than PR CI: runs all 29 E2E files vs CI's 2-file Tier 1. * chore: bump version and changelog (v0.23.1) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: document local CI gate for v0.23.1 CLAUDE.md gains key-files entries for docker-compose.ci.yml, scripts/ci-local.sh, scripts/select-e2e.ts + e2e-test-map.ts, and the scripts/run-e2e.sh argv tweak. Pre-ship requirements section now lists the Docker-based local gate as Path A alongside the manual lifecycle. CONTRIBUTING.md tests section adds the bun run ci:local / ci:local:diff / ci:select-e2e block with prerequisites (Docker engine + gitleaks) and the GBRAIN_CI_PG_PORT override. AGENTS.md "Before shipping" promotes ci:local as the easiest path and keeps the manual lifecycle as a fallback. README.md Contributing section points to ci:local for the full gate. CHANGELOG.md untouched — v0.23.1 entry already finalized. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat: SHARD=N/M env support in scripts/run-e2e.sh Filters the E2E file list to every M-th file starting at index N (1-indexed). Sequential execution within a shard preserves the TRUNCATE CASCADE no-race property documented at the top of the file. Empty-shard handling under `set -u` uses ${arr[@]:-} fallback. Standalone change; not yet wired up in ci-local.sh. * feat: 4-way parallel E2E shards in ci:local Replaces the single postgres service with 4 (postgres-1..4) on host ports 5434-5437. scripts/ci-local.sh fans 4 workers via xargs -P4 inside the runner container; each pinned to its own DATABASE_URL via SHARD=N/4. Wall-time on a 16-core host: ~6 min sequential -> ~1.5-2 min sharded. Total full-gate wall-time goes from ~25 min to ~3-5 min warm. Also handles git-worktree (Conductor) layouts: when /app/.git is a file instead of a directory, parse the gitdir + commondir and bind-mount the shared host gitdir at its absolute path. Without this, in-container `git ls-files` (used by scripts/check-trailing-newline.sh and friends) exits 128 with "not a git repository". Also runs `git config --global --add safe.directory '*'` inside the container so the root-uid container can read host-uid gitdir without "dubious ownership" rejection. CHANGELOG entry updated to cover the speedup. - docker-compose.ci.yml: 4 pgvector services + per-shard named volumes - scripts/ci-local.sh: parallel xargs orchestration + worktree mount fix - CHANGELOG.md v0.23.1: 4-way sharded wall-time, 36 E2E files, --no-shard flag * chore: regenerate llms-full.txt for v0.23.1 doc updates Required by test/build-llms.test.ts case 4 — committed llms-full.txt must match `bun run build:llms` output. The CHANGELOG + CLAUDE.md updates in this branch shifted bytes; regen catches up. * feat: scripts/run-unit-shard.sh + slow-test convention Tier 1 + Tier 4 plumbing: - scripts/run-unit-shard.sh: SHARD=N/M filter for unit files (excludes test/e2e/*). Excludes *.slow.test.ts (Tier 4 convention) so the fast shard fan-out skips known-slow files; CI's `bun run test` still includes them via default discovery. - scripts/run-slow-tests.sh: companion that runs ONLY *.slow.test.ts. Wired as `bun run test:slow`. - scripts/profile-tests.sh: portable awk parser that extracts the top-N slowest tests from any captured `bun test` output. Wired as `bun run test:profile`. Use it to pick demotion candidates. * feat: PGLite snapshot fixture for ~4.5x faster cold init (Tier 3) scripts/build-pglite-snapshot.ts boots a fresh PGLite, runs the full initSchema() (forward bootstrap + 30 migrations), and dumps the post-init state to test/fixtures/pglite-snapshot.tar plus a SHA-256 schema hash sidecar (.version). Both gitignored — built on demand via `bun run build:pglite-snapshot`. PGLiteEngine.connect() reads GBRAIN_PGLITE_SNAPSHOT env: validates the sidecar hash against the in-process MIGRATIONS hash, loads via PGLite's loadDataDir blob, sets _snapshotLoaded so initSchema() short-circuits. Measured per-file cold init drops from 828ms → 181ms. Bootstrap-correctness tests (bootstrap.test.ts, schema-bootstrap-coverage.test.ts) explicitly delete the env at file top so they keep exercising the cold path they verify. * feat: --classify-only + heartbeat tolerance fix (Tiers 2 + flake fix) - scripts/select-e2e.ts: --classify-only flag emits EMPTY|DOC_ONLY|SRC. Used by ci-local.sh's --diff fast-path to skip the heavy gate when only docs changed. - test/progress.test.ts: startHeartbeat tolerance widened to 1-20 over 200ms (was 2-6 over 85ms). Under 4-way parallel shard load on a contended host, setTimeout's effective quantum balloons and the tight bound flakes. The test still verifies "fires multiple times, stops cleanly" — exact count was never load-bearing. * feat: 4-way unit + E2E sharding in ci-local.sh + CHANGELOG (Tiers 1-4) ci-local.sh ties the four tiers together: - Tier 2: pre-flight diff classification on host. DOC_ONLY exits in ~5s (gitleaks only, no postgres, no container). - Tier 1: guards + typecheck run ONCE before fan-out. xargs -P4 then spawns 4 shards inside the runner container, each running unit phase (env -u DATABASE_URL bash run-unit-shard.sh) followed by E2E phase (DATABASE_URL=postgres-N bash run-e2e.sh) — both sharded N/4. Per-shard logs in /tmp/shard-logs/shard-N.log; printed in shard order at the end. - Tier 3: snapshot fixture built once at runner startup if missing, GBRAIN_PGLITE_SNAPSHOT exported so all shards inherit. - Tier 4: run-unit-shard.sh excludes *.slow.test.ts; run-slow-tests.sh + test:slow npm script handle the demoted set. - --no-shard preserves the legacy single-process flow for debug. package.json: build:pglite-snapshot, test:slow, test:profile scripts. Measured wall-time on 16-core host: 100s warm (down from ~22 min cold single-process). 4 shards × ~640-1024 unit tests each, plus 9 E2E files each. PGLite snapshot saves 4.5× per cold init (828ms → 181ms). CHANGELOG.md updated with measured numbers + four-tier breakdown. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
347 lines
14 KiB
Bash
Executable File
347 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# scripts/ci-local.sh
|
|
#
|
|
# Local CI gate. Runs the same checks GH Actions does (and a stricter superset
|
|
# of E2E) inside Docker. See docker-compose.ci.yml.
|
|
#
|
|
# Modes:
|
|
# bash scripts/ci-local.sh # full local gate: gitleaks + unit + ALL E2E (4-way sharded)
|
|
# bash scripts/ci-local.sh --diff # full local gate: gitleaks + unit + selected E2E (4-way sharded)
|
|
# bash scripts/ci-local.sh --no-pull # skip docker compose pull (offline / debug)
|
|
# bash scripts/ci-local.sh --clean # nuke named volumes for cold debug
|
|
# bash scripts/ci-local.sh --no-shard # debug: run E2E sequentially against postgres-1 only
|
|
#
|
|
# 4-way E2E sharding: 4 pgvector services on host ports 5434-5437. The 36 E2E
|
|
# files split N/4 per shard; shards run in parallel. Within a shard, files run
|
|
# sequentially (TRUNCATE CASCADE no-race property documented in run-e2e.sh).
|
|
# Wall-time on a 16-core host: ~6 min sequential -> ~1.5-2 min sharded.
|
|
#
|
|
# Stronger than PR CI: PR CI runs only Tier 1's 2 files; this runs all 36.
|
|
|
|
set -euo pipefail
|
|
|
|
cd "$(dirname "$0")/.."
|
|
|
|
COMPOSE_FILE="docker-compose.ci.yml"
|
|
|
|
DIFF=0
|
|
NO_PULL=0
|
|
CLEAN=0
|
|
NO_SHARD=0
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--diff) DIFF=1 ;;
|
|
--no-pull) NO_PULL=1 ;;
|
|
--clean) CLEAN=1 ;;
|
|
--no-shard) NO_SHARD=1 ;;
|
|
*)
|
|
echo "Usage: $0 [--diff] [--no-pull] [--clean] [--no-shard]" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
cleanup() {
|
|
echo ""
|
|
echo "[ci-local] Tearing down postgres..."
|
|
docker compose -f "$COMPOSE_FILE" down --remove-orphans 2>&1 | tail -5 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
if [ "$CLEAN" = "1" ]; then
|
|
echo "[ci-local] --clean: removing named volumes..."
|
|
docker compose -f "$COMPOSE_FILE" down -v --remove-orphans 2>&1 | tail -5 || true
|
|
fi
|
|
|
|
# Tier 2: --diff fast-path. If the diff is doc-only (or empty), skip the
|
|
# whole heavy gate (postgres + bun install + unit + E2E) and just verify
|
|
# gitleaks on host. Doc-only diffs go from ~25 min to ~5 seconds.
|
|
if [ "$DIFF" = "1" ]; then
|
|
CLASSIFICATION=$(bun run scripts/select-e2e.ts --classify-only 2>/dev/null || echo "ERR")
|
|
case "$CLASSIFICATION" in
|
|
DOC_ONLY)
|
|
echo "[ci-local] --diff: diff is doc-only — skipping postgres + unit + E2E (Tier 2 fast-path)."
|
|
echo "[ci-local] Running gitleaks on host as the only gate..."
|
|
if ! command -v gitleaks >/dev/null 2>&1; then
|
|
echo "[ci-local] WARN: gitleaks not installed; skipping. brew install gitleaks." >&2
|
|
else
|
|
gitleaks dir . --redact --no-banner
|
|
gitleaks git . --redact --no-banner --log-opts="origin/master..HEAD"
|
|
fi
|
|
echo "[ci-local] Doc-only fast-path complete. No code paths exercised."
|
|
trap - EXIT
|
|
exit 0
|
|
;;
|
|
EMPTY)
|
|
echo "[ci-local] --diff: diff is empty (clean branch) — running full gate per fail-closed contract."
|
|
;;
|
|
SRC)
|
|
echo "[ci-local] --diff: diff touches src/ — running selected E2E + full unit phase."
|
|
;;
|
|
*)
|
|
echo "[ci-local] WARN: select-e2e.ts --classify-only returned '$CLASSIFICATION' — running full gate." >&2
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Pre-flight: postgres host ports for 4 shards. Defaults to 5434-5437 (avoid
|
|
# 5432 manual gbrain-test-pg, 5433 commonly held by sibling projects).
|
|
# GBRAIN_CI_PG_PORT defines BASE; shards take BASE..BASE+3.
|
|
PG_PORT_BASE="${GBRAIN_CI_PG_PORT:-5434}"
|
|
for shard in 1 2 3 4; do
|
|
port=$((PG_PORT_BASE + shard - 1))
|
|
PORT_OWNER=$(docker ps --filter "publish=$port" --format "{{.Names}}" | head -1)
|
|
if [ -n "$PORT_OWNER" ]; then
|
|
echo "[ci-local] ERROR: host port $port (shard $shard) is already used by docker container '$PORT_OWNER'." >&2
|
|
echo "[ci-local] Either stop that container or run with: GBRAIN_CI_PG_PORT=NNNN bun run ci:local" >&2
|
|
exit 1
|
|
fi
|
|
if lsof -iTCP:"$port" -sTCP:LISTEN -P -n >/dev/null 2>&1; then
|
|
echo "[ci-local] ERROR: host port $port (shard $shard) is held by a non-docker process." >&2
|
|
echo "[ci-local] Run with: GBRAIN_CI_PG_PORT=NNNN bun run ci:local" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
export GBRAIN_CI_PG_PORT="$PG_PORT_BASE"
|
|
export GBRAIN_CI_PG_PORT_2=$((PG_PORT_BASE + 1))
|
|
export GBRAIN_CI_PG_PORT_3=$((PG_PORT_BASE + 2))
|
|
export GBRAIN_CI_PG_PORT_4=$((PG_PORT_BASE + 3))
|
|
|
|
# Step 0: gitleaks on the host (no docker, no postgres, no bun needed).
|
|
# Mirrors test.yml's separate gitleaks job. Fail loudly if not installed.
|
|
echo "[ci-local] gitleaks detect (host)..."
|
|
if ! command -v gitleaks >/dev/null 2>&1; then
|
|
echo "[ci-local] ERROR: gitleaks not installed on host." >&2
|
|
echo "[ci-local] macOS: brew install gitleaks" >&2
|
|
echo "[ci-local] Linux: https://github.com/gitleaks/gitleaks/releases" >&2
|
|
exit 1
|
|
fi
|
|
# Two scopes for pre-push:
|
|
# 1. Working-tree files (catch uncommitted secrets sitting in files)
|
|
# 2. Branch commits vs origin/master (catch secrets committed on this branch)
|
|
# Full-history scan is ~4 min on this repo's 3700+ commits; not useful pre-push.
|
|
gitleaks dir . --redact --no-banner
|
|
gitleaks git . --redact --no-banner --log-opts="origin/master..HEAD"
|
|
|
|
# Step 1: pull. Refreshes pgvector + oven/bun:1 (both are `image:` not `build:`).
|
|
if [ "$NO_PULL" = "0" ]; then
|
|
echo "[ci-local] Pulling base images (use --no-pull to skip)..."
|
|
docker compose -f "$COMPOSE_FILE" pull 2>&1 | tail -5
|
|
fi
|
|
|
|
# Step 2: 4 postgres shards up + wait for healthy.
|
|
echo "[ci-local] Starting 4 postgres shards..."
|
|
docker compose -f "$COMPOSE_FILE" up -d postgres-1 postgres-2 postgres-3 postgres-4
|
|
echo "[ci-local] Waiting for all 4 postgres shards healthy..."
|
|
for i in {1..40}; do
|
|
all_healthy=1
|
|
for shard in 1 2 3 4; do
|
|
status=$(docker compose -f "$COMPOSE_FILE" ps --format json postgres-$shard 2>/dev/null | grep -o '"Health":"[^"]*"' | head -1 | sed 's/.*":"//;s/"//')
|
|
if [ "$status" != "healthy" ]; then
|
|
all_healthy=0
|
|
break
|
|
fi
|
|
done
|
|
if [ "$all_healthy" = "1" ]; then
|
|
echo "[ci-local] All 4 postgres shards healthy."
|
|
break
|
|
fi
|
|
if [ "$i" = "40" ]; then
|
|
echo "[ci-local] ERROR: not all postgres shards became healthy in 40 attempts" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# Step 3: smoke-test run-e2e.sh argv + shard handling.
|
|
echo "[ci-local] Smoke: run-e2e.sh argv + shard..."
|
|
SMOKE_NO_ARGS=$(bash scripts/run-e2e.sh --dry-run-list | wc -l | tr -d ' ')
|
|
EXPECTED_ALL=$(ls test/e2e/*.test.ts | wc -l | tr -d ' ')
|
|
if [ "$SMOKE_NO_ARGS" != "$EXPECTED_ALL" ]; then
|
|
echo "[ci-local] ERROR: --dry-run-list (no args) printed $SMOKE_NO_ARGS, expected $EXPECTED_ALL" >&2
|
|
exit 1
|
|
fi
|
|
SMOKE_ONE_ARG=$(bash scripts/run-e2e.sh --dry-run-list test/e2e/sync.test.ts)
|
|
if [ "$SMOKE_ONE_ARG" != "test/e2e/sync.test.ts" ]; then
|
|
echo "[ci-local] ERROR: --dry-run-list with 1 arg printed '$SMOKE_ONE_ARG'" >&2
|
|
exit 1
|
|
fi
|
|
SHARD_TOTAL=$(( $(SHARD=1/4 bash scripts/run-e2e.sh --dry-run-list | wc -l) + \
|
|
$(SHARD=2/4 bash scripts/run-e2e.sh --dry-run-list | wc -l) + \
|
|
$(SHARD=3/4 bash scripts/run-e2e.sh --dry-run-list | wc -l) + \
|
|
$(SHARD=4/4 bash scripts/run-e2e.sh --dry-run-list | wc -l) ))
|
|
if [ "$SHARD_TOTAL" != "$EXPECTED_ALL" ]; then
|
|
echo "[ci-local] ERROR: shards 1-4 covered $SHARD_TOTAL files, expected $EXPECTED_ALL" >&2
|
|
exit 1
|
|
fi
|
|
echo "[ci-local] Smoke OK ($SMOKE_NO_ARGS files no-arg, 1 single-arg, ${SHARD_TOTAL}=4-shard total)."
|
|
|
|
# Step 4: build the runner-side command.
|
|
# Tier 1: 4-shard parallel UNIT + E2E. Each shard runs ~46 unit files + ~9
|
|
# E2E files against postgres-N. Guards + typecheck run ONCE before fan-out.
|
|
# --no-shard runs the legacy unsharded flow (debug aid).
|
|
if [ "$NO_SHARD" = "1" ]; then
|
|
if [ "$DIFF" = "1" ]; then
|
|
RUN_PHASES_CMD='echo "[runner] guards + typecheck"
|
|
bash scripts/check-jsonb-pattern.sh
|
|
bash scripts/check-progress-to-stdout.sh
|
|
bash scripts/check-trailing-newline.sh
|
|
bash scripts/check-wasm-embedded.sh
|
|
bun run typecheck
|
|
echo "[runner] unit (unsharded, DATABASE_URL unset)"
|
|
env -u DATABASE_URL bash scripts/run-unit-shard.sh
|
|
echo "[runner] e2e (unsharded, --diff selected)"
|
|
SELECTED=$(bun run scripts/select-e2e.ts)
|
|
if [ -z "$SELECTED" ]; then
|
|
echo "[runner] selector emitted nothing (doc-only diff); skipping E2E."
|
|
else
|
|
DATABASE_URL=postgresql://postgres:postgres@postgres-1:5432/gbrain_test echo "$SELECTED" | xargs bash scripts/run-e2e.sh
|
|
fi'
|
|
else
|
|
RUN_PHASES_CMD='echo "[runner] guards + typecheck"
|
|
bash scripts/check-jsonb-pattern.sh
|
|
bash scripts/check-progress-to-stdout.sh
|
|
bash scripts/check-trailing-newline.sh
|
|
bash scripts/check-wasm-embedded.sh
|
|
bun run typecheck
|
|
echo "[runner] unit (unsharded, DATABASE_URL unset)"
|
|
env -u DATABASE_URL bash scripts/run-unit-shard.sh
|
|
echo "[runner] e2e (unsharded)"
|
|
DATABASE_URL=postgresql://postgres:postgres@postgres-1:5432/gbrain_test bash scripts/run-e2e.sh'
|
|
fi
|
|
else
|
|
# Tier 1 sharded path. Each shard runs unit+E2E sequentially against its
|
|
# own postgres-N. Shards run in parallel via xargs -P4.
|
|
if [ "$DIFF" = "1" ]; then
|
|
DIFF_E2E_PREP='SELECTED=$(bun run scripts/select-e2e.ts)
|
|
if [ -z "$SELECTED" ]; then
|
|
echo "" > /tmp/e2e-selected.txt
|
|
else
|
|
echo "$SELECTED" | tr " " "\n" | grep -v "^$" > /tmp/e2e-selected.txt
|
|
fi'
|
|
else
|
|
# Empty file -> run-e2e.sh uses default glob (all 36 E2E files).
|
|
DIFF_E2E_PREP='> /tmp/e2e-selected.txt'
|
|
fi
|
|
RUN_PHASES_CMD="echo \"[runner] guards + typecheck (run once before sharding)\"
|
|
bash scripts/check-jsonb-pattern.sh
|
|
bash scripts/check-progress-to-stdout.sh
|
|
bash scripts/check-trailing-newline.sh
|
|
bash scripts/check-wasm-embedded.sh
|
|
bun run typecheck
|
|
echo \"[runner] Tier 3: building PGLite snapshot fixture (cached across reruns)\"
|
|
if [ ! -f test/fixtures/pglite-snapshot.tar ] || [ ! -f test/fixtures/pglite-snapshot.version ]; then
|
|
bun run build:pglite-snapshot
|
|
else
|
|
echo \"[runner] snapshot fixture exists; engine will validate hash at load time\"
|
|
fi
|
|
export GBRAIN_PGLITE_SNAPSHOT=test/fixtures/pglite-snapshot.tar
|
|
echo \"[runner] resolving E2E file selection (--diff aware)\"
|
|
${DIFF_E2E_PREP}
|
|
mkdir -p /tmp/shard-logs
|
|
echo \"[runner] Tier 1: 4-shard parallel unit + E2E (xargs -P4)\"
|
|
set +e
|
|
printf '%s\\n' 1 2 3 4 | xargs -P4 -I{} sh -c '
|
|
shard=\$1
|
|
log=/tmp/shard-logs/shard-\${shard}.log
|
|
echo \"[shard \${shard}] start\" > \$log
|
|
echo \"[shard \${shard}] unit phase (SHARD=\${shard}/4, DATABASE_URL unset)\" >> \$log
|
|
env -u DATABASE_URL SHARD=\${shard}/4 bash scripts/run-unit-shard.sh >> \$log 2>&1
|
|
unit_exit=\$?
|
|
if [ \$unit_exit -ne 0 ]; then
|
|
echo \"[shard \${shard}] UNIT FAILED (exit=\$unit_exit)\" >> \$log
|
|
exit \$unit_exit
|
|
fi
|
|
echo \"[shard \${shard}] e2e phase (SHARD=\${shard}/4, DATABASE_URL=postgres-\${shard})\" >> \$log
|
|
if [ -s /tmp/e2e-selected.txt ]; then
|
|
SHARD=\${shard}/4 \\
|
|
DATABASE_URL=postgresql://postgres:postgres@postgres-\${shard}:5432/gbrain_test \\
|
|
xargs -a /tmp/e2e-selected.txt bash scripts/run-e2e.sh >> \$log 2>&1
|
|
else
|
|
SHARD=\${shard}/4 \\
|
|
DATABASE_URL=postgresql://postgres:postgres@postgres-\${shard}:5432/gbrain_test \\
|
|
bash scripts/run-e2e.sh >> \$log 2>&1
|
|
fi
|
|
e2e_exit=\$?
|
|
if [ \$e2e_exit -ne 0 ]; then
|
|
echo \"[shard \${shard}] E2E FAILED (exit=\$e2e_exit)\" >> \$log
|
|
exit \$e2e_exit
|
|
fi
|
|
echo \"[shard \${shard}] DONE\" >> \$log
|
|
' _ {}
|
|
shard_xargs_exit=\$?
|
|
set -e
|
|
echo \"\"
|
|
echo \"=== SHARD LOGS (last 30 lines each + unit/e2e summaries) ===\"
|
|
for s in 1 2 3 4; do
|
|
echo \"\"
|
|
echo \"--- shard \$s ---\"
|
|
if [ -f /tmp/shard-logs/shard-\$s.log ]; then
|
|
# Pull the unit + E2E summary lines explicitly so they survive even if
|
|
# the file is huge. Match: bun's '<N> pass / <N> fail' pairs, run-e2e.sh's
|
|
# 'Files: ... / Tests: ...' summary, and our own shard markers.
|
|
grep -E '^\\[shard|^Files: |^Tests: |Ran [0-9]+ tests|^[[:space:]]+[0-9]+ (pass|fail|skip)\$' /tmp/shard-logs/shard-\$s.log || true
|
|
echo \" (last 30 lines for context)\"
|
|
tail -30 /tmp/shard-logs/shard-\$s.log
|
|
else
|
|
echo \"(no log file written — shard never started)\"
|
|
fi
|
|
done
|
|
echo \"\"
|
|
if [ \$shard_xargs_exit -ne 0 ]; then
|
|
echo \"[runner] One or more shards failed (xargs exit=\$shard_xargs_exit). See SHARD LOGS above.\"
|
|
exit \$shard_xargs_exit
|
|
fi
|
|
echo \"[runner] All 4 shards passed.\""
|
|
fi
|
|
|
|
INNER_CMD=$(cat <<'EOF'
|
|
set -euo pipefail
|
|
echo "[runner] bun version: $(bun --version)"
|
|
# oven/bun:1 omits git; many unit tests use mkdtemp + git init for fixtures.
|
|
if ! command -v git >/dev/null 2>&1; then
|
|
echo "[runner] Installing git (debian apt)..."
|
|
apt-get update -qq >/dev/null
|
|
apt-get install -y -qq git ca-certificates >/dev/null
|
|
fi
|
|
# Container runs as root (uid 0) against a host-uid bind-mount; mark repo +
|
|
# any worktree gitdir as safe so `git status` etc. don't refuse.
|
|
git config --global --add safe.directory '*' || true
|
|
if [ ! -d /app/node_modules ] || [ -z "$(ls -A /app/node_modules 2>/dev/null)" ]; then
|
|
echo "[runner] First run (or --clean): bun install --frozen-lockfile"
|
|
bun install --frozen-lockfile
|
|
fi
|
|
__RUN_PHASES__
|
|
EOF
|
|
)
|
|
INNER_CMD="${INNER_CMD/__RUN_PHASES__/$RUN_PHASES_CMD}"
|
|
|
|
# Conductor / git-worktree support: when `.git` is a file (not a directory),
|
|
# it points at a host gitdir outside the bind-mount. Without remounting that
|
|
# path, scripts/check-trailing-newline.sh and any other in-container `git`
|
|
# call exits 128 ("not a git repository"). Resolve the host gitdir + the
|
|
# shared common gitdir and bind-mount them at the same absolute paths.
|
|
EXTRA_MOUNTS=()
|
|
if [ -f .git ]; then
|
|
WORKTREE_GITDIR=$(awk '{print $2}' .git)
|
|
if [ -d "$WORKTREE_GITDIR" ]; then
|
|
COMMONDIR_FILE="$WORKTREE_GITDIR/commondir"
|
|
if [ -f "$COMMONDIR_FILE" ]; then
|
|
COMMON_REL=$(cat "$COMMONDIR_FILE")
|
|
COMMON_GITDIR=$(cd "$WORKTREE_GITDIR" && cd "$COMMON_REL" && pwd)
|
|
else
|
|
COMMON_GITDIR="$WORKTREE_GITDIR"
|
|
fi
|
|
# Mount the higher-level common gitdir; covers worktrees/<name> automatically.
|
|
EXTRA_MOUNTS+=( -v "${COMMON_GITDIR}:${COMMON_GITDIR}:ro" )
|
|
echo "[ci-local] Worktree detected; mounting shared gitdir: $COMMON_GITDIR"
|
|
fi
|
|
fi
|
|
|
|
echo "[ci-local] Running checks inside runner container..."
|
|
docker compose -f "$COMPOSE_FILE" run --rm "${EXTRA_MOUNTS[@]:-}" runner bash -c "$INNER_CMD"
|
|
|
|
echo ""
|
|
echo "[ci-local] All checks passed."
|