refactor: adapter 구현중..
This commit is contained in:
+123
-17
@@ -9,7 +9,11 @@ import {
|
||||
RELEASE_CANDIDATE_EVIDENCE_PATHS,
|
||||
RELEASE_CANDIDATE_MANIFEST_PATH,
|
||||
} from "../lib/release-candidate.ts";
|
||||
import { validatePackageScriptGraph } from "../lib/package-script-graph.ts";
|
||||
import {
|
||||
validateInstallScriptPolicy,
|
||||
validateNpmScopeEnvironment,
|
||||
validatePackageScriptGraph,
|
||||
} from "../lib/package-script-graph.ts";
|
||||
import { PROMOTED_UPLOAD_PATHS } from "./promotion-artifacts.ts";
|
||||
|
||||
const ciActionRegistrationSchema = z
|
||||
@@ -201,7 +205,22 @@ const artifactSchemaSchema = z.discriminatedUnion("kind", [
|
||||
kind: z.literal("json"),
|
||||
maxBytes: z.number().int().min(1).max(268_435_456),
|
||||
executableSchemaId: z.enum([
|
||||
"generic-json-object",
|
||||
"automated-a11y",
|
||||
"manual-a11y",
|
||||
"architecture-dependency-report",
|
||||
"design-system-contract",
|
||||
"i18n-contract",
|
||||
"diagnostics-contract",
|
||||
"realtime-boundaries",
|
||||
"optional-recipes",
|
||||
"optional-recipe-fixtures",
|
||||
"registry-compatibility-fixtures",
|
||||
"reproducible-build",
|
||||
"supply-chain-fixtures",
|
||||
"supply-chain-provider-fixtures",
|
||||
"compatibility-fixtures",
|
||||
"documentation-review",
|
||||
"hosting-headers",
|
||||
"coverage-summary-v8",
|
||||
"risk-coverage-v3",
|
||||
"build-manifest",
|
||||
@@ -318,12 +337,6 @@ const uploadStep = z
|
||||
const downloadStep = z
|
||||
.object({ kind: z.literal("download"), transferId: id, path: workflowPath })
|
||||
.strict();
|
||||
const validateCandidateArchiveStep = z
|
||||
.object({
|
||||
kind: z.literal("validate-candidate-archive"),
|
||||
archivePath: workflowPath,
|
||||
})
|
||||
.strict();
|
||||
const extractStep = z
|
||||
.object({
|
||||
kind: z.literal("extract"),
|
||||
@@ -360,7 +373,6 @@ const jobStepSchema = z.discriminatedUnion("kind", [
|
||||
archiveCandidateStep,
|
||||
uploadStep,
|
||||
downloadStep,
|
||||
validateCandidateArchiveStep,
|
||||
extractStep,
|
||||
providerStep,
|
||||
validateProviderStep,
|
||||
@@ -374,7 +386,7 @@ const jobSchema = z
|
||||
displayName: workflowValue,
|
||||
kind: z.enum(["gate-matrix", "gate-single", "immutable", "provider", "promotion"]),
|
||||
needs: z.array(id).max(32),
|
||||
condition: z.enum(["always", "merge", "release", "production", "field", "documentation"]),
|
||||
condition: z.enum(["always", "needs-success", "merge", "release", "production", "field", "documentation"]),
|
||||
timeoutMinutes: z.number().int().positive(),
|
||||
gateIds: z.array(id).max(64),
|
||||
browserGateIds: z.array(id).max(64),
|
||||
@@ -455,7 +467,43 @@ function canonicalGateShapeSha256(gates: CiGateContract["gates"]): string {
|
||||
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
||||
}
|
||||
|
||||
export function parseCiGateContract(value: unknown): CiGateContract {
|
||||
function canonicalAuthorityBaselineFailures(contract: CiGateContract): string[] {
|
||||
const failures: string[] = [];
|
||||
const commandReferenceCount = contract.gates.reduce(
|
||||
(total, gate) => total + gate.commandIds.length,
|
||||
0,
|
||||
);
|
||||
if (contract.gates.length !== 26) {
|
||||
failures.push(`gate authority baseline must contain exactly 26 gates; received ${contract.gates.length}`);
|
||||
}
|
||||
if (contract.commands.length !== 81 || commandReferenceCount !== 93) {
|
||||
failures.push(
|
||||
`command authority baseline must contain exactly 81 definitions and 93 references; received ${contract.commands.length} definitions and ${commandReferenceCount} references`,
|
||||
);
|
||||
}
|
||||
if (contract.artifacts.length !== 105) {
|
||||
failures.push(`artifact authority baseline must contain exactly 105 artifacts; received ${contract.artifacts.length}`);
|
||||
}
|
||||
if (contract.stages.length !== 5) {
|
||||
failures.push(`stage authority baseline must contain exactly 5 stages; received ${contract.stages.length}`);
|
||||
}
|
||||
const expectedRetentionClasses = [
|
||||
{ id: "merge-cycle", policy: "at least through pull-request readiness decision" },
|
||||
{ id: "release-coherence", policy: "at least until the next release is promoted" },
|
||||
{ id: "prod-drill", policy: "at least until the next production promotion decision" },
|
||||
{ id: "field", policy: "through the 28-day window and aggregation" },
|
||||
{ id: "documentation", policy: "through documentation readiness review" },
|
||||
];
|
||||
if (JSON.stringify(contract.retention.classes) !== JSON.stringify(expectedRetentionClasses)) {
|
||||
failures.push("retention registry must contain exactly the five canonical retention classes");
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
export function parseCiGateContract(
|
||||
value: unknown,
|
||||
options: LoadCiGateContractOptions = {},
|
||||
): CiGateContract {
|
||||
const result = ciGateContractSchema.safeParse(value);
|
||||
if (!result.success) {
|
||||
const diagnostic = result.error.issues
|
||||
@@ -463,6 +511,12 @@ export function parseCiGateContract(value: unknown): CiGateContract {
|
||||
.join("\n");
|
||||
throw new TypeError(`CI gate contract invalid:\n${diagnostic}`);
|
||||
}
|
||||
if ((options.mode ?? "canonical") === "canonical") {
|
||||
const failures = canonicalAuthorityBaselineFailures(result.data);
|
||||
if (failures.length > 0) {
|
||||
throw new TypeError(`CI gate contract invalid:\n${failures.map((failure) => `root: ${failure}`).join("\n")}`);
|
||||
}
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
@@ -470,12 +524,12 @@ export async function loadCiGateContract(
|
||||
root = process.cwd(),
|
||||
options: LoadCiGateContractOptions = {},
|
||||
): Promise<CiGateContract> {
|
||||
const mode = options.mode ?? "canonical";
|
||||
const [rawContract, rawPackage] = await Promise.all([
|
||||
readFile(path.join(root, "config/ci/gates.json"), "utf8"),
|
||||
readFile(path.join(root, "package.json"), "utf8"),
|
||||
]);
|
||||
const contract = parseCiGateContract(JSON.parse(rawContract));
|
||||
const mode = options.mode ?? "canonical";
|
||||
const contract = parseCiGateContract(JSON.parse(rawContract), { mode });
|
||||
if (
|
||||
mode === "canonical" &&
|
||||
canonicalGateShapeSha256(contract.gates) !== CANONICAL_GATE_SHAPE_SHA256
|
||||
@@ -510,13 +564,57 @@ export async function loadCiGateContract(
|
||||
throw new TypeError(`canonical check:ci dependency drift: ${script}`);
|
||||
}
|
||||
}
|
||||
const graphFailures = validatePackageScriptGraph(packageDocument.scripts, "check:ci");
|
||||
const contractEntryScripts = [
|
||||
...new Set(contract.commands.map((command) => command.script)),
|
||||
];
|
||||
const graphFailures = [
|
||||
...validatePackageScriptGraph(packageDocument.scripts, "check:ci"),
|
||||
...contractEntryScripts.flatMap((script) =>
|
||||
validatePackageScriptGraph(packageDocument.scripts, script)
|
||||
),
|
||||
];
|
||||
if (graphFailures.length > 0) {
|
||||
throw new TypeError(`CI package script graph invalid:\n${graphFailures.join("\n")}`);
|
||||
throw new TypeError(
|
||||
`CI package script graph invalid:\n${[...new Set(graphFailures)].join("\n")}`,
|
||||
);
|
||||
}
|
||||
const installPolicyFailures = validateInstallScriptPolicy(
|
||||
packageDocument.scripts,
|
||||
contractEntryScripts,
|
||||
);
|
||||
if (installPolicyFailures.length > 0) {
|
||||
throw new TypeError(
|
||||
`CI package script install policy invalid:\n${installPolicyFailures.join("\n")}`,
|
||||
);
|
||||
}
|
||||
return contract;
|
||||
}
|
||||
|
||||
export async function withCiGatePreflight<Result>(
|
||||
root: string,
|
||||
gateId: string | undefined,
|
||||
execute: (context: Readonly<{
|
||||
contract: CiGateContract;
|
||||
contractIndex: CiGateContractIndex;
|
||||
gateId: string;
|
||||
gate: CiGate;
|
||||
}>) => Promise<Result> | Result,
|
||||
): Promise<Result> {
|
||||
const npmScopeEnvironmentFailures = validateNpmScopeEnvironment(process.env);
|
||||
if (npmScopeEnvironmentFailures.length > 0) {
|
||||
throw new TypeError(
|
||||
`CI runner npm scope environment invalid:\n${npmScopeEnvironmentFailures.join("\n")}`,
|
||||
);
|
||||
}
|
||||
const contract = await loadCiGateContract(root);
|
||||
const contractIndex = indexCiGateContract(contract);
|
||||
const gate = gateId ? contractIndex.gates.get(gateId) : undefined;
|
||||
if (!gateId || !gate) {
|
||||
throw new TypeError("CI gate id must be FE-GATE-001..FE-GATE-026");
|
||||
}
|
||||
return execute({ contract, contractIndex, gateId, gate });
|
||||
}
|
||||
|
||||
export function indexCiGateContract(contract: CiGateContract): CiGateContractIndex {
|
||||
return Object.freeze({
|
||||
commands: new Map(contract.commands.map((entry) => [entry.id, entry])),
|
||||
@@ -625,6 +723,14 @@ function validateContractSemantics(
|
||||
issue(`unknown retention class ${gate.retentionClassId} for ${gate.id}`);
|
||||
}
|
||||
}
|
||||
const referencedRetentionClasses = new Set(
|
||||
contract.gates.map(({ retentionClassId }) => retentionClassId),
|
||||
);
|
||||
for (const retentionClass of contract.retention.classes) {
|
||||
if (!referencedRetentionClasses.has(retentionClass.id)) {
|
||||
issue(`orphan retention class: ${retentionClass.id}`);
|
||||
}
|
||||
}
|
||||
const referencedCommands = new Set(contract.gates.flatMap(({ commandIds }) => commandIds));
|
||||
for (const command of contract.commands) {
|
||||
if (!referencedCommands.has(command.id)) issue(`orphan command: ${command.id}`);
|
||||
@@ -751,7 +857,7 @@ function validateContractSemantics(
|
||||
immutable_build: ["immutable", ["release_gate"], "release"],
|
||||
vulnerability_provider: ["provider", ["immutable_build"], "always"],
|
||||
provenance_provider: ["provider", ["immutable_build"], "always"],
|
||||
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "always"],
|
||||
promotion: ["promotion", ["immutable_build", "vulnerability_provider", "provenance_provider"], "needs-success"],
|
||||
production_gate: ["gate-matrix", ["promotion"], "production"],
|
||||
field_gate: ["gate-single", ["production_gate"], "field"],
|
||||
documentation_gate: ["gate-single", [], "documentation"],
|
||||
@@ -1026,7 +1132,7 @@ function validateJobStepKinds(
|
||||
"gate-matrix": new Set(["checkout", "setup-node", "frozen-install", "browser-install", "run-gate", "upload"]),
|
||||
"gate-single": new Set(["checkout", "setup-node", "frozen-install", "run-gate", "upload"]),
|
||||
immutable: new Set(["checkout", "setup-node", "frozen-install", "run-gate", "archive-candidate", "upload"]),
|
||||
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "validate-candidate-archive", "extract", "run-provider", "validate-provider-evidence", "upload"]),
|
||||
provider: new Set(["checkout", "setup-node", "frozen-install", "download", "extract", "run-provider", "validate-provider-evidence", "upload"]),
|
||||
promotion: new Set(["checkout", "setup-node", "frozen-install", "download", "verify-promotion", "upload", "cleanup-promotion"]),
|
||||
};
|
||||
for (const step of job.steps) {
|
||||
|
||||
Reference in New Issue
Block a user