name: Backend CI on: push: branches: [main] pull_request: branches: [main] jobs: lint: name: Lint & Format Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" - name: Install ruff run: pip install ruff - name: Ruff lint run: ruff check . - name: Ruff format check run: ruff format --check . type-check: name: Import & Syntax Validation runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip - name: Install dependencies run: pip install -r requirements.txt - name: Validate Python syntax (compile all) run: python -m compileall app/ main.py -q - name: Verify imports resolve run: python -c "from app.core.config import get_settings; from app.models.db import Base; print('All imports OK')" tests: name: Pytest runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip - name: Install dependencies run: | pip install -r requirements.txt pip install -r requirements-dev.txt - name: Run pytest with coverage env: DATABASE_URL: "sqlite:///:memory:" JWT_SECRET: "${{ github.run_id }}-${{ github.run_attempt }}-ci-ephemeral" ENVIRONMENT: test LLM_API_KEY: test-key CORS_ORIGINS: '["http://testserver"]' run: pytest tests/ --cov=app --cov-report=xml --cov-report=term - name: Upload coverage artifact uses: actions/upload-artifact@v4 if: always() with: name: backend-coverage path: coverage.xml retention-days: 14 migration-check: name: Database Migration Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip - name: Install dependencies run: pip install -r requirements.txt - name: Verify Alembic config run: python -c "from alembic.config import Config; c = Config('alembic.ini'); print('Alembic config OK')" dependency-audit: name: Dependency Vulnerability Scan runs-on: ubuntu-latest # Don't block PR merging on upstream CVE noise; surface findings # without failing. `fail_on_vuln` can be flipped to true once we've # triaged the existing vulnerability backlog. continue-on-error: true steps: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: python-version: "3.12" cache: pip - name: Install pip-audit run: pip install pip-audit - name: Audit backend requirements # --desc: show a one-line description of each CVE # --no-strict: twikit is installed from a Codeberg VCS URL (phin fork # that fixes the KEY_BYTE indices bug in upstream 2.3.3). VCS # dependencies are not registered on PyPI and cannot be audited by # pip-audit. --no-strict (the default without --strict) treats # unauditable packages as warnings rather than hard failures, so # the scan still covers all 40+ PyPI-sourced dependencies. # --ignore-vuln: deliberately ignored CVEs with justification: # CVE-2026-1839 (transformers) — affects Trainer._load_rng_state, # only reachable when loading malicious rng_state.pth checkpoints # during training. We do inference-only, never Trainer, never # deserialize external .pth files. Fix is in 5.0.0rc3 (pre-release); # re-evaluate when 5.0.0 ships stable. # CVE-2025-2953 (torch) — local DoS in torch.mkldnn_max_pool2d. # Attack requires local access; no remote exploit path. Fix is in # 2.7.1rc1 (pre-release); re-evaluate when stable 2.7.x ships. # CVE-2025-3730 (torch) — local DoS in torch.nn.functional.ctc_loss. # Attack requires local access; no remote exploit path. Fix is in # 2.8.0; re-evaluate when 2.8.0 ships stable. run: | pip-audit --requirement requirements.txt --desc \ --ignore-vuln CVE-2026-1839 \ --ignore-vuln CVE-2025-2953 \ --ignore-vuln CVE-2025-3730 docker-build: name: Docker Build runs-on: ubuntu-latest needs: [lint, type-check, tests] services: postgres: image: pgvector/pgvector:pg16 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: depscreen ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v5 - name: Build Docker image run: docker build -t depscreen-backend . - name: Verify container starts run: | docker run -d --name test-container \ --network host \ -e DATABASE_URL=postgresql://postgres:postgres@localhost:5432/depscreen \ -e LLM_API_KEY=test \ -e LLM_BASE_URL=https://test.example.com \ -e LLM_MODEL=test \ -e JWT_SECRET=test-secret \ -e ENVIRONMENT=testing \ -p 8000:8000 \ depscreen-backend echo "Waiting for container to start..." for i in $(seq 1 60); do if curl -sf http://localhost:8000/health/live > /dev/null 2>&1; then echo "Health check passed after ${i}s" break fi sleep 2 done curl -f http://localhost:8000/health/live || (docker logs test-container && exit 1) docker stop test-container