267 lines
7.8 KiB
YAML
267 lines
7.8 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
pull_request:
|
|
branches: [main, master]
|
|
|
|
# Cancel in-progress runs for the same branch/PR to save runner minutes.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
# ---------------------------------------------------------------------------
|
|
# LINT — parallel linting for all three services
|
|
# ---------------------------------------------------------------------------
|
|
python-lint:
|
|
name: Lint Python (Ruff)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- name: Install Ruff
|
|
run: pip install ruff
|
|
|
|
- name: Ruff check
|
|
run: ruff check backend/
|
|
|
|
- name: Ruff format check
|
|
run: ruff format --check backend/
|
|
|
|
go-lint:
|
|
name: Lint Go (golangci-lint)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-go@v5
|
|
with:
|
|
go-version: "1.25"
|
|
|
|
- name: golangci-lint
|
|
# golangci-lint doesn't support Go 1.25 yet — run vet as a stand-in
|
|
run: cd poller && go vet ./...
|
|
|
|
frontend-lint:
|
|
name: Lint Frontend (ESLint + tsc)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
working-directory: frontend
|
|
run: npm ci
|
|
|
|
- name: ESLint
|
|
working-directory: frontend
|
|
run: npx eslint .
|
|
|
|
- name: TypeScript type check
|
|
working-directory: frontend
|
|
run: npx tsc --noEmit
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TEST — parallel test suites for all three services
|
|
# ---------------------------------------------------------------------------
|
|
backend-test:
|
|
name: Test Backend (pytest)
|
|
runs-on: ubuntu-latest
|
|
|
|
services:
|
|
postgres:
|
|
image: timescale/timescaledb:latest-pg17
|
|
env:
|
|
POSTGRES_DB: tod_test
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: postgres
|
|
ports:
|
|
- 5432:5432
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- 6379:6379
|
|
options: >-
|
|
--health-cmd "redis-cli ping"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
nats:
|
|
image: nats:2-alpine
|
|
ports:
|
|
- 4222:4222
|
|
options: >-
|
|
--health-cmd "true"
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
env:
|
|
ENVIRONMENT: dev
|
|
DATABASE_URL: "postgresql+asyncpg://postgres:postgres@localhost:5432/tod_test"
|
|
SYNC_DATABASE_URL: "postgresql+psycopg2://postgres:postgres@localhost:5432/tod_test"
|
|
APP_USER_DATABASE_URL: "postgresql+asyncpg://app_user:app_password@localhost:5432/tod_test"
|
|
TEST_DATABASE_URL: "postgresql+asyncpg://postgres:postgres@localhost:5432/tod_test"
|
|
TEST_APP_USER_DATABASE_URL: "postgresql+asyncpg://app_user:app_password@localhost:5432/tod_test"
|
|
CREDENTIAL_ENCRYPTION_KEY: "LLLjnfBZTSycvL2U07HDSxUeTtLxb9cZzryQl0R9E4w="
|
|
JWT_SECRET_KEY: "change-this-in-production-use-a-long-random-string"
|
|
REDIS_URL: "redis://localhost:6379/0"
|
|
NATS_URL: "nats://localhost:4222"
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-python@v5
|
|
with:
|
|
python-version: "3.12"
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ~/.cache/pip
|
|
key: pip-${{ hashFiles('backend/pyproject.toml') }}
|
|
restore-keys: pip-
|
|
|
|
- name: Install backend dependencies
|
|
working-directory: backend
|
|
run: pip install -e ".[dev]"
|
|
|
|
- name: Set up test database roles
|
|
env:
|
|
PGPASSWORD: postgres
|
|
run: |
|
|
# Create app_user role for RLS-enforced connections
|
|
psql -h localhost -U postgres -d tod_test -c "
|
|
CREATE ROLE app_user WITH LOGIN PASSWORD 'app_password' NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
GRANT CONNECT ON DATABASE tod_test TO app_user;
|
|
GRANT USAGE ON SCHEMA public TO app_user;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO app_user;
|
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO app_user;
|
|
" || true
|
|
|
|
# Create poller_user role
|
|
psql -h localhost -U postgres -d tod_test -c "
|
|
DO \$\$
|
|
BEGIN
|
|
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'poller_user') THEN
|
|
CREATE ROLE poller_user WITH LOGIN PASSWORD 'poller_password' NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
END IF;
|
|
END
|
|
\$\$;
|
|
GRANT CONNECT ON DATABASE tod_test TO poller_user;
|
|
GRANT USAGE ON SCHEMA public TO poller_user;
|
|
" || true
|
|
|
|
- name: Run backend tests
|
|
working-directory: backend
|
|
run: python -m pytest tests/ -x -v --tb=short
|
|
|
|
poller-test:
|
|
name: Test Go Poller
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-go@v5
|
|
with:
|
|
go-version: "1.25"
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: ~/go/pkg/mod
|
|
key: go-${{ hashFiles('poller/go.sum') }}
|
|
restore-keys: go-
|
|
|
|
- name: Run poller tests
|
|
working-directory: poller
|
|
run: go test ./... -v -count=1
|
|
|
|
frontend-test:
|
|
name: Test Frontend (Vitest)
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
working-directory: frontend
|
|
run: npm ci
|
|
|
|
- name: Run frontend tests
|
|
working-directory: frontend
|
|
run: npx vitest run
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# BUILD — sequential Docker builds + Trivy scans (depends on lint + test)
|
|
# ---------------------------------------------------------------------------
|
|
build:
|
|
name: Build & Scan Docker Images
|
|
runs-on: ubuntu-latest
|
|
needs: [python-lint, go-lint, frontend-lint, backend-test, poller-test, frontend-test]
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# Build and scan each image SEQUENTIALLY to avoid OOM.
|
|
# Each multi-stage build (Go, Python/pip, Node/tsc) can peak at 1-2 GB.
|
|
# Running them in parallel would exceed typical runner memory.
|
|
|
|
- name: Build API image
|
|
run: docker build -f infrastructure/docker/Dockerfile.api -t tod-api:ci .
|
|
|
|
- name: Scan API image
|
|
uses: aquasecurity/trivy-action@v0.33.1
|
|
continue-on-error: true
|
|
with:
|
|
image-ref: "tod-api:ci"
|
|
format: "table"
|
|
exit-code: "1"
|
|
severity: "HIGH,CRITICAL"
|
|
|
|
- name: Build Poller image
|
|
run: docker build -f poller/Dockerfile -t tod-poller:ci ./poller
|
|
|
|
- name: Scan Poller image
|
|
uses: aquasecurity/trivy-action@v0.33.1
|
|
continue-on-error: true
|
|
with:
|
|
image-ref: "tod-poller:ci"
|
|
format: "table"
|
|
exit-code: "1"
|
|
severity: "HIGH,CRITICAL"
|
|
|
|
- name: Build Frontend image
|
|
run: docker build -f infrastructure/docker/Dockerfile.frontend -t tod-frontend:ci .
|
|
|
|
- name: Scan Frontend image
|
|
uses: aquasecurity/trivy-action@v0.33.1
|
|
continue-on-error: true
|
|
with:
|
|
image-ref: "tod-frontend:ci"
|
|
format: "table"
|
|
exit-code: "1"
|
|
severity: "HIGH,CRITICAL"
|