Spaces:
Running
Running
File size: 7,367 Bytes
31b3c47 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import ts from "typescript";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
// CRLF-normalized so the line-anchored checks hold on a Windows checkout.
const CI = readFileSync(join(ROOT, ".github/workflows/ci.yml"), "utf8").replace(/\r\n/g, "\n");
const PACKAGE = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf8"));
const LOCK = JSON.parse(readFileSync(join(ROOT, "package-lock.json"), "utf8"));
// The SHAs szl-holdings/.github already runs, so the org has one pin per action.
const APPROVED_PINS = {
"actions/checkout": "3d3c42e5aac5ba805825da76410c181273ba90b1", // v7.0.1
"actions/setup-node": "820762786026740c76f36085b0efc47a31fe5020", // v7.0.0
"actions/setup-python": "5fda3b95a4ea91299a34e894583c3862153e4b97", // v7.0.0
"step-security/harden-runner": "e14015d583714f6e62063499dc959a02595150a1", // v2.21.1
};
function onBlock() {
const match = CI.match(/^on:\n([\s\S]*?)^(?=\S)/m);
assert.ok(match, "missing on: block");
return match[1];
}
function jobSteps(job) {
const match = CI.match(
new RegExp(String.raw`^ ${job}:\n([\s\S]*?)(?=^ [A-Za-z_][\w-]*:\n|(?![\s\S]))`, "m"),
);
assert.ok(match, `missing job ${job}`);
return match[1];
}
test("CI runs on every pull request and every push to main, with no path filter", () => {
const on = onBlock();
assert.match(on, /^ {2}push:\n {4}branches: \[main\]$/m);
assert.match(on, /^ {2}pull_request:$/m);
assert.doesNotMatch(on, /paths(?:-ignore)?:/);
assert.doesNotMatch(on, /branches-ignore:/);
});
test("CI is read-only", () => {
assert.match(CI, /^permissions:\n {2}contents: read\n/m);
assert.doesNotMatch(CI, /:\s*write\b/);
assert.doesNotMatch(CI, /secrets\./);
assert.doesNotMatch(CI, /persist-credentials: true/);
});
test("every action is pinned to the SHA the org already uses", () => {
const uses = [...CI.matchAll(/^\s*uses:\s*(\S+)/gm)].map((match) => match[1]);
assert.ok(uses.length >= 6);
for (const value of uses) {
const [action, ref] = value.split("@");
assert.ok(action in APPROVED_PINS, `unapproved action: ${value}`);
assert.equal(ref, APPROVED_PINS[action], `wrong pin for ${action}`);
}
});
test("each job hardens the runner before anything else runs", () => {
for (const job of ["node", "python", "space"]) {
const steps = jobSteps(job);
const firstUses = steps.match(/^\s*uses:\s*(\S+)/m);
assert.ok(firstUses, `${job}: no steps`);
assert.match(firstUses[1], /^step-security\/harden-runner@/, `${job}: first step`);
}
});
test("the node job runs the full npm gate", () => {
const steps = jobSteps("node");
const order = ["npm ci", "npm run typecheck", "npm run lint", "npm test", "npm run build"];
let last = -1;
for (const command of order) {
const at = steps.indexOf(`run: ${command}\n`);
assert.notEqual(at, -1, `node job does not run: ${command}`);
assert.ok(at > last, `${command} is out of order`);
last = at;
}
for (const script of ["typecheck", "lint", "test", "build"]) {
assert.ok(PACKAGE.scripts[script], `package.json has no "${script}" script`);
}
});
test("TypeScript stays on the supported 6.0 patch line with an exact matching lock", () => {
// typescript-eslint 8.70.1 and 8.71.0 admit >=4.8.4 <6.1.0, not TS 7.
// A future minor/major migration must qualify its lint peer contract too.
assert.match(PACKAGE.devDependencies.typescript, /^~6\.0\.\d+$/);
assert.equal(LOCK.packages[""].devDependencies.typescript, PACKAGE.devDependencies.typescript);
assert.match(LOCK.packages["node_modules/typescript"].version, /^6\.0\.\d+$/);
assert.equal(
LOCK.packages["node_modules/typescript-eslint"].peerDependencies.typescript,
">=4.8.4 <6.1.0",
);
});
test("CI never overrides incompatible dependency peers to pass installation", () => {
assert.doesNotMatch(CI, /--legacy-peer-deps|--force\b|strict-peer-deps:\s*false/);
});
test("Nitro's optional cache peer is explicit without overriding Babel's major", () => {
assert.equal(PACKAGE.devDependencies["lru-cache"], "11.5.3");
assert.equal(LOCK.packages[""].devDependencies["lru-cache"], "11.5.3");
assert.equal(LOCK.packages["node_modules/lru-cache"].version, "11.5.3");
assert.equal(
LOCK.packages["node_modules/@babel/helper-compilation-targets/node_modules/lru-cache"].version,
"5.1.1",
);
});
test("the Space smoke retries health, root and source readback", () => {
const steps = jobSteps("space");
assert.match(steps, /fetch_with_retry\(\)/);
assert.match(
steps,
/fetch_with_retry http:\/\/127\.0\.0\.1:7860\/healthz \/tmp\/nexus-health\.json/,
);
assert.match(steps, /fetch_with_retry http:\/\/127\.0\.0\.1:7860\/ \/tmp\/nexus-root\.html/);
assert.match(
steps,
/fetch_with_retry http:\/\/127\.0\.0\.1:7860\/api\/build-info \/tmp\/nexus-build\.json/,
);
assert.match(steps, /--connect-timeout 2 --max-time 10/);
});
test("the Space smoke waits for Docker health instead of sampling once", () => {
const steps = jobSteps("space");
assert.match(steps, /wait_for_docker_health\(\)/);
assert.match(steps, /for attempt in \$\(seq 1 60\)/);
assert.match(steps, /if \[ "\$state" = healthy \]/);
assert.match(steps, /if \[ "\$state" = unhealthy \]/);
assert.match(steps, /container health did not stabilize/);
assert.doesNotMatch(
steps,
/test "\$\(docker inspect --format '\{\{\.State\.Health\.Status\}\}' "\$name"\)" = healthy/,
);
});
test("the source alias resolves without deprecated baseUrl or a suppression", () => {
const { config, error } = ts.readConfigFile(join(ROOT, "tsconfig.json"), ts.sys.readFile);
assert.equal(error, undefined);
assert.equal(config.compilerOptions.baseUrl, undefined);
assert.equal(config.compilerOptions.ignoreDeprecations, undefined);
const parsed = ts.parseJsonConfigFileContent(config, ts.sys, ROOT);
assert.equal(parsed.errors.length, 0);
const resolved = ts.resolveModuleName(
"@/lib/nexus/types",
join(ROOT, "src/routes/index.tsx"),
parsed.options,
ts.sys,
).resolvedModule;
assert.ok(resolved, "the compiler must retain the @/ source alias");
assert.equal(
resolved.resolvedFileName.replaceAll("\\", "/"),
join(ROOT, "src/lib/nexus/types.ts").replaceAll("\\", "/"),
);
});
test("the node job runs on both Node LTS lines the repository uses", () => {
const steps = jobSteps("node");
assert.match(steps, /^ {8}node: \["22", "24"\]$/m);
assert.match(steps, /node-version: \$\{\{ matrix\.node \}\}/);
assert.match(steps, /fail-fast: false/);
});
test("npm test runs the cross-platform runner, not a shell glob", () => {
assert.equal(PACKAGE.scripts.test, "node --experimental-strip-types scripts/run-tests.mjs");
assert.doesNotMatch(PACKAGE.scripts.test, /[*']/);
});
test("the python job compiles, self-tests and runs pytest when tests exist", () => {
const steps = jobSteps("python");
assert.match(steps, /git ls-files -z -- '\*\.py' \| xargs -0 -r python -m py_compile/);
assert.match(steps, /run: python server\.py --selftest\n/);
assert.match(steps, /run: python source_bound_server\.py --selftest\n/);
assert.match(steps, /python -m pytest -q/);
});
|