mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
Every stage now starts from node:22-trixie-slim, pinned by its multi-arch index digest, so the native modules load their glibc builds. The arm64 musl Skia build needs ARMv8.2 (LSE atomics, RCpc loads) and dies with SIGILL on an ARMv8.0 core such as the Raspberry Pi 4's Cortex-A72; the glibc build runs there (#1311). The runner keeps what it had: tzdata, TZ, the build-time OS upgrade, the nextjs user (uid 1001, now in group nodejs gid 1001), the backup and GeoLite2 directories and every build-time assertion. pg_dump and pg_restore of major 18 come from the PGDG apt repository, whose signing key is fetched by node and checked against a pinned SHA-256 before apt trusts it; PATH names the major 18 directory so the binary does not depend on Debian's version-picking wrapper. The canvas hoist takes the -gnu binary package by the pinned version and still fails the build on a mismatch. bookworm-slim was the other candidate; on the day of the switch it carried a CRITICAL zlib finding that trixie-slim does not.
257 lines
11 KiB
YAML
257 lines
11 KiB
YAML
name: Security & Quality
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
branches: [main]
|
|
schedule:
|
|
# Daily, 05:47 UTC. Carries the dependency audit only — every other job
|
|
# here answers "is this change safe" and has nothing to say about a tree
|
|
# nobody touched. See the note on `dependency-audit`.
|
|
- cron: "47 5 * * *"
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
# The static checks: everything in the old single quality job except the
|
|
# unit suite, which runs sharded below.
|
|
static:
|
|
name: Lint, Typecheck & OpenAPI
|
|
runs-on: ubuntu-24.04
|
|
if: github.event_name != 'schedule'
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
|
|
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: 22
|
|
cache: "pnpm"
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- run: pnpm db:generate
|
|
|
|
# Blocking since v1.15.20 — the settings-split refactor that kept
|
|
# this warn-only is long done and the tree lints clean.
|
|
- name: ESLint
|
|
run: pnpm lint
|
|
|
|
# Prettier formatting gate. The format pass excludes generator-owned
|
|
# artifacts via .prettierignore (notably docs/api/openapi.yaml, which
|
|
# the OpenAPI drift check below enforces byte-for-byte).
|
|
- name: Prettier format check
|
|
run: pnpm format:check
|
|
|
|
- name: TypeScript strict check
|
|
run: pnpm typecheck
|
|
|
|
# OpenAPI spec drift gate — hard-fail (flipped from warn-only in v1.4.25).
|
|
#
|
|
# The Zod registry is the source of truth for the public API contract.
|
|
# Any route signature change must be reflected in `docs/api/openapi.yaml`
|
|
# so downstream consumers (notably the v1.5 iOS Swift codegen) stay in
|
|
# lockstep. If this step fails, run `pnpm openapi:generate` locally and
|
|
# commit the regenerated spec.
|
|
- name: OpenAPI spec drift check
|
|
run: |
|
|
pnpm openapi:generate
|
|
if ! git diff --exit-code docs/api/openapi.yaml; then
|
|
echo "::error::OpenAPI drift detected — regenerate the spec via 'pnpm openapi:generate' and commit the result. The Zod registry under src/lib/openapi/ is the source of truth for the public API contract."
|
|
exit 1
|
|
fi
|
|
echo "::notice::OpenAPI spec in sync with Zod source schemas."
|
|
|
|
# The unit suite in three slices of about equal duration. `--shard` goes
|
|
# through the duration sequencer in `vitest.config.mts`, which deals files
|
|
# out by their measured time (`tests/durations/unit.json`) instead of by
|
|
# count. Each slice uploads its JSON report, which is what
|
|
# `scripts/test-durations.mjs` regenerates the timing file from.
|
|
unit:
|
|
runs-on: ubuntu-24.04
|
|
if: github.event_name != 'schedule'
|
|
timeout-minutes: 30
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
shard: [1, 2, 3]
|
|
name: unit shard ${{ matrix.shard }}/3
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
|
|
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: 22
|
|
cache: "pnpm"
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
- run: pnpm db:generate
|
|
|
|
# The hero-geometry check in the unit suite renders the real panel in a
|
|
# real browser and measures the boxes. Without a browser here it caught
|
|
# its own launch failure and passed anyway, so from its first day until
|
|
# v1.38 it measured nothing on any run of this job. The browser is part
|
|
# of the gate now; the test refuses to skip when CI is set. Every slice
|
|
# installs it, because the sequencer decides which slice the file lands in.
|
|
#
|
|
# Headless shell only: `chromium.launch()` resolves to
|
|
# chromium_headless_shell, which is what the failing runs named, and the
|
|
# full headed build is several times the download for nothing this job
|
|
# uses. The e2e job keeps `--with-deps chromium` — it drives a real app.
|
|
- name: Cache Playwright headless shell
|
|
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
with:
|
|
path: ~/.cache/ms-playwright
|
|
key: playwright-shell-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
|
|
restore-keys: |
|
|
playwright-shell-${{ runner.os }}-
|
|
|
|
- run: pnpm exec playwright install --only-shell chromium
|
|
|
|
# vitest directly, not `pnpm test --`: pnpm hands the separator through
|
|
# literally and the shard flag would never reach vitest.
|
|
- name: Unit tests
|
|
run: >-
|
|
pnpm exec vitest run --shard=${{ matrix.shard }}/3
|
|
--reporter=default --reporter=json
|
|
--outputFile.json=unit-report-${{ matrix.shard }}.json
|
|
|
|
- name: Upload the timing report
|
|
if: always()
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: unit-report-${{ matrix.shard }}
|
|
path: unit-report-${{ matrix.shard }}.json
|
|
if-no-files-found: ignore
|
|
retention-days: 14
|
|
|
|
# Branch protection requires a check named exactly "Lint, Typecheck & Test".
|
|
# That used to be one job running everything in turn; it is now this one,
|
|
# which answers for the static checks and every unit slice, so a half-green
|
|
# set cannot satisfy the gate.
|
|
quality:
|
|
name: Lint, Typecheck & Test
|
|
runs-on: ubuntu-24.04
|
|
needs: [static, unit]
|
|
if: always() && github.event_name != 'schedule'
|
|
timeout-minutes: 5
|
|
steps:
|
|
- name: The static checks and every unit slice finished green
|
|
run: |
|
|
if [ "${{ needs.static.result }}" != "success" ] || [ "${{ needs.unit.result }}" != "success" ]; then
|
|
echo "static: ${{ needs.static.result }}, unit: ${{ needs.unit.result }}"
|
|
exit 1
|
|
fi
|
|
|
|
# Not on pull requests. `pnpm audit` answers a question about the published
|
|
# advisory database, not about the diff: the same lockfile passes on Monday
|
|
# and fails on Tuesday because someone else published, and the branch that
|
|
# goes red changed nothing. Seventeen of the forty-one failures this workflow
|
|
# produced in thirty days were exactly that, and a gate that fails for
|
|
# reasons the author cannot act on is a gate people learn to re-run rather
|
|
# than read. It runs on every push to `main` and once a day on the schedule
|
|
# above, which is the cadence the question actually has. What ships is still
|
|
# gated: `docker-security` scans the built image on every push to `main`, and
|
|
# `trivy.yml` scans the lockfile (dev dependencies included) and the
|
|
# published image daily.
|
|
dependency-audit:
|
|
name: Dependency Audit
|
|
runs-on: ubuntu-24.04
|
|
# Runs on pull requests too, but advisory: an advisory published against a
|
|
# dependency the branch did not touch must not block the branch. The push
|
|
# run on main and the daily schedule stay blocking, and a tag is only ever
|
|
# cut from a main whose push run is green.
|
|
continue-on-error: ${{ github.event_name == 'pull_request' }}
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
|
|
|
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
with:
|
|
node-version: 22
|
|
cache: "pnpm"
|
|
|
|
- run: pnpm install --frozen-lockfile
|
|
|
|
# Blocking since v1.15.20 — a HIGH/CRITICAL advisory against a
|
|
# production dependency fails the build. If an advisory has no
|
|
# upstream fix yet, pin an `auditConfig.ignoreCves` entry in
|
|
# package.json (pnpm's documented suppression mechanism) with a
|
|
# comment referencing the tracking issue, rather than flipping
|
|
# this step back to warn-only.
|
|
- name: Audit for known vulnerabilities
|
|
run: pnpm audit --prod --audit-level=high
|
|
|
|
secret-scan:
|
|
name: Secret Scanning
|
|
runs-on: ubuntu-24.04
|
|
if: github.event_name != 'schedule'
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Gitleaks secret scan
|
|
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
docker-security:
|
|
name: Container Security
|
|
runs-on: ubuntu-24.04
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
|
|
- name: Build Docker image
|
|
run: docker build -t healthlog:scan .
|
|
|
|
# The release binary, pinned, straight from the Trivy release. The apt
|
|
# route ran `apt-get update` across every repository on the runner
|
|
# image, and an unrelated one (the Chrome repository the runner ships
|
|
# with) answering with a corrupt index failed the step twice in a row
|
|
# on a release commit. The installer downloads the tarball for this
|
|
# exact version and checks it against the published checksums.
|
|
#
|
|
# The installer itself is fetched from a COMMIT and verified before it
|
|
# runs. A tag can be moved to other content, and piping a moving ref
|
|
# straight into a shell runs whatever the ref points at that morning
|
|
# with the runner's privileges. `TRIVY_INSTALL_COMMIT` is the commit
|
|
# `v0.74.0` points at; bump both together, and take the new checksum
|
|
# from the file at that commit.
|
|
- name: Install Trivy
|
|
env:
|
|
TRIVY_VERSION: v0.74.0
|
|
TRIVY_INSTALL_COMMIT: e1fd17a0ea4a8cf24bc4b4dd7e2cfbf4bb31b994
|
|
TRIVY_INSTALL_SHA256: e00df553be558995b994758bc8995956554a16937f456dc6615b0cc411bfec7a
|
|
run: |
|
|
curl -sfL \
|
|
"https://raw.githubusercontent.com/aquasecurity/trivy/${TRIVY_INSTALL_COMMIT}/contrib/install.sh" \
|
|
-o trivy-install.sh
|
|
echo "${TRIVY_INSTALL_SHA256} trivy-install.sh" | sha256sum -c -
|
|
sh trivy-install.sh -b /usr/local/bin "${TRIVY_VERSION}"
|
|
rm trivy-install.sh
|
|
trivy --version
|
|
|
|
# Blocking since v1.15.20 — a HIGH/CRITICAL finding in the built
|
|
# image fails the job. For a finding without an upstream fix
|
|
# (e.g. a base-image CVE Debian has not patched yet), add the CVE
|
|
# id to a `.trivyignore` file at the repository root — Trivy reads
|
|
# it automatically — one CVE per line, each with a comment naming
|
|
# the tracking issue and the re-check condition. Never suppress a
|
|
# finding that has a fixed version available; bump instead.
|
|
- name: Trivy container scan
|
|
run: trivy image --format table --severity CRITICAL,HIGH --exit-code 1 healthlog:scan
|