Update docs and CI to local testing
This commit is contained in:
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -60,7 +60,7 @@ removal trigger.
|
|||||||
|
|
||||||
## Verification recipe
|
## Verification recipe
|
||||||
|
|
||||||
Run these (in order) before declaring a change done. They mirror CI:
|
Run these (in order) before declaring a change done:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -rf .next # only if you moved/renamed routes or layouts
|
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 UI-only changes, also: `npm run storybook` and visually confirm.
|
||||||
For E2E-relevant changes: `npm run e2e`.
|
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
|
## Where else to look
|
||||||
|
|
||||||
|
|||||||
+5
-13
@@ -26,16 +26,9 @@ deployment-pipeline work.
|
|||||||
production, or any shared database. Add a **new** migration that
|
production, or any shared database. Add a **new** migration that
|
||||||
corrects the schema instead. Full policy:
|
corrects the schema instead. Full policy:
|
||||||
[docs/guides/backend-roadmap.md](docs/guides/backend-roadmap.md) §8.
|
[docs/guides/backend-roadmap.md](docs/guides/backend-roadmap.md) §8.
|
||||||
- **CI smoke:** [`.gitea/workflows/migrate-smoke.yaml`](.gitea/workflows/migrate-smoke.yaml)
|
- Any change under **`prisma/`**: run **`npm run migrate:smoke`** (see
|
||||||
spins up a throwaway Postgres and runs `npm run db:deploy` whenever
|
[docs/testing-guide.md](docs/testing-guide.md#running-tests), **Prisma**
|
||||||
`prisma/**` changes on a PR (or via `workflow_dispatch`). If the
|
under *Running tests*).
|
||||||
runner cannot run Docker/Postgres, run the same check locally before
|
|
||||||
merging migration changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d postgres
|
|
||||||
npm run db:deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
### API routes
|
### API routes
|
||||||
|
|
||||||
@@ -87,7 +80,6 @@ Ticket 17.
|
|||||||
|
|
||||||
1. Branch from `main`: `git checkout -b feature/<short-name>`.
|
1. Branch from `main`: `git checkout -b feature/<short-name>`.
|
||||||
2. Make the change and add/update tests.
|
2. Make the change and add/update tests.
|
||||||
3. `npm test && npm run e2e` (and `npm run storybook:build` if you touched
|
3. Before merging, run [docs/testing-guide.md](docs/testing-guide.md#running-tests) *Running tests*.
|
||||||
stories).
|
|
||||||
4. Commit using a clear message (`feat:`, `fix:`, `chore:`, …).
|
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
@@ -6,8 +6,8 @@ User-facing docs. Implementation conventions live in `.cursor/rules/`.
|
|||||||
|
|
||||||
- [create-flow.md](./create-flow.md) — Custom create-rule wizard: stages,
|
- [create-flow.md](./create-flow.md) — Custom create-rule wizard: stages,
|
||||||
URLs, persistence. Source of truth for product/eng alignment.
|
URLs, persistence. Source of truth for product/eng alignment.
|
||||||
- [testing-guide.md](./testing-guide.md) — Testing philosophy and what to
|
- [testing-guide.md](./testing-guide.md) — Testing philosophy, layer
|
||||||
cover at each layer.
|
coverage, and Prisma migration smoke before merge.
|
||||||
|
|
||||||
## Author guides (`guides/`)
|
## Author guides (`guides/`)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
### 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.
|
- **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-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.
|
- **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.
|
- **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. |
|
| 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. |
|
| 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. |
|
| 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. |
|
| 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
|
### 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).
|
**Depends on:** nothing in production; **Docker** on the developer machine.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
**Goal:** Catch broken SQL migrations before merge.
|
**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 (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.
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
**Acceptance criteria:**
|
**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 |
|
| 8 | 8 | Templates in UI |
|
||||||
| 9 | 9 | Web vitals persistence |
|
| 9 | 9 | Web vitals persistence |
|
||||||
| 10 | 10 | Public rule detail (optional) |
|
| 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** |
|
| 12 | 12 | Ops admin handoff (Cloudron) **Done** |
|
||||||
| 13 | 13 | API errors + request-id logging |
|
| 13 | 13 | API errors + request-id logging |
|
||||||
| 14 | 14 | Session lifecycle + cleanup |
|
| 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)
|
## 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) |
|
| 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 |
|
| 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) |
|
| 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) |
|
| 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 | [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.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` |
|
| 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` |
|
||||||
|
|||||||
@@ -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.).
|
- **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).
|
- **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`).
|
- **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)
|
### HTTP API (implemented in repo)
|
||||||
|
|
||||||
|
|||||||
+28
-9
@@ -3,6 +3,34 @@
|
|||||||
This is the **why** of testing in CommunityRule. For file layout, helper
|
This is the **why** of testing in CommunityRule. For file layout, helper
|
||||||
APIs, and required imports see `.cursor/rules/testing.mdc`.
|
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
|
## Philosophy
|
||||||
|
|
||||||
- **Test behaviour, not implementation.** Assert on what a user can see and
|
- **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.
|
- Hook internals or memoization specifics.
|
||||||
- Responsive visibility in JSDOM — use Playwright instead.
|
- 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
|
## Adding tests for a new component
|
||||||
|
|
||||||
1. Create `app/components/<Name>/`.
|
1. Create `app/components/<Name>/`.
|
||||||
|
|||||||
+2
-1
@@ -44,7 +44,8 @@
|
|||||||
"analyze:server": "ANALYZE=true npm run build",
|
"analyze:server": "ANALYZE=true npm run build",
|
||||||
"analyze:browser": "BUNDLE_ANALYZE=true npm run build",
|
"analyze:browser": "BUNDLE_ANALYZE=true npm run build",
|
||||||
"bundle:analyze": "node scripts/bundle-analyzer.js",
|
"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": {
|
"dependencies": {
|
||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
|
|||||||
Executable
+46
@@ -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"
|
||||||
Reference in New Issue
Block a user