name: CI on: pull_request: push: branches: [master] workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: check: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install uv run: python -m pip install --upgrade pip uv - name: Cache uv uses: actions/cache@v4 with: path: | ~/.cache/uv backend/.venv key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} - name: Set up Node id: setup-node uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: frontend/package-lock.json - name: Install backend dependencies run: make backend-sync - name: Install frontend dependencies run: make frontend-sync - name: Cache Next.js build cache uses: actions/cache@v4 with: path: | frontend/.next/cache key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}- - name: Run backend checks env: # Keep CI builds deterministic. NEXT_TELEMETRY_DISABLED: "1" AUTH_MODE: "clerk" CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} run: | make backend-lint make backend-typecheck make backend-coverage - name: Run frontend checks env: # Keep CI builds deterministic. NEXT_TELEMETRY_DISABLED: "1" NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_AUTH_MODE: "clerk" CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | make frontend-lint make frontend-typecheck make frontend-test make frontend-build - name: Docs quality gates (lint + relative link check) run: | make docs-check - name: Upload coverage artifacts if: always() uses: actions/upload-artifact@v4 with: name: coverage if-no-files-found: ignore path: | backend/coverage.xml frontend/coverage/** installer: runs-on: ubuntu-latest needs: [check] steps: - name: Checkout uses: actions/checkout@v4 - name: Validate installer shell syntax run: bash -n install.sh - name: Installer smoke test (docker mode) run: | ./install.sh \ --mode docker \ --backend-port 18000 \ --frontend-port 13000 \ --public-host localhost \ --api-url http://localhost:18000 \ --token-mode generate curl -fsS http://127.0.0.1:18000/healthz >/dev/null curl -fsS http://127.0.0.1:13000 >/dev/null - name: Cleanup docker stack after docker mode if: always() run: | docker compose -f compose.yml --env-file .env down -v --remove-orphans || true - name: Installer smoke test (local mode) run: | ./install.sh \ --mode local \ --backend-port 18001 \ --frontend-port 13001 \ --public-host localhost \ --api-url http://localhost:18001 \ --token-mode generate \ --db-mode docker \ --start-services yes curl -fsS http://127.0.0.1:18001/healthz >/dev/null curl -fsS http://127.0.0.1:13001 >/dev/null - name: Cleanup local processes and docker resources if: always() run: | if [ -f .install-logs/backend.pid ]; then kill "$(cat .install-logs/backend.pid)" || true; fi if [ -f .install-logs/frontend.pid ]; then kill "$(cat .install-logs/frontend.pid)" || true; fi docker compose -f compose.yml --env-file .env down -v --remove-orphans || true e2e: runs-on: ubuntu-latest needs: [check] steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Node id: setup-node uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: frontend/package-lock.json - name: Install frontend dependencies run: make frontend-sync - name: Cache Next.js build cache uses: actions/cache@v4 with: path: | frontend/.next/cache key: nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('frontend/package-lock.json') }} restore-keys: | nextjs-${{ runner.os }}-node-${{ steps.setup-node.outputs.node-version }}- - name: Start frontend (dev server) env: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_AUTH_MODE: "clerk" NEXT_TELEMETRY_DISABLED: "1" CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | cd frontend npm run dev -- --hostname 0.0.0.0 --port 3000 & for i in {1..60}; do if curl -sf http://localhost:3000/ > /dev/null; then exit 0; fi sleep 2 done echo "Frontend did not start" exit 1 - name: Run Cypress E2E env: NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} NEXT_PUBLIC_AUTH_MODE: "clerk" NEXT_TELEMETRY_DISABLED: "1" # Clerk testing tokens (official @clerk/testing Cypress integration) CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }} # Also set for the app itself. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }} run: | cd frontend npm run e2e -- --browser chrome - name: Upload Cypress artifacts if: failure() uses: actions/upload-artifact@v4 with: name: cypress-artifacts if-no-files-found: ignore path: | frontend/cypress/screenshots/** frontend/cypress/videos/**