CI Pipeline #1582
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI Pipeline | |
| on: | |
| workflow_dispatch: {} | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| branches: | |
| - main | |
| env: | |
| NODE_VERSION: '20' | |
| jobs: | |
| # Quality checks - runs on all branches and PRs | |
| # Self-host deploy: .github/workflows/deploy-selfhost.yml on push to main | |
| quality: | |
| name: Code Quality Checks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| # SSOT: lint + umlauts + typecheck + build are bundled in the `verify` npm | |
| # script (package.json). CI calls it verbatim so the gating chain can't | |
| # drift from what runs locally. Do not re-inline these checks here — the | |
| # umlaut gate (Swiss ä/ö/ü convention) lives inside `verify` too. | |
| - name: Verify (lint + umlauts + typecheck + build) | |
| run: npm run verify | |
| env: | |
| # Build-time placeholders so strict env validation doesn't fail during CI compile. | |
| AUTH_SECRET: ci-build-placeholder-secret-32chars | |
| DB_HOST: localhost | |
| DB_NAME: revampit_ci | |
| DB_USER: ci | |
| DB_PASSWORD: ci | |
| # Auth smoke gate: verifies prod login when E2E secrets are present. | |
| # Prefers dual-persona admin credentials; falls back to legacy AUTH_TEST_*. | |
| auth-smoke: | |
| name: Auth Smoke Test | |
| runs-on: ubuntu-latest | |
| needs: quality | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright browser | |
| run: npx playwright install --with-deps chromium | |
| - name: Run auth smoke (if credentials configured) | |
| env: | |
| AUTH_TEST_EMAIL: ${{ secrets.AUTH_TEST_EMAIL }} | |
| AUTH_TEST_PASSWORD: ${{ secrets.AUTH_TEST_PASSWORD }} | |
| AUTH_TEST_ADMIN_EMAIL: ${{ secrets.AUTH_TEST_ADMIN_EMAIL }} | |
| AUTH_TEST_ADMIN_PASSWORD: ${{ secrets.AUTH_TEST_ADMIN_PASSWORD }} | |
| run: | | |
| EMAIL="${AUTH_TEST_EMAIL:-${AUTH_TEST_ADMIN_EMAIL:-}}" | |
| PASSWORD="${AUTH_TEST_PASSWORD:-${AUTH_TEST_ADMIN_PASSWORD:-}}" | |
| if [ -z "$EMAIL" ] || [ -z "$PASSWORD" ]; then | |
| echo "No AUTH_TEST_* or AUTH_TEST_ADMIN_* secrets — skipping auth smoke." | |
| exit 0 | |
| fi | |
| export AUTH_TEST_EMAIL="$EMAIL" | |
| export AUTH_TEST_PASSWORD="$PASSWORD" | |
| PLAYWRIGHT_BASE_URL="${PLAYWRIGHT_BASE_URL:-https://revampit.orangecat.ch}" \ | |
| npx playwright test tests/e2e/auth-smoke.spec.ts --project=chromium --reporter=line | |
| # Prod health gate: full dual-persona route inventory (186 routes) when passwords set. | |
| # Primary gate is post-deploy in deploy-selfhost.yml; this catches prod regressions on PRs too. | |
| inventory-smoke: | |
| name: Dual-Persona Inventory Smoke | |
| runs-on: ubuntu-latest | |
| needs: quality | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright browser | |
| run: npx playwright install --with-deps chromium | |
| - name: Run inventory smoke (if dual-persona secrets configured) | |
| env: | |
| AUTH_TEST_USER_PASSWORD: ${{ secrets.AUTH_TEST_USER_PASSWORD }} | |
| AUTH_TEST_ADMIN_PASSWORD: ${{ secrets.AUTH_TEST_ADMIN_PASSWORD }} | |
| AUTH_TEST_USER_EMAIL: ${{ secrets.AUTH_TEST_USER_EMAIL }} | |
| AUTH_TEST_ADMIN_EMAIL: ${{ secrets.AUTH_TEST_ADMIN_EMAIL }} | |
| run: bash scripts/e2e-inventory-prod.sh | |
| # Migration drift check — applies every SQL migration in scripts/db/migrations/ | |
| # to a throwaway Postgres container. Uses pgvector image so 005-hirn-ai-rag.sql | |
| # can CREATE EXTENSION vector. Runs on PRs and main pushes. | |
| migrations: | |
| name: Migration Drift Check | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg17 | |
| env: | |
| POSTGRES_PASSWORD: ci | |
| POSTGRES_DB: revampit_drift | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Apply migrations in order | |
| env: | |
| PGHOST: localhost | |
| PGPORT: 5432 | |
| PGUSER: postgres | |
| PGPASSWORD: ci | |
| PGDATABASE: revampit_drift | |
| run: bash scripts/db/apply-migrations-ci.sh | |
| e2e-local: | |
| name: Local E2E Journeys | |
| runs-on: ubuntu-latest | |
| needs: [quality, migrations] | |
| services: | |
| postgres: | |
| image: pgvector/pgvector:pg17 | |
| env: | |
| POSTGRES_PASSWORD: ci | |
| POSTGRES_DB: revampit_e2e | |
| options: >- | |
| --health-cmd "pg_isready -U postgres" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| env: | |
| AUTH_SECRET: ci-e2e-placeholder-secret-32chars | |
| NEXTAUTH_URL: http://localhost:3001 | |
| AUTH_URL: http://localhost:3001 | |
| PLAYWRIGHT_BASE_URL: http://localhost:3001 | |
| DB_HOST: localhost | |
| DB_PORT: 5432 | |
| DB_NAME: revampit_e2e | |
| DB_USER: postgres | |
| DB_PASSWORD: ci | |
| DB_SSL: false | |
| PGHOST: localhost | |
| PGPORT: 5432 | |
| PGUSER: postgres | |
| PGPASSWORD: ci | |
| PGDATABASE: revampit_e2e | |
| AUTH_TEST_EMAIL: e2e-admin@revampit.test | |
| AUTH_TEST_PASSWORD: E2EAdmin123! | |
| AUTH_TEST_ADMIN_EMAIL: e2e-admin@revampit.test | |
| AUTH_TEST_ADMIN_PASSWORD: E2EAdmin123! | |
| AUTH_TEST_SECOND_ADMIN_EMAIL: e2e-admin2@revampit.test | |
| AUTH_TEST_SECOND_ADMIN_PASSWORD: E2EAdmin123! | |
| AUTH_TEST_USER_EMAIL: e2e-user@revampit.test | |
| AUTH_TEST_USER_PASSWORD: E2EUser123! | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Install Playwright browser | |
| run: npx playwright install --with-deps chromium | |
| - name: Apply migrations | |
| run: bash scripts/db/apply-migrations-ci.sh | |
| - name: Seed E2E data | |
| run: npm run e2e:seed | |
| - name: Build app | |
| run: npm run build | |
| - name: Start app | |
| run: | | |
| npm run start -- -p 3001 > revampit-e2e.log 2>&1 & | |
| for i in {1..90}; do | |
| if curl -fsS http://localhost:3001/api/health >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| cat revampit-e2e.log | |
| exit 1 | |
| - name: Run Playwright E2E journeys | |
| run: npm run test:e2e:journeys -- --project=chromium --reporter=line | |
| - name: Upload E2E artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: e2e-local-artifacts | |
| path: | | |
| playwright-report/ | |
| test-results/ | |
| revampit-e2e.log | |
| if-no-files-found: ignore | |
| # Unit tests (still non-blocking while suite matures) | |
| test: | |
| name: Run Tests | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run tests | |
| run: npm run test | |
| continue-on-error: true | |
| # Dependency advisories. Blocks a PR that INTRODUCES a critical; high and | |
| # below are reported but do not fail, because a third party publishing an | |
| # advisory must not be able to jam the merge train (auto-merge refuses a red | |
| # base). Advisories that appear with no code change are caught by the | |
| # scheduled security-audit.yml instead — this job only sees what a run touches. | |
| security: | |
| name: Dependency Security Audit | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Audit runtime dependencies | |
| env: | |
| FAIL_ON: critical | |
| run: bash scripts/ci/dependency-audit.sh | |
| # Red-main alarm for the DISPATCHED path. | |
| # | |
| # Every merge here is an auto-merge made with GITHUB_TOKEN, which fires no | |
| # push event — so the sweep re-arms this workflow by dispatch, and a | |
| # dispatched run emits no `workflow_run` event for main-red-alert.yml to | |
| # catch. That workflow is therefore silent on almost every main CI run this | |
| # repo produces (observed 2026-08-06: main went red on a66baa55 and no issue | |
| # was filed). A dispatched run does the handoff itself; the push path keeps | |
| # using main-red-alert.yml. One trigger per path, one shared policy script. | |
| post-main: | |
| name: Main Red Alert (dispatched) | |
| # MUST list every other job in this file. The alarm's verdict has to mean | |
| # the same thing as the merge gate's, and the gate reads the RUN's | |
| # conclusion — which every job contributes to. Enforced by | |
| # src/__tests__/ci/main-red-verdict.test.ts, because this list has already | |
| # drifted once (see the commit that added this comment). | |
| needs: [quality, auth-smoke, inventory-smoke, migrations, e2e-local, test, security] | |
| if: always() && github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| permissions: | |
| issues: write | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPO: ${{ github.repository }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Resolve this run's verdict | |
| id: verdict | |
| env: | |
| R_QUALITY: ${{ needs.quality.result }} | |
| R_AUTH: ${{ needs.auth-smoke.result }} | |
| R_INVENTORY: ${{ needs.inventory-smoke.result }} | |
| R_MIGRATIONS: ${{ needs.migrations.result }} | |
| R_E2E: ${{ needs.e2e-local.result }} | |
| R_TEST: ${{ needs.test.result }} | |
| R_SECURITY: ${{ needs.security.result }} | |
| run: | | |
| set -euo pipefail | |
| # The question this answers is NOT "did the code break?" but "is main | |
| # blocking the merge queue?" — because that is what the auto-merge | |
| # green-base guard asks, and the two must agree. | |
| # | |
| # So anything that is not success-or-skipped counts as red, including | |
| # `cancelled`. A cancelled job on main blocks every open PR exactly as | |
| # hard as a failing one; treating it as a non-event is what let main | |
| # sit red and SILENT for ~14h on 2026-08-07 while 11 PRs waited. | |
| # (`skipped` stays green: jobs here are conditional on event type.) | |
| results="$R_QUALITY $R_AUTH $R_INVENTORY $R_MIGRATIONS $R_E2E $R_TEST $R_SECURITY" | |
| conclusion=success | |
| for result in $results; do | |
| case "$result" in | |
| success|skipped) ;; | |
| *) conclusion=failure ;; | |
| esac | |
| done | |
| echo "conclusion=$conclusion" >> "$GITHUB_OUTPUT" | |
| echo "verdict: $conclusion (from: $results)" | |
| - name: File or resolve the main-red issue | |
| env: | |
| CONCLUSION: ${{ steps.verdict.outputs.conclusion }} | |
| RUN_SHA: ${{ github.sha }} | |
| RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} | |
| run: bash scripts/ci/main-red-alert.sh |