fix: harden CI evidence and removal contracts

This commit is contained in:
DongHyeonka
2026-08-02 14:48:04 +09:00
parent 1bb2cc4a20
commit f49d147b01
20 changed files with 1175 additions and 767 deletions
+118 -1
View File
@@ -123,6 +123,7 @@ describe("CI gate contract", () => {
["duplicate stage id", (value: Record<string, any>) => value.stages.push({ ...value.stages[0] }), /duplicate stage id/i],
["duplicate job id", (value: Record<string, any>) => value.jobs.push({ ...value.jobs[0] }), /duplicate job id/i],
["duplicate artifact path", (value: Record<string, any>) => value.artifacts.push({ ...value.artifacts[0], id: "duplicate-path" }), /duplicate artifact path/i],
["missing artifact production classification", (value: Record<string, any>) => delete value.artifacts[0].production, /production/i],
["unknown command reference", (value: Record<string, any>) => value.gates[0].commandIds.push("missing-command"), /unknown command missing-command/i],
["unknown artifact reference", (value: Record<string, any>) => (value.gates[0].logArtifactId = "missing-artifact"), /unknown artifact missing-artifact/i],
["unknown schema reference", (value: Record<string, any>) => (value.artifacts[0].schemaId = "missing-schema"), /unknown artifact schema missing-schema/i],
@@ -177,6 +178,27 @@ describe("CI gate contract", () => {
await expect(loadCiGateContract(root)).rejects.toThrow(/missing package scripts/i);
});
it("rejects swapped canonical gate command ownership", async () => {
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-gate-shape-"));
temporaryRoots.push(root);
await mkdir(path.join(root, "config/ci"), { recursive: true });
const contract = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>;
const security = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-013");
const documentation = contract.gates.find((gate: Record<string, any>) => gate.id === "FE-GATE-017");
[security.commandIds, documentation.commandIds] = [
documentation.commandIds,
security.commandIds,
];
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), await readFile("package.json"));
await expect(loadCiGateContract(root)).rejects.toThrow(
/canonical gate semantic shape|lacks a bound producer command/i,
);
});
it.each(["check:artifact-schemas", "check:ci-workflow"])(
"rejects a missing nested check:ci dependency: %s",
async (removedScript) => {
@@ -191,11 +213,29 @@ describe("CI gate contract", () => {
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
await expect(loadCiGateContract(root)).rejects.toThrow(
/missing package scripts|package script graph invalid/i,
/missing package scripts|package script graph invalid|canonical check:ci dependency/i,
);
},
);
it.each(["check:artifact-schemas", "check:ci-workflow"])(
"rejects a no-op nested check:ci dependency: %s",
async (bypassedScript) => {
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-script-meaning-"));
temporaryRoots.push(root);
await mkdir(path.join(root, "config/ci"), { recursive: true });
const contract = await loadCiGateContract(process.cwd());
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
scripts: Record<string, string>;
};
packageDocument.scripts[bypassedScript] = "true";
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
await expect(loadCiGateContract(root)).rejects.toThrow(/canonical check:ci dependency/i);
},
);
it.each([
["true bypass", "true"],
["direct self recursion", "corepack pnpm check:ci"],
@@ -292,6 +332,79 @@ describe("CI gate contract", () => {
expect(log.byteLength).toBeLessThanOrEqual(8_192);
expect(log.toString("utf8")).toMatch(/aggregate output|INFRASTRUCTURE_FAILURE/i);
}, 20_000);
it("rejects stale command-generated evidence from a successful no-op producer", async () => {
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-stale-evidence-"));
temporaryRoots.push(root);
await mkdir(path.join(root, "config/ci"), { recursive: true });
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
const contract = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>;
const command = contract.commands.find(
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
);
command.script = "test:stale-evidence-noop";
const evidence = contract.artifacts.find(
(entry: Record<string, any>) => entry.path === "artifacts/tests/runtime-schema.xml",
);
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
scripts: Record<string, string>;
};
packageDocument.scripts[command.script] = "true";
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
await writeFile(
path.join(root, evidence.path),
'<testsuite name="stale" tests="0" failures="0"/>\n',
);
const result = spawnSync(
process.execPath,
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
);
expect(result.status).toBe(1);
expect(
await readFile(path.join(root, "artifacts/quality/gates/FE-GATE-004.txt"), "utf8"),
).toMatch(/not freshly produced/i);
});
it("accepts a fresh deterministic rewrite with identical evidence bytes", async () => {
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-identical-rewrite-"));
temporaryRoots.push(root);
await mkdir(path.join(root, "config/ci"), { recursive: true });
await mkdir(path.join(root, "artifacts/tests"), { recursive: true });
const contract = JSON.parse(
JSON.stringify(await loadCiGateContract(process.cwd())),
) as Record<string, any>;
const command = contract.commands.find(
(entry: Record<string, any>) => entry.id === "test-runtime-schema",
);
command.script = "test:identical-evidence-rewrite";
const evidence = contract.artifacts.find(
(entry: Record<string, any>) => entry.path === "artifacts/tests/runtime-schema.xml",
);
const evidenceBytes = '<testsuite name="deterministic" tests="0" failures="0"/>\n';
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
scripts: Record<string, string>;
};
packageDocument.scripts[command.script] =
`node -e 'require("node:fs").writeFileSync("${evidence.path}", Buffer.from("${Buffer.from(evidenceBytes).toString("base64")}", "base64"))'`;
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
await writeFile(path.join(root, evidence.path), evidenceBytes);
const result = spawnSync(
process.execPath,
[path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-004"],
{ cwd: root, encoding: "utf8", env: { ...process.env, CI: "false" } },
);
expect(result.status).toBe(0);
expect(result.stdout).toMatch(/FE-GATE-004 runtime-schema: PASS/);
});
});
describe("CI workflow generation", () => {
@@ -311,6 +424,10 @@ describe("CI workflow generation", () => {
);
expect(first).not.toContain("process_dist_sha256");
expect(first).toContain("persist-credentials: false");
expect(
first.match(/corepack pnpm install --frozen-lockfile --ignore-scripts/gu),
).toHaveLength(9);
expect(first).not.toMatch(/corepack pnpm install --frozen-lockfile$/mu);
expect(first).toContain("verify-ci-candidate-archive.ts --archive");
expect(first).toContain("--extract-to");
expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u);