#!/usr/bin/env bash # Ensure dependencies are installed (worktrees start with no node_modules) if [ ! -d node_modules ]; then echo "node_modules missing, running npm install..." npm install --prefer-offline || exit 1 fi echo "Checking PR status for current branch..." BRANCH=$(git branch --show-current) if [ -n "$BRANCH" ] && [ "$BRANCH" != "main" ] && [ "$BRANCH" != "master" ]; then PR_STATE=$(gh pr view "$BRANCH" --json state --jq '.state' 2>&1) if [ "$PR_STATE" = "MERGED" ] || [ "$PR_STATE" = "CLOSED" ]; then echo "" echo "============================================================" echo "ERROR: PR for branch '$BRANCH' is $PR_STATE." echo "Do NOT push to a merged/closed PR branch — commits will be orphaned." echo "Run: git checkout main && git pull && git checkout -b fix/new-branch" echo "============================================================" exit 1 fi echo " Branch PR state: ${PR_STATE:-unknown}" # Guard against branch contamination from a dirty local main. # A feature/fix branch should never have more than 20 commits — if it does, # the branch was likely created from a local main that had unmerged feature branches. COMMIT_COUNT=$(git rev-list --count origin/main..HEAD 2>/dev/null || echo 0) if [ "$COMMIT_COUNT" -gt 20 ]; then echo "" echo "============================================================" echo "ERROR: Branch '$BRANCH' is $COMMIT_COUNT commits ahead of origin/main." echo "This usually means it was branched from a dirty local main." echo "Fix: create branches from a worktree or use: git checkout -b origin/main" echo "============================================================" exit 1 fi fi echo "Checking scripts/package-lock.json sync..." if git diff --name-only origin/main -- scripts/package.json | grep -q .; then if ! git diff --name-only origin/main -- scripts/package-lock.json | grep -q .; then echo "" echo "============================================================" echo "ERROR: scripts/package.json was modified but scripts/package-lock.json was not committed." echo "Run: cd scripts && npm install && cd .." echo "Then: git add scripts/package-lock.json && git commit --amend --no-edit" echo "============================================================" exit 1 fi fi echo "Running type check..." npm run typecheck || exit 1 echo "Running API type check..." npm run typecheck:api || exit 1 echo "Running CJS syntax check..." for f in scripts/*.cjs; do [ -f "$f" ] && node -c "$f" || exit 1 done echo "Running Unicode safety check..." node scripts/check-unicode-safety.mjs || exit 1 echo "Running architectural boundary check..." npm run lint:boundaries || exit 1 echo "Running rate-limit policy coverage check..." npm run lint:rate-limit-policies || exit 1 echo "Running premium-fetch parity check..." npm run lint:premium-fetch || exit 1 echo "Running edge function bundle check..." while IFS= read -r f; do npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null 2>/dev/null || { echo "ERROR: esbuild failed to bundle $f — this will break Vercel deployment" npx esbuild "$f" --bundle --format=esm --platform=browser --outfile=/dev/null exit 1 } done < <(find api/ -name "*.js" -not -name "_*"; find api/ -maxdepth 1 -name "*.ts" -not -name "_*") CHANGED_FILES=$(git diff --name-only origin/main...HEAD 2>/dev/null || echo "") if [ -z "$CHANGED_FILES" ]; then CHANGED_FILES=$(git diff --name-only HEAD~1 2>/dev/null || echo "") fi if [ -z "$CHANGED_FILES" ]; then echo "WARNING: Could not determine changed files, running full test suite as safety fallback." RUN_ALL=true fi # Determine which test categories to run based on changed files RUN_RESILIENCE=false RUN_SEED=false RUN_SERVER=false RUN_FRONTEND=false ${RUN_ALL:=false} if echo "$CHANGED_FILES" | grep -q "^scripts/"; then RUN_SEED=true; fi if echo "$CHANGED_FILES" | grep -q "^server/"; then RUN_SERVER=true; fi if echo "$CHANGED_FILES" | grep -q "resilience"; then RUN_RESILIENCE=true; fi if echo "$CHANGED_FILES" | grep -q "^src/\|^api/"; then RUN_FRONTEND=true; fi if echo "$CHANGED_FILES" | grep -q "^tests/\|package.json\|tsconfig"; then RUN_ALL=true; fi if [ "$RUN_ALL" = true ]; then echo "Running full unit test suite (test files or config changed)..." timeout 300 npx tsx --test tests/*.test.mjs tests/*.test.mts || exit 1 else if [ "$RUN_RESILIENCE" = true ]; then echo "Running resilience tests (resilience files changed)..." timeout 120 npx tsx --test tests/resilience-*.test.mjs tests/resilience-*.test.mts || exit 1 fi if [ "$RUN_SEED" = true ]; then echo "Running seed tests (scripts/ changed)..." SEED_TESTS=$(echo "$CHANGED_FILES" | grep "^scripts/" | sed 's|scripts/seed-\(.*\)\.mjs|tests/\1-seed.test.mjs|' | grep "^tests/" | while read -r t; do [ -f "$t" ] && echo "$t"; done) if [ -n "$SEED_TESTS" ]; then timeout 120 npx tsx --test $SEED_TESTS || exit 1 else echo " No matching seed tests found, skipping." fi fi if [ "$RUN_SERVER" = true ]; then echo "Running server handler tests (server/ changed)..." timeout 120 npx tsx --test tests/handlers.test.mts tests/server-handlers.test.mjs || exit 1 fi if [ "$RUN_RESILIENCE" = false ] && [ "$RUN_SEED" = false ] && [ "$RUN_SERVER" = false ]; then echo "Skipping unit tests (frontend-only changes)." fi fi echo "Running edge function tests..." if [ "$RUN_FRONTEND" = true ] || [ "$RUN_ALL" = true ]; then node --test tests/edge-functions.test.mjs || exit 1 else echo " Skipped (no api/ or src/ changes)." fi echo "Running markdown lint..." if echo "$CHANGED_FILES" | grep -qE "\.md$|\.mdx$" || [ "$RUN_ALL" = true ]; then npm run lint:md || exit 1 else echo " Skipped (no markdown changes)." fi echo "Running MDX lint (Mintlify compatibility)..." if echo "$CHANGED_FILES" | grep -q "\.mdx$" || [ "$RUN_ALL" = true ]; then node --test tests/mdx-lint.test.mjs || exit 1 else echo " Skipped (no MDX changes)." fi echo "Running proto freshness check..." if git diff --name-only origin/main -- proto/ src/generated/ docs/api/ Makefile | grep -q .; then # Only prepend $HOME/go/bin when buf is NOT already resolvable on the # caller's PATH. An unconditional prepend would shadow a developer's # preferred `buf` (e.g. Homebrew) with a potentially stale # ~/go/bin/buf left over from an older `go install buf@`, which # is the exact mixed-version failure this PR is trying to eliminate. # Plugin resolution is handled inside `make generate` via a pinned # PLUGIN_DIR, so the hook no longer needs to prepend for plugin # discovery — only for the `buf` binary itself, and only when there's # no other candidate. if ! command -v buf >/dev/null 2>&1 && [ -x "$HOME/go/bin/buf" ]; then export PATH="$HOME/go/bin:$PATH" fi # Plugin-presence check: consider a plugin "available" if it's on # PATH OR at the default `go install` location ($HOME/go/bin). # `make generate` resolves plugins via its own PLUGIN_DIR (GOBIN, # then first-entry GOPATH/bin), which matches the $HOME/go/bin # default on almost every dev setup. Anything exotic (custom GOBIN # not on PATH) still runs `make generate` cleanly because the # Makefile's own plugin-executable guard fires there. if command -v buf &>/dev/null && { command -v protoc-gen-ts-client &>/dev/null || [ -x "$HOME/go/bin/protoc-gen-ts-client" ]; }; then make generate if ! git diff --exit-code src/generated/ docs/api/; then echo "" echo "============================================================" echo "ERROR: Proto-generated code is out of date." echo "Run 'make generate' locally and commit the updated files." echo "============================================================" exit 1 fi UNTRACKED=$(git ls-files --others --exclude-standard src/generated/ docs/api/) if [ -n "$UNTRACKED" ]; then echo "" echo "============================================================" echo "ERROR: Untracked generated files found:" echo "$UNTRACKED" echo "Run 'make generate' locally and commit the new files." echo "============================================================" exit 1 fi echo "Proto-generated code is up to date." else echo "WARNING: buf or protoc plugins not installed, skipping proto freshness check." echo " Install with: make install-buf install-plugins" fi else echo "No proto-related changes, skipping." fi echo "Running pro-test bundle freshness check..." # public/pro/ is NOT built by Vercel — the root build script only runs the # main app's vite build. The /pro marketing app under pro-test/ must be # rebuilt manually and its output committed to public/pro/. If source # changes ship without the rebuild, production /pro serves the old bundle # (see PR #3227 → #3228: source fix merged but deploy still broken because # public/pro/ was stale). # # Scope to the BRANCH delta ($CHANGED_FILES, computed earlier from # `git diff origin/main...HEAD`), not the worktree, so unstaged local # pro-test/ scratch edits don't trigger a slow rebuild on unrelated # branches. RUN_ALL forces the check when the branch delta couldn't be # computed (matches the safety fallback used by the test runners above). # # Trigger on EITHER pro-test/ OR public/pro/ to match the CI workflow's # path filter — a bundle-only PR that edits public/pro/ without touching # pro-test/ (e.g. a fix-forward rebuild, or a hand-edit) is exactly the # class of change this guard is meant to validate. if [ "$RUN_ALL" = true ] || echo "$CHANGED_FILES" | grep -qE "^(pro-test|public/pro)/"; then if [ ! -d pro-test/node_modules ]; then echo " Installing pro-test deps..." (cd pro-test && npm install --prefer-offline) || exit 1 fi (cd pro-test && npm run build >/dev/null) || { echo "ERROR: pro-test build failed" exit 1 } if ! git diff --exit-code public/pro/; then echo "" echo "============================================================" echo "ERROR: pro-test sources changed but public/pro/ is stale." echo "Vercel ships whatever is committed under public/pro/ —" echo "it does NOT rebuild pro-test on deploy." echo "Fix:" echo " cd pro-test && npm run build && cd .." echo " git add public/pro/ && git commit --amend --no-edit" echo "============================================================" exit 1 fi UNTRACKED=$(git ls-files --others --exclude-standard public/pro/) if [ -n "$UNTRACKED" ]; then echo "" echo "============================================================" echo "ERROR: Untracked files in public/pro/ after rebuild:" echo "$UNTRACKED" echo "Run: git add public/pro/ && git commit --amend --no-edit" echo "============================================================" exit 1 fi echo " pro-test bundle is up to date." else echo " Skipped (no pro-test/ or public/pro/ changes in branch)." fi echo "Running version sync check..." npm run version:check || exit 1