fix(resilience): regenerate wg0.conf on startup, plus three stranded fixes #211
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 | |
| 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==0.16.5 | |
| - name: Ruff check | |
| run: ruff check backend/ setup.py | |
| - name: Ruff format check | |
| run: ruff format --check backend/ setup.py | |
| go-lint: | |
| name: Lint Go (golangci-lint) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| module: [poller, winbox-worker] | |
| 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 ${{ matrix.module }} && go vet ./... | |
| - name: gofmt check | |
| run: | | |
| cd ${{ matrix.module }} | |
| unformatted=$(gofmt -l .) | |
| if [ -n "$unformatted" ]; then | |
| echo "These files are not gofmt-clean:" | |
| echo "$unformatted" | |
| exit 1 | |
| fi | |
| 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 -b --pretty false | |
| # --------------------------------------------------------------------------- | |
| # 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 | |
| winbox-worker-test: | |
| name: Test WinBox Worker (Go) | |
| 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('winbox-worker/go.sum') }} | |
| restore-keys: go- | |
| - name: Run winbox-worker tests | |
| working-directory: winbox-worker | |
| 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, | |
| winbox-worker-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 . | |
| # Two scans per image: CRITICAL blocks the build, HIGH is reported but | |
| # never blocks. ignore-unfixed is load-bearing on both — without it, a | |
| # CVE in a base image with no available patch would block every build | |
| # forever, which is exactly how these scans ended up permanently | |
| # continue-on-error in the first place. | |
| # | |
| # These eight blocks are deliberately spelled out rather than shared via | |
| # a YAML anchor: GitHub Actions' workflow parser rejects anchors, aliases | |
| # and merge keys outright, and the failure mode is the ENTIRE file | |
| # failing to load — no jobs, no annotation, just "this run likely failed | |
| # because of a workflow file issue". PyYAML accepts them happily, so | |
| # linting the file locally with a general-purpose YAML parser will not | |
| # catch it. If you change the scan policy, change it in all eight. | |
| # | |
| # `version:` is pinned deliberately. trivy-action@v0.33.1 defaults to | |
| # trivy v0.65.0, and upstream removed that release's assets on | |
| # 2026-08-31 — the git tag still resolves, so the installer reports | |
| # "found version: 0.65.0" and then exits 1 with no message when the | |
| # tarball 404s. Inheriting the action's default means an upstream yank | |
| # breaks this build with no change on our side. Bump this pin | |
| # deliberately; do not remove it. | |
| - name: Scan API image (CRITICAL) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-api:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "CRITICAL" | |
| - name: Scan API image (HIGH) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| continue-on-error: true | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-api:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "HIGH" | |
| - name: Build Poller image | |
| run: docker build -f poller/Dockerfile -t tod-poller:ci ./poller | |
| - name: Scan Poller image (CRITICAL) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-poller:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "CRITICAL" | |
| - name: Scan Poller image (HIGH) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| continue-on-error: true | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-poller:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "HIGH" | |
| - name: Build Frontend image | |
| run: docker build -f infrastructure/docker/Dockerfile.frontend -t tod-frontend:ci . | |
| - name: Scan Frontend image (CRITICAL) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-frontend:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "CRITICAL" | |
| - name: Scan Frontend image (HIGH) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| continue-on-error: true | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-frontend:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "HIGH" | |
| - name: Build WinBox Worker image | |
| run: docker build -f winbox-worker/Dockerfile -t tod-winbox-worker:ci ./winbox-worker | |
| - name: Scan WinBox Worker image (CRITICAL) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-winbox-worker:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "CRITICAL" | |
| - name: Scan WinBox Worker image (HIGH) | |
| uses: aquasecurity/trivy-action@v0.33.1 | |
| continue-on-error: true | |
| with: | |
| version: "v0.74.0" | |
| image-ref: "tod-winbox-worker:ci" | |
| format: "table" | |
| exit-code: "1" | |
| ignore-unfixed: true | |
| severity: "HIGH" | |
| # Not continue-on-error: a WinBox version that builds but can't | |
| # actually start a session must fail this job, not just get logged. | |
| - name: Smoke test WinBox Worker session startup | |
| run: winbox-worker/scripts/smoke_test.sh tod-winbox-worker:ci |