The server gate adds `vite build` (and with it devOnlyGuard, which until now first ran inside the image build, after main was pushed) and `npm audit --omit=dev --audit-level=high`. pip-audit is blocking: when it was finally read it held three urllib3 advisories with a fix available. urllib3 goes 2.7.0 -> 2.8.0 as a single in-place pin, passing pip check, rather than a regeneration that would move every package. The dev toolchain's 18 advisories are gone: - vite 5 -> 6.4.3 (path traversal, fs.deny bypass, and esbuild <= 0.24.2 through it); - vitest 3 -> 4.1.11 (mocker path traversal); - typescript-eslint 8.46 -> 8.71; - `npm audit fix` for the rest. tsc, eslint, knip, all 2269 tests and the build pass unchanged. scripts/check-node-modules.mjs checks that frontend/node_modules holds exactly what package-lock.json pins. The local gate's frontend lanes judge the host tree, and nothing tied it to the lock the image installs.
129 lines
7.3 KiB
YAML
129 lines
7.3 KiB
YAML
# Server-side mirror of the local `siftlode check` gate (E1 S3 / DX-1), so `main` is always green
|
|
# independent of any one machine. Two jobs match the two local lanes:
|
|
# frontend — tsc (3 projects) + eslint + prettier --check + knip (both modes) + vitest + vite build
|
|
# + npm audit of the production dependencies
|
|
# backend — ruff + basedpyright + the full pytest suite (pure-logic + the DB-backed lane) against
|
|
# a real Postgres + pip-audit of the lock
|
|
#
|
|
# They are declared side by side but they DO NOT run in parallel: the lab has a single act_runner
|
|
# with one slot, so they queue. Measured across runs #86-#91 — the backend job starts at the exact
|
|
# second the frontend one ends, every time, and the pair costs ~4 + ~6 = **~10 minutes from the
|
|
# push**. Anything that waits on this gate has to budget for the sum, not the max; the ops
|
|
# `release` read-back timed out on its first real run for believing otherwise.
|
|
#
|
|
# Runner: the lab act_runner (LXC 115) mounts the Docker socket, so service containers work. The backend
|
|
# runs pytest NATIVELY against a `postgres` service (not the local docker-compose.test.yml, whose
|
|
# bind-mount is unreliable under a mounted-socket runner); fidelity is kept by installing the SAME pinned
|
|
# requirements, Postgres 16, and ffmpeg, and by setting SIFTLODE_TEST_REQUIRE_DB=1 so a DB/ffmpeg that
|
|
# failed to come up is a hard error, never a silent skip.
|
|
#
|
|
# The frontend container's node major is NOT free: it must equal the prod build image's and the dev
|
|
# machine's, because vitest/tsc read the engine's own Intl and web-API implementations and those split
|
|
# between majors. `.nvmrc` is the single source of truth and
|
|
# `frontend/src/lib/nodeEngine.contract.test.ts` fails this gate if any of them drift apart.
|
|
name: gate
|
|
|
|
# Branch pushes only. A release pushes `main` and then the `v<ver>` tag, and a bare `on: [push]`
|
|
# treated the tag as a second push of the IDENTICAL commit — every release ran this whole gate
|
|
# twice, to the same verdict, for nothing (runs #81-#90 are five such pairs). `branches: ['**']`
|
|
# keeps every branch covered and drops tag refs.
|
|
#
|
|
# It has to be `branches`, NOT the `tags-ignore` this obviously wants to be. Gitea's matchPushEvent
|
|
# requires EVERY named filter to match, and each one bails on the wrong ref kind: `tags-ignore`
|
|
# starts with `if !refName.IsTag() { break }`, so a filter meant to skip tags rejects every BRANCH
|
|
# push and the gate stops running entirely. `branches` bails the same way on a tag, which is the
|
|
# direction we want. (Measured in go-gitea v1.27.3 modules/actions/workflows.go, not assumed — it
|
|
# differs from GitHub, where `tags-ignore` alone leaves branches unfiltered.)
|
|
#
|
|
# If that filter were ever wrong the gate would not run at all — which is why `siftlode release`
|
|
# now ends by reading the verdict back for the sha it pushed, and says so out loud when there is
|
|
# no run for it. A gate that silently stops running is the failure this repo already lived through.
|
|
on:
|
|
push:
|
|
branches: ["**"]
|
|
pull_request:
|
|
|
|
jobs:
|
|
frontend:
|
|
runs-on: ubuntu-latest
|
|
container: node:24-bookworm
|
|
defaults:
|
|
run:
|
|
working-directory: frontend
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
- run: npm ci
|
|
- run: npm run typecheck
|
|
- run: npm run lint
|
|
- run: npm run format:check
|
|
- run: npm run knip
|
|
- run: npm run test
|
|
# The production bundle (E32): before this, `vite build` — and devOnlyGuard, which fails it if
|
|
# dev-only code reaches the bundle — first ran inside the image build, after main was pushed.
|
|
- run: npx vite build
|
|
# What ships to the browser: a known CVE at high or above in a production dependency fails.
|
|
# (The dev tooling's own advisories are burned down by hand; see docs/gates.md.)
|
|
- run: npm audit --omit=dev --audit-level=high
|
|
|
|
backend:
|
|
runs-on: ubuntu-latest
|
|
# A combined python+node image: python 3.12 (matches the prod Dockerfile) for the tests, and node
|
|
# so the JS actions (checkout) run. actions/setup-python is NOT usable here — the runner can't reach
|
|
# GitHub's python-versions download, so it must be baked into the image.
|
|
#
|
|
# This node runs the checkout action, never our code, so it is deliberately OUTSIDE the engine
|
|
# contract the frontend job is held to — it tracks `.nvmrc` as hygiene (node 20 went EOL in April
|
|
# 2026), but a missing upstream tag here must never be able to block the gate.
|
|
container: nikolaik/python-nodejs:python3.12-nodejs24
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: siftlode
|
|
POSTGRES_PASSWORD: siftlode
|
|
POSTGRES_DB: siftlode_test
|
|
options: >-
|
|
--health-cmd "pg_isready -U siftlode -d siftlode_test"
|
|
--health-interval 2s
|
|
--health-timeout 3s
|
|
--health-retries 30
|
|
env:
|
|
DATABASE_URL: postgresql+psycopg://siftlode:siftlode@postgres:5432/siftlode_test
|
|
SIFTLODE_TEST_REQUIRE_DB: "1"
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
# ffmpeg for the poster/thumbnail tests; postgresql-client for the readiness wait below.
|
|
- run: apt-get update && apt-get install -y --no-install-recommends ffmpeg postgresql-client
|
|
- name: Wait for Postgres
|
|
run: until pg_isready -h postgres -U siftlode -d siftlode_test; do sleep 1; done
|
|
- run: pip install --no-cache-dir -r backend/requirements.lock -r backend/requirements-dev.txt
|
|
- name: ruff
|
|
run: python -m ruff check --config backend/ruff.toml backend scripts
|
|
- name: ruff format --check
|
|
run: python -m ruff format --check --config backend/ruff.toml backend scripts
|
|
# The ninth lane (E27 S3). Run from backend/ so it resolves exactly as the local lane does —
|
|
# pyrightconfig.json's include/extraPaths are relative to the config, but the baseline is
|
|
# found relative to the working directory. New errors fail; the ~200 pre-existing ones are in
|
|
# .basedpyright/baseline.json and burn down epic by epic. Matching is per (file, rule, column
|
|
# span) plus a count, so a new error of the SAME shape in a file where a sibling was just
|
|
# fixed is absorbed — burn the hot files down first.
|
|
#
|
|
# The second step is not redundant. basedpyright TIGHTENS the baseline in place when the count
|
|
# drops, with no --writebaseline, and CI throws its workspace away — so without this a
|
|
# burn-down is green here and red on the developer's `siftlode check`, which has the same
|
|
# guard. Two gates over one tree must not answer differently.
|
|
- name: basedpyright
|
|
run: python -m basedpyright --project pyrightconfig.json
|
|
working-directory: backend
|
|
- name: basedpyright baseline is committed
|
|
run: git diff --exit-code -- backend/.basedpyright/baseline.json
|
|
- name: pytest
|
|
run: python -m pytest
|
|
working-directory: backend
|
|
# Dependency CVE audit over the pinned lock. BLOCKING since E32: it ran advisory-only, and when
|
|
# it was finally read it had three urllib3 advisories with a fix available (bumped in the same
|
|
# change). An advisory without a fix is answered with `--ignore-vuln <id>` here, a reason and a
|
|
# date in the comment, and a line in docs/gates.md.
|
|
- name: pip-audit
|
|
run: pip install --no-cache-dir pip-audit && pip-audit -r backend/requirements.lock
|