refactor: generate CI workflow from gate contracts

This commit is contained in:
DongHyeonka
2026-08-02 13:53:25 +09:00
parent 777ce5c9ed
commit 1bb2cc4a20
45 changed files with 8599 additions and 1177 deletions
@@ -10,6 +10,9 @@ import {
} from "node:fs/promises";
import path from "node:path";
import { parseCiGateContract } from "./contracts/ci-gates.ts";
import { generateCiWorkflow } from "./generate-ci-workflow.ts";
const fixtureRoot = path.resolve(
".tmp/browser-file-storage-runtime-removal",
);
@@ -29,6 +32,16 @@ const runtimePaths = [
const runtimeSourceRoots = runtimePaths.filter((entry) =>
entry.startsWith("src/"),
);
const removedScripts = new Set([
"test:browser-capabilities",
"verify:browser-capability-evidence",
"check:browser-file-storage-boundaries",
"test:browser-file-storage-removal",
]);
const removedEvidencePathFragments = [
"browser-capabilities",
"browser-file-storage-runtime-removal",
] as const;
const copyTargets = [
"src",
"tests",
@@ -281,12 +294,7 @@ const packagePath = path.join(fixtureRoot, "package.json");
const packageDocument = JSON.parse(await readFile(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
for (const script of [
"test:browser-capabilities",
"verify:browser-capability-evidence",
"check:browser-file-storage-boundaries",
"test:browser-file-storage-removal",
]) {
for (const script of removedScripts) {
delete packageDocument.scripts[script];
}
await writeFile(
@@ -308,39 +316,80 @@ await rm(
path.join(fixtureRoot, "scripts/test-browser-file-storage-runtime-removal.ts"),
{ force: true },
);
// The root snapshot locks the full repository inventory. This removal fixture
// validates its smaller registry through check:ci and its regenerated workflow.
await rm(
path.join(fixtureRoot, "tests/unit/ci-workflow-generation.test.ts"),
{ force: true },
);
await rm(
path.join(
fixtureRoot,
"tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap",
),
{ force: true },
);
const gatesPath = path.join(fixtureRoot, "config/ci/gates.json");
const gatesDocument = JSON.parse(
await readFile(gatesPath, "utf8"),
) as {
gates: Record<
string,
{
steps: Array<{ script: string }>;
evidence: string[];
}
>;
};
for (const gate of Object.values(gatesDocument.gates)) {
gate.steps = gate.steps.filter(
({ script }) =>
![
"test:browser-capabilities",
"verify:browser-capability-evidence",
"check:browser-file-storage-boundaries",
"test:browser-file-storage-removal",
].includes(script),
const gatesDocument = structuredClone(
parseCiGateContract(JSON.parse(await readFile(gatesPath, "utf8"))),
);
const removedCommandIds = new Set(
gatesDocument.commands
.filter(({ script }) => removedScripts.has(script))
.map(({ id }) => id),
);
if (removedCommandIds.size !== removedScripts.size) {
throw new Error("Browser file/storage CI command removal set is incomplete");
}
const removedArtifactIds = new Set(
gatesDocument.artifacts
.filter(({ path: artifactPath }) =>
removedEvidencePathFragments.some((fragment) =>
artifactPath.includes(fragment),
),
)
.map(({ id }) => id),
);
for (const fragment of removedEvidencePathFragments) {
if (
!gatesDocument.artifacts.some(({ path: artifactPath }) =>
artifactPath.includes(fragment),
)
) {
throw new Error(`Browser file/storage CI evidence is missing: ${fragment}`);
}
}
gatesDocument.commands = gatesDocument.commands.filter(
({ id }) => !removedCommandIds.has(id),
);
gatesDocument.artifacts = gatesDocument.artifacts.filter(
({ id }) => !removedArtifactIds.has(id),
);
for (const gate of gatesDocument.gates) {
gate.commandIds = gate.commandIds.filter(
(commandId) => !removedCommandIds.has(commandId),
);
gate.evidence = gate.evidence.filter(
(evidence) =>
!evidence.includes("browser-capabilities") &&
!evidence.includes("browser-file-storage-runtime-removal"),
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
(artifactId) => !removedArtifactIds.has(artifactId),
);
}
const referencedSchemaIds = new Set(
gatesDocument.artifacts.map(({ schemaId }) => schemaId),
);
gatesDocument.artifactSchemas = gatesDocument.artifactSchemas.filter(
({ id }) => referencedSchemaIds.has(id),
);
const validatedGates = parseCiGateContract(gatesDocument);
await writeFile(
gatesPath,
`${JSON.stringify(gatesDocument, null, 2)}\n`,
`${JSON.stringify(validatedGates, null, 2)}\n`,
);
await generateCiWorkflow({
root: fixtureRoot,
contract: validatedGates,
check: false,
});
await assertNoRuntimeImports(fixtureRoot);
const checks: Array<readonly [string, boolean]> = [