test(ci): deterministic deploy gate + advisory quality suite; sync README versions
CI / check (push) Successful in 32s
CI / tests-imaging-browsers (push) Successful in 1m34s
CI / tests-integration (push) Successful in 1m59s
CI / tests-unit (push) Successful in 2m2s
CI / tests-quality (push) Successful in 1m1s
CI / tests-ui (push) Failing after 3m20s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

The deploy gate ran e2e specs that depend on live content and data, so a missing
upload (the /help article images) or a legacy malformed avatar figure could turn
a safe code deploy red. Split the suites by what they actually guarantee:

- Blocking gate (`pnpm test:e2e`, `playwright.config.ts`): only checks the CMS
  can guarantee itself — anonymous redirects, register form fields, the captcha
  DOM contract and the cache-control headers (deterministic origin-direct).
- Advisory suite (`pnpm test:e2e:quality`, `playwright.quality.config.ts`):
  broken images, layout overflow and warm-page latency, which depend on live
  content/upload state and the host. It runs as a `continue-on-error` CI job
  against the public site so a real content problem stays visible without
  blocking the pipeline.

Also add `scripts/sync-readme-versions.mjs` (+ tests, docs and a CI check) so
the pinned versions in README.md can never drift from package.json / .nvmrc.
This commit is contained in:
openhands committed 2026-10-15 16:26:47 +02:00
1 parent ef2f8c30f0
commit ef6c671903
12 files changed
+419 -48

No files matched your search

+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
/**
* Collapse the version facts the README advertises into a single object, read
* from the source of truth (`package.json` + `.nvmrc`). Kept as a pure function
* so the test suite can pin the extraction without touching the filesystem.
*/
export function collectVersions(pkg, nvmrc) {
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
const require = (name) => {
const value = deps[name];
if (typeof value !== "string" || !/^\d+\.\d+/.test(value))
throw new Error(`package.json is missing a concrete ${name} version`);
return value;
};
const pnpm = /^pnpm@([^+]+)/.exec(pkg.packageManager ?? "")?.[1];
if (!pnpm) throw new Error("package.json packageManager must pin pnpm");
const node = nvmrc.trim();
if (!/^\d+\.\d+\.\d+$/.test(node))
throw new Error(".nvmrc must pin an exact Node.js version");
return {
pnpm,
node,
next: require("next"),
react: require("react"),
typescript: require("typescript"),
drizzleOrm: require("drizzle-orm"),
zod: require("zod"),
vitest: require("vitest"),
biome: require("@biomejs/biome"),
playwright: require("@playwright/test"),
};
}
/** The single "Core stack" line, built from the live versions. */
export function coreStackLine(v) {
return `Core stack: **Next.js ${v.next}** (App Router) · **React ${v.react}** · **TypeScript ${v.typescript}** · **Drizzle ORM ${v.drizzleOrm}** · **Zod ${v.zod}** · **Vitest ${v.vitest}** · **Biome ${v.biome}** · **Playwright ${v.playwright}**.`;
}
const CORE_STACK_RE = /^Core stack: \*\*Next\.js .*$/m;
/**
* Rewrite every managed version reference in `readme` to match `versions`.
* Returns the new text plus a human-readable list of what changed. Throws if a
* managed anchor is missing, so a README restructure fails loudly instead of
* silently dropping out of sync.
*/
export function syncReadmeVersions(readme, versions) {
const changes = [];
let next = readme;
// `| Node.js | 26.10.0 | … (>=26.10.0 <27) |` — version cell + engines note.
next = replaceVersionCell(
next,
"Node\\.js",
versions.node,
"Node.js",
changes,
);
const nodeMajor = Number.parseInt(versions.node.split(".")[0], 10);
const nodeNote = /(>=)\d+\.\d+\.\d+( <)\d+/;
if (!nodeNote.test(next))
throw new Error("README is missing the Node.js engines note");
next = next.replace(nodeNote, `$1${versions.node}$2${nodeMajor + 1}`);
// `| pnpm | 12.11.2 | … |`.
next = replaceVersionCell(next, "pnpm", versions.pnpm, "pnpm", changes);
// `Core stack: **Next.js …** · …`.
const stackMatch = CORE_STACK_RE.exec(next);
if (!stackMatch) throw new Error("README is missing the core stack line");
const expectedStack = coreStackLine(versions);
if (stackMatch[0] !== expectedStack) {
changes.push("core stack line");
next = next.replace(CORE_STACK_RE, expectedStack);
}
return { readme: next, changes };
}
/**
* Replace the version token of a `| name | version | … |` table row, padding
* the trailing spaces so the closing pipe stays in the same column when the
* new version is a different length.
*/
function replaceVersionCell(readme, namePattern, version, label, changes) {
const re = new RegExp(
`^(\\| ${namePattern}(?: +)\\| )(\\S+)( +)(\\|.*)$`,
"m",
);
const match = re.exec(readme);
if (!match)
throw new Error(`README is missing the ${label} requirements row`);
const [, pre, current, pad, post] = match;
if (current === version) return readme;
changes.push(`${label} ${current} -> ${version}`);
const padLength = Math.max(1, pad.length - (version.length - current.length));
return readme.replace(re, `${pre}${version}${" ".repeat(padLength)}${post}`);
}
function main() {
const check = process.argv.includes("--check");
const pkg = JSON.parse(
readFileSync(resolve(projectRoot, "package.json"), "utf8"),
);
const nvmrc = readFileSync(resolve(projectRoot, ".nvmrc"), "utf8");
const readmePath = resolve(projectRoot, "README.md");
const readme = readFileSync(readmePath, "utf8");
const { readme: updated, changes } = syncReadmeVersions(
readme,
collectVersions(pkg, nvmrc),
);
if (changes.length === 0) {
console.log("README versions are in sync with package.json / .nvmrc");
return;
}
if (check) {
console.error("README versions are out of date:");
for (const change of changes) console.error(` - ${change}`);
console.error("Run `pnpm readme:versions` to fix.");
process.exitCode = 1;
return;
}
writeFileSync(readmePath, updated);
console.log("README versions updated:");
for (const change of changes) console.log(` - ${change}`);
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) main();