Skip to content

fix(resilience): regenerate wg0.conf on startup, plus three stranded fixes #211

fix(resilience): regenerate wg0.conf on startup, plus three stranded fixes

fix(resilience): regenerate wg0.conf on startup, plus three stranded fixes #211

Workflow file for this run

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