Files
2026-07-23 11:58:07 +02:00

158 lines
5.1 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR).
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
# The Node major selects the bundled npm (22 -> npm 10, 24 -> npm 11), and those npm
# majors disagree on whether a violated *optional* peer dependency is fatal. If the
# image builds the SPA on a different major than CI validates it on, `npm ci` can pass
# every job here and still fail `docker build` on the same lockfile. Fail on drift
# rather than trusting the "keep in sync" comment in the Dockerfile.
toolchain:
name: Toolchain (node parity)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Dockerfile node major matches ci.yml node-version
run: |
set -euo pipefail
# `|| true`: a no-match grep exits 1, which under `set -e` would abort the
# step before the explicit error below could explain what went wrong.
docker_major="$(grep -oP '^FROM node:\K[0-9]+' Dockerfile | sort -u || true)"
ci_major="$(grep -oP 'node-version:\s*"\K[0-9]+' .github/workflows/ci.yml | sort -u || true)"
echo "Dockerfile: ${docker_major:-<none found>}"
echo "ci.yml: ${ci_major:-<none found>}"
if [ -z "$docker_major" ] || [ -z "$ci_major" ]; then
echo "::error::could not parse a node major — did the FROM or node-version syntax change?"
exit 1
fi
if [ "$(printf '%s\n' "$docker_major" | wc -l)" -ne 1 ] \
|| [ "$(printf '%s\n' "$ci_major" | wc -l)" -ne 1 ]; then
echo "::error::more than one distinct node major declared; unify them first"
exit 1
fi
if [ "$docker_major" != "$ci_major" ]; then
echo "::error::node major drift — Dockerfile is on $docker_major, ci.yml on $ci_major"
exit 1
fi
echo "OK — both build the SPA on node $docker_major"
backend:
name: Backend (ruff + pytest)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.12", "3.13"]
defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
cache-dependency-path: backend/pyproject.toml
- name: Install dependencies
run: pip install -e ".[dev]"
- name: Lint (ruff)
run: ruff check .
- name: Test (pytest)
run: pytest
frontend:
name: Frontend (typecheck + build)
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build (tsc --noEmit && vite build)
run: npm run build
- name: Test (node --test)
run: npm test
# Dependency CVE scanning of what actually ships (Python + frontend packages). The OS
# layer of the image is covered separately by the image-scan job below.
security:
name: Security (dependency audit)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
cache: pip
cache-dependency-path: backend/pyproject.toml
# Audit runtime deps only (install without the dev extras) so build/test tooling
# doesn't muddy the shipped-surface signal.
- name: Audit Python dependencies (pip-audit)
working-directory: backend
run: |
pip install -e .
pip install pip-audit
pip-audit
- uses: actions/setup-node@v7
with:
node-version: "24"
cache: npm
cache-dependency-path: frontend/package-lock.json
# --omit=dev: the shipped bundle contains only prod deps; esbuild/vite (build-time,
# dev-server advisories) aren't served. Gate on high+ severity in that tree.
- name: Audit frontend production dependencies (npm audit)
working-directory: frontend
run: npm audit --omit=dev --audit-level=high
# Scans the built image, covering what the dependency audits can't: the OS packages in
# the python:3.12-slim base (glibc, openssl, …). ignore-unfixed so we only fail on CVEs
# that actually have a fix (a slim base always carries a few unpatched ones); the remedy
# is usually a rebuild or bumping the base pin.
image-scan:
name: Security (image scan)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Build image
run: docker build -t joulenap:ci .
- name: Scan image (Trivy)
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: joulenap:ci
severity: HIGH,CRITICAL
ignore-unfixed: true
exit-code: "1"