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.
89 lines
3.9 KiB
TypeScript
89 lines
3.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { expect, it } from "vitest";
|
|
|
|
// The Python major is a contract, not a coincidence — the host half. The twin of
|
|
// `nodeEngine.contract.test.ts`, and written the day after that one was: `0.93.2` shipped because
|
|
// four machines disagreed about Node, and the Python side turned out to have the identical shape
|
|
// with nobody watching. Prod (`Dockerfile`), the test image (`backend/Dockerfile.test`) and CI
|
|
// (`check.yml`) each spelled 3.12 as its own literal, there was no `.python-version`, and the dev
|
|
// machine had only 3.13 and 3.14 installed — so every ad-hoc host script ran two majors ahead of
|
|
// production.
|
|
//
|
|
// 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. 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
|
|
//
|
|
// 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");
|
|
|
|
function versionIn(source: string, pattern: RegExp, where: string): string {
|
|
const m = pattern.exec(source);
|
|
// A rename or a moved line lands here. Re-point the test — the contract did not go away with it.
|
|
expect(m, `${pattern} not found in ${where}`).not.toBeNull();
|
|
return `${m![1]}.${m![2]}`;
|
|
}
|
|
|
|
const pinned = versionIn(readRoot(".python-version"), /^(\d+)\.(\d+)\s*$/m, ".python-version");
|
|
|
|
it("pins a plausible Python version", () => {
|
|
// Guards against a typo'd `.python-version` silently agreeing with nothing.
|
|
expect(Number(pinned.split(".")[1])).toBeGreaterThanOrEqual(12);
|
|
expect(pinned.split(".")[0]).toBe("3");
|
|
});
|
|
|
|
it("runs the shipped backend on the pinned version", () => {
|
|
// The image our users' instance actually runs.
|
|
expect(versionIn(readRoot("Dockerfile"), /^FROM python:(\d+)\.(\d+)-slim/m, "Dockerfile")).toBe(
|
|
pinned,
|
|
);
|
|
});
|
|
|
|
it("tests on the pinned version", () => {
|
|
// The image the whole pytest suite runs in — including the backend half of this contract, which
|
|
// asserts its own `sys.version_info` against the pin.
|
|
expect(
|
|
versionIn(
|
|
readRoot("backend/Dockerfile.test"),
|
|
/^FROM python:(\d+)\.(\d+)-slim/m,
|
|
"Dockerfile.test",
|
|
),
|
|
).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.
|
|
expect(
|
|
versionIn(
|
|
readRoot("backend/pyrightconfig.json"),
|
|
/"pythonVersion"\s*:\s*"(\d+)\.(\d+)"/,
|
|
"pyrightconfig.json",
|
|
),
|
|
).toBe(pinned);
|
|
});
|
|
|
|
it("lints against the pinned version", () => {
|
|
expect(
|
|
versionIn(readRoot("backend/ruff.toml"), /^target-version = "py(\d)(\d+)"$/m, "ruff.toml"),
|
|
).toBe(pinned);
|
|
});
|
|
|
|
it("resolves the lock on the pinned version", () => {
|
|
// The seventh spelling, and the sneakiest: `requirements.lock`'s own regeneration recipe names an
|
|
// image. Wheels and environment markers are resolved by THAT interpreter, so a recipe left behind
|
|
// produces a lock that does not match the image installing it — the same split as the others, one
|
|
// indirection further out, and with nothing red to show for it.
|
|
expect(
|
|
versionIn(
|
|
readRoot("backend/requirements.lock"),
|
|
/docker run [^\n]*python:(\d+)\.(\d+)-slim/,
|
|
"requirements.lock",
|
|
),
|
|
).toBe(pinned);
|
|
});
|