Update docs and CI to local testing

This commit is contained in:
adilallo
2026-04-23 19:00:55 -06:00
parent 701db2aa1a
commit ce204bff03
10 changed files with 98 additions and 587 deletions
-469
View File
@@ -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:-<empty>}"
# 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:-<empty>}"
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
-74
View File
@@ -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
+3 -1
View File
@@ -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
+5 -13
View File
@@ -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/<short-name>`.
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.
+2 -2
View File
@@ -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/`)
+11 -17
View File
@@ -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-72CR-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
| 48 | **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 **1011** 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-72CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-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-72CR-79**, **CR-83**, **CR-84**, **CR-85**, **CR-88**, **CR-89** are **Done**; **CR-77** (publish) **Done**; **CR-80CR-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 **1011** 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` |
+1 -1
View File
@@ -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)
+28 -9
View File
@@ -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/<Name>/`.
+2 -1
View File
@@ -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",
+46
View File
@@ -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"