fix: address prelaunch quality audit and signup alerting #290
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI & Deploy | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| unit-component-tests: | |
| name: Unit & Component Tests | |
| runs-on: ubuntu-latest | |
| env: | |
| POSTHOG_PERSONAL_API_KEY: "" | |
| POSTHOG_PROJECT_ID: "" | |
| POSTHOG_API_KEY: "" | |
| NEXT_PUBLIC_POSTHOG_KEY: "" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.6" | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Test E2E environment safety | |
| run: bun test scripts/e2e-env.test.ts | |
| - name: Build shared package | |
| run: cd packages/shared && bun run build | |
| - name: Run shared package tests | |
| run: cd packages/shared && bun test | |
| - name: Build CLI | |
| run: cd packages/cli && bun run build | |
| - name: Run CLI tests | |
| run: cd packages/cli && bun test | |
| - name: Build MCP | |
| run: cd packages/mcp && bun run build | |
| - name: Run MCP tests | |
| run: cd packages/mcp && bun test | |
| - name: Generate Prisma client | |
| run: cd backend && bun x prisma generate | |
| - name: Type check backend | |
| run: cd backend && bun x tsc -p tsconfig.build.json --noEmit | |
| - name: Type check frontend | |
| run: cd apps/web && bun x tsc --noEmit | |
| - name: Type check site | |
| run: cd apps/site && bun x tsc --noEmit | |
| - name: Run backend tests | |
| run: cd backend && bun run test | |
| - name: Test published-course remediation | |
| run: bun test backend/scripts/__tests__/audit-published-courses.test.ts | |
| - name: Run frontend tests | |
| run: cd apps/web && bun run test | |
| - name: Run site tests | |
| run: cd apps/site && bun run test | |
| - name: Build frontend | |
| run: cd apps/web && bun run build | |
| - name: Build site | |
| run: cd apps/site && bun run build | |
| e2e-tests: | |
| name: E2E Tests | |
| needs: unit-component-tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| env: | |
| POSTHOG_PERSONAL_API_KEY: "" | |
| POSTHOG_PROJECT_ID: "" | |
| POSTHOG_API_KEY: "" | |
| NEXT_PUBLIC_POSTHOG_KEY: "" | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.6" | |
| - uses: supabase/setup-cli@v1 | |
| with: | |
| version: 2.90.0 | |
| - name: Start isolated local Supabase | |
| run: | | |
| # Keep test auth settings and signing keys outside the deployment config. | |
| supabase_dir="$RUNNER_TEMP/graspful-e2e-supabase" | |
| mkdir -p "$supabase_dir" | |
| supabase init --workdir "$supabase_dir" | |
| SUPABASE_TEST_DIR="$supabase_dir" python3 - <<'PYCONFIG' | |
| import os | |
| from pathlib import Path | |
| config = Path(os.environ["SUPABASE_TEST_DIR"]) / "supabase/config.toml" | |
| contents = config.read_text() | |
| contents = contents.replace('site_url = "http://127.0.0.1:3000"', 'site_url = "http://localhost:3001"') | |
| contents = contents.replace('additional_redirect_urls = ["https://127.0.0.1:3000"]', 'additional_redirect_urls = ["http://localhost:3001/**", "http://localhost:3002/**", "http://graspful.ai:3001/**", "http://app.graspful.ai:3001/**"]') | |
| contents = contents.replace('# signing_keys_path = "./signing_keys.json"', 'signing_keys_path = "./signing_keys.json"') | |
| assert 'signing_keys_path = "./signing_keys.json"' in contents | |
| assert 'site_url = "http://localhost:3001"' in contents | |
| assert 'http://localhost:3002/**' in contents | |
| config.write_text(contents) | |
| (config.parent / "signing_keys.json").write_text("[]") | |
| PYCONFIG | |
| chmod 600 "$supabase_dir/supabase/signing_keys.json" | |
| supabase gen signing-key --workdir "$supabase_dir" >/dev/null 2>&1 | |
| # Prisma owns the public schema, so apply SQL migrations after Prisma. | |
| supabase start --workdir "$supabase_dir" -x realtime,storage-api,imgproxy,postgres-meta,studio,edge-runtime,logflare,vector,supavisor > "$RUNNER_TEMP/supabase-start.log" 2>&1 | |
| - name: Extract local Supabase keys | |
| id: supabase | |
| run: | | |
| supabase status --workdir "$RUNNER_TEMP/graspful-e2e-supabase" --output json > "$RUNNER_TEMP/supabase-status.json" | |
| for key in ANON_KEY SERVICE_ROLE_KEY; do | |
| value=$(jq -er ".$key" "$RUNNER_TEMP/supabase-status.json") | |
| echo "::add-mask::$value" | |
| echo "$key=$value" >> "$GITHUB_OUTPUT" | |
| done | |
| echo "SUPABASE_URL=$(jq -er '.API_URL' "$RUNNER_TEMP/supabase-status.json")" >> "$GITHUB_OUTPUT" | |
| echo "DB_URL=$(jq -er '.DB_URL' "$RUNNER_TEMP/supabase-status.json")" >> "$GITHUB_OUTPUT" | |
| # The backend verifies asymmetric JWTs through Supabase JWKS. | |
| supabase_url=$(jq -er '.API_URL' "$RUNNER_TEMP/supabase-status.json") | |
| curl --fail --silent "$supabase_url/auth/v1/.well-known/jwks.json" | jq -e '.keys | length > 0' | |
| - name: Install dependencies | |
| run: bun install --frozen-lockfile | |
| - name: Build shared package | |
| run: cd packages/shared && bun run build | |
| - name: Generate Prisma client | |
| run: cd backend && bun x prisma generate | |
| - name: Run Prisma migrations | |
| run: cd backend && bun x prisma migrate deploy | |
| env: | |
| DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| DIRECT_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| - name: Apply auth triggers and RLS policies | |
| run: | | |
| for migration in supabase/migrations/*.sql; do | |
| psql "$DATABASE_URL" --set ON_ERROR_STOP=1 --file "$migration" | |
| done | |
| env: | |
| DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| - name: Seed database | |
| run: cd backend && bun x prisma db seed | |
| env: | |
| DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| DIRECT_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| - name: Seed brands | |
| run: cd backend && bun x ts-node prisma/seeds/brands.ts | |
| env: | |
| DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| DIRECT_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| - name: Build backend | |
| run: cd backend && bun run build | |
| - name: Build frontend | |
| run: cd apps/web && bun run build | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ steps.supabase.outputs.SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ steps.supabase.outputs.ANON_KEY }} | |
| NEXT_PUBLIC_BACKEND_URL: http://localhost:3000/api/v1 | |
| - name: Build site | |
| run: cd apps/site && bun run build | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ steps.supabase.outputs.SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ steps.supabase.outputs.ANON_KEY }} | |
| NEXT_PUBLIC_BACKEND_URL: http://localhost:3000/api/v1 | |
| - name: Install Playwright browsers | |
| run: cd apps/web && bun x playwright install --with-deps chromium | |
| - name: Start services and run both E2E suites | |
| run: | | |
| # Migrations ran above. Start the compiled API without start:prod's migration side effect. | |
| (cd backend && TS_NODE_PROJECT=tsconfig.runtime.json node -r tsconfig-paths/register dist/main.js) > "$RUNNER_TEMP/backend-e2e.log" 2>&1 & | |
| backend_pid=$! | |
| (cd apps/web && NODE_ENV=production PORT=3001 bun run start) > "$RUNNER_TEMP/web-e2e.log" 2>&1 & | |
| web_pid=$! | |
| (cd apps/site && NODE_ENV=production bun run start) > "$RUNNER_TEMP/site-e2e.log" 2>&1 & | |
| site_pid=$! | |
| trap 'kill "$backend_pid" "$web_pid" "$site_pid" 2>/dev/null || true' EXIT | |
| timeout 60 bash -c 'until curl -sf http://localhost:3000/api/v1/health >/dev/null; do sleep 2; done' | |
| timeout 120 bash -c 'until curl -sf http://localhost:3001 >/dev/null; do sleep 2; done' | |
| timeout 120 bash -c 'until curl -sf http://localhost:3002 >/dev/null; do sleep 2; done' | |
| # Run both suites even when the first fails, so both reports are available. | |
| web_status=0 | |
| (cd apps/web && bun run test:e2e) || web_status=$? | |
| site_status=0 | |
| (cd apps/site && bun run test:e2e) || site_status=$? | |
| if [ "$web_status" -ne 0 ] || [ "$site_status" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| env: | |
| CI: "true" | |
| E2E_REUSE_EXISTING_SERVER: "1" | |
| NODE_ENV: development | |
| DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| DIRECT_URL: ${{ steps.supabase.outputs.DB_URL }} | |
| SUPABASE_URL: ${{ steps.supabase.outputs.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }} | |
| APP_URL: http://localhost:3001 | |
| ALLOWED_ORIGINS: http://localhost:3001,http://localhost:3002,http://graspful.ai:3001,http://app.graspful.ai:3001 | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ steps.supabase.outputs.SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ steps.supabase.outputs.ANON_KEY }} | |
| NEXT_PUBLIC_BACKEND_URL: http://localhost:3000/api/v1 | |
| - name: Upload Playwright reports | |
| uses: actions/upload-artifact@v4 | |
| if: ${{ !cancelled() }} | |
| with: | |
| name: playwright-reports | |
| path: | | |
| apps/web/playwright-report/ | |
| apps/site/playwright-report/ | |
| retention-days: 14 | |
| - name: Upload service logs on failure | |
| uses: actions/upload-artifact@v4 | |
| if: failure() | |
| with: | |
| name: e2e-service-logs | |
| path: ${{ runner.temp }}/*-e2e.log | |
| retention-days: 7 | |
| deploy-backend: | |
| name: Deploy Backend | |
| needs: | |
| - unit-component-tests | |
| - e2e-tests | |
| if: github.ref == 'refs/heads/main' && github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - uses: oven-sh/setup-bun@v2 | |
| with: | |
| bun-version: "1.3.6" | |
| - name: Deploy backend to Railway | |
| run: bun x @railway/cli up --service ${{ vars.RAILWAY_SERVICE_NAME }} | |
| env: | |
| RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} | |
| # Frontend deploys automatically via Vercel git integration |