diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml deleted file mode 100644 index 97d98f2..0000000 --- a/.gitea/workflows/ci.yaml +++ /dev/null @@ -1,469 +0,0 @@ -name: CI Pipeline -run-name: "${{ gitea.actor }} triggered CI pipeline" - -on: - workflow_dispatch: {} # Manual trigger only - run tests locally before merging - # Auto-runs disabled for solo development - # Re-enable when ready for collaborators: - # pull_request: - # branches: [main] - # types: [opened, reopened, synchronize] - -env: - NODE_VERSION: "20" - NEXT_TELEMETRY_DISABLED: "1" - -jobs: - test: - runs-on: [self-hosted, macos-latest] - env: - NODE_OPTIONS: "--max_old_space_size=8192 --max_semi_space_size=128" - CI: true - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - run: npm test -- --reporter=dot --maxConcurrency=1 - # If the Codecov Action fails on Gitea, replace this with the bash uploader below - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - token: "${{ secrets.CODECOV_TOKEN }}" - files: ./coverage/lcov.info - flags: unittests - - # Bash uploader alternative (uncomment if the action above has issues) - # - name: Upload coverage to Codecov (bash) - # run: | - # curl -s https://codecov.io/bash > codecov.sh - # bash codecov.sh -t "${{ secrets.CODECOV_TOKEN }}" -f coverage/lcov.info -F unittests - - e2e: - runs-on: [self-hosted, macos-latest] - strategy: - matrix: - browser: [chromium, firefox, webkit] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - name: Install Playwright - run: "npx playwright install --with-deps ${{ matrix.browser }}" - - run: npm run build - - - name: E2E (start + test + teardown) - run: | - set -euo pipefail - - export PORT="${PORT:-3010}" - export HOST="127.0.0.1" - mkdir -p .next - - # ensure build exists - test -d .next || { echo "❌ Missing .next build output"; exit 1; } - - echo "πŸš€ Starting Next.js server for E2E testing..." - - # Start Next directly with node so $! is the real node PID - node node_modules/next/dist/bin/next start -p "$PORT" -H "$HOST" > .next/runner.log 2>&1 & - SVPID=$! - echo "$SVPID" > .next/runner.pid - echo "🌐 Server PID: $SVPID" - - # Wait for readiness - echo "⏳ Waiting for server to be ready..." - npx wait-on -t 120000 "tcp:$HOST:$PORT" - curl -fsS "http://$HOST:$PORT" >/dev/null - echo "βœ… App is responding at http://$HOST:$PORT" - - # Run tests - echo "πŸ§ͺ Running E2E tests for ${{ matrix.browser }}..." - BASE_URL="http://$HOST:$PORT" npx playwright test --project=${{ matrix.browser }} --reporter=list || TEST_EXIT_CODE=$? - - # Teardown - echo "🧹 Cleaning up server..." - kill "$SVPID" 2>/dev/null || true - echo "βœ… Server cleanup complete" - env: - NEXT_TELEMETRY_DISABLED: "1" - NODE_ENV: production - NODE_OPTIONS: "--max-old-space-size=8192" - - # package artifacts (keeps file count small) - - name: Package E2E artifacts - if: failure() - run: | - tar -czf playwright-${{ matrix.browser }}.tgz playwright-report test-results || true - - - name: Upload E2E artifacts - if: failure() - uses: actions/upload-artifact@v3 - with: - name: playwright-results-${{ matrix.browser }} - path: playwright-${{ matrix.browser }}.tgz - retention-days: 30 - - visual-regression: - runs-on: [self-hosted, macos-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - name: Install Playwright - run: npx playwright install --with-deps - - run: npm run build - # 1) Sanity check that the build exists - - name: Verify Next build output - run: | - set -euo pipefail - ls -la .next || true - test -f .next/BUILD_ID || (echo "No Next build output (.next) – did build fail?" && exit 1) - - - name: Visual Regression (start + test + teardown) - run: | - set -euo pipefail - - export PORT="${PORT:-3010}" - export HOST="127.0.0.1" - mkdir -p .next - - # ensure build exists - test -d .next || { echo "❌ Missing .next build output"; exit 1; } - - echo "πŸš€ Starting Next.js server for visual regression testing..." - - # Ensure port is free before starting - echo "πŸ” Checking if port $PORT is available..." - if lsof -ti:$PORT >/dev/null 2>&1; then - echo "⚠️ Port $PORT is in use, killing existing processes..." - lsof -ti:$PORT | xargs kill -9 2>/dev/null || true - sleep 2 - fi - - # Start Next with explicit memory settings for CI stability - echo "πŸš€ Starting Next.js server on $HOST:$PORT..." - - # Set environment variable and start server - export NODE_OPTIONS="--max-old-space-size=4096" - nohup node node_modules/next/dist/bin/next start -p "$PORT" -H "$HOST" > .next/runner.log 2>&1 & - SVPID=$! - echo "$SVPID" > .next/runner.pid - echo "🌐 Server PID: $SVPID" - - # Give the server a moment to start - sleep 5 - - # Check if the server process is still running - if ! kill -0 "$SVPID" 2>/dev/null; then - echo "❌ Server process died immediately after starting" - echo "πŸ“‹ Server logs:" - cat .next/runner.log || true - exit 1 - fi - echo "βœ… Server process is running (PID: $SVPID)" - - # Wait for readiness with better error handling - echo "⏳ Waiting for server to be ready..." - npx wait-on -t 120000 "tcp:$HOST:$PORT" - - # Verify server is actually responding to all test routes - echo "πŸ” Verifying server readiness for all test routes..." - for i in {1..15}; do - # Check all routes that will be tested in visual regression - if curl -fsS "http://$HOST:$PORT" >/dev/null 2>&1 && \ - curl -fsS "http://$HOST:$PORT/blog" >/dev/null 2>&1 && \ - curl -fsS "http://$HOST:$PORT/blog/resolving-active-conflicts" >/dev/null 2>&1; then - echo "βœ… App is responding to all test routes at http://$HOST:$PORT" - break - else - echo "⏳ Attempt $i/15: Server not ready for all routes yet, waiting..." - sleep 3 - if [ $i -eq 15 ]; then - echo "❌ Server failed to respond to all routes after 15 attempts" - echo "πŸ“‹ Server logs:" - cat .next/runner.log || true - echo "πŸ” Testing individual routes:" - curl -I "http://$HOST:$PORT" || echo "❌ Homepage failed" - curl -I "http://$HOST:$PORT/blog" || echo "❌ Blog failed" - curl -I "http://$HOST:$PORT/blog/resolving-active-conflicts" || echo "❌ Blog post failed" - exit 1 - fi - fi - done - - # Give server a moment to fully settle after all routes are ready - echo "⏳ Allowing server to fully settle..." - sleep 10 - - # Final verification that server is still responding - echo "πŸ” Final server health check..." - if ! curl -fsS "http://$HOST:$PORT" >/dev/null 2>&1; then - echo "❌ Server health check failed after settlement period" - echo "πŸ“‹ Server logs:" - cat .next/runner.log || true - exit 1 - fi - echo "βœ… Server is healthy and ready for tests" - - # Run visual regression tests with server monitoring - echo "πŸ§ͺ Running visual regression tests..." - - # Start comprehensive server monitoring in background - ( - while true; do - # Check if server process is still running - if ! kill -0 "$SVPID" 2>/dev/null; then - echo "❌ Server process died during test execution" - echo "πŸ“‹ Server logs:" - cat .next/runner.log || true - break - fi - - # Check if server is responding - if ! curl -fsS "http://$HOST:$PORT" >/dev/null 2>&1; then - echo "⚠️ Server health check failed - server may have crashed" - echo "πŸ“‹ Current server logs:" - tail -20 .next/runner.log || true - break - fi - sleep 5 - done - ) & - HEALTH_PID=$! - - # Run tests with increased timeout and conservative settings for CI stability - BASE_URL="http://$HOST:$PORT" npx playwright test tests/e2e/visual-regression.spec.ts --timeout=120000 --workers=1 --retries=1 - - # Stop health monitoring - kill $HEALTH_PID 2>/dev/null || true - - # Teardown with better error handling - echo "🧹 Cleaning up server..." - kill "$SVPID" 2>/dev/null || true - - # Wait for server to actually stop - for i in {1..10}; do - if ! kill -0 "$SVPID" 2>/dev/null; then - echo "βœ… Server process stopped" - break - else - echo "⏳ Waiting for server to stop... ($i/10)" - sleep 2 - if [ $i -eq 10 ]; then - echo "⚠️ Force killing server process" - kill -9 "$SVPID" 2>/dev/null || true - fi - fi - done - - echo "βœ… Server cleanup complete" - env: - NEXT_TELEMETRY_DISABLED: "1" - NODE_ENV: production - NODE_OPTIONS: "--max-old-space-size=8192" - - - name: Package visual artifacts - if: failure() - run: | - # Include server logs for debugging - echo "πŸ“‹ Server logs for debugging:" - cat .next/runner.log || echo "No server logs found" - - # Package test results and logs - tar -czf visual-regression.tgz test-results tests/e2e/visual-regression.spec.ts-snapshots .next/runner.log || true - - - name: Upload visual artifacts - if: failure() - uses: actions/upload-artifact@v3 - with: - name: visual-regression-results - path: visual-regression.tgz - retention-days: 30 - - - name: Stop app - if: always() - run: | - if [ -f .next/runner.pid ]; then - kill $(cat .next/runner.pid) 2>/dev/null || true - fi - - performance: - runs-on: [self-hosted, macos-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - - name: Build application - run: npm run build - - # 1) Sanity check that the build exists - - name: Verify Next build output - run: | - set -euo pipefail - ls -la .next || true - test -f .next/BUILD_ID || (echo "No Next build output (.next) – did build fail?" && exit 1) - - - name: Install Chrome via Puppeteer (mac_arm) - run: | - # Install Chrome (arm64) into a local cache - set -euo pipefail - mkdir -p .cache/puppeteer - - # 1) Install and capture the build id that was actually installed - INSTALL_OUT="$(npx @puppeteer/browsers install chrome@stable --platform=mac_arm --path .cache/puppeteer)" - echo "$INSTALL_OUT" - - # INSTALL_OUT looks like: "chrome@140.0.7339.80 /abs/path/to/.../Google Chrome for Testing" - BUILD_ID="$(printf '%s\n' "$INSTALL_OUT" | awk '{print $1}' | cut -d@ -f2)" - echo "Detected Chrome build: $BUILD_ID" - - # 2) Ask for the executable path using the explicit build id - CHROME_PATH="$(npx @puppeteer/browsers executable-path chrome@"$BUILD_ID" --platform=mac_arm --path .cache/puppeteer || true)" - echo "Chrome executable path (via CLI): ${CHROME_PATH:-}" - - # 3) Fallback: resolve the binary directly from the cache if the CLI returned empty - if [ -z "$CHROME_PATH" ]; then - CHROME_PATH="$(/usr/bin/find ".cache/puppeteer/chrome" -type f -path "*/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" -print -quit || true)" - echo "Chrome executable path (via find): ${CHROME_PATH:-}" - fi - - # 4) Hard fail if still empty - if [ -z "$CHROME_PATH" ] || [ ! -x "$CHROME_PATH" ]; then - echo "❌ Chrome path is empty or not executable" - ls -la .cache/puppeteer || true - exit 1 - fi - - # 5) Export for subsequent steps in this job and later ones - echo "CHROME_PATH=$CHROME_PATH" >> "$GITHUB_ENV" - "$CHROME_PATH" --version || true - - - name: Ensure arm64 Node for Lighthouse - run: | - set -euo pipefail - echo "node before: $(node -v) arch=$(node -p 'process.arch')" - if [ "$(node -p 'process.arch')" != "arm64" ]; then - NODE_VER=20.17.0 - curl -fsSLO "https://nodejs.org/dist/v${NODE_VER}/node-v${NODE_VER}-darwin-arm64.tar.xz" - tar -xJf "node-v${NODE_VER}-darwin-arm64.tar.xz" - # Make arm64 node take effect in THIS step: - export PATH="$PWD/node-v${NODE_VER}-darwin-arm64/bin:$PATH" - # And persist for subsequent steps: - echo "$PWD/node-v${NODE_VER}-darwin-arm64/bin" >> "$GITHUB_PATH" - fi - echo "node after: $(node -v) arch=$(node -p 'process.arch')" - echo "uname -m: $(uname -m)" - # Get Chrome path for this step - CHROME_PATH=$(npx @puppeteer/browsers executable-path chrome@stable --platform=mac_arm --path .cache/puppeteer) - echo "Chrome path: $CHROME_PATH" - "$CHROME_PATH" --version || true - - - name: Performance (start + test + teardown) - run: | - set -euo pipefail - - export PORT=3010 HOST=127.0.0.1 - mkdir -p .next - test -d .next || { echo "❌ Missing .next build output"; exit 1; } - - echo "πŸš€ Starting Next.js server for performance testing..." - node node_modules/next/dist/bin/next start -p "$PORT" -H "$HOST" > .next/runner.log 2>&1 & - SVPID=$! - npx wait-on -t 120000 "tcp:$HOST:$PORT" - curl -fsS "http://$HOST:$PORT" >/dev/null - echo "βœ… App is responding at http://$HOST:$PORT" - - # Ensure we're using arm64 Node for Lighthouse - echo "Node arch: $(node -p "process.arch")" - - # Get Chrome path directly in this step (same logic as installation step) - INSTALL_OUT="$(npx @puppeteer/browsers install chrome@stable --platform=mac_arm --path .cache/puppeteer 2>/dev/null || true)" - BUILD_ID="$(printf '%s\n' "$INSTALL_OUT" | awk '{print $1}' | cut -d@ -f2)" - echo "Using Chrome build: $BUILD_ID" - - # Try CLI first, then fallback to find - CHROME_PATH="$(npx @puppeteer/browsers executable-path chrome@"$BUILD_ID" --platform=mac_arm --path .cache/puppeteer 2>/dev/null || true)" - if [ -z "$CHROME_PATH" ]; then - CHROME_PATH="$(/usr/bin/find ".cache/puppeteer/chrome" -type f -path "*/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" -print -quit 2>/dev/null || true)" - fi - - echo "Chrome path: $CHROME_PATH" - - # Verify Chrome path is not empty - if [ -z "$CHROME_PATH" ]; then - echo "❌ Chrome path is empty - Chrome installation may have failed" - exit 1 - fi - - # Verify Chrome executable exists and is executable - if [ ! -x "$CHROME_PATH" ]; then - echo "❌ Chrome executable not found or not executable: $CHROME_PATH" - ls -la .cache/puppeteer/ || true - exit 1 - fi - - "$CHROME_PATH" --version - - # Run LHCI with arm64 Node + arm64 Chrome - # Test homepage and blog pages using config file - npx lhci autorun --chrome-path="$CHROME_PATH" - - kill "$SVPID" 2>/dev/null || true - env: - NEXT_TELEMETRY_DISABLED: "1" - NODE_ENV: production - NODE_OPTIONS: "--max-old-space-size=8192" - - - name: Upload Performance Artifacts - if: failure() - uses: actions/upload-artifact@v3 - with: - name: performance-results - path: | - lhci-results - .next/analyze - .next/monitoring - .next/web-vitals - .next/test-results - retention-days: 30 - - lint: - runs-on: [self-hosted, macos-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - name: Prisma schema - run: npx prisma validate - env: - DATABASE_URL: postgresql://ci:ci@127.0.0.1:5432/ci - - run: npm run lint - - run: npm exec prettier -- --check "**/*.{js,jsx,ts,tsx,json,css,md}" - - build: - runs-on: [self-hosted, macos-latest] - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - run: npm ci --no-audit --fund=false - - run: npm run build - - run: npm run storybook:build:github diff --git a/.gitea/workflows/migrate-smoke.yaml b/.gitea/workflows/migrate-smoke.yaml deleted file mode 100644 index 73b5796..0000000 --- a/.gitea/workflows/migrate-smoke.yaml +++ /dev/null @@ -1,74 +0,0 @@ -name: Migrate Smoke -run-name: "${{ gitea.actor }} triggered migrate smoke" - -on: - workflow_dispatch: {} - pull_request: - branches: [main] - paths: - - "prisma/**" - - ".gitea/workflows/migrate-smoke.yaml" - push: - branches: [main] - paths: - - "prisma/**" - - ".gitea/workflows/migrate-smoke.yaml" - -env: - NODE_VERSION: "20" - NEXT_TELEMETRY_DISABLED: "1" - # Non-default host port so a local dev Postgres on 5432 keeps working. - PG_HOST_PORT: "5433" - POSTGRES_USER: communityrule - POSTGRES_PASSWORD: communityrule - POSTGRES_DB: communityrule - -jobs: - migrate: - runs-on: [self-hosted, macos-latest] - env: - CI: true - DATABASE_URL: "postgresql://communityrule:communityrule@127.0.0.1:5433/communityrule" - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "${{ env.NODE_VERSION }}" - cache: npm - - - name: Start Postgres - run: | - set -euo pipefail - docker rm -f migrate-smoke-pg >/dev/null 2>&1 || true - docker run -d --name migrate-smoke-pg \ - -e POSTGRES_USER="$POSTGRES_USER" \ - -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ - -e POSTGRES_DB="$POSTGRES_DB" \ - -p "${PG_HOST_PORT}:5432" \ - postgres:16-alpine - - - name: Wait for Postgres - run: | - set -euo pipefail - for i in {1..30}; do - if docker exec migrate-smoke-pg pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then - echo "Postgres ready after ${i}s" - exit 0 - fi - sleep 1 - done - echo "Postgres did not become ready in 30s" - docker logs migrate-smoke-pg || true - exit 1 - - - run: npm ci --no-audit --fund=false - - - name: Apply migrations - run: npm run db:deploy - - - name: Verify Prisma can connect to migrated DB - run: echo "SELECT 1;" | npx --no-install prisma db execute --stdin --url "$DATABASE_URL" - - - name: Stop Postgres - if: always() - run: docker rm -f migrate-smoke-pg >/dev/null 2>&1 || true diff --git a/AGENTS.md b/AGENTS.md index fd83a27..c750780 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,7 +60,7 @@ removal trigger. ## Verification recipe -Run these (in order) before declaring a change done. They mirror CI: +Run these (in order) before declaring a change done: ```bash rm -rf .next # only if you moved/renamed routes or layouts @@ -71,6 +71,8 @@ npx next build # production build + route manifest For UI-only changes, also: `npm run storybook` and visually confirm. For E2E-relevant changes: `npm run e2e`. +For changes under `prisma/`: `npm run migrate:smoke` (see +[docs/testing-guide.md](docs/testing-guide.md) Β§ *Running tests*). ## Where else to look diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d93d4d5..97ab22d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,16 +26,9 @@ deployment-pipeline work. production, or any shared database. Add a **new** migration that corrects the schema instead. Full policy: [docs/guides/backend-roadmap.md](docs/guides/backend-roadmap.md) Β§8. -- **CI smoke:** [`.gitea/workflows/migrate-smoke.yaml`](.gitea/workflows/migrate-smoke.yaml) - spins up a throwaway Postgres and runs `npm run db:deploy` whenever - `prisma/**` changes on a PR (or via `workflow_dispatch`). If the - runner cannot run Docker/Postgres, run the same check locally before - merging migration changes: - - ```bash - docker compose up -d postgres - npm run db:deploy - ``` +- Any change under **`prisma/`**: run **`npm run migrate:smoke`** (see + [docs/testing-guide.md](docs/testing-guide.md#running-tests), **Prisma** + under *Running tests*). ### API routes @@ -87,7 +80,6 @@ Ticket 17. 1. Branch from `main`: `git checkout -b feature/`. 2. Make the change and add/update tests. -3. `npm test && npm run e2e` (and `npm run storybook:build` if you touched - stories). +3. Before merging, run [docs/testing-guide.md](docs/testing-guide.md#running-tests) *Running tests*. 4. Commit using a clear message (`feat:`, `fix:`, `chore:`, …). -5. Open a PR; CI runs unit, E2E, visual regression, and Lighthouse. +5. Open a pull request. diff --git a/docs/README.md b/docs/README.md index 4dd9b30..ba49832 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,8 +6,8 @@ User-facing docs. Implementation conventions live in `.cursor/rules/`. - [create-flow.md](./create-flow.md) β€” Custom create-rule wizard: stages, URLs, persistence. Source of truth for product/eng alignment. -- [testing-guide.md](./testing-guide.md) β€” Testing philosophy and what to - cover at each layer. +- [testing-guide.md](./testing-guide.md) β€” Testing philosophy, layer + coverage, and Prisma migration smoke before merge. ## Author guides (`guides/`) diff --git a/docs/guides/backend-linear-tickets.md b/docs/guides/backend-linear-tickets.md index 0f070f8..6796275 100644 --- a/docs/guides/backend-linear-tickets.md +++ b/docs/guides/backend-linear-tickets.md @@ -11,7 +11,7 @@ A backend review was merged into **[docs/backend-roadmap.md](backend-roadmap.md) ### Audit note (Linear CR-72+ vs repo, 2026-04) - **Done in Linear and shipped:** **CR-72–CR-76**, **CR-77** (publish from create flow), **CR-78** (template seed), **CR-79**, **CR-88**, **CR-89**. The **CR-72 β†’ CR-83** numbering is the original **sequential plan**, not current blocking order; the **core product vertical** through publish + templates is effectively complete in-repo. -- **Backlog (still open):** **CR-80** (web vitals β€” file-based route remains), **CR-81** (public rule detail β€” no `GET /api/rules/[id]` or marketing detail page yet), **CR-82** (CI migrate smoke), **CR-86** (profile + account + draft resume β€” UI mostly placeholder), **CR-90** / **CR-91**, **CR-93** (template grid facets on marketing). **CR-84 Done** β€” canonical error contract `{ error: { code, message }, details? }` and `x-request-id` propagation shipped via `lib/server/{responses,requestId,apiRoute}.ts`; auth + drafts + rules routes migrated, remaining `app/api/*` are a follow-up pass. **CR-85 Done** β€” multi-device session policy + lazy expired-row cleanup (per-user prune on every sign-in plus ~5% global sweep, no cron); ADR comment block in [`lib/server/session.ts`](../../lib/server/session.ts). +- **Backlog (still open):** **CR-80** (web vitals β€” file-based route remains), **CR-81** (public rule detail β€” no `GET /api/rules/[id]` or marketing detail page yet), **CR-86** (profile + account + draft resume β€” UI mostly placeholder), **CR-90** / **CR-91**, **CR-93** (template grid facets on marketing). **CR-82** (migrate smoke): **local** `npm run migrate:smoke` + [CONTRIBUTING.md](../../CONTRIBUTING.md) / [docs/testing-guide.md](../testing-guide.md) β€” in-repo Gitea workflow YAML **removed**; optional future remote job if hosted runners return. **CR-84 Done** β€” canonical error contract `{ error: { code, message }, details? }` and `x-request-id` propagation shipped via `lib/server/{responses,requestId,apiRoute}.ts`; auth + drafts + rules routes migrated, remaining `app/api/*` are a follow-up pass. **CR-85 Done** β€” multi-device session policy + lazy expired-row cleanup (per-user prune on every sign-in plus ~5% global sweep, no cron); ADR comment block in [`lib/server/session.ts`](../../lib/server/session.ts). - **CR-83 Done (admin handoff + cutover plan):** [`docs/guides/ops-backend-deploy.md`](ops-backend-deploy.md) shipped. Cloudron admin access on `cloud.medlab.host` granted; doc now covers (a) what's in place, (b) the side-by-side β†’ apex cutover plan, and (c) the two open product questions + registry decision still outstanding. Steady-state operator runbook is split out into a follow-up β€” see [Ticket 12 / CR-83 follow-ups](#follow-up-tickets-filed-under-cr-83) below. Key new finding: legacy `communityrule.info` is a single Cloudron **LAMP** app (`lamp.cloudronapp.php74@5.1.2`) hosting marketing site + Express/MySQL backend + a broken Flask chatbot all in one container; all three retire together via CR-99 + CR-101. - **CR-86** is **no longer blocked** by publish β€” **CR-77** is **Done**; profile work is gated by **implementation**, not waiting on publish wiring. - **Not in this ticket list** but called out in **[docs/backend-roadmap.md](backend-roadmap.md):** shared **rate-limit store** (e.g. Redis) before multi-instance; **`GET /api/create-flow/methods`** exists for facet scoring (Ticket 16 / CR-88) but is not duplicated as a separate doc ticket. @@ -56,7 +56,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)β€”admi | 4–8 | **No** | Local or staging URL is still β€œyour” deployβ€”admin only if that URL is on their infra. | | 9 | **No** to implement; **Yes** when **production** uses multiple instances or read-only FS | **Default** is external RUM/log drain; Postgres vitals only if ops explicitly wants one datastoreβ€”may need vendor keys for SaaS. | | 10 | **No** to code | Same deploy pipeline as the rest of the app. | -| 11 | **Maybe** | Whoever owns **Gitea runners**: can they run Postgres in CI? Not the same as production server, but often the same β€œinfra” person. | +| 11 | **No** | **Migrate smoke** is local (`npm run migrate:smoke`); no server for CI. | | 12 | **Yesβ€”this is the handoff ticket** | You (or admin) write **`docs/ops-backend-deploy.md`** so deploy steps are explicit; **you need admin input** to fill in hostnames, DB provider, SMTP, backup policy. | ### One-line summary @@ -513,26 +513,20 @@ _Section B β€” Final Review screen `+` button per category:_ --- -## Ticket 11 β€” CI: database migration smoke (optional, runner-dependent) +## Ticket 11 β€” Database migration smoke (local) -**Depends on:** existing [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml). - -**Server / admin:** **Not production server**β€”but you may need whoever runs **Gitea/self-hosted runners** to allow **Postgres in CI** (Docker service / sidecar) or to accept a **manual migrate** process documented instead. +**Depends on:** nothing in production; **Docker** on the developer machine. **Goal:** Catch broken SQL migrations before merge. -**Context:** Lint job already runs `prisma validate` with a dummy `DATABASE_URL`. **Migrate** needs a real Postgres reachable from the runner. - -**Implementation:** - -1. If Gitea runners support **Docker sidecar** or **postgres service**, add a job: start Postgres, set `DATABASE_URL`, `npx prisma migrate deploy`, then run a minimal test that hits `/api/health` with DB connected (may require `next build` + short `next start` + curl). -2. If **macOS self-hosted** runners cannot run service containers easily, document in CONTRIBUTING: β€œrun `migrate deploy` against staging before prod” and keep validate-only in CI. +**Implementation (shipped in repo):** `npm run migrate:smoke` runs [`scripts/migrate-smoke-local.sh`](../../scripts/migrate-smoke-local.sh) β€” ephemeral Postgres on `127.0.0.1:5433`, `prisma migrate deploy`, connection check, teardown. Documented in [CONTRIBUTING.md](../../CONTRIBUTING.md) and [docs/testing-guide.md](../testing-guide.md) Β§ *Running tests* (Prisma). In-repo **Gitea workflow YAML** for this was removed in favor of the local script; reintroducing a remote job is optional if self-hosted runners are available again. **Acceptance criteria:** -- [ ] Broken migration fails CI **or** documented alternative process is explicit. +- [x] Documented + scripted local migrate smoke before merge. +- [ ] (Optional) Remote CI job, if you later restore runners and want parity. -**Files:** [.gitea/workflows/ci.yaml](.gitea/workflows/ci.yaml), [CONTRIBUTING.md](CONTRIBUTING.md). +**Files:** [`scripts/migrate-smoke-local.sh`](../../scripts/migrate-smoke-local.sh), [`package.json`](../../package.json) (`migrate:smoke`), [CONTRIBUTING.md](../../CONTRIBUTING.md), [docs/testing-guide.md](../testing-guide.md). --- @@ -694,7 +688,7 @@ All six are titled `[Backend] …`, assigned to Vinod, in the **community-rule** | 8 | 8 | Templates in UI | | 9 | 9 | Web vitals persistence | | 10 | 10 | Public rule detail (optional) | -| 11 | 11 | CI migrate smoke (optional) | +| 11 | 11 | Migrate smoke (local) **Done in repo** | | 12 | 12 | Ops admin handoff (Cloudron) **Done** | | 13 | 13 | API errors + request-id logging | | 14 | 14 | Session lifecycle + cleanup | @@ -712,7 +706,7 @@ Tickets **10–11** can be deferred without blocking the core β€œauth + drafts + ## Linear (Community-rule team) -**Main chain (historical):** **CR-72 β†’ CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72–CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80–CR-82** remain **Backlog** (web vitals, public rule detail, CI migrate smoke). **CR-83** (admin handoff) shipped as a narrow handoff sheet; the actual Cloudron deployment pipeline is split into the **`[Backend]` follow-up tickets** filed under it (env-var bridging β†’ image registry β†’ staging β†’ production cutover β†’ operator runbook β†’ legacy decommission). **Parallel (still open):** **CR-86** / Ticket 15 (**Backlog** β€” publish **not** a blocker); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior). +**Main chain (historical):** **CR-72 β†’ CR-83** was the original **strict sequence**; **repo + Linear status today:** **CR-72–CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80–CR-81** remain **Backlog** (web vitals, public rule detail). **CR-82** covered by local `migrate:smoke` (see Ticket 11). **CR-83** (admin handoff) shipped as a narrow handoff sheet; the actual Cloudron deployment pipeline is split into the **`[Backend]` follow-up tickets** filed under it (env-var bridging β†’ image registry β†’ staging β†’ production cutover β†’ operator runbook β†’ legacy decommission). **Parallel (still open):** **CR-86** / Ticket 15 (**Backlog** β€” publish **not** a blocker); **CR-93** (**Backlog**); **CR-90** / Ticket 18 (stakeholder invites); **CR-91** / Ticket 19 (`Add` button behavior). | Doc ticket | Linear | Title (short) | | ---------: | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | @@ -726,7 +720,7 @@ Tickets **10–11** can be deferred without blocking the core β€œauth + drafts + | 8 | [CR-79](https://linear.app/community-rule/issue/CR-79/backend-load-rule-templates-from-get-apitemplates-in-ui) | Templates in UI | | 9 | [CR-80](https://linear.app/community-rule/issue/CR-80/backend-persist-web-vitals-outside-next-db-or-external-rum) | Web vitals (prefer external) | | 10 | [CR-81](https://linear.app/community-rule/issue/CR-81/backend-public-rule-detail-page-get-apirulesid-optional) | Public rule detail (optional) | -| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | CI migrate smoke (optional) | +| 11 | [CR-82](https://linear.app/community-rule/issue/CR-82/backend-ci-postgres-migration-smoke-optional) | Local migrate smoke (**Done in repo**; optional remote CI) | | 12 | [CR-83](https://linear.app/community-rule/issue/CR-83/backend-stagingproduction-runbook-admin-handoff-docsops-backend) | Ops admin handoff (Cloudron) **Done** | | 12.1 | [CR-96](https://linear.app/community-rule/issue/CR-96/backend-bridge-cloudron-env-vars-to-canonical-names) | `[Backend] Bridge CLOUDRON_* env vars to canonical names` | | 12.2 | [CR-97](https://linear.app/community-rule/issue/CR-97/backend-container-image-registry-choose-build-push) | `[Backend] Container image registry: choose, build, push` | diff --git a/docs/guides/backend-roadmap.md b/docs/guides/backend-roadmap.md index 3935349..b9a0bd2 100644 --- a/docs/guides/backend-roadmap.md +++ b/docs/guides/backend-roadmap.md @@ -11,7 +11,7 @@ Temporary working notes for building the backend. Safe to delete once the stack - **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.). - **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per β€œCreate rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-structure` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts). - **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts): **production default** is **`external`** (structured logs; no `.next` writes). **`local`** file-based mode remains for development (`WEB_VITALS_STORAGE`). -- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition. +- **Pre-merge checks:** run locally (see [docs/testing-guide.md](../testing-guide.md) Β§ *Running tests*; [CONTRIBUTING.md](../CONTRIBUTING.md) pull request workflow). No in-repo remote CI workflow; production deploy is out of band ([ops-backend-deploy.md](ops-backend-deploy.md)). ### HTTP API (implemented in repo) diff --git a/docs/testing-guide.md b/docs/testing-guide.md index f00b24e..1ae818e 100644 --- a/docs/testing-guide.md +++ b/docs/testing-guide.md @@ -3,6 +3,34 @@ This is the **why** of testing in CommunityRule. For file layout, helper APIs, and required imports see `.cursor/rules/testing.mdc`. +## Running tests + +```bash +npx tsc --noEmit +npm test +npx next build +npm run e2e # when routes, auth, or critical flows change +npm run storybook:build # when stories/ change +npm run test:component # components only, faster inner loop +npm run visual:update # after UI changes to visual regression tests +``` + +**Prisma** (`prisma/**` changes): **requires Docker.** + +```bash +npm run migrate:smoke +``` + +Starts a throwaway Postgres on `127.0.0.1:5433`, runs `prisma migrate +deploy`, checks the connection, then removes the container. Port **5433** +avoids clashing with `docker compose` on **5432**. If you already use +Compose on 5432: `docker compose up -d postgres` then +`DATABASE_URL=postgresql://communityrule:communityrule@127.0.0.1:5432/communityrule npm run db:deploy`. + +Do not rewrite migrations already applied to shared DBs β€” see +[CONTRIBUTING.md](../CONTRIBUTING.md) and +[guides/backend-roadmap.md](guides/backend-roadmap.md) Β§8. + ## Philosophy - **Test behaviour, not implementation.** Assert on what a user can see and @@ -45,15 +73,6 @@ APIs, and required imports see `.cursor/rules/testing.mdc`. - Hook internals or memoization specifics. - Responsive visibility in JSDOM β€” use Playwright instead. -## Running tests - -```bash -npm test # vitest run with coverage -npm run test:component # vitest, components only (faster inner loop) -npm run e2e # playwright (alias: test:e2e) -npm run visual:update # refresh playwright screenshots after UI changes -``` - ## Adding tests for a new component 1. Create `app/components//`. diff --git a/package.json b/package.json index 7d7b75c..7fb55b4 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "analyze:server": "ANALYZE=true npm run build", "analyze:browser": "BUNDLE_ANALYZE=true npm run build", "bundle:analyze": "node scripts/bundle-analyzer.js", - "db:deploy": "prisma migrate deploy" + "db:deploy": "prisma migrate deploy", + "migrate:smoke": "./scripts/migrate-smoke-local.sh" }, "dependencies": { "@mdx-js/loader": "^3.1.1", diff --git a/scripts/migrate-smoke-local.sh b/scripts/migrate-smoke-local.sh new file mode 100755 index 0000000..970ce3d --- /dev/null +++ b/scripts/migrate-smoke-local.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Ephemeral Postgres on host 5433, prisma migrate deploy, verify, teardown. +set -euo pipefail + +PG_HOST_PORT="${PG_HOST_PORT:-5433}" +POSTGRES_USER="${POSTGRES_USER:-communityrule}" +POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-communityrule}" +POSTGRES_DB="${POSTGRES_DB:-communityrule}" +CONTAINER_NAME="${CONTAINER_NAME:-migrate-smoke-pg}" +export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@127.0.0.1:${PG_HOST_PORT}/${POSTGRES_DB}" + +cleanup() { + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM + +echo "β†’ Starting throwaway Postgres on 127.0.0.1:${PG_HOST_PORT} (container: ${CONTAINER_NAME})" +docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +docker run -d --name "$CONTAINER_NAME" \ + -e "POSTGRES_USER=$POSTGRES_USER" \ + -e "POSTGRES_PASSWORD=$POSTGRES_PASSWORD" \ + -e "POSTGRES_DB=$POSTGRES_DB" \ + -p "${PG_HOST_PORT}:5432" \ + postgres:16-alpine + +echo "β†’ Waiting for Postgres..." +for i in {1..30}; do + if docker exec "$CONTAINER_NAME" pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" >/dev/null 2>&1; then + echo "β†’ Postgres ready after ${i}s" + break + fi + if [ "$i" -eq 30 ]; then + echo "Postgres did not become ready in 30s" >&2 + docker logs "$CONTAINER_NAME" || true + exit 1 + fi + sleep 1 +done + +echo "β†’ prisma migrate deploy" +npm run db:deploy + +echo "β†’ Verifying connection (SELECT 1)" +echo "SELECT 1;" | npx --no-install prisma db execute --stdin --url "$DATABASE_URL" + +echo "β†’ migrate smoke OK"