@kungfu-tech/buildchain 2.8.0 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions/promote-buildchain-ref/README.md +13 -0
- package/bin/buildchain.mjs +8 -0
- package/dist/site/agent-index.json +2 -1
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +298 -0
- package/dist/site/release-provenance.json +2 -0
- package/docs/MAP.md +9 -0
- package/docs/cli.md +39 -0
- package/docs/migration-inventory.md +11 -0
- package/docs/release-candidate.md +38 -1
- package/docs/release-governance.md +18 -0
- package/docs/release-passport.md +126 -0
- package/docs/reusable-build-surface.md +79 -3
- package/docs/site-bundle-contract.md +6 -0
- package/docs/versioning.md +1 -1
- package/package.json +4 -2
- package/packages/core/buildchain-contract.js +524 -0
- package/packages/core/index.js +23 -0
- package/packages/core/kfd-gate.js +1064 -4
- package/packages/core/release-passport.js +298 -0
- package/scripts/buildchain-contract-lock.mjs +170 -0
- package/scripts/check-inventory.mjs +105 -3
- package/scripts/ensure-github-release.mjs +5 -5
- package/scripts/generate-site-bundle.mjs +4 -0
- package/scripts/release-candidate-resolver.mjs +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import fs from "node:fs";
|
|
3
4
|
import http from "node:http";
|
|
4
5
|
import https from "node:https";
|
|
@@ -6,8 +7,11 @@ import os from "node:os";
|
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import {
|
|
8
9
|
createKfd1ReleaseGateEvidence,
|
|
10
|
+
createKfd3CollaborationInterfaceReleaseGateEvidence,
|
|
9
11
|
resolveKfd1Metadata,
|
|
12
|
+
resolveKfd3Metadata,
|
|
10
13
|
validateKfd1ReleaseGateEvidence,
|
|
14
|
+
validateKfd3CollaborationInterfaceReleaseGateEvidence,
|
|
11
15
|
} from "./kfd-gate.js";
|
|
12
16
|
|
|
13
17
|
export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
|
|
@@ -16,6 +20,7 @@ export const IMPACT_LEDGER_CONTRACT = "kungfu-buildchain-impact";
|
|
|
16
20
|
export const AGENT_INDEX_CONTRACT = "kungfu-buildchain-agent-index";
|
|
17
21
|
export const PRODUCT_MECHANISM_CONTRACT = "kungfu-buildchain-product-mechanism";
|
|
18
22
|
export const RELEASE_CHECK_REPORT_CONTRACT = "kungfu-buildchain-release-check-report";
|
|
23
|
+
export const KFD2_RELEASE_TRUST_PASSPORT_CONTRACT = "kungfu-buildchain-kfd-2-release-trust-passport-audit";
|
|
19
24
|
|
|
20
25
|
const CONTRACTS = new Set([
|
|
21
26
|
RELEASE_PASSPORT_CONTRACT,
|
|
@@ -275,6 +280,36 @@ function parseJsonInputWithMeta(value, fallback = undefined) {
|
|
|
275
280
|
};
|
|
276
281
|
}
|
|
277
282
|
|
|
283
|
+
function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "command" } = {}) {
|
|
284
|
+
const normalized = String(command || "").trim();
|
|
285
|
+
if (!normalized) {
|
|
286
|
+
return { value: undefined, path: "", sha256: "" };
|
|
287
|
+
}
|
|
288
|
+
const result = spawnSync(normalized, [], {
|
|
289
|
+
cwd,
|
|
290
|
+
shell: true,
|
|
291
|
+
encoding: "utf8",
|
|
292
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
293
|
+
});
|
|
294
|
+
if (result.error) {
|
|
295
|
+
throw new Error(`${label} failed to start: ${result.error.message}`);
|
|
296
|
+
}
|
|
297
|
+
if (result.status !== 0) {
|
|
298
|
+
const stderr = String(result.stderr || "").trim();
|
|
299
|
+
throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
|
|
300
|
+
}
|
|
301
|
+
const stdout = String(result.stdout || "").trim();
|
|
302
|
+
if (!stdout) {
|
|
303
|
+
throw new Error(`${label} produced no JSON on stdout`);
|
|
304
|
+
}
|
|
305
|
+
const parsed = JSON.parse(stdout);
|
|
306
|
+
return {
|
|
307
|
+
value: parsed,
|
|
308
|
+
path: "",
|
|
309
|
+
sha256: sha256Text(stableJson(parsed)),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
278
313
|
function discoverAssetsFromDir(dir) {
|
|
279
314
|
if (!dir || !fs.existsSync(dir)) {
|
|
280
315
|
return [];
|
|
@@ -518,6 +553,166 @@ function normalizeTrustedPublishing(value = undefined, { workflow = {}, publish
|
|
|
518
553
|
};
|
|
519
554
|
}
|
|
520
555
|
|
|
556
|
+
function arrayOrEmpty(value) {
|
|
557
|
+
return Array.isArray(value) ? value : [];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function normalizeKfd2Claim(raw = {}, index = 0) {
|
|
561
|
+
const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
562
|
+
const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
|
|
563
|
+
const machineEvidence = arrayOrEmpty(claim.machineEvidence || claim.machine_evidence || claim.evidence);
|
|
564
|
+
const hashes = claim.hashes && typeof claim.hashes === "object" && !Array.isArray(claim.hashes) ? claim.hashes : {};
|
|
565
|
+
const artifacts = arrayOrEmpty(claim.artifacts || claim.artifactCoordinates || claim.artifact_coordinates);
|
|
566
|
+
const verification = claim.verification && typeof claim.verification === "object" && !Array.isArray(claim.verification) ? claim.verification : {};
|
|
567
|
+
const auditBoundary = claim.auditBoundary && typeof claim.auditBoundary === "object" && !Array.isArray(claim.auditBoundary)
|
|
568
|
+
? claim.auditBoundary
|
|
569
|
+
: claim.audit_boundary && typeof claim.audit_boundary === "object" && !Array.isArray(claim.audit_boundary)
|
|
570
|
+
? claim.audit_boundary
|
|
571
|
+
: {};
|
|
572
|
+
const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
|
|
573
|
+
const residualRisk = arrayOrEmpty(claim.residualRisk || claim.residual_risk);
|
|
574
|
+
const missingBindings = [];
|
|
575
|
+
if (sourceBindings.length === 0) missingBindings.push("declared-sources");
|
|
576
|
+
if (machineEvidence.length === 0) missingBindings.push("machine-readable-evidence");
|
|
577
|
+
if (Object.keys(hashes).length === 0) missingBindings.push("hashes");
|
|
578
|
+
if (artifacts.length === 0) missingBindings.push("artifact-coordinates");
|
|
579
|
+
if (!verification.result && !verification.status) missingBindings.push("verification-result");
|
|
580
|
+
if (Object.keys(auditBoundary).length === 0) missingBindings.push("audit-boundary");
|
|
581
|
+
if (!responsibility.owner && !responsibility.sourceOwner && !responsibility.sourceContractOwner && !responsibility.releasePassportProofOwner) {
|
|
582
|
+
missingBindings.push("responsibility-state");
|
|
583
|
+
}
|
|
584
|
+
if (!Array.isArray(claim.residualRisk || claim.residual_risk)) missingBindings.push("residual-risk");
|
|
585
|
+
const proseOnly = Boolean(claim.proseOnly || claim.prose_only || claim.support === "prose" || claim.supportLevel === "prose");
|
|
586
|
+
const explicitStatus = optionalString(claim.status || claim.result);
|
|
587
|
+
const status = explicitStatus || (missingBindings.length > 0 ? "failed" : (proseOnly || residualRisk.length > 0) ? "downgraded" : "passed");
|
|
588
|
+
return {
|
|
589
|
+
id: optionalString(claim.id || `claim-${index + 1}`),
|
|
590
|
+
public: claim.public === undefined ? true : Boolean(claim.public),
|
|
591
|
+
claim: optionalString(claim.claim || claim.statement || claim.summary),
|
|
592
|
+
sourceBindings,
|
|
593
|
+
machineEvidence,
|
|
594
|
+
hashes,
|
|
595
|
+
artifacts,
|
|
596
|
+
verification,
|
|
597
|
+
auditBoundary,
|
|
598
|
+
responsibility,
|
|
599
|
+
residualRisk,
|
|
600
|
+
proseOnly,
|
|
601
|
+
missingBindings,
|
|
602
|
+
status,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function kfd2ClaimFromKfd1World(world = {}, index = 0) {
|
|
607
|
+
const sourceSurfaces = arrayOrEmpty(world.sourceVerification?.surfaces);
|
|
608
|
+
const artifactSurfaces = arrayOrEmpty(world.artifactVerification?.surfaces);
|
|
609
|
+
return normalizeKfd2Claim({
|
|
610
|
+
id: `kfd-1:${world.id || index + 1}`,
|
|
611
|
+
public: true,
|
|
612
|
+
claim: `KFD-1 contract world ${world.id || index + 1} is verified from declared source surfaces to packaged artifact bytes.`,
|
|
613
|
+
sourceBindings: sourceSurfaces.map((surface) => ({
|
|
614
|
+
id: surface.name,
|
|
615
|
+
path: surface.sourcePath,
|
|
616
|
+
sha256: surface.actualSha256 || surface.expectedSha256,
|
|
617
|
+
})),
|
|
618
|
+
machineEvidence: [
|
|
619
|
+
{ id: "kfd-1-witness", sha256: world.preBuildWitnessSha256 },
|
|
620
|
+
{ id: "source-verification", status: world.sourceVerification?.status || "" },
|
|
621
|
+
{ id: "artifact-verification", status: world.artifactVerification?.status || "" },
|
|
622
|
+
],
|
|
623
|
+
hashes: {
|
|
624
|
+
witnessSha256: world.preBuildWitnessSha256,
|
|
625
|
+
sourceSha256: world.sourceHashes?.sha256 || "",
|
|
626
|
+
artifactSha256: world.artifactHashes?.sha256 || "",
|
|
627
|
+
},
|
|
628
|
+
artifacts: artifactSurfaces.map((surface) => ({
|
|
629
|
+
id: surface.name,
|
|
630
|
+
path: surface.artifactPath,
|
|
631
|
+
sha256: surface.actualSha256,
|
|
632
|
+
})),
|
|
633
|
+
verification: {
|
|
634
|
+
result: world.result === "passed" ? "passed" : "failed",
|
|
635
|
+
source: world.sourceVerification?.status || "",
|
|
636
|
+
artifact: world.artifactVerification?.status || "",
|
|
637
|
+
},
|
|
638
|
+
auditBoundary: world.selfHostingBoundary,
|
|
639
|
+
responsibility: world.responsibility,
|
|
640
|
+
residualRisk: world.selfHostingBoundary?.residualRisk || [],
|
|
641
|
+
}, index);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function kfd2ClaimFromKfd3Interface(entry = {}, index = 0) {
|
|
645
|
+
return normalizeKfd2Claim({
|
|
646
|
+
id: `kfd-3:${entry.id || index + 1}`,
|
|
647
|
+
public: true,
|
|
648
|
+
claim: `KFD-3 collaboration interface ${entry.id || index + 1} exposes only declared public participant-facing shipped surfaces within its audit boundary.`,
|
|
649
|
+
sourceBindings: arrayOrEmpty(entry.declaredSurfaces).map((surface) => ({
|
|
650
|
+
id: surface.id,
|
|
651
|
+
kind: surface.kind,
|
|
652
|
+
sourcePath: surface.sourcePath,
|
|
653
|
+
})),
|
|
654
|
+
machineEvidence: [
|
|
655
|
+
{ id: "prebuild-witness", ...entry.witnessEvidence?.prebuild },
|
|
656
|
+
{ id: "artifact-witness", ...entry.witnessEvidence?.artifact },
|
|
657
|
+
{ id: "declared-capability-verification", result: entry.declaredCapabilityVerification?.result || "" },
|
|
658
|
+
{ id: "reverse-audit", result: entry.reverseAudit?.status || "" },
|
|
659
|
+
],
|
|
660
|
+
hashes: {
|
|
661
|
+
prebuildWitnessSha256: entry.preBuildWitnessSha256,
|
|
662
|
+
artifactWitnessSha256: entry.artifactWitnessSha256,
|
|
663
|
+
prebuildCanonicalSha256: entry.witnessEvidence?.prebuild?.canonicalSha256 || "",
|
|
664
|
+
artifactCanonicalSha256: entry.witnessEvidence?.artifact?.canonicalSha256 || "",
|
|
665
|
+
},
|
|
666
|
+
artifacts: entry.artifactWitness?.artifact?.name || entry.artifactWitness?.artifact?.path
|
|
667
|
+
? [entry.artifactWitness.artifact]
|
|
668
|
+
: arrayOrEmpty(entry.exposedSurfaces).map((surface) => ({ id: surface.id, kind: surface.kind })),
|
|
669
|
+
verification: {
|
|
670
|
+
result: entry.comparison?.status === "passed" ? "passed" : "failed",
|
|
671
|
+
declaredCapabilityVerification: entry.declaredCapabilityVerification?.result || "",
|
|
672
|
+
reverseAudit: entry.reverseAudit?.status || "",
|
|
673
|
+
},
|
|
674
|
+
auditBoundary: entry.auditBoundary,
|
|
675
|
+
responsibility: entry.responsibility,
|
|
676
|
+
residualRisk: entry.residualRisk,
|
|
677
|
+
status: entry.trustProof?.result === "pass"
|
|
678
|
+
? (arrayOrEmpty(entry.residualRisk).length > 0 ? "downgraded" : "passed")
|
|
679
|
+
: "failed",
|
|
680
|
+
}, index);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function createKfd2ReleaseTrustPassportAudit({ explicitClaims = [], kfd1Section = undefined, kfd3Section = undefined, verifiedAt = nowIso() } = {}) {
|
|
684
|
+
const generatedClaims = [
|
|
685
|
+
...arrayOrEmpty(kfd1Section?.contractWorlds).map((world, index) => kfd2ClaimFromKfd1World(world, index)),
|
|
686
|
+
...arrayOrEmpty(kfd3Section?.collaborationInterfaces).map((entry, index) => kfd2ClaimFromKfd3Interface(entry, index)),
|
|
687
|
+
];
|
|
688
|
+
const claims = [
|
|
689
|
+
...generatedClaims,
|
|
690
|
+
...explicitClaims.map((claim, index) => normalizeKfd2Claim(claim, generatedClaims.length + index)),
|
|
691
|
+
];
|
|
692
|
+
if (claims.length === 0) {
|
|
693
|
+
return undefined;
|
|
694
|
+
}
|
|
695
|
+
const failed = claims.filter((claim) => claim.public && claim.status === "failed");
|
|
696
|
+
const downgraded = claims.filter((claim) => claim.public && (claim.status === "downgraded" || claim.proseOnly));
|
|
697
|
+
return {
|
|
698
|
+
schemaVersion: 1,
|
|
699
|
+
contract: KFD2_RELEASE_TRUST_PASSPORT_CONTRACT,
|
|
700
|
+
status: failed.length > 0 ? "failed" : downgraded.length > 0 ? "downgraded" : "passed",
|
|
701
|
+
verifiedAt,
|
|
702
|
+
auditBoundary: {
|
|
703
|
+
scope: "public release claims visible to humans or agents",
|
|
704
|
+
policy: "public claims must bind declared sources, machine-readable evidence, hashes, artifact coordinates, verification results, audit boundaries, responsibility state, and residual risk",
|
|
705
|
+
},
|
|
706
|
+
claims,
|
|
707
|
+
summary: {
|
|
708
|
+
claimCount: claims.length,
|
|
709
|
+
failed: failed.length,
|
|
710
|
+
downgraded: downgraded.length,
|
|
711
|
+
proseOnly: claims.filter((claim) => claim.proseOnly).length,
|
|
712
|
+
},
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
|
|
521
716
|
function normalizeTransactionResult(value = {}) {
|
|
522
717
|
const result = {};
|
|
523
718
|
if (value.command) {
|
|
@@ -627,6 +822,8 @@ export function createReleasePassport({
|
|
|
627
822
|
impact = undefined,
|
|
628
823
|
workflow = {},
|
|
629
824
|
kfd1 = undefined,
|
|
825
|
+
kfd2Claims = [],
|
|
826
|
+
kfd3 = undefined,
|
|
630
827
|
} = {}) {
|
|
631
828
|
const normalizedTag = nonEmptyString(tag, "tag");
|
|
632
829
|
const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
|
|
@@ -644,6 +841,12 @@ export function createReleasePassport({
|
|
|
644
841
|
const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
|
|
645
842
|
const kfd1Metadata = resolveKfd1Metadata();
|
|
646
843
|
const normalizedKfd1 = kfd1?.passportSection ? kfd1 : undefined;
|
|
844
|
+
const normalizedKfd3 = kfd3?.passportSection ? kfd3 : undefined;
|
|
845
|
+
const normalizedKfd2 = createKfd2ReleaseTrustPassportAudit({
|
|
846
|
+
explicitClaims: kfd2Claims,
|
|
847
|
+
kfd1Section: normalizedKfd1?.passportSection,
|
|
848
|
+
kfd3Section: normalizedKfd3?.passportSection,
|
|
849
|
+
});
|
|
647
850
|
const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
|
|
648
851
|
const normalizedPublishSummary = normalizePublishSummary({
|
|
649
852
|
packageSet: normalizedPackageSet,
|
|
@@ -745,6 +948,8 @@ export function createReleasePassport({
|
|
|
745
948
|
...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
|
|
746
949
|
...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
|
|
747
950
|
...(normalizedKfd1 ? { [normalizedKfd1.key || kfd1Metadata.key]: normalizedKfd1.passportSection } : {}),
|
|
951
|
+
...(normalizedKfd2 ? { "kfd-2": normalizedKfd2 } : {}),
|
|
952
|
+
...(normalizedKfd3 ? { [normalizedKfd3.key || "kfd-3"]: normalizedKfd3.passportSection } : {}),
|
|
748
953
|
versionImpact: normalizedImpact.versionImpact,
|
|
749
954
|
surfaceImpacts: normalizedImpact.surfaceImpacts,
|
|
750
955
|
artifacts: [
|
|
@@ -781,6 +986,8 @@ export function createReleasePassport({
|
|
|
781
986
|
})),
|
|
782
987
|
distTagPromotionEvidence: normalizedDistTagPromotionEvidence?.path || "",
|
|
783
988
|
kfd1: normalizedKfd1 ? `${normalizedKfd1.key || kfd1Metadata.key}` : "",
|
|
989
|
+
kfd2: normalizedKfd2 ? "kfd-2" : "",
|
|
990
|
+
kfd3: normalizedKfd3 ? `${normalizedKfd3.key || "kfd-3"}` : "",
|
|
784
991
|
impact: impactPath,
|
|
785
992
|
checkReport: checkReportPath,
|
|
786
993
|
agentIndex: agentIndexPath,
|
|
@@ -815,6 +1022,10 @@ export function collectGitHubReleasePassport({
|
|
|
815
1022
|
platformManifestJsons = [],
|
|
816
1023
|
distTagEvidenceJson = "",
|
|
817
1024
|
kfd1WitnessJsons = [],
|
|
1025
|
+
kfd2ClaimJsons = [],
|
|
1026
|
+
kfd3PrebuildWitnessJsons = [],
|
|
1027
|
+
kfd3ArtifactWitnessJsons = [],
|
|
1028
|
+
kfd3ArtifactVerifyCommand = "",
|
|
818
1029
|
releaseJsonExtra = "",
|
|
819
1030
|
publishJson = "",
|
|
820
1031
|
workflow = {},
|
|
@@ -837,6 +1048,27 @@ export function collectGitHubReleasePassport({
|
|
|
837
1048
|
.filter(Boolean)
|
|
838
1049
|
.map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined))
|
|
839
1050
|
.filter((meta) => meta.value);
|
|
1051
|
+
const kfd2ClaimMetas = (kfd2ClaimJsons || [])
|
|
1052
|
+
.filter(Boolean)
|
|
1053
|
+
.map((claimJson) => parseJsonInputWithMeta(claimJson, undefined))
|
|
1054
|
+
.filter((meta) => meta.value);
|
|
1055
|
+
const kfd3PrebuildWitnessMetas = (kfd3PrebuildWitnessJsons || [])
|
|
1056
|
+
.filter(Boolean)
|
|
1057
|
+
.map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined))
|
|
1058
|
+
.filter((meta) => meta.value);
|
|
1059
|
+
const kfd3ArtifactWitnessMetas = (kfd3ArtifactWitnessJsons || [])
|
|
1060
|
+
.filter(Boolean)
|
|
1061
|
+
.map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined))
|
|
1062
|
+
.filter((meta) => meta.value);
|
|
1063
|
+
const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
|
|
1064
|
+
command: kfd3ArtifactVerifyCommand,
|
|
1065
|
+
cwd,
|
|
1066
|
+
label: "KFD-3 artifact verify command",
|
|
1067
|
+
});
|
|
1068
|
+
const kfd3ArtifactWitnesses = [
|
|
1069
|
+
...kfd3ArtifactWitnessMetas.map((meta) => meta.value),
|
|
1070
|
+
...(kfd3ArtifactCommandMeta.value ? [kfd3ArtifactCommandMeta.value] : []),
|
|
1071
|
+
];
|
|
840
1072
|
const publish = parseJsonInput(publishJson, {});
|
|
841
1073
|
const assets = [
|
|
842
1074
|
...(Array.isArray(release.assets) ? release.assets : []),
|
|
@@ -855,6 +1087,13 @@ export function collectGitHubReleasePassport({
|
|
|
855
1087
|
artifacts: assets,
|
|
856
1088
|
witnesses: kfd1WitnessMetas.map((meta) => meta.value),
|
|
857
1089
|
});
|
|
1090
|
+
const kfd3 = createKfd3CollaborationInterfaceReleaseGateEvidence({
|
|
1091
|
+
prebuildWitnesses: kfd3PrebuildWitnessMetas.map((meta) => meta.value),
|
|
1092
|
+
artifactWitnesses: kfd3ArtifactWitnesses,
|
|
1093
|
+
prebuildWitnessMetas: kfd3PrebuildWitnessMetas,
|
|
1094
|
+
artifactWitnessMetas: kfd3ArtifactWitnessMetas,
|
|
1095
|
+
artifactCommandMeta: kfd3ArtifactCommandMeta.value ? kfd3ArtifactCommandMeta : undefined,
|
|
1096
|
+
});
|
|
858
1097
|
const passport = createReleasePassport({
|
|
859
1098
|
cwd,
|
|
860
1099
|
repository,
|
|
@@ -892,6 +1131,8 @@ export function collectGitHubReleasePassport({
|
|
|
892
1131
|
publish,
|
|
893
1132
|
impact,
|
|
894
1133
|
kfd1,
|
|
1134
|
+
kfd2Claims: kfd2ClaimMetas.map((meta) => meta.value),
|
|
1135
|
+
kfd3,
|
|
895
1136
|
publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
|
|
896
1137
|
transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
|
|
897
1138
|
workflow,
|
|
@@ -945,6 +1186,50 @@ function validateContract(value, expectedContract, label, issues) {
|
|
|
945
1186
|
}
|
|
946
1187
|
}
|
|
947
1188
|
|
|
1189
|
+
function validateKfd2ReleaseTrustPassportAudit(section, issues) {
|
|
1190
|
+
if (!section) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
if (typeof section !== "object" || Array.isArray(section)) {
|
|
1194
|
+
issues.push(issue("error", "kfd-2.object", "kfd-2 release trust passport audit must be a JSON object"));
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
if (section.contract !== KFD2_RELEASE_TRUST_PASSPORT_CONTRACT) {
|
|
1198
|
+
issues.push(issue("error", "kfd-2.contract", `kfd-2 contract must be ${KFD2_RELEASE_TRUST_PASSPORT_CONTRACT}`));
|
|
1199
|
+
}
|
|
1200
|
+
const claims = Array.isArray(section.claims) ? section.claims : [];
|
|
1201
|
+
if (claims.length === 0) {
|
|
1202
|
+
issues.push(issue("error", "kfd-2.claims.empty", "kfd-2 audit must enumerate at least one public release claim"));
|
|
1203
|
+
}
|
|
1204
|
+
for (const [index, claim] of claims.entries()) {
|
|
1205
|
+
if (!claim.id || !claim.claim) {
|
|
1206
|
+
issues.push(issue("error", `kfd-2.claims[${index}].identity`, "public release claim must include id and statement"));
|
|
1207
|
+
}
|
|
1208
|
+
if (claim.public === false) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const missing = Array.isArray(claim.missingBindings) ? claim.missingBindings : [];
|
|
1212
|
+
if (missing.length > 0 || claim.status === "failed") {
|
|
1213
|
+
issues.push(issue("error", `kfd-2.claims[${index}].bindings`, "public release claim is missing machine-verifiable trust bindings", {
|
|
1214
|
+
id: claim.id || "",
|
|
1215
|
+
missingBindings: missing,
|
|
1216
|
+
}));
|
|
1217
|
+
} else if (claim.status === "downgraded" || claim.proseOnly) {
|
|
1218
|
+
issues.push(issue("warning", `kfd-2.claims[${index}].downgraded`, "public release claim is downgraded and needs human review", {
|
|
1219
|
+
id: claim.id || "",
|
|
1220
|
+
proseOnly: Boolean(claim.proseOnly),
|
|
1221
|
+
}));
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (section.status === "failed") {
|
|
1225
|
+
issues.push(issue("error", "kfd-2.status", "kfd-2 release trust passport audit must not contain failed public claims"));
|
|
1226
|
+
} else if (section.status === "downgraded") {
|
|
1227
|
+
issues.push(issue("warning", "kfd-2.status", "kfd-2 release trust passport audit is downgraded by prose-only or residual-risk claims"));
|
|
1228
|
+
} else if (section.status !== "passed") {
|
|
1229
|
+
issues.push(issue("error", "kfd-2.status", "kfd-2 status must be passed, downgraded, or failed"));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
948
1233
|
function resolveSiblingJson(basePath, relativePath) {
|
|
949
1234
|
if (!basePath || !relativePath || /^https?:\/\//.test(relativePath)) {
|
|
950
1235
|
return undefined;
|
|
@@ -1194,6 +1479,19 @@ export function createReleaseCheckReport({
|
|
|
1194
1479
|
const requiredSurfaceImpacts = surfaceImpactRequirement({ passport, impact });
|
|
1195
1480
|
const kfd1Metadata = resolveKfd1Metadata();
|
|
1196
1481
|
issues.push(...validateKfd1ReleaseGateEvidence(passport?.[kfd1Metadata.key], { metadata: kfd1Metadata }));
|
|
1482
|
+
validateKfd2ReleaseTrustPassportAudit(passport?.["kfd-2"], issues);
|
|
1483
|
+
const fallbackKfd3Section = passport?.["kfd-3"];
|
|
1484
|
+
try {
|
|
1485
|
+
const kfd3Metadata = resolveKfd3Metadata();
|
|
1486
|
+
const kfd3Section = passport?.[kfd3Metadata.key] || fallbackKfd3Section;
|
|
1487
|
+
if (kfd3Section) {
|
|
1488
|
+
issues.push(...validateKfd3CollaborationInterfaceReleaseGateEvidence(kfd3Section, { metadata: kfd3Metadata }));
|
|
1489
|
+
}
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
if (fallbackKfd3Section) {
|
|
1492
|
+
issues.push(issue("error", "kfd-3.metadata", error.message));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1197
1495
|
if (requiredSurfaceImpacts.required && surfaceImpacts.length === 0) {
|
|
1198
1496
|
issues.push(issue("error", "impact.surfaceImpacts.required", "surfaceImpacts[] is required for this release passport type", requiredSurfaceImpacts));
|
|
1199
1497
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import {
|
|
6
|
+
createBuildchainContractLock,
|
|
7
|
+
createBuildchainContractWorld,
|
|
8
|
+
evaluateBuildchainContractLock,
|
|
9
|
+
readBuildchainContractLock,
|
|
10
|
+
readBuildchainContractWorld,
|
|
11
|
+
renderBuildchainContractDriftIssueBody,
|
|
12
|
+
} from "../packages/core/buildchain-contract.js";
|
|
13
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
14
|
+
|
|
15
|
+
function env(name, fallback = "") {
|
|
16
|
+
return process.env[name] || fallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function boolEnv(name, fallback = false) {
|
|
20
|
+
const value = String(process.env[name] ?? "").trim().toLowerCase();
|
|
21
|
+
if (!value) {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
return ["1", "true", "yes", "on"].includes(value);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readCurrentContract(contractPath, runtimeRoot) {
|
|
28
|
+
if (contractPath && fs.existsSync(contractPath)) {
|
|
29
|
+
return readBuildchainContractWorld(contractPath);
|
|
30
|
+
}
|
|
31
|
+
return createBuildchainContractWorld({ root: runtimeRoot || process.cwd() });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeJson(filePath, value) {
|
|
35
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function appendSummary(markdown) {
|
|
40
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
41
|
+
if (!summaryPath) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
fs.appendFileSync(summaryPath, `${markdown.trim()}\n\n`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function issueModeAllows(mode, evaluation) {
|
|
48
|
+
if (!evaluation.issueRecommended) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
if (mode === "off") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
if (mode === "breaking-only") {
|
|
55
|
+
return evaluation.status === "breaking-drift";
|
|
56
|
+
}
|
|
57
|
+
return mode === "compatible-and-breaking";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function checkBuildchainContractLock({
|
|
61
|
+
lockPath = env("BUILDCHAIN_CONTRACT_LOCK_PATH", "buildchain.contract-lock.json"),
|
|
62
|
+
currentContractPath = env("BUILDCHAIN_CONTRACT_CURRENT_PATH", ".buildchain/runtime/dist/site/buildchain-contract.json"),
|
|
63
|
+
runtimeRoot = env("BUILDCHAIN_RUNTIME_ROOT", ".buildchain/runtime"),
|
|
64
|
+
runtimeRef = env("BUILDCHAIN_RUNTIME_REF"),
|
|
65
|
+
runtimeSha = env("BUILDCHAIN_RUNTIME_SHA"),
|
|
66
|
+
runtimeClass = env("BUILDCHAIN_RUNTIME_CLASS"),
|
|
67
|
+
compatibilityPolicy = env("BUILDCHAIN_CONTRACT_COMPATIBILITY_POLICY"),
|
|
68
|
+
issueMode = env("BUILDCHAIN_CONTRACT_DRIFT_ISSUE_MODE", "compatible-and-breaking"),
|
|
69
|
+
issueBodyPath = env("BUILDCHAIN_CONTRACT_DRIFT_ISSUE_BODY", ".buildchain/contract-drift/issue-body.md"),
|
|
70
|
+
repository = env("GITHUB_REPOSITORY"),
|
|
71
|
+
workflow = env("GITHUB_WORKFLOW"),
|
|
72
|
+
runUrl = env("BUILDCHAIN_WORKFLOW_RUN_URL"),
|
|
73
|
+
} = {}) {
|
|
74
|
+
const current = readCurrentContract(currentContractPath, runtimeRoot);
|
|
75
|
+
const lock = readBuildchainContractLock(lockPath);
|
|
76
|
+
const evaluation = evaluateBuildchainContractLock({
|
|
77
|
+
lock,
|
|
78
|
+
current,
|
|
79
|
+
runtimeRef,
|
|
80
|
+
runtimeSha,
|
|
81
|
+
runtimeClass,
|
|
82
|
+
compatibilityPolicy,
|
|
83
|
+
});
|
|
84
|
+
const shouldIssue = issueModeAllows(issueMode, evaluation);
|
|
85
|
+
if (shouldIssue) {
|
|
86
|
+
const body = renderBuildchainContractDriftIssueBody({
|
|
87
|
+
repository,
|
|
88
|
+
workflow,
|
|
89
|
+
runUrl,
|
|
90
|
+
lockPath,
|
|
91
|
+
evaluation,
|
|
92
|
+
});
|
|
93
|
+
fs.mkdirSync(path.dirname(issueBodyPath), { recursive: true });
|
|
94
|
+
fs.writeFileSync(issueBodyPath, `${body}\n`);
|
|
95
|
+
}
|
|
96
|
+
appendSummary([
|
|
97
|
+
"## Buildchain contract lock",
|
|
98
|
+
"",
|
|
99
|
+
`- Status: \`${evaluation.status}\``,
|
|
100
|
+
`- Compatible: \`${evaluation.compatible ? "true" : "false"}\``,
|
|
101
|
+
`- Runtime ref: \`${runtimeRef || "(unknown)"}\``,
|
|
102
|
+
`- Runtime SHA: \`${runtimeSha || "(unknown)"}\``,
|
|
103
|
+
`- Contract digest: \`${current.contractDigest}\``,
|
|
104
|
+
`- Compatibility digest: \`${current.compatibilityDigest}\``,
|
|
105
|
+
evaluation.reasons?.length ? `- Reasons: ${evaluation.reasons.join("; ")}` : "",
|
|
106
|
+
shouldIssue ? `- Drift issue body: \`${issueBodyPath}\`` : "",
|
|
107
|
+
].filter(Boolean).join("\n"));
|
|
108
|
+
writeGitHubOutputs({
|
|
109
|
+
"contract-lock-status": evaluation.status,
|
|
110
|
+
"contract-lock-compatible": String(evaluation.compatible === true),
|
|
111
|
+
"contract-lock-drift": String(evaluation.drift === true),
|
|
112
|
+
"contract-lock-issue-needed": String(shouldIssue),
|
|
113
|
+
"contract-lock-issue-body-file": shouldIssue ? issueBodyPath : "",
|
|
114
|
+
"contract-digest": current.contractDigest,
|
|
115
|
+
"contract-compatibility-digest": current.compatibilityDigest,
|
|
116
|
+
"accepted-contract-digest": evaluation.accepted?.contractDigest || "",
|
|
117
|
+
"accepted-buildchain-sha": evaluation.accepted?.resolvedSha || "",
|
|
118
|
+
"current-buildchain-sha": runtimeSha || "",
|
|
119
|
+
});
|
|
120
|
+
if (!evaluation.ok) {
|
|
121
|
+
throw new Error(`Buildchain contract drift is not compatible: ${(evaluation.reasons || []).join("; ")}`);
|
|
122
|
+
}
|
|
123
|
+
return { evaluation, current, shouldIssue };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function writeBuildchainContractLock({
|
|
127
|
+
output = env("BUILDCHAIN_CONTRACT_LOCK_PATH", "buildchain.contract-lock.json"),
|
|
128
|
+
currentContractPath = env("BUILDCHAIN_CONTRACT_CURRENT_PATH", "dist/site/buildchain-contract.json"),
|
|
129
|
+
runtimeRoot = env("BUILDCHAIN_RUNTIME_ROOT", process.cwd()),
|
|
130
|
+
buildchainRef = env("BUILDCHAIN_RUNTIME_REF", "v2"),
|
|
131
|
+
resolvedSha = env("BUILDCHAIN_RUNTIME_SHA"),
|
|
132
|
+
compatibilityPolicy = env("BUILDCHAIN_CONTRACT_COMPATIBILITY_POLICY", "major-compatible"),
|
|
133
|
+
acceptedAt = env("BUILDCHAIN_CONTRACT_ACCEPTED_AT") || new Date().toISOString(),
|
|
134
|
+
} = {}) {
|
|
135
|
+
const contractWorld = readCurrentContract(currentContractPath, runtimeRoot);
|
|
136
|
+
const lock = createBuildchainContractLock({
|
|
137
|
+
buildchainRef,
|
|
138
|
+
resolvedSha,
|
|
139
|
+
contractWorld,
|
|
140
|
+
compatibilityPolicy,
|
|
141
|
+
acceptedAt,
|
|
142
|
+
});
|
|
143
|
+
writeJson(output, lock);
|
|
144
|
+
return lock;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function main(argv = process.argv.slice(2)) {
|
|
148
|
+
const command = argv[0] || "check";
|
|
149
|
+
if (command === "check") {
|
|
150
|
+
checkBuildchainContractLock();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (command === "write-lock") {
|
|
154
|
+
const outputFlag = argv.indexOf("--output");
|
|
155
|
+
const output = outputFlag >= 0 ? argv[outputFlag + 1] : undefined;
|
|
156
|
+
const lock = writeBuildchainContractLock({ output });
|
|
157
|
+
process.stdout.write(`${JSON.stringify(lock, null, 2)}\n`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`unknown buildchain contract lock command: ${command}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
164
|
+
try {
|
|
165
|
+
main();
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`buildchain contract lock: ${error.message}`);
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
}
|
|
170
|
+
}
|