Skip to content

Merge pull request #55 from AttiR/feature/52-sql-agent-production-layer #64

Merge pull request #55 from AttiR/feature/52-sql-agent-production-layer

Merge pull request #55 from AttiR/feature/52-sql-agent-production-layer #64

Workflow file for this run

# =================================================================
# 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"