name: CI on: push: branches: [main] pull_request: branches: [main] jobs: backend: runs-on: ubuntu-latest services: postgres: image: pgvector/pgvector:pg16 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: resolutionflow_test # No host port mapping. Tests connect to `postgres:5432` (the service # container's docker-network DNS name), not `localhost:5432`. With # multiple Gitea runners on the same homelab box, host-port mapping # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # second fails with "port is already allocated". options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 env: DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/resolutionflow_test DATABASE_URL_SYNC: postgresql://postgres:postgres@postgres:5432/resolutionflow_test # conftest.py reads DATABASE_TEST_URL only (DATABASE_URL is intentionally # not consulted after the dab740d test-isolation hardening). The CI test # DB is the same postgres service, so point DATABASE_TEST_URL at it # explicitly — without this, conftest falls back to localhost:5432 and # all tests fail at fixture setup with "connection refused". DATABASE_TEST_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/resolutionflow_test SECRET_KEY: ci-test-secret-key-not-for-production DEBUG: "true" APP_NAME: ResolutionFlow TEST_DB_NAME: resolutionflow_test DB_APP_ROLE_PASSWORD: app_secret_ci steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" - name: Cache pip uses: actions/cache@v3 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ hashFiles('backend/requirements.txt', 'backend/requirements-dev.txt') }} restore-keys: | pip-${{ runner.os }}- - name: Install system dependencies run: | apt-get update apt-get install -y libpango1.0-dev libcairo2-dev libgdk-pixbuf-2.0-dev libffi-dev libjpeg-dev zlib1g-dev - name: Install dependencies run: pip install --break-system-packages -r backend/requirements.txt -r backend/requirements-dev.txt - name: Run Alembic migrations run: cd backend && alembic upgrade head - name: Check tenant filter enforcement run: cd backend && python scripts/check_tenant_filters.py - name: Run tests with coverage # `-n auto` parallelizes across all runner cores via pytest-xdist. # conftest.py creates a per-worker DB (resolutionflow_test_gw0, # resolutionflow_test_gw1, …) so the per-test DROP SCHEMA doesn't # race across workers. Master/serial runs keep the base DB. # term-missing dropped — the custom "Display coverage summary" step # below parses coverage.json and prints the same info more concisely. # --maxfail=10 short-circuits on structural breakage so we don't burn # 25 minutes when a fixture explodes. run: cd backend && python -m pytest --override-ini="addopts=" -n auto --maxfail=10 --cov=app --cov-report=json:coverage.json --cov-fail-under=50 - name: Display coverage summary if: always() run: | cd backend python -c " import json with open('coverage.json') as f: data = json.load(f) total = data['totals']['percent_covered_display'] print(f'Total coverage: {total}%') print() print('Module coverage:') for fname, fdata in sorted(data['files'].items()): pct = fdata['summary']['percent_covered_display'] if float(pct) < 80: print(f' WARNING {fname}: {pct}%') else: print(f' OK {fname}: {pct}%') " frontend: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: "20" - name: Cache npm uses: actions/cache@v3 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - name: Install dependencies run: cd frontend && npm ci - name: Lint run: cd frontend && npm run lint - name: Test with coverage run: cd frontend && npm run test:coverage - name: Build run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" npm run build # Build artifact intentionally NOT uploaded. The e2e job below builds # its own frontend rather than downloading one from this job, so there # is no need for the cross-job artifact handoff (which previously broke # on actions/upload-artifact@v4 GHES support and forced a v3 pin). # Decoupling also lets e2e start immediately rather than waiting for # this job to finish — important on a multi-runner setup. e2e: runs-on: ubuntu-latest services: postgres: image: pgvector/pgvector:pg16 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: resolutionflow_test # No host port mapping. Tests connect to `postgres:5432` (the service # container's docker-network DNS name), not `localhost:5432`. With # multiple Gitea runners on the same homelab box, host-port mapping # would race — two backend/e2e jobs both binding 0.0.0.0:5432 → the # second fails with "port is already allocated". options: >- --health-cmd "pg_isready -U postgres" --health-interval 10s --health-timeout 5s --health-retries 5 env: PLAYWRIGHT_DATABASE_URL: postgresql+asyncpg://postgres:postgres@postgres:5432/resolutionflow_test PLAYWRIGHT_DATABASE_URL_SYNC: postgresql://postgres:postgres@postgres:5432/resolutionflow_test PLAYWRIGHT_API_ORIGIN: http://127.0.0.1:8000 PLAYWRIGHT_BASE_URL: http://127.0.0.1:4173 PLAYWRIGHT_SECRET_KEY: ci-playwright-secret-key PLAYWRIGHT_TEST_EMAIL: teamadmin@resolutionflow.example.com PLAYWRIGHT_TEST_PASSWORD: TestPass123! # AI-touching endpoints (POST /ai-sessions, /chat, /respond, etc.) are # gated by `_require_ai_enabled()`, which returns 503 when no provider # key is set. Tests that exercise those flows stub the AI calls in the # browser via `page.route`, so the backend never actually contacts # Anthropic — but the gate still has to pass. A stub value is enough. ANTHROPIC_API_KEY: ci-stub-key-not-used-by-tests steps: - uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v5 with: python-version: "3.12" - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: "20" - name: Cache pip uses: actions/cache@v3 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ hashFiles('backend/requirements.txt', 'backend/requirements-dev.txt') }} restore-keys: | pip-${{ runner.os }}- - name: Cache npm uses: actions/cache@v3 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | npm-${{ runner.os }}- - name: Install backend dependencies run: pip install --break-system-packages -r backend/requirements.txt -r backend/requirements-dev.txt - name: Install frontend dependencies run: cd frontend && npm ci - name: Build frontend # Building inline (instead of downloading an artifact from the # frontend job) drops the cross-job dependency, so e2e can start # immediately on a free runner. Adds ~1-2 min of build time, but # eliminates the artifact-upload mechanism entirely (no more # v3/v4 GHES headaches) and saves ~5 min of waiting. run: cd frontend && NODE_OPTIONS="--max-old-space-size=4096" VITE_API_URL="${PLAYWRIGHT_API_ORIGIN}" npm run build - name: Install Playwright browser run: cd frontend && npx playwright install --with-deps chromium - name: Run Playwright smoke tests run: cd frontend && npm run test:e2e - name: Upload Playwright report if: always() uses: actions/upload-artifact@v3 with: name: playwright-report path: | frontend/playwright-report frontend/test-results if-no-files-found: ignore