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
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:
1 parent
ef2f8c30f0
commit
ef6c671903
12 files changed
+419
-48
No files matched your search
@@ -20,6 +20,7 @@ This directory contains utility scripts for managing the EpicNext-Cms project.
|
||||
| `perf-report.sh` | Runs performance profiling. |
|
||||
| `setup-dev.sh` | Configures development environment. |
|
||||
| `check-updates.sh` | Checks for package and image updates. |
|
||||
| `sync-readme-versions.mjs` | Syncs the versions in `README.md` with `package.json` / `.nvmrc`. |
|
||||
| `maintenance.sh` | Toggles maintenance mode flag. |
|
||||
| `doctor.sh` | Runs a system diagnostic report. |
|
||||
| `setup-cron.sh` | Configures automated cron jobs. |
|
||||
@@ -30,3 +31,18 @@ Run any script directly, or use the dashboard:
|
||||
```bash
|
||||
./scripts/dashboard.sh
|
||||
```
|
||||
|
||||
### Keeping the README versions in sync
|
||||
|
||||
Bumping a dependency (or the pinned Node.js version) leaves `README.md` stale.
|
||||
Rather than editing it by hand, run:
|
||||
|
||||
```bash
|
||||
pnpm readme:versions # rewrite README.md to match package.json / .nvmrc
|
||||
pnpm readme:versions:check # verify only; exits 1 when out of date (used in CI)
|
||||
```
|
||||
|
||||
It manages the `pnpm` and `Node.js` rows of the System Requirements table and
|
||||
the "Core stack" line. The CI `check` job runs the `--check` variant, so a
|
||||
version change cannot reach `main` with a stale README — the pipeline fails
|
||||
until you run `pnpm readme:versions`.
|
||||
@@ -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();
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
collectVersions,
|
||||
coreStackLine,
|
||||
syncReadmeVersions,
|
||||
} from "./sync-readme-versions.mjs";
|
||||
|
||||
const versions = {
|
||||
pnpm: "12.11.2",
|
||||
node: "26.10.0",
|
||||
next: "16.4.1",
|
||||
react: "19.3.1",
|
||||
typescript: "7.0.2",
|
||||
drizzleOrm: "0.45.4",
|
||||
zod: "4.6.7",
|
||||
vitest: "5.0.4",
|
||||
biome: "2.5.15",
|
||||
playwright: "1.64.1",
|
||||
};
|
||||
|
||||
const pkg = {
|
||||
packageManager: `pnpm@${versions.pnpm}+sha512.deadbeef`,
|
||||
dependencies: {
|
||||
next: versions.next,
|
||||
react: versions.react,
|
||||
zod: versions.zod,
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: versions.typescript,
|
||||
"drizzle-orm": versions.drizzleOrm,
|
||||
vitest: versions.vitest,
|
||||
"@biomejs/biome": versions.biome,
|
||||
"@playwright/test": versions.playwright,
|
||||
},
|
||||
};
|
||||
|
||||
const readme = [
|
||||
"| Node.js | 26.10.0 | Pinned in `.nvmrc` (`>=26.10.0 <27`) |",
|
||||
"| pnpm | 12.11.2 | Pinned via `packageManager` |",
|
||||
"",
|
||||
coreStackLine(versions),
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
describe("collectVersions", () => {
|
||||
it("pulls every managed version from package.json and .nvmrc", () => {
|
||||
expect(collectVersions(pkg, `${versions.node}\n`)).toEqual(versions);
|
||||
});
|
||||
|
||||
it("rejects a packageManager without a pinned pnpm", () => {
|
||||
expect(() =>
|
||||
collectVersions({ ...pkg, packageManager: "npm@10" }, "26.10.0"),
|
||||
).toThrow(/pnpm/);
|
||||
});
|
||||
|
||||
it("rejects a ranged dependency instead of a pinned one", () => {
|
||||
const ranged = {
|
||||
...pkg,
|
||||
dependencies: { ...pkg.dependencies, next: "^16" },
|
||||
};
|
||||
expect(() => collectVersions(ranged, "26.10.0")).toThrow(/next/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncReadmeVersions", () => {
|
||||
it("reports no changes when the README already matches", () => {
|
||||
const result = syncReadmeVersions(readme, versions);
|
||||
expect(result.changes).toEqual([]);
|
||||
expect(result.readme).toBe(readme);
|
||||
});
|
||||
|
||||
it("updates the pnpm row and keeps the table aligned", () => {
|
||||
const bumped = { ...versions, pnpm: "13.0.0" };
|
||||
const original = readme.split("\n").find((line) => line.includes("pnpm"));
|
||||
const { readme: updated, changes } = syncReadmeVersions(readme, bumped);
|
||||
const line = updated.split("\n").find((l) => l.includes("pnpm"));
|
||||
|
||||
expect(changes).toContain("pnpm 12.11.2 -> 13.0.0");
|
||||
expect(line).toContain("| 13.0.0");
|
||||
expect(line?.length).toBe(original?.length);
|
||||
expect(line?.endsWith("|\n") || line?.endsWith("|")).toBe(true);
|
||||
});
|
||||
|
||||
it("rewrites the core stack line from the live versions", () => {
|
||||
const bumped = { ...versions, next: "17.0.0", zod: "5.0.0" };
|
||||
const { readme: updated, changes } = syncReadmeVersions(readme, bumped);
|
||||
expect(changes).toContain("core stack line");
|
||||
expect(updated).toContain(coreStackLine(bumped));
|
||||
expect(updated).not.toContain("Next.js 16.4.1");
|
||||
});
|
||||
|
||||
it("updates both the Node version cell and the engines note", () => {
|
||||
const bumped = { ...versions, node: "27.3.1" };
|
||||
const { readme: updated, changes } = syncReadmeVersions(readme, bumped);
|
||||
expect(changes).toContain("Node.js 26.10.0 -> 27.3.1");
|
||||
expect(updated).toContain("| 27.3.1 ");
|
||||
expect(updated).toContain("`>=27.3.1 <28`");
|
||||
});
|
||||
|
||||
it("fails loudly when a managed anchor is missing", () => {
|
||||
expect(() =>
|
||||
syncReadmeVersions("no table here\nno core stack", versions),
|
||||
).toThrow(/requirements row/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("coreStackLine", () => {
|
||||
it("renders the documented ordering", () => {
|
||||
expect(coreStackLine(versions)).toBe(
|
||||
"Core stack: **Next.js 16.4.1** (App Router) · **React 19.3.1** · **TypeScript 7.0.2** · **Drizzle ORM 0.45.4** · **Zod 4.6.7** · **Vitest 5.0.4** · **Biome 2.5.15** · **Playwright 1.64.1**.",
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user