build: the gate is scripts/check.sh, and CI goes
The Gitea Actions gate repeated, after the merge, what a release already runs before it, and its runner shared the git host's container: one run there starved Gitea of memory until it stopped answering. The lanes now live in scripts/check.sh, run the same by the release dispatcher and by hand, and the gate-contract test holds its lines (the ruff and pip-audit checks read it instead of the workflow). The two audits only CI ran join the gate: pip-audit over the lock (pinned in the test image, blocking at any severity) and npm audit of the production dependencies. The engine contracts drop the CI containers; the register records that pushes have no server-side gate.
This commit is contained in:
@@ -105,7 +105,7 @@ function restrictedEntries(entry: Linter.RuleEntry | undefined): Set<string> {
|
||||
// the lines that run the lanes. Any change fails until the snapshot is regenerated, so a narrowing
|
||||
// arrives as a diff of `gates.snapshot.json` in review instead of slipping past a detector. (Three
|
||||
// review rounds kept finding shapes the detector this replaced did not model — a `basePath` global
|
||||
// ignore, a widened glob filed under a narrow row, a substring check on a CI line. A comparison
|
||||
// ignore, a widened glob filed under a narrow row, a substring check on a gate line. A comparison
|
||||
// against everything has no shape to miss.)
|
||||
//
|
||||
// Regenerate: GATE_SNAPSHOT=update npx vitest run src/lib/gates.contract.test.ts
|
||||
@@ -285,7 +285,7 @@ function meaningful(path: string): string[] {
|
||||
});
|
||||
}
|
||||
const lanes = () => ({
|
||||
".gitea/workflows/check.yml": meaningful(".gitea/workflows/check.yml"),
|
||||
"scripts/check.sh": meaningful("scripts/check.sh"),
|
||||
"backend/docker-compose.test.yml": meaningful("backend/docker-compose.test.yml"),
|
||||
"backend/Dockerfile.test": meaningful("backend/Dockerfile.test"),
|
||||
"frontend/package.json scripts": (
|
||||
@@ -485,19 +485,17 @@ describe("the gate register (docs/gates.md)", () => {
|
||||
});
|
||||
// Every ruff lane passes `--config backend/ruff.toml`, which makes ruff ignore any other config
|
||||
// file it would discover (measured: a nested `ruff.toml` selecting the `D` rules changed nothing).
|
||||
// So that flag is what makes this file the whole configuration. The snapshot holds every CI line;
|
||||
// these two are spelled out here because the reason is (round 3: a substring check let an extra
|
||||
// `--config "lint.extend-ignore=…"` override through).
|
||||
it("runs every CI ruff step on backend/ruff.toml and nothing else", () => {
|
||||
const steps = read(".gitea/workflows/check.yml")
|
||||
// So that flag is what makes this file the whole configuration. The snapshot holds every line of
|
||||
// the gate script; these two are spelled out because the reason is (round 3: a substring check let
|
||||
// an extra `--config "lint.extend-ignore=…"` override through).
|
||||
it("runs both ruff lanes on backend/ruff.toml and nothing else", () => {
|
||||
const lanes = read("scripts/check.sh")
|
||||
.split("\n")
|
||||
.filter((l) => /\bruff\b/.test(l) && !l.trim().startsWith("#"))
|
||||
.filter((l) => /python -m ruff (check|format)/.test(l))
|
||||
.map((l) => l.trim());
|
||||
expect(steps).toEqual([
|
||||
"- 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",
|
||||
expect(lanes).toEqual([
|
||||
"ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }",
|
||||
"ruff_format() { ruff_pinned && python -m ruff format --check --config backend/ruff.toml backend scripts; }",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -577,9 +575,9 @@ describe("the gate register (docs/gates.md)", () => {
|
||||
expires: /\n\s*expired_at:\s*\d{4}-\d{2}-\d{2}/.test(chunk),
|
||||
}));
|
||||
expect(entries.filter((e) => !e.statement || !e.expires).map((e) => e.id)).toEqual([]);
|
||||
const pipIgnores = read(".gitea/workflows/check.yml")
|
||||
const pipIgnores = read("scripts/check.sh")
|
||||
.split("\n")
|
||||
.filter((l) => !l.trim().startsWith("#")) // the step's comment explains the flag; it is not one
|
||||
.filter((l) => !l.trim().startsWith("#")) // the lane's comment explains the flag; it is not one
|
||||
.flatMap((l) => [...l.matchAll(/--ignore-vuln[ =](\S+)/g)].map((m) => m[1]!));
|
||||
const found = [...entries.map((e) => e.id), ...pipIgnores].map((id): [string, string] => [
|
||||
"accepted",
|
||||
|
||||
@@ -2222,68 +2222,6 @@
|
||||
}
|
||||
},
|
||||
"lanes": {
|
||||
".gitea/workflows/check.yml": [
|
||||
"name: gate",
|
||||
"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",
|
||||
" - run: npx vite build",
|
||||
" - run: npm audit --omit=dev --audit-level=high",
|
||||
" backend:",
|
||||
" runs-on: ubuntu-latest",
|
||||
" 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",
|
||||
" - 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",
|
||||
" - 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",
|
||||
" - name: pip-audit",
|
||||
" run: pip install --no-cache-dir pip-audit && pip-audit -r backend/requirements.lock"
|
||||
],
|
||||
"backend/Dockerfile.test": [
|
||||
"FROM python:3.12-slim",
|
||||
"WORKDIR /app",
|
||||
@@ -2366,6 +2304,77 @@
|
||||
" },",
|
||||
" },",
|
||||
"});"
|
||||
],
|
||||
"scripts/check.sh": [
|
||||
"set -u",
|
||||
"cd \"$(dirname \"$0\")/..\" || exit 2",
|
||||
"mode=\"${1:-all}\"",
|
||||
"case \"$mode\" in all | lint) ;; *) echo \"usage: scripts/check.sh [lint]\" >&2; exit 2 ;; esac",
|
||||
"export MSYS_NO_PATHCONV=1 # Git Bash: do not rewrite the in-container paths below",
|
||||
"TESTCOMPOSE=backend/docker-compose.test.yml",
|
||||
"failed=()",
|
||||
"lane() { # lane <name> <command…> — run it, remember a failure, never stop",
|
||||
" local name=\"$1\"",
|
||||
" shift",
|
||||
" echo \">> $name…\"",
|
||||
" \"$@\" || failed+=(\"$name\")",
|
||||
"}",
|
||||
"ruff_pinned() {",
|
||||
" local pin host",
|
||||
" pin=\"$(sed -n 's/^ruff==\\([0-9.]*\\).*/\\1/p' backend/requirements-dev.txt)\"",
|
||||
" host=\"$(python -m ruff --version 2>/dev/null | awk '{print $2}')\"",
|
||||
" [ -n \"$pin\" ] && [ \"$host\" = \"$pin\" ] && return 0",
|
||||
" echo \"ruff ${host:-missing} on the host, ${pin:-nothing} pinned — python -m pip install ruff==$pin\" >&2",
|
||||
" return 1",
|
||||
"}",
|
||||
"ruff_check() { ruff_pinned && python -m ruff check --config backend/ruff.toml backend scripts; }",
|
||||
"ruff_format() { ruff_pinned && python -m ruff format --check --config backend/ruff.toml backend scripts; }",
|
||||
"basedpyright() {",
|
||||
" local before",
|
||||
" before=\"$(git hash-object backend/.basedpyright/baseline.json)\"",
|
||||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps --build test \\",
|
||||
" python -m basedpyright --project pyrightconfig.json || return 1",
|
||||
" if [ \"$(git hash-object backend/.basedpyright/baseline.json)\" != \"$before\" ]; then",
|
||||
" echo \"this run rewrote backend/.basedpyright/baseline.json (errors were fixed) — commit it\" >&2",
|
||||
" return 1",
|
||||
" fi",
|
||||
" git diff HEAD --quiet -- backend/.basedpyright/baseline.json && return 0",
|
||||
" echo \"backend/.basedpyright/baseline.json is uncommitted — commit it\" >&2",
|
||||
" return 1",
|
||||
"}",
|
||||
"frontend() { (cd frontend && \"$@\"); }",
|
||||
"lane \"ruff\" ruff_check",
|
||||
"lane \"ruff format --check\" ruff_format",
|
||||
"lane \"basedpyright (new errors only)\" basedpyright",
|
||||
"lane \"lock check (frontend/node_modules == package-lock.json)\" node scripts/check-node-modules.mjs",
|
||||
"lane \"tsc (src + configs + e2e)\" frontend npm run --silent typecheck",
|
||||
"lane \"eslint (warnings fail too)\" frontend npm run --silent lint",
|
||||
"lane \"prettier --check\" frontend npm run --silent format:check",
|
||||
"lane \"knip (unused, and unused by what ships)\" frontend npm run --silent knip",
|
||||
"pytest_lane() { # the full suite against a throwaway Postgres; --pull keeps the base image current",
|
||||
" docker compose -f \"$TESTCOMPOSE\" build --pull -q test || return 1",
|
||||
" local rc=0",
|
||||
" docker compose -f \"$TESTCOMPOSE\" up --build --abort-on-container-exit --exit-code-from test || rc=1",
|
||||
" docker compose -f \"$TESTCOMPOSE\" down -v >/dev/null 2>&1",
|
||||
" return $rc",
|
||||
"}",
|
||||
"pip_audit() {",
|
||||
" docker compose -f \"$TESTCOMPOSE\" run --rm --no-deps test \\",
|
||||
" pip-audit -r requirements.lock --no-deps --disable-pip",
|
||||
"}",
|
||||
"if [ \"$mode\" = all ]; then",
|
||||
" lane \"vitest (+ coverage report)\" frontend npm run --silent test -- --coverage",
|
||||
" lane \"vite build\" frontend npx vite build --logLevel warn",
|
||||
" lane \"pytest (pure-logic + DB lane)\" pytest_lane",
|
||||
" lane \"pip-audit (backend lock)\" pip_audit",
|
||||
" lane \"npm audit (production dependencies)\" frontend npm audit --omit=dev --audit-level=high",
|
||||
"fi",
|
||||
"if [ ${#failed[@]} -eq 0 ]; then",
|
||||
" echo \">> gate ($mode): green ✓\"",
|
||||
" exit 0",
|
||||
"fi",
|
||||
"echo \">> gate ($mode): RED — ${failed[*]}\" >&2",
|
||||
"exit 1"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,18 +48,6 @@ it("builds the shipped bundle on the pinned engine", () => {
|
||||
).toBe(pinned);
|
||||
});
|
||||
|
||||
it("runs the server gate on the pinned engine", () => {
|
||||
// The frontend job only — the backend job's node runs the checkout action, never our code, so it is
|
||||
// deliberately not part of this contract (see the note in check.yml).
|
||||
expect(
|
||||
majorIn(
|
||||
readRoot(".gitea/workflows/check.yml"),
|
||||
/^\s*container: node:(\d+)-bookworm$/m,
|
||||
"check.yml",
|
||||
),
|
||||
).toBe(pinned);
|
||||
});
|
||||
|
||||
it("declares the pinned engine to npm and to tsc", () => {
|
||||
expect(pkg.engines?.node).toBe(`${pinned}.x`);
|
||||
// Types a major ahead of the runtime make tsc green over APIs the shipped engine does not have —
|
||||
|
||||
@@ -11,12 +11,12 @@ import { expect, it } from "vitest";
|
||||
//
|
||||
// It lives in the FRONTEND suite for one boring reason: this is the only runner that can see the
|
||||
// whole repository. The backend suite runs inside the test image, which mounts `backend/` alone, so
|
||||
// it cannot read the repo-root Dockerfile or the workflow. It holds the other half of the chain —
|
||||
// it cannot read the repo-root Dockerfile. It holds the other half of the chain —
|
||||
// including "the interpreter running these tests IS the pinned one", which only it can assert:
|
||||
//
|
||||
// backend/tests/test_python_engine_contract.py : running interpreter == pyrightconfig == ruff.toml
|
||||
// here : .python-version == pyrightconfig == ruff.toml
|
||||
// == Dockerfile == Dockerfile.test == check.yml
|
||||
// == Dockerfile == Dockerfile.test
|
||||
//
|
||||
// To move the version: edit `.python-version`, then follow the failures in both files.
|
||||
const readRoot = (rel: string) => readFileSync(new URL(`../../../${rel}`, import.meta.url), "utf8");
|
||||
@@ -55,18 +55,6 @@ it("tests on the pinned version", () => {
|
||||
).toBe(pinned);
|
||||
});
|
||||
|
||||
it("runs the server gate on the pinned version", () => {
|
||||
// The backend job's container. Its NODE is deliberately outside the Node contract (it only runs
|
||||
// the checkout action), but its PYTHON runs our code and our tests, so it is inside this one.
|
||||
expect(
|
||||
versionIn(
|
||||
readRoot(".gitea/workflows/check.yml"),
|
||||
/^\s*container: nikolaik\/python-nodejs:python(\d+)\.(\d+)-nodejs\d+$/m,
|
||||
"check.yml",
|
||||
),
|
||||
).toBe(pinned);
|
||||
});
|
||||
|
||||
it("type-checks against the pinned version", () => {
|
||||
// A type checker aimed at another version green-lights APIs the shipped interpreter does not have
|
||||
// — the same defect as the image split, moved to check time.
|
||||
|
||||
Reference in New Issue
Block a user