239 lines
8.8 KiB
YAML
239 lines
8.8 KiB
YAML
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
|