Merge pull request #55 from AttiR/feature/52-sql-agent-production-layer #64
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
| # ================================================================= | |
| # InsightIQ — CI Pipeline | |
| # Triggers: every push + pull request to main/develop | |
| # | |
| # Jobs: | |
| # backend-ci — ruff lint, pytest unit tests (no DB, no API) | |
| # sql-eval — only if backend/eval paths changed; smoke 5-call eval + red team | |
| # frontend-ci — TypeScript typecheck, production build | |
| # security — Trivy dependency scan (advisory, never blocks) | |
| # ci-success — gate job that CD depends on | |
| # | |
| # Cost: ~2 min/run. GitHub free tier = 2000 min/month = ~1000 runs free. | |
| # ================================================================= | |
| name: CI | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| types: [opened, synchronize, reopened] | |
| # Cancel stale in-progress runs when new commit arrives | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # ── Detect path changes (skip paid sql-eval on README / frontend-only PRs) ─ | |
| changes: | |
| name: "Detect changed paths" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| outputs: | |
| sql_eval: ${{ steps.paths.outputs.sql_eval }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect sql-eval paths (git diff — no GitHub API) | |
| id: paths | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE="${{ github.event.pull_request.base.sha }}" | |
| elif [ -n "${{ github.event.before }}" ] && [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then | |
| BASE="${{ github.event.before }}" | |
| else | |
| BASE="$(git rev-parse HEAD~1 2>/dev/null || echo "")" | |
| fi | |
| if [ -z "$BASE" ]; then | |
| echo "sql_eval=true" >> "$GITHUB_OUTPUT" | |
| echo "No base ref — running sql-eval" | |
| exit 0 | |
| fi | |
| CHANGED="$(git diff --name-only "$BASE" HEAD)" | |
| echo "Changed files:" | |
| echo "$CHANGED" | |
| if echo "$CHANGED" | grep -qE '^(backend/|tests/|database/|pytest\.ini)'; then | |
| echo "sql_eval=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "sql_eval=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── Backend: lint + unit tests ─────────────────────────────── | |
| backend-ci: | |
| name: "Backend — lint & test" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 8 | |
| env: | |
| # Unit tests don't need a real DB or API key | |
| ANTHROPIC_API_KEY: sk-ant-ci-placeholder-not-real | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_URL_RO: postgresql://test:test@localhost:5432/test | |
| JWT_SECRET_KEY: ci-test-secret-exactly-32-chars-ok | |
| GOOGLE_CLIENT_ID: ci-test.apps.googleusercontent.com | |
| LOG_LEVEL: WARNING | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/requirements.txt | |
| - name: Install dependencies | |
| run: pip install -r backend/requirements.txt | |
| - name: Lint — ruff check | |
| run: cd backend && python -m ruff check . --output-format=github | |
| - name: Format check — ruff format | |
| run: cd backend && python -m ruff format . --check | |
| - name: Unit tests (no DB, no Anthropic API) | |
| run: | | |
| mkdir -p test-results | |
| cd backend | |
| python -m pytest \ | |
| ../tests/test_sql_agent.py \ | |
| ../tests/test_auth.py::TestJWT \ | |
| ../tests/test_auth.py::TestRateLimit \ | |
| ../tests/test_eval.py \ | |
| -v --tb=short -m "not integration and not eval_live" \ | |
| --junitxml=../test-results/unit.xml | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: backend-test-results | |
| path: test-results/ | |
| retention-days: 7 | |
| # ── SQL eval + red team (merge gate, path-filtered) ──────────── | |
| sql-eval: | |
| name: "SQL evals + guardrail red-team" | |
| needs: changes | |
| if: needs.changes.outputs.sql_eval == 'true' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| services: | |
| postgres: | |
| image: postgres:16 | |
| env: | |
| POSTGRES_USER: test | |
| POSTGRES_PASSWORD: test | |
| POSTGRES_DB: test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} | |
| DATABASE_URL: postgresql://test:test@localhost:5432/test | |
| DATABASE_URL_RO: postgresql://test:test@localhost:5432/test | |
| JWT_SECRET_KEY: ci-test-secret-exactly-32-chars-ok | |
| GOOGLE_CLIENT_ID: ci-test.apps.googleusercontent.com | |
| LOG_LEVEL: WARNING | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| cache: pip | |
| cache-dependency-path: backend/requirements.txt | |
| - name: Install dependencies | |
| run: pip install -r backend/requirements.txt | |
| - name: Install PostgreSQL client | |
| run: sudo apt-get update && sudo apt-get install -y postgresql-client | |
| - name: Load eval fixture database | |
| env: | |
| PGPASSWORD: test | |
| run: psql -h localhost -U test -d test -v ON_ERROR_STOP=1 -f tests/eval/seed.sql | |
| - name: SQL evals + guardrail red-team (5 Claude calls) | |
| run: | | |
| mkdir -p test-results | |
| cd backend | |
| python -m pytest ../tests/eval/ -v -m ci_gate --tb=short \ | |
| --junitxml=../test-results/sql-eval.xml | |
| - name: Upload sql-eval results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: sql-eval-results | |
| path: test-results/sql-eval.xml | |
| retention-days: 7 | |
| # ── Frontend: typecheck + build ────────────────────────────── | |
| frontend-ci: | |
| name: "Frontend — typecheck & build" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 8 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Node 20 | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install | |
| run: cd frontend && npm ci --frozen-lockfile | |
| - name: TypeScript typecheck | |
| run: cd frontend && npm run typecheck | |
| - name: Production build (verify bundle compiles cleanly) | |
| run: cd frontend && npm run build | |
| env: | |
| # Quoted — bare https://... breaks YAML parsing of the value | |
| VITE_PUBLIC_SITE_URL: "https://example.com" | |
| VITE_GOOGLE_CLIENT_ID: ${{ secrets.VITE_GOOGLE_CLIENT_ID }} | |
| VITE_API_BASE_URL: "" | |
| # ── Security: Trivy scan (advisory — does NOT block ci-success / CD) ─ | |
| # Docker-based scan — avoids setup-trivy download failures on GitHub-hosted runners. | |
| security: | |
| name: "Security — Trivy" | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Scan for CRITICAL/HIGH vulnerabilities | |
| run: | | |
| docker run --rm \ | |
| -v "${GITHUB_WORKSPACE}:/workspace" \ | |
| aquasec/trivy:0.63.0 fs \ | |
| --severity CRITICAL,HIGH \ | |
| --exit-code 0 \ | |
| --format table \ | |
| /workspace | |
| # ── Gate: CD depends on this ───────────────────────────────── | |
| ci-success: | |
| name: "CI passed ✓" | |
| runs-on: ubuntu-latest | |
| needs: [backend-ci, changes, sql-eval, frontend-ci] | |
| if: always() | |
| steps: | |
| - name: Verify required jobs passed | |
| run: | | |
| backend="${{ needs.backend-ci.result }}" | |
| sql_eval="${{ needs.sql-eval.result }}" | |
| frontend="${{ needs.frontend-ci.result }}" | |
| echo "backend-ci: $backend" | |
| echo "sql-eval: $sql_eval (skipped when only README/frontend/etc. changed)" | |
| echo "frontend-ci: $frontend" | |
| # skipped sql-eval is OK — path filter excluded this push | |
| if [[ "$sql_eval" != "success" && "$sql_eval" != "skipped" ]]; then | |
| echo "sql-eval failed" | |
| exit 1 | |
| fi | |
| if [[ "$backend" != "success" || "$frontend" != "success" ]]; then | |
| echo "CI failed — CD will not run" | |
| exit 1 | |
| fi | |
| echo "All CI checks passed" |