편집 폼이 릴리즈 목록 아래에 열렸다. 릴리즈가 열 개 스무 개로 늘면 편집하려고 목록 전체를 지나 내려가야 하고, 저장 버튼은 그보다 더 아래에 있다. 목록이 길어질수록 편집이 멀어지는 구조다. `/studio/releases/:id` 를 연다 — 문서와 프로젝트가 각자 편집 주소를 갖는 것과 같은 이유다. 목록의 "편집" 은 토글이 아니라 링크가 되고, 새 릴리즈를 만들면 곧바로 그 화면으로 간다(만들자마자 편집할 것이 분명하다). 라우트가 하나 늘어 CI 게이트가 함께 움직였다 — 아티팩트 기준선 133→134, 증거 개수 112→113, 게이트 형태 다이제스트 재계산(f9e7e521… 을 이전 gates.json 에서 먼저 재현해 계산 방법을 확인했다), 서빙 패턴 하나 추가. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEHXspz4rv5pB5wiiSsVDu
1665 lines
77 KiB
TypeScript
1665 lines
77 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { constants } from "node:fs";
|
|
import { spawnSync } from "node:child_process";
|
|
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
CI_ACTION_REGISTRY,
|
|
indexCiGateContract,
|
|
loadCiGateContract,
|
|
parseCiActionRegistry,
|
|
parseCiGateContract,
|
|
resolveCiActionUses,
|
|
withCiGatePreflight,
|
|
} from "../../scripts/contracts/ci-gates.ts";
|
|
import {
|
|
createCiWorkflowGenerator,
|
|
generateCiWorkflow,
|
|
renderCiWorkflow,
|
|
type CiWorkflowFileSystem,
|
|
} from "../../scripts/generate-ci-workflow.ts";
|
|
import {
|
|
validateInstallScriptPolicy,
|
|
validatePackageScriptGraph,
|
|
} from "../../scripts/lib/package-script-graph.ts";
|
|
|
|
const temporaryRoots: string[] = [];
|
|
const npmPostScriptForeignScopeCommands = [
|
|
"npm run nested --workspace fixture",
|
|
"npm run nested --workspace=fixture",
|
|
"npm run nested -w fixture",
|
|
"npm run nested --prefix fixture",
|
|
"npm run nested --prefix=fixture",
|
|
"npm test --workspace fixture",
|
|
"npm start --prefix fixture",
|
|
] as const;
|
|
const npmLifecycleHookDispatches = [
|
|
["npm run nested", "nested"],
|
|
["npm run-script nested", "nested"],
|
|
["npm start", "start"],
|
|
["npm stop", "stop"],
|
|
["npm test", "test"],
|
|
["npm restart", "restart"],
|
|
] as const;
|
|
const npmDispatchExpansionCommands = [
|
|
"SCOPE=--workspace; npm run nested $SCOPE fixture",
|
|
"SCOPE=--workspace; npm run nested ${SCOPE} fixture",
|
|
"npm run nested $SCOPE fixture",
|
|
"npm test ${SCOPE}",
|
|
"npm run nested --if-present=${FLAG}",
|
|
] as const;
|
|
const npmDispatchScopeEnvironmentCommands = [
|
|
"npm_config_workspace=fixture npm run nested",
|
|
"NPM_CONFIG_WORKSPACES=true npm test",
|
|
"env npm_config_prefix=fixture npm run nested",
|
|
"/usr/bin/env npm_config_workspace=fixture npm run nested",
|
|
"export NpM_CoNfIg_PrEfIx=fixture && npm run nested",
|
|
"/bin/env npm_config_prefix=fixture npm run nested",
|
|
"command env npm_config_workspace=fixture npm run nested",
|
|
"command /opt/reviewer/bin/env NPM_CONFIG_WORKSPACES=true npm test",
|
|
"npm_config_prefix=fixture; export npm_config_prefix; npm run nested",
|
|
"npm_config_workspace=fixture && export npm_config_workspace && npm run nested",
|
|
"export npm_config_workspaces; npm_config_workspaces=true; npm test",
|
|
"set -a; npm_config_prefix=fixture; npm run nested",
|
|
"DYNAMIC_SCOPE=workspace; env npm_config_${DYNAMIC_SCOPE}=fixture npm run nested",
|
|
"DYNAMIC_SCOPE=prefix; npm_config_${DYNAMIC_SCOPE}=fixture npm run nested",
|
|
"DYNAMIC_SCOPE=workspaces; export npm_config_${DYNAMIC_SCOPE}=true; npm test",
|
|
"ENV_WRAPPER=env; $ENV_WRAPPER npm_config_prefix=fixture npm run nested",
|
|
"set +a; npm run nested",
|
|
"unset npm_config_prefix; npm run nested",
|
|
"export -n npm_config_workspace; npm run nested",
|
|
"eval 'export npm_config_prefix=fixture'; npm run nested",
|
|
". ./scope-env.sh; npm run nested",
|
|
"source ./scope-env.sh; npm run nested",
|
|
] as const;
|
|
const npmShellPrefixScopeEnvironmentCommands = [
|
|
"exec env npm_config_workspace=fixture npm run nested",
|
|
"exec /bin/env npm_config_prefix=fixture npm run nested",
|
|
"command exec env npm_config_workspaces=true npm test",
|
|
"command -- env npm_config_prefix=fixture npm run nested",
|
|
"command -p env npm_config_workspace=fixture npm run nested",
|
|
"SAFE=1 export npm_config_prefix=fixture; npm run nested",
|
|
"SAFE=1 set -a; npm_config_prefix=fixture; npm run nested",
|
|
"command --unknown env npm_config_prefix=fixture npm run nested",
|
|
"exec -a reviewer env npm_config_workspace=fixture npm run nested",
|
|
] as const;
|
|
const npmIndirectConfigAuthorityCommands = [
|
|
"npm --userconfig fixture run nested",
|
|
"npm --globalconfig=fixture test",
|
|
"npm --userconfig fixture ci --ignore-scripts",
|
|
"npm --globalconfig fixture audit",
|
|
"NPM_CONFIG_USERCONFIG=fixture npm run nested",
|
|
"npm_config_globalconfig=fixture npm test",
|
|
"env NPM_CONFIG_USERCONFIG=fixture npm ci --ignore-scripts",
|
|
"/usr/bin/env npm_config_globalconfig=fixture npm audit",
|
|
] as const;
|
|
const npmPreDelimiterGlobCommands = [
|
|
"npm run nested *",
|
|
"npm test ?",
|
|
"npm run nested [a-z]*",
|
|
] as const;
|
|
const unmodeledManagerPrefixCommands = [
|
|
"nice env npm_config_workspace=fixture npm run nested",
|
|
"/usr/bin/nice env npm_config_prefix=fixture npm run nested",
|
|
"nohup env npm_config_workspaces=true npm test",
|
|
"SAFE=1 nice env npm_config_workspace=fixture npm run nested",
|
|
"nice npm run nested",
|
|
"time pnpm nested",
|
|
] as const;
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
async function createUnsafeCiGateFixture(
|
|
command: string,
|
|
additionalScripts: Readonly<Record<string, string>> = {},
|
|
): Promise<string> {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-unsafe-command-"));
|
|
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 packageDocument = JSON.parse(await readFile("package.json", "utf8")) as {
|
|
scripts: Record<string, string>;
|
|
};
|
|
contract.commands[0].script = "unsafe:preflight";
|
|
packageDocument.scripts["unsafe:preflight"] = command;
|
|
Object.assign(packageDocument.scripts, additionalScripts);
|
|
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
|
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
|
return root;
|
|
}
|
|
|
|
describe("CI gate contract", () => {
|
|
it.each([
|
|
["unknown-field.json", /unrecognized|unknown/i],
|
|
["duplicate-gate-id.json", /duplicate gate id/i],
|
|
["missing-artifact-schema.json", /unknown artifact schema/i],
|
|
])("rejects %s before projection", async (fixture, diagnostic) => {
|
|
const candidate = JSON.parse(
|
|
await readFile(`tests/fixtures/ci-contract/${fixture}`, "utf8"),
|
|
);
|
|
expect(() => parseCiGateContract(candidate)).toThrow(diagnostic);
|
|
});
|
|
|
|
it.each([
|
|
["unknown-job-dependency.json", /unknown job dependency/i],
|
|
["job-cycle.json", /job dependency cycle/i],
|
|
["unowned-gate.json", /unowned gate: FE-GATE-001/i],
|
|
["multiply-owned-gate.json", /multiply owned gate: FE-GATE-001/i],
|
|
])("applies and rejects physical semantic fixture %s", async (fixture, diagnostic) => {
|
|
const mutation = JSON.parse(
|
|
await readFile(`tests/fixtures/ci-contract/${fixture}`, "utf8"),
|
|
) as Readonly<{
|
|
jobId: string;
|
|
needs?: string[];
|
|
removeGateId?: string;
|
|
addGateId?: string;
|
|
}>;
|
|
const candidate = JSON.parse(
|
|
JSON.stringify(await loadCiGateContract(process.cwd())),
|
|
) as Record<string, any>;
|
|
const job = candidate.jobs.find(
|
|
(entry: Record<string, any>) => entry.id === mutation.jobId,
|
|
);
|
|
if (!job) throw new Error(`fixture job does not exist: ${mutation.jobId}`);
|
|
if (mutation.needs) job.needs = mutation.needs;
|
|
if (mutation.removeGateId) {
|
|
job.gateIds = job.gateIds.filter((id: string) => id !== mutation.removeGateId);
|
|
}
|
|
if (mutation.addGateId) job.gateIds.push(mutation.addGateId);
|
|
expect(() => parseCiGateContract(candidate)).toThrow(diagnostic);
|
|
});
|
|
|
|
it("loads the strict v2 registry and preserves the reviewed baseline", async () => {
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const index = indexCiGateContract(contract);
|
|
expect(contract.schemaVersion).toBe(2);
|
|
expect(contract.gates.map(({ id }) => id)).toEqual(
|
|
Array.from({ length: 27 }, (_, index) =>
|
|
`FE-GATE-${String(index + 1).padStart(3, "0")}`,
|
|
),
|
|
);
|
|
expect(contract.jobs).toHaveLength(9);
|
|
// Final fix wave item 1: FE-GATE-010 gained `check-tech-log-contract`.
|
|
// Alignment follow-up item 2: FE-GATE-007 gained `test-tech-log`.
|
|
// Dev release manifest drift fix item 2: FE-GATE-010 gained
|
|
// `check-dev-release-manifest`.
|
|
expect(contract.commands).toHaveLength(85);
|
|
expect(contract.gates.reduce((total, gate) => total + gate.commandIds.length, 0)).toBe(97);
|
|
expect(contract.commands.filter(({ expect }) => expect === "fail")).toHaveLength(23);
|
|
// Template merge. Recounted from the merged config/ci/gates.json rather
|
|
// than taking either side's number.
|
|
// Task 11 added TECH_LOG_STUDIO_ASSETS's manual accessibility evidence.
|
|
// Alignment follow-up item 2 added the TechLog junit report.
|
|
expect(contract.gates.reduce((total, gate) => total + gate.evidenceArtifactIds.length, 0)).toBe(113);
|
|
expect(contract.artifacts).toHaveLength(134);
|
|
expect(contract.stages).toHaveLength(5);
|
|
expect(contract.retention.classes).toHaveLength(5);
|
|
expect(index.gates.get("FE-GATE-015")?.commandIds).toHaveLength(2);
|
|
expect(index.gates.get("FE-GATE-020")?.name).toBe("removability");
|
|
});
|
|
|
|
it("rejects accessibility evidence registrations that drift from the installed route scope", async () => {
|
|
const candidate = JSON.parse(
|
|
JSON.stringify(await loadCiGateContract(process.cwd())),
|
|
) as Record<string, any>;
|
|
const gate = candidate.gates.find(
|
|
(entry: Record<string, any>) => entry.id === "FE-GATE-009",
|
|
);
|
|
const artifactId =
|
|
"artifact-artifacts-tests-a11y-manual-TECH-LOG-HOME-md";
|
|
candidate.artifacts = candidate.artifacts.filter(
|
|
(entry: Record<string, any>) => entry.id !== artifactId,
|
|
);
|
|
gate.evidenceArtifactIds = gate.evidenceArtifactIds.filter(
|
|
(id: string) => id !== artifactId,
|
|
);
|
|
|
|
expect(() =>
|
|
parseCiGateContract(candidate, { mode: "removal-fixture" }),
|
|
).toThrow(/FE-GATE-009 manual accessibility evidence.*installed route scope/i);
|
|
});
|
|
|
|
it("keeps the action registry recursively immutable and resolves only known actions", () => {
|
|
expect(Object.isFrozen(CI_ACTION_REGISTRY)).toBe(true);
|
|
expect(Object.values(CI_ACTION_REGISTRY).every((action) => Object.isFrozen(action))).toBe(true);
|
|
expect(resolveCiActionUses("checkout")).toBe(
|
|
"https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5",
|
|
);
|
|
expect(() => resolveCiActionUses("actions/checkout@v4" as never)).toThrow(/unknown CI action/i);
|
|
});
|
|
|
|
it.each([
|
|
["unknown action", (registry: Record<string, any>) => { registry.unknown = registry.checkout; }, /unrecognized|unknown/i],
|
|
["movable branch", (registry: Record<string, any>) => { registry.checkout.revision = "v4"; }, /full 40-hex commit SHA/i],
|
|
["short SHA", (registry: Record<string, any>) => { registry.checkout.revision = "34e114876b0b"; }, /full 40-hex commit SHA/i],
|
|
["relative repository", (registry: Record<string, any>) => { registry.checkout.repository = "actions/checkout"; }, /absolute upstream GitHub URL/i],
|
|
])("rejects an unsafe CI action registry mutation: %s", (_name, mutate, diagnostic) => {
|
|
const candidate = JSON.parse(JSON.stringify(CI_ACTION_REGISTRY)) as Record<string, any>;
|
|
mutate(candidate);
|
|
expect(() => parseCiActionRegistry(candidate)).toThrow(diagnostic);
|
|
});
|
|
|
|
it.each([
|
|
["unknown nested field", (value: Record<string, any>) => (value.commands[0].unknown = true), /unrecognized|unknown/i],
|
|
["duplicate command id", (value: Record<string, any>) => value.commands.push({ ...value.commands[0] }), /duplicate command id/i],
|
|
["duplicate command tuple", (value: Record<string, any>) => value.commands.push({ ...value.commands[0], id: "duplicate-tuple" }), /duplicate command tuple/i],
|
|
["duplicate command reference", (value: Record<string, any>) => value.gates[0].commandIds.push(value.gates[0].commandIds[0]), /duplicate command reference within gate/i],
|
|
["duplicate artifact id", (value: Record<string, any>) => value.artifacts.push({ ...value.artifacts[0] }), /duplicate artifact id/i],
|
|
["duplicate artifact schema id", (value: Record<string, any>) => value.artifactSchemas.push({ ...value.artifactSchemas[0] }), /duplicate artifact schema id/i],
|
|
["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],
|
|
["unknown retention reference", (value: Record<string, any>) => (value.gates[0].retentionClassId = "missing-retention"), /unknown retention class missing-retention/i],
|
|
["unsafe artifact path", (value: Record<string, any>) => (value.artifacts[0].path = "../escape"), /unsafe repository path/i],
|
|
["artifact path controls", (value: Record<string, any>) => (value.artifacts[0].path = "artifacts/bad\n.json"), /control and Unicode line-break characters/i],
|
|
["empty commands", (value: Record<string, any>) => (value.commands = []), /too small|at least 1/i],
|
|
["empty artifacts", (value: Record<string, any>) => (value.artifacts = []), /too small|at least 1/i],
|
|
["empty gates", (value: Record<string, any>) => (value.gates = []), /too small|at least 1/i],
|
|
["empty gate command refs", (value: Record<string, any>) => (value.gates[0].commandIds = []), /too small|at least 1/i],
|
|
["empty gate evidence refs", (value: Record<string, any>) => (value.gates[0].evidenceArtifactIds = []), /too small|at least 1/i],
|
|
["unowned gate", (value: Record<string, any>) => (value.jobs[0].gateIds = value.jobs[0].gateIds.filter((id: string) => id !== "FE-GATE-001")), /unowned gate: FE-GATE-001/i],
|
|
["multiply owned gate", (value: Record<string, any>) => value.jobs[1].gateIds.push("FE-GATE-001"), /multiply owned gate: FE-GATE-001/i],
|
|
["duplicated immutable gate", (value: Record<string, any>) => value.jobs[1].gateIds.push("FE-GATE-015"), /release matrix duplicates FE-GATE-015/i],
|
|
["unreachable producer", (value: Record<string, any>) => (value.jobs.find((job: any) => job.id === "promotion").needs = ["immutable_build", "provenance_provider"]), /download producer vulnerability_provider is unreachable/i],
|
|
["missing producer", (value: Record<string, any>) => { const producer = value.jobs.find((job: any) => job.id === "immutable_build"); producer.steps = producer.steps.filter((step: any) => step.kind !== "upload"); }, /unknown download transfer release-candidate|job step sequence drift/i],
|
|
["promotion rebuild authority", (value: Record<string, any>) => value.jobs.find((job: any) => job.id === "promotion").steps.splice(3, 0, { kind: "archive-candidate", stepId: "bad", archivePath: ".release/bad.tar.gz", members: ["dist"], archiveOutputName: "bad", distOutputName: "bad-dist" }), /promotion job must not build|step kind archive-candidate is forbidden/i],
|
|
["browser gate drift", (value: Record<string, any>) => (value.jobs[0].browserGateIds = ["FE-GATE-008"]), /browser gate set drift/i],
|
|
["missing gate execution step", (value: Record<string, any>) => (value.jobs[0].steps = value.jobs[0].steps.filter((step: any) => step.kind !== "run-gate")), /canonical job step sequence drift/i],
|
|
["browser install drift", (value: Record<string, any>) => (value.jobs[0].steps = value.jobs[0].steps.filter((step: any) => step.kind !== "browser-install")), /browser install step drift|job step sequence drift/i],
|
|
["job environment drift", (value: Record<string, any>) => value.jobs[0].environment.push({ name: "UNOWNED", value: "x" }), /job environment binding drift/i],
|
|
["job kind drift", (value: Record<string, any>) => (value.jobs[0].kind = "gate-single"), /job graph drift/i],
|
|
["job needs drift", (value: Record<string, any>) => (value.jobs[1].needs = []), /job graph drift/i],
|
|
["job condition drift", (value: Record<string, any>) => (value.jobs[1].condition = "always"), /job graph drift/i],
|
|
["promotion success condition drift", (value: Record<string, any>) => (value.jobs.find((job: any) => job.id === "promotion").condition = "always"), /job graph drift|promotion.*condition/i],
|
|
["candidate output identity drift", (value: Record<string, any>) => { const job = value.jobs.find((candidate: any) => candidate.id === "immutable_build"); job.steps.find((step: any) => step.kind === "archive-candidate").archiveOutputName = "renamed"; }, /candidate output identity drift/i],
|
|
["stage cycle", (value: Record<string, any>) => (value.stages[0].needs = ["release"]), /stage dependency cycle/i],
|
|
["provider adapter target drift", (value: Record<string, any>) => (value.providerAdapter = "package.json"), /canonical generated workflow/i],
|
|
["promotion standalone extraction", (value: Record<string, any>) => value.jobs.find((candidate: any) => candidate.id === "promotion").steps.splice(6, 0, { kind: "extract", archivePath: ".release/candidate/candidate.tar.gz", targetRoot: ".release/verified-candidate" }), /step kind extract is forbidden|job step sequence drift/i],
|
|
["normalized upload root", (value: Record<string, any>) => (value.jobs[0].steps.find((step: any) => step.kind === "upload").paths = ["foo/.."]), /unsafe workflow path/i],
|
|
["immutable archive field drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "immutable_build").steps.find((step: any) => step.kind === "archive-candidate").archivePath = ".release/other.tar.gz"), /candidate output identity drift|archive and upload fields must remain linked/i],
|
|
["provider role drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "vulnerability_provider").steps.find((step: any) => step.kind === "run-provider").provider = "provenance"), /provider archive, extraction, evidence, and upload fields must remain linked/i],
|
|
["provider archive SHA environment drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "vulnerability_provider").environment.find((entry: any) => entry.name === "CANDIDATE_ARCHIVE_SHA256").value = "0".repeat(64)), /job environment binding drift/i],
|
|
["promotion transfer swap", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "promotion").steps.find((step: any) => step.kind === "download").transferId = "vulnerability-provider-evidence"), /promotion download fields.*remain linked|duplicate.*download/i],
|
|
["raw provider upload", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "vulnerability_provider").steps.find((step: any) => step.kind === "upload").paths = ["provider-evidence/untrusted/vulnerability-report.json"]), /provider archive, extraction, evidence, and upload fields must remain linked/i],
|
|
["intervening promotion step", (value: Record<string, any>) => { const steps = value.jobs.find((candidate: any) => candidate.id === "promotion").steps; steps.splice(steps.findIndex((step: any) => step.kind === "upload"), 0, { kind: "frozen-install" }); }, /promotion verification and upload must be immediately adjacent/i],
|
|
["promotion upload path drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "promotion").steps.find((step: any) => step.kind === "upload").paths[0] = ".release/promoted-staging/replaced.tar.gz"), /exact five typed paths/i],
|
|
["always promotion upload", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "promotion").steps.find((step: any) => step.kind === "upload").always = true), /promotion upload must not use always/i],
|
|
["provider nonce output step drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "vulnerability_provider").steps.find((step: any) => step.kind === "run-provider").stepId = "renamed"), /provider.*linked|step identity/i],
|
|
["promotion nonce binding drift", (value: Record<string, any>) => (value.jobs.find((candidate: any) => candidate.id === "promotion").environment.find((entry: any) => entry.name === "VULNERABILITY_INVOCATION_NONCE").value = "5".repeat(64)), /job environment binding drift/i],
|
|
["missing promotion cleanup", (value: Record<string, any>) => { const job = value.jobs.find((candidate: any) => candidate.id === "promotion"); job.steps = job.steps.filter((step: any) => step.kind !== "cleanup-promotion"); }, /job step sequence drift|cleanup/i],
|
|
])("rejects semantic mutation: %s", async (_name, mutate, diagnostic) => {
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const candidate = JSON.parse(JSON.stringify(contract)) as Record<string, any>;
|
|
mutate(candidate);
|
|
expect(() => parseCiGateContract(candidate)).toThrow(diagnostic);
|
|
});
|
|
|
|
it("rejects package scripts missing from the shared command registry", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-package-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
|
await writeFile(path.join(root, "package.json"), '{"scripts":{}}\n');
|
|
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(
|
|
/lacks a bound producer command/i,
|
|
);
|
|
});
|
|
|
|
it("rejects canonical gate name drift through the semantic-shape digest", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-gate-name-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 gate = contract.gates.find(
|
|
(candidate: Record<string, any>) => candidate.id === "FE-GATE-013",
|
|
);
|
|
gate.name = `${gate.name}-renamed`;
|
|
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/i,
|
|
);
|
|
});
|
|
|
|
it.each(["check:artifact-schemas", "check:ci-workflow"])(
|
|
"rejects a missing nested check:ci dependency: %s",
|
|
async (removedScript) => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-script-graph-"));
|
|
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>;
|
|
};
|
|
delete packageDocument.scripts[removedScript];
|
|
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|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"],
|
|
["alias cycle", "corepack pnpm check:ci-alias"],
|
|
["option-form gate alias", "corepack pnpm --silent ci:gate"],
|
|
])("rejects non-canonical check:ci orchestration: %s", async (_name, command) => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-closed-script-"));
|
|
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["check:ci"] = command;
|
|
packageDocument.scripts["check:ci-alias"] = "corepack pnpm check:ci";
|
|
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(/exact canonical non-recursive orchestration/i);
|
|
});
|
|
|
|
it.each([
|
|
"npm ci --ignore-scripts --no-ignore-scripts",
|
|
"pnpm install --ignore-scripts --config.ignore-scripts=false",
|
|
"pnpm ln fixture",
|
|
])("rejects an unsafe lifecycle in every contract command entry: %s", async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command);
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|ignore-scripts/i,
|
|
);
|
|
});
|
|
|
|
it("does not enter the production runner callback when contract preflight fails", async () => {
|
|
const root = await createUnsafeCiGateFixture(
|
|
"npm ci --ignore-scripts --no-ignore-scripts",
|
|
);
|
|
const contracts = await import("../../scripts/contracts/ci-gates.ts");
|
|
const preflight = (contracts as typeof contracts & {
|
|
withCiGatePreflight?: (
|
|
root: string,
|
|
gateId: string | undefined,
|
|
execute: (context: unknown) => Promise<void>,
|
|
) => Promise<void>;
|
|
}).withCiGatePreflight;
|
|
expect(preflight).toBeTypeOf("function");
|
|
if (!preflight) return;
|
|
let executed = false;
|
|
await expect(
|
|
preflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|ignore-scripts/i);
|
|
expect(executed).toBe(false);
|
|
});
|
|
|
|
it.each(npmPostScriptForeignScopeCommands)(
|
|
"rejects post-script foreign npm scope before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
start: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
...npmDispatchExpansionCommands,
|
|
...npmDispatchScopeEnvironmentCommands,
|
|
])(
|
|
"rejects dynamic npm dispatch scope before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each(npmShellPrefixScopeEnvironmentCommands)(
|
|
"rejects unsafe npm shell prefix before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each(unmodeledManagerPrefixCommands)(
|
|
"rejects an unmodeled manager prefix before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each(npmIndirectConfigAuthorityCommands)(
|
|
"rejects indirect npm config authority before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each(npmPreDelimiterGlobCommands)(
|
|
"rejects pre-delimiter npm glob expansion before entering the runner callback: %s",
|
|
async (command) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
});
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|not safely parseable/i,
|
|
);
|
|
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("rejects scoped npm lifecycle before entering the runner callback", async () => {
|
|
const root = await createUnsafeCiGateFixture(
|
|
"npm_config_prefix=fixture npm ci --ignore-scripts",
|
|
);
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|not safely parseable/i);
|
|
expect(executed).toBe(false);
|
|
});
|
|
|
|
it.each(npmLifecycleHookDispatches)(
|
|
"rejects unsafe npm hooks before entering the runner callback: %s",
|
|
async (command, scriptName) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
[`pre${scriptName}`]: "npm ci",
|
|
[scriptName]: "echo root main safe",
|
|
[`post${scriptName}`]: "npm ci",
|
|
});
|
|
|
|
await expect(loadCiGateContract(root)).rejects.toThrow(
|
|
/install policy|install-bearing|ignore-scripts/i,
|
|
);
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|ignore-scripts/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("rejects inherited npm scope configuration before entering the runner callback", async () => {
|
|
const variable = "NpM_CoNfIg_WoRkSpAcE";
|
|
const previous = process.env[variable];
|
|
process.env[variable] = "fixture";
|
|
let executed = false;
|
|
try {
|
|
await expect(
|
|
withCiGatePreflight(process.cwd(), "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/npm.*scope.*environment|npm_config_workspace/i);
|
|
expect(executed).toBe(false);
|
|
} finally {
|
|
if (previous === undefined) delete process.env[variable];
|
|
else process.env[variable] = previous;
|
|
}
|
|
});
|
|
|
|
it.each(["NPM_CONFIG_USERCONFIG", "npm_config_globalconfig"])(
|
|
"rejects inherited indirect npm config authority before callback: %s",
|
|
async (variable) => {
|
|
const previous = process.env[variable];
|
|
process.env[variable] = "fixture";
|
|
let executed = false;
|
|
try {
|
|
await expect(
|
|
withCiGatePreflight(process.cwd(), "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/npm.*scope.*environment|npm_config/i);
|
|
expect(executed).toBe(false);
|
|
} finally {
|
|
if (previous === undefined) delete process.env[variable];
|
|
else process.env[variable] = previous;
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["npm run nested --ignore-scripts=false", "nested"],
|
|
["npm test -- --ignore-scripts=true", "test"],
|
|
])(
|
|
"keeps unsafe npm hooks inside contract preflight without effective suppression: %s",
|
|
async (command, scriptName) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
[`pre${scriptName}`]: "npm ci",
|
|
[scriptName]: "echo root main safe",
|
|
[`post${scriptName}`]: "npm ci",
|
|
});
|
|
let executed = false;
|
|
await expect(
|
|
withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
}),
|
|
).rejects.toThrow(/install policy|install-bearing|ignore-scripts/i);
|
|
expect(executed).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["npm --ignore-scripts=true run nested", "nested"],
|
|
["npm test --ignore-scripts=true", "test"],
|
|
["npm restart --ignore-scripts true", "restart"],
|
|
["npm --ignore-scripts run nested", "nested"],
|
|
["npm run nested --ignore-scripts", "nested"],
|
|
["npm run-script nested --ignore-scripts", "nested"],
|
|
["npm start --ignore-scripts", "start"],
|
|
["npm stop --ignore-scripts", "stop"],
|
|
["npm restart --ignore-scripts", "restart"],
|
|
])(
|
|
"allows contract execution when effective true suppresses npm hooks: %s",
|
|
async (command, scriptName) => {
|
|
const root = await createUnsafeCiGateFixture(command, {
|
|
[`pre${scriptName}`]: "npm ci",
|
|
[scriptName]: "echo root main safe",
|
|
[`post${scriptName}`]: "npm ci",
|
|
});
|
|
let executed = false;
|
|
await withCiGatePreflight(root, "FE-GATE-001", async () => {
|
|
executed = true;
|
|
});
|
|
expect(executed).toBe(true);
|
|
},
|
|
);
|
|
|
|
it("detects reachable alias cycles and option-form ci:gate invocations", () => {
|
|
expect(validatePackageScriptGraph({ "check:ci": "pnpm alias", alias: "pnpm check:ci" }, "check:ci"))
|
|
.toEqual(expect.arrayContaining([expect.stringMatching(/cycle/i)]));
|
|
expect(validatePackageScriptGraph({ "check:ci": "pnpm alias", alias: "pnpm --silent ci:gate", "ci:gate": "node scripts/run-ci-gate.ts" }, "check:ci"))
|
|
.toEqual(expect.arrayContaining([expect.stringMatching(/must not invoke ci:gate/i)]));
|
|
});
|
|
|
|
it.each(npmLifecycleHookDispatches)(
|
|
"traverses existing npm pre/main/post scripts in lifecycle order: %s",
|
|
(gate, scriptName) => {
|
|
const preScript = `pre${scriptName}`;
|
|
const postScript = `post${scriptName}`;
|
|
const scripts = {
|
|
gate,
|
|
[preScript]: "npm run missing:pre",
|
|
[scriptName]: "npm run missing:main",
|
|
[postScript]: "npm run missing:post",
|
|
};
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([
|
|
`package script missing: ${preScript} -> missing:pre`,
|
|
`package script missing: ${scriptName} -> missing:main`,
|
|
`package script missing: ${postScript} -> missing:post`,
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each(npmLifecycleHookDispatches)(
|
|
"applies install policy to existing npm pre/main/post scripts: %s",
|
|
(gate, scriptName) => {
|
|
const preScript = `pre${scriptName}`;
|
|
const postScript = `post${scriptName}`;
|
|
const scripts = {
|
|
gate,
|
|
[preScript]: "npm ci",
|
|
[scriptName]: "npm ci",
|
|
[postScript]: "npm ci",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([
|
|
`install-bearing package script must use --ignore-scripts: ${preScript}`,
|
|
`install-bearing package script must use --ignore-scripts: ${scriptName}`,
|
|
`install-bearing package script must use --ignore-scripts: ${postScript}`,
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["npm --ignore-scripts=true run nested", "nested"],
|
|
["npm --ignore-scripts true run nested", "nested"],
|
|
["npm run nested --ignore-scripts=true", "nested"],
|
|
["npm test --ignore-scripts=true", "test"],
|
|
["npm restart --ignore-scripts true", "restart"],
|
|
["npm --ignore-scripts run nested", "nested"],
|
|
["npm run nested --ignore-scripts", "nested"],
|
|
["npm run-script nested --ignore-scripts", "nested"],
|
|
["npm start --ignore-scripts", "start"],
|
|
["npm stop --ignore-scripts", "stop"],
|
|
["npm restart --ignore-scripts", "restart"],
|
|
])("omits npm hooks for effective suppression: %s", (gate, scriptName) => {
|
|
const scripts = {
|
|
gate,
|
|
[`pre${scriptName}`]: "npm ci",
|
|
[scriptName]: "echo root main safe",
|
|
[`post${scriptName}`]: "npm run missing:post",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
["npm --ignore-scripts=false run nested", "nested"],
|
|
["npm test --ignore-scripts=false", "test"],
|
|
["npm --ignore-scripts=true run nested --no-ignore-scripts", "nested"],
|
|
["npm restart --ignore-scripts=true --ignore-scripts=false", "restart"],
|
|
["npm run nested -- --ignore-scripts=true", "nested"],
|
|
["npm test -- --ignore-scripts=true", "test"],
|
|
])("keeps npm hooks for non-effective or post-delimiter suppression: %s", (gate, scriptName) => {
|
|
const preScript = `pre${scriptName}`;
|
|
const postScript = `post${scriptName}`;
|
|
const scripts = {
|
|
gate,
|
|
[preScript]: "npm ci",
|
|
[scriptName]: "echo root main safe",
|
|
[postScript]: "npm run missing:post",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
`install-bearing package script must use --ignore-scripts: ${preScript}`,
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
`package script missing: ${postScript} -> missing:post`,
|
|
);
|
|
});
|
|
|
|
it("rejects reachable nested package installs without lifecycle suppression", () => {
|
|
const unsafe = {
|
|
gate: "corepack pnpm nested",
|
|
nested: "corepack pnpm install --frozen-lockfile",
|
|
};
|
|
const safe = {
|
|
...unsafe,
|
|
nested: "corepack pnpm install --frozen-lockfile --ignore-scripts",
|
|
};
|
|
expect(validateInstallScriptPolicy(unsafe, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: nested",
|
|
);
|
|
expect(validateInstallScriptPolicy(safe, ["gate"])).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"corepack pnpm install --frozen-lockfile && echo --ignore-scripts",
|
|
"corepack pnpm install && corepack pnpm install --ignore-scripts",
|
|
])("rejects an unsafe install invocation masked by another token: %s", (nested) => {
|
|
const scripts = {
|
|
gate: "corepack pnpm nested",
|
|
nested,
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: nested",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"corepack pnpm --dir=fixture install",
|
|
"corepack pnpm --dir fixture install",
|
|
"pnpm -C fixture install",
|
|
"pnpm --dir . install",
|
|
"pnpm i",
|
|
"npm --prefix fixture install",
|
|
"npm ci",
|
|
"npm i",
|
|
"yarn --cwd fixture install",
|
|
])("rejects install aliases and manager-global-option forms: %s", (nested) => {
|
|
expect(validateInstallScriptPolicy({ gate: "pnpm nested", nested }, ["gate"]))
|
|
.toContain("install-bearing package script must use --ignore-scripts: nested");
|
|
});
|
|
|
|
it.each([
|
|
"corepack pnpm --dir",
|
|
"pnpm --unknown-manager-option install --ignore-scripts",
|
|
])("fails closed for an unparsed reachable manager invocation: %s", (nested) => {
|
|
expect(validateInstallScriptPolicy({ gate: "pnpm nested", nested }, ["gate"]))
|
|
.toContain("install-bearing package script must use --ignore-scripts: nested");
|
|
});
|
|
|
|
it("treats a single ampersand as a command boundary for install policy", () => {
|
|
const scripts = {
|
|
gate: "pnpm install & pnpm install --ignore-scripts",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm install --ignore-scripts=false",
|
|
"pnpm install --ignore-scripts --ignore-scripts=false",
|
|
"npm ci --ignore-scripts=false",
|
|
"npm install --ignore-scripts=true --ignore-scripts=false",
|
|
"yarn install --ignore-scripts=false",
|
|
"yarn install --ignore-scripts --ignore-scripts=false",
|
|
"pnpm install --ignore-scripts=",
|
|
"npm ci --ignore-scripts=maybe",
|
|
"yarn install --ignore-scripts false",
|
|
"pnpm install --ignore-scripts=false --ignore-scripts=true",
|
|
])("rejects ineffective or contradictory lifecycle suppression: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm install --ignore-scripts=true",
|
|
"npm ci --ignore-scripts",
|
|
"yarn install --ignore-scripts=true --ignore-scripts=true",
|
|
])("accepts an unambiguous effective ignore-scripts true: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm --silent run nested",
|
|
"yarn nested",
|
|
"yarn run nested",
|
|
"corepack yarn nested",
|
|
])("follows manager-option and yarn script invocations into unsafe installs: %s", (gate) => {
|
|
const scripts = { gate, nested: "npm ci" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: nested",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"npm --silent run missing",
|
|
"corepack yarn run missing",
|
|
])("uses the manager parser for package graph dependencies: %s", (gate) => {
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toContain(
|
|
"package script missing: gate -> missing",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"yarn workspace fixture run nested",
|
|
"yarn workspace fixture nested",
|
|
"yarn workspaces foreach -A run nested",
|
|
])("fails closed for an unmodeled Yarn workspace dispatcher: %s", (gate) => {
|
|
const scripts = { gate, nested: "npm ci" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm install # --ignore-scripts",
|
|
"npm ci # --ignore-scripts=true",
|
|
])("fails closed when an unquoted shell comment masks lifecycle suppression: %s", (gate) => {
|
|
const scripts = { gate };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it("keeps a quoted hash as ordinary lifecycle argument content", () => {
|
|
const scripts = { gate: "pnpm install '#' --ignore-scripts" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm ci --ignore-scripts --no-ignore-scripts",
|
|
"pnpm install --ignore-scripts --config.ignore-scripts=false",
|
|
"pnpm install --ignore-scripts --unknown-lifecycle-setting",
|
|
"pnpm install --config.ignore-scripts=maybe --ignore-scripts",
|
|
])("rejects contradictory, malformed, or unknown lifecycle settings: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"npm ci --ignore-scripts true",
|
|
"pnpm install --config.ignore-scripts",
|
|
"pnpm install --config.ignore-scripts=true",
|
|
"pnpm install --config.ignore-scripts true --frozen-lockfile",
|
|
])("accepts a supported canonical lifecycle suppression form: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toEqual([]);
|
|
});
|
|
|
|
it("canonicalizes a lifecycle builtin alias before package-script lookup", () => {
|
|
const scripts = {
|
|
gate: "pnpm ln fixture",
|
|
ln: "npm ci",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm frobnicate",
|
|
"yarn frobnicate",
|
|
])("fails closed for an unknown manager subcommand absent from root scripts: %s", (gate) => {
|
|
const scripts = { gate };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm --filter fixture run nested",
|
|
"pnpm --dir fixture run nested",
|
|
"pnpm -C fixture nested",
|
|
"npm --workspace fixture run nested",
|
|
"npm --prefix fixture run nested",
|
|
"yarn --cwd fixture run nested",
|
|
])("fails closed when package-script dispatch changes authoritative manifest scope: %s", (gate) => {
|
|
const scripts = { gate, nested: "echo root-only safe" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each(npmPostScriptForeignScopeCommands)(
|
|
"fails closed when post-script npm options change package-script scope: %s",
|
|
(gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
start: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
...npmDispatchExpansionCommands,
|
|
...npmDispatchScopeEnvironmentCommands,
|
|
])("fails closed for dynamic npm dispatch scope: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each(npmShellPrefixScopeEnvironmentCommands)(
|
|
"fails closed for unsafe npm shell prefix: %s",
|
|
(gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(unmodeledManagerPrefixCommands)(
|
|
"fails closed for an unmodeled manager prefix: %s",
|
|
(gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(npmIndirectConfigAuthorityCommands)(
|
|
"fails closed for indirect npm config authority: %s",
|
|
(gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(npmPreDelimiterGlobCommands)(
|
|
"fails closed for unquoted glob expansion before npm's literal delimiter: %s",
|
|
(gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"npm_config_prefix=fixture npm ci --ignore-scripts",
|
|
"env npm_config_workspace=fixture npm audit",
|
|
])("rejects npm scope environment for every npm command class: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"npm run nested --workspaces",
|
|
"npm run nested -wfixture",
|
|
"npm run nested -w=fixture",
|
|
"npm run nested --unknown-manager-option",
|
|
])("fails closed for additional or unknown pre-delimiter npm options: %s", (gate) => {
|
|
const scripts = { gate, nested: "echo root-only safe" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm run nested --filter foreign",
|
|
"pnpm nested --dir foreign",
|
|
"yarn run nested --cwd foreign",
|
|
"yarn nested --cwd foreign",
|
|
])("treats other-manager tokens after a script name as script arguments: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm run nested -- --workspace foreign",
|
|
"npm run-script nested -- --prefix foreign",
|
|
"npm test -- --workspace foreign",
|
|
"npm start -- --unknown-manager-option",
|
|
"SCOPE=--workspace; npm run nested -- $SCOPE fixture",
|
|
"SCOPE=--prefix; npm test -- ${SCOPE} fixture",
|
|
])("treats npm tokens after the script-argument delimiter as script arguments: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
start: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"SAFE=value npm run nested",
|
|
"npm_config_loglevel=warn npm run nested",
|
|
"/bin/env npm_config_loglevel=warn npm run nested",
|
|
"command env SAFE=value npm run nested",
|
|
"command -- env SAFE=value npm run nested",
|
|
"command -p env SAFE=value npm run nested",
|
|
"command /opt/reviewer/bin/env SAFE=value npm run nested",
|
|
"exec env SAFE=value npm run nested",
|
|
"exec /bin/env SAFE=value npm run nested",
|
|
"command exec env SAFE=value npm run nested",
|
|
"SAFE=1 command -- npm run nested",
|
|
"command exec env SAFE=value corepack npm run nested",
|
|
"SAFE=1 corepack pnpm nested",
|
|
"exec corepack pnpm nested",
|
|
"MESSAGE='npm_config_workspace=fixture' npm run nested",
|
|
"MESSAGE=\"npm_config_${SAFE_NAME}=fixture\" npm run nested",
|
|
"echo 'npm_config_workspace=fixture' && npm run nested",
|
|
"echo 'set -a; npm_config_prefix=fixture' && npm run nested",
|
|
"export npm_config_loglevel=warn && npm run nested",
|
|
"SAFE=value; export SAFE; npm run nested",
|
|
"SAFE=1 export npm_config_loglevel=warn; npm run nested",
|
|
"SAFE=1 set -a; SAFE_TWO=2; npm run nested",
|
|
"command npm run nested",
|
|
"exec npm run nested",
|
|
"env -i SAFE=value npm run nested",
|
|
"/usr/bin/env --ignore-environment npm test",
|
|
"env -u npm_config_workspace npm run nested",
|
|
"env --unset=NPM_CONFIG_PREFIX npm test",
|
|
"env -- npm run nested",
|
|
"env '$NAME=fixture' npm run nested",
|
|
"SCOPE=--workspace; pnpm run nested $SCOPE fixture",
|
|
"SCOPE=--workspace; yarn run nested ${SCOPE} fixture",
|
|
])("preserves unrelated environment and other-manager script arguments: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm run nested '*'",
|
|
'npm test "?"',
|
|
"npm run nested \\*",
|
|
"npm run nested -- *",
|
|
"npm test -- [a-z]*",
|
|
])("preserves quoted, escaped, or post-delimiter glob arguments: %s", (gate) => {
|
|
const scripts = { gate, nested: "echo root-only safe", test: "echo root-only safe" };
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm run nested",
|
|
"npm run nested --if-present",
|
|
"npm run nested --ignore-scripts=true",
|
|
"npm test --foreground-scripts",
|
|
"npm start",
|
|
])("keeps current-tree npm dispatch and exact harmless options supported: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
nested: "echo root-only safe",
|
|
start: "echo root-only safe",
|
|
test: "echo root-only safe",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm --dir fixture install --ignore-scripts",
|
|
"npm --prefix fixture ci --ignore-scripts",
|
|
"yarn --cwd fixture install --ignore-scripts",
|
|
])("still classifies externally scoped lifecycle commands by suppression: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toEqual([]);
|
|
});
|
|
|
|
it.each([
|
|
"npm init unreviewed-tool",
|
|
"npm explore fixture -- npm ci",
|
|
"npm audit fix",
|
|
"yarn npm publish",
|
|
])("fails closed for an argument-sensitive or mutating builtin dispatcher: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each(["npm audit", "pnpm audit"])(
|
|
"accepts only an exact bare read-only audit builtin: %s",
|
|
(gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toEqual([]);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"npm --silent audit",
|
|
"npm --prefix fixture audit",
|
|
"npm -- audit",
|
|
"npm audit --silent",
|
|
"npm audit --",
|
|
"pnpm --silent audit",
|
|
"pnpm --dir fixture audit",
|
|
"pnpm -- audit",
|
|
"pnpm audit --silent",
|
|
"pnpm audit --",
|
|
])("rejects a read-only builtin with manager syntax or arguments: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm exec npm ci",
|
|
"corepack pnpm exec npm install",
|
|
"pnpm dlx npm ci",
|
|
"npm exec pnpm install",
|
|
"pnpm exec unreviewed-tool",
|
|
])("fails closed for nested manager and unknown executable dispatchers: %s", (gate) => {
|
|
expect(validateInstallScriptPolicy({ gate }, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph({ gate }, "gate")).toContain(
|
|
"package script manager invocation is not safely parseable: gate",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"pnpm add fixture",
|
|
"npm update fixture",
|
|
"yarn rebuild fixture",
|
|
"corepack pnpm rebuild fixture",
|
|
])("treats lifecycle mutation builtins as unsafe even when a script has the same name: %s", (gate) => {
|
|
const scripts = {
|
|
gate,
|
|
add: "echo script",
|
|
update: "echo script",
|
|
rebuild: "echo script",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: gate",
|
|
);
|
|
expect(validatePackageScriptGraph(scripts, "gate")).not.toEqual(
|
|
expect.arrayContaining([expect.stringMatching(/package script missing|cycle/u)]),
|
|
);
|
|
});
|
|
|
|
it("allows an explicit run of a script whose name collides with a manager builtin", () => {
|
|
const scripts = {
|
|
gate: "pnpm run add",
|
|
add: "npm ci",
|
|
};
|
|
expect(validateInstallScriptPolicy(scripts, ["gate"])).toContain(
|
|
"install-bearing package script must use --ignore-scripts: add",
|
|
);
|
|
});
|
|
|
|
it("rejects an invalid package graph before the gate runner can spawn it", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-runner-preflight-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
const marker = path.join(root, "spawned");
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as { scripts: Record<string, string> };
|
|
packageDocument.scripts["check:ci"] = `node -e 'require("node:fs").writeFileSync(${JSON.stringify(marker)}, "spawned")'`;
|
|
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
|
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
|
const result = spawnSync(process.execPath, [path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-010"], { cwd: root, encoding: "utf8" });
|
|
expect(result.status).not.toBe(0);
|
|
await expect(readFile(marker)).rejects.toMatchObject({ code: "ENOENT" });
|
|
});
|
|
|
|
it("records checked-in workflow drift as a failing typed report", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-contract-workflow-drift-"));
|
|
temporaryRoots.push(root);
|
|
await mkdir(path.join(root, "config/ci"), { recursive: true });
|
|
await mkdir(path.join(root, ".gitea/workflows"), { recursive: true });
|
|
await mkdir(path.join(root, "artifacts/quality"), { recursive: true });
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
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 writeFile(path.join(root, ".nvmrc"), await readFile(".nvmrc"));
|
|
const drift = Buffer.from("drifted workflow\n", "utf8");
|
|
await writeFile(path.join(root, ".gitea/workflows/quality-gates.yml"), drift);
|
|
const result = spawnSync(process.execPath, [path.resolve("scripts/check-ci-contract.ts")], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
});
|
|
expect(result.status).toBe(1);
|
|
const report = JSON.parse(
|
|
await readFile(path.join(root, "artifacts/quality/ci-contract.json"), "utf8"),
|
|
) as { passed: boolean; failures: string[]; workflowSha256: string };
|
|
expect(report.passed).toBe(false);
|
|
expect(report.failures).toEqual(expect.arrayContaining([expect.stringMatching(/workflow drift/i)]));
|
|
expect(report.workflowSha256).toBe(
|
|
await import("node:crypto").then(({ createHash }) => createHash("sha256").update(drift).digest("hex")),
|
|
);
|
|
});
|
|
|
|
it("caps aggregate gate output at the log schema before later commands can accumulate", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-gate-output-budget-"));
|
|
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 gate = contract.gates.find((entry: any) => entry.id === "FE-GATE-001");
|
|
const command = contract.commands.find((entry: any) => entry.id === gate.commandIds[0]);
|
|
command.script = "test:huge-output";
|
|
const logArtifact = contract.artifacts.find((entry: any) => entry.id === gate.logArtifactId);
|
|
const logSchema = contract.artifactSchemas.find((entry: any) => entry.id === logArtifact.schemaId);
|
|
logSchema.maxBytes = 8_192;
|
|
const packageDocument = JSON.parse(await readFile("package.json", "utf8")) as { scripts: Record<string, string> };
|
|
packageDocument.scripts["test:huge-output"] = "node -e \"process.stdout.write('x'.repeat(20000))\"";
|
|
await writeFile(path.join(root, "config/ci/gates.json"), `${JSON.stringify(contract)}\n`);
|
|
await writeFile(path.join(root, "package.json"), `${JSON.stringify(packageDocument)}\n`);
|
|
const environment = { ...process.env, CI: "false" };
|
|
const result = spawnSync(process.execPath, [path.resolve("scripts/run-ci-gate.ts"), "FE-GATE-001"], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: environment,
|
|
timeout: 15_000,
|
|
});
|
|
expect(result.status).toBe(1);
|
|
const log = await readFile(path.join(root, logArtifact.path));
|
|
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", () => {
|
|
it("renders the complete workflow deterministically with one final LF", async () => {
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const first = renderCiWorkflow(contract);
|
|
const second = renderCiWorkflow(contract);
|
|
expect(second).toBe(first);
|
|
expect(first).toMatchSnapshot();
|
|
expect(first).toMatch(/^# GENERATED FILE/u);
|
|
expect(first.endsWith("\n")).toBe(true);
|
|
expect(first.endsWith("\n\n")).toBe(false);
|
|
expect(first).not.toContain("\r");
|
|
expect(first).not.toMatch(/\\\$\{\{/u);
|
|
expect(first).toContain(
|
|
'node scripts/verify-ci-candidate-archive.ts --archive ".release/release-candidate-${{ gitea.run_id }}-${{ gitea.run_attempt }}.tar.gz" --github-output "$GITHUB_OUTPUT"',
|
|
);
|
|
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).not.toContain("--extract-to");
|
|
expect(first).not.toMatch(/\btar\s+[^\n]*--extract/u);
|
|
expect(first).toContain("node scripts/stage-verified-promotion.ts");
|
|
expect(first).toContain("outputs:\n invocation_nonce: ${{ steps.supervise_vulnerability.outputs.invocation_nonce }}");
|
|
expect(first).toContain("outputs:\n invocation_nonce: ${{ steps.supervise_provenance.outputs.invocation_nonce }}");
|
|
expect(first).toContain('VULNERABILITY_INVOCATION_NONCE: "${{ needs.vulnerability_provider.outputs.invocation_nonce }}"');
|
|
expect(first).toContain('PROVENANCE_INVOCATION_NONCE: "${{ needs.provenance_provider.outputs.invocation_nonce }}"');
|
|
expect(first).toContain("${{ steps.finalize.outputs.staging_root }}/release-candidate.tar.gz");
|
|
expect(first).not.toContain(".release/promoted-staging");
|
|
const promotionJobStart = first.indexOf(" promotion:\n");
|
|
const productionJobStart = first.indexOf(" production_gate:\n");
|
|
expect(promotionJobStart).toBeGreaterThan(0);
|
|
expect(productionJobStart).toBeGreaterThan(promotionJobStart);
|
|
const promotionJob = first.slice(promotionJobStart, productionJobStart);
|
|
expect(promotionJob.slice(0, promotionJob.indexOf(" steps:\n"))).not.toMatch(/^\s+if:/mu);
|
|
expect(promotionJob).not.toContain("cancelled()");
|
|
const finalizerIndex = first.indexOf("node scripts/stage-verified-promotion.ts");
|
|
const promotedUploadIndex = first.indexOf("Upload promoted release");
|
|
const cleanupStepIndex = first.indexOf("- name: Always remove private promotion staging");
|
|
const cleanupIndex = first.indexOf("node scripts/cleanup-verified-promotion.ts");
|
|
expect(finalizerIndex).toBeGreaterThan(0);
|
|
expect(promotedUploadIndex).toBeGreaterThan(finalizerIndex);
|
|
expect(cleanupStepIndex).toBeGreaterThan(promotedUploadIndex);
|
|
expect(cleanupIndex).toBeGreaterThan(cleanupStepIndex);
|
|
expect(first.slice(promotedUploadIndex, cleanupStepIndex)).not.toContain("if: always()");
|
|
expect(first.slice(cleanupStepIndex, cleanupIndex)).toContain("if: always()");
|
|
expect(first.slice(cleanupStepIndex, cleanupIndex)).toContain(
|
|
'if [ -n "$PROMOTION_STAGING_ROOT" ] && [ -n "$PROMOTION_CLEANUP_TOKEN" ] && [ -n "$PROMOTION_RUNNER_TEMP_DEV" ] && [ -n "$PROMOTION_RUNNER_TEMP_INO" ] && [ -n "$PROMOTION_STAGING_DEV" ] && [ -n "$PROMOTION_STAGING_INO" ]; then',
|
|
);
|
|
const actionUses = [...first.matchAll(/^\s+-?\s*uses: (.+)$/gmu)].map((match) => match[1]);
|
|
expect(actionUses).toHaveLength(32);
|
|
expect(new Set(actionUses)).toEqual(
|
|
new Set([
|
|
"https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5",
|
|
"https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020",
|
|
"https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7",
|
|
"https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7",
|
|
]),
|
|
);
|
|
expect(actionUses.every((uses) => /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+@[0-9a-f]{40}$/u.test(uses!))).toBe(true);
|
|
});
|
|
|
|
it("derives download artifact names from the typed upload producer", async () => {
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const candidate = JSON.parse(JSON.stringify(contract)) as Record<string, any>;
|
|
const producer = candidate.jobs.find(
|
|
(job: Record<string, any>) => job.id === "immutable_build",
|
|
);
|
|
const upload = producer.steps.find(
|
|
(step: Record<string, any>) => step.kind === "upload",
|
|
);
|
|
upload.name = "renamed-candidate-${{ gitea.run_id }}";
|
|
const rendered = renderCiWorkflow(
|
|
candidate as unknown as Awaited<ReturnType<typeof loadCiGateContract>>,
|
|
);
|
|
expect(rendered.match(/name: "renamed-candidate-\$\{\{ gitea\.run_id \}\}"/gu)).toHaveLength(4);
|
|
});
|
|
|
|
it("check mode reports missing and byte-level drift without writing", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-workflow-check-"));
|
|
temporaryRoots.push(root);
|
|
await writeFile(path.join(root, "package.json"), '{"scripts":{}}\n');
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const missing = await generateCiWorkflow({ root, contract, check: true });
|
|
expect(missing).toEqual(
|
|
expect.objectContaining({ written: false, matches: false, firstDifferenceLine: 1 }),
|
|
);
|
|
|
|
const target = path.join(root, ".gitea/workflows/quality-gates.yml");
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await writeFile(target, renderCiWorkflow(contract).replace("permissions:", "permissions: "));
|
|
const drift = await generateCiWorkflow({ root, contract, check: true });
|
|
expect(drift.written).toBe(false);
|
|
expect(drift.matches).toBe(false);
|
|
expect(drift.firstDifferenceByte).toBeGreaterThan(0);
|
|
expect(await readFile(target, "utf8")).toContain("permissions: ");
|
|
});
|
|
|
|
it("check mode rejects CRLF and extra final newlines as byte drift", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-workflow-newline-"));
|
|
temporaryRoots.push(root);
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const target = path.join(root, ".gitea/workflows/quality-gates.yml");
|
|
await mkdir(path.dirname(target), { recursive: true });
|
|
await writeFile(target, `${renderCiWorkflow(contract).replaceAll("\n", "\r\n")}\r\n`);
|
|
const result = await generateCiWorkflow({ root, contract, check: true });
|
|
expect(result.matches).toBe(false);
|
|
expect(result.firstDifferenceLine).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("writes a missing workflow and then passes byte-for-byte check mode", async () => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-workflow-write-"));
|
|
temporaryRoots.push(root);
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const written = await generateCiWorkflow({ root, contract, check: false });
|
|
expect(written).toEqual(expect.objectContaining({ written: true, matches: true }));
|
|
const checked = await generateCiWorkflow({ root, contract, check: true });
|
|
expect(checked).toEqual(expect.objectContaining({ written: false, matches: true }));
|
|
});
|
|
|
|
it.each(["ancestor", "leaf"])("rejects a %s symlink in check and write modes without touching its canary", async (kind) => {
|
|
const root = await mkdtemp(path.join(tmpdir(), "ci-workflow-symlink-root-"));
|
|
const outside = await mkdtemp(path.join(tmpdir(), "ci-workflow-symlink-outside-"));
|
|
temporaryRoots.push(root, outside);
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const canary = path.join(outside, "canary");
|
|
await writeFile(canary, "unchanged\n");
|
|
if (kind === "ancestor") {
|
|
await mkdir(path.join(outside, "workflows"), { recursive: true });
|
|
await writeFile(path.join(outside, "workflows/quality-gates.yml"), renderCiWorkflow(contract));
|
|
await symlink(outside, path.join(root, ".gitea"));
|
|
} else {
|
|
await mkdir(path.join(root, ".gitea/workflows"), { recursive: true });
|
|
await writeFile(path.join(outside, "quality-gates.yml"), renderCiWorkflow(contract));
|
|
await symlink(path.join(outside, "quality-gates.yml"), path.join(root, ".gitea/workflows/quality-gates.yml"));
|
|
}
|
|
await expect(generateCiWorkflow({ root, contract, check: true })).rejects.toThrow(/unsafe/i);
|
|
await expect(generateCiWorkflow({ root, contract, check: false })).rejects.toThrow(/unsafe/i);
|
|
await expect(readFile(canary, "utf8")).resolves.toBe("unchanged\n");
|
|
});
|
|
|
|
it("preserves the destination and cleans only its owned temp when atomic write fails", async () => {
|
|
const contract = await loadCiGateContract(process.cwd());
|
|
const removed: string[] = [];
|
|
const flags: Array<{ flags: number; mode: number }> = [];
|
|
let renamed = false;
|
|
const fileSystem: CiWorkflowFileSystem = {
|
|
mkdir: async () => undefined,
|
|
readFile: async () => Buffer.from("existing workflow\n"),
|
|
open: async (_target, openFlags, mode) => {
|
|
flags.push({ flags: openFlags, mode });
|
|
return {
|
|
writeFile: async () => {
|
|
throw new Error("injected write failure");
|
|
},
|
|
chmod: async () => undefined,
|
|
sync: async () => undefined,
|
|
close: async () => undefined,
|
|
};
|
|
},
|
|
openDirectory: async () => ({
|
|
sync: async () => undefined,
|
|
close: async () => undefined,
|
|
}),
|
|
rename: async () => {
|
|
renamed = true;
|
|
},
|
|
rm: async (target) => {
|
|
removed.push(target);
|
|
},
|
|
};
|
|
const generate = createCiWorkflowGenerator({
|
|
fileSystem,
|
|
createNonce: () => "owned",
|
|
});
|
|
await expect(
|
|
generate({ root: "/tmp/ci-workflow-atomic", contract, check: false }),
|
|
).rejects.toThrow(/injected write failure/u);
|
|
expect(renamed).toBe(false);
|
|
expect(removed).toEqual([
|
|
"/tmp/ci-workflow-atomic/.gitea/workflows/.quality-gates.yml.owned.tmp",
|
|
]);
|
|
expect(flags).toEqual([
|
|
{
|
|
flags:
|
|
constants.O_WRONLY |
|
|
constants.O_CREAT |
|
|
constants.O_EXCL |
|
|
constants.O_NOFOLLOW,
|
|
mode: 0o644,
|
|
},
|
|
]);
|
|
});
|
|
});
|