The public site shipped a Releases page and a footer link to a release, and neither could ever have content: the read path existed, the write path did not. This adds the seven release operations to the contract contribution and the gateway, and a Studio screen that can actually write one. The editor is six markdown fields rather than one, because that is what the contract models and what a release note is — why, what, what a reader notices, what it leaves in the code, how it was verified, what is still open. Publishing is separate from saving: a draft saves in any state, but the public query keys on PUBLISHED alone, so publish is where completeness is demanded. No new CSS. The screen reuses the document editor's field classes and the working-copy list's row classes, so it inherits Studio's spacing and type instead of introducing a second look. The footer previously linked `/releases/0.1.0` — a version that did not exist, so the link 404'd, and one that would have gone stale at 0.2.0 anyway. It now points at the changelog index, which is the only place that knows what the latest release is and which reads correctly when there are none. Route inventory, navigation order, message catalog, manual accessibility evidence, artifact baseline, and the pinned gate-shape digest all move with the new route. The digest was recomputed by first reproducing the previous constant from the previous gates.json, so the computation is known to be the one it was pinned under.
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(111);
|
|
expect(contract.artifacts).toHaveLength(132);
|
|
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,
|
|
},
|
|
]);
|
|
});
|
|
});
|