@kungfu-tech/buildchain 3.0.6 → 3.0.7-alpha.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/README.md +4 -2
- package/actions/promote-buildchain-ref/README.md +10 -0
- package/actions/release-tail/README.md +17 -0
- package/bin/buildchain.mjs +11 -0
- package/bin/internal/command-registry.mjs +2 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +1 -1
- package/contracts/engineering-housekeeper-v1.schema.json +143 -0
- package/contracts/fixtures/engineering-housekeeper-v1/cases.json +68 -0
- package/contracts/fixtures/release-tail-capabilities-v1/kungfu-alpha.json +273 -0
- package/contracts/publication-rehearsal-capsule-v1.schema.json +173 -0
- package/contracts/release-tail-capabilities-v1.schema.json +199 -0
- package/contracts/release-tail-provider-bindings-v1.schema.json +56 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +42 -27
- package/dist/site/buildchain-site.json +384 -33
- package/dist/site/capability-registry.json +15 -12
- package/dist/site/cli-registry.json +102 -0
- package/dist/site/controller-registry.json +19 -3
- package/dist/site/kfd-claims.json +359 -17
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +5882 -3605
- package/dist/site/page-registry.json +348 -21
- package/dist/site/public-surface-audit.json +396 -16
- package/dist/site/publication-authority-registry.json +106 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +7 -0
- package/dist/site/schemas/publication-rehearsal-capsule-v1.schema.json +269 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +353 -0
- package/dist/site/schemas/release-tail-provider-bindings-v1.schema.json +94 -0
- package/dist/site/site-manifest.json +35 -11
- package/dist/site/workflow-registry.json +204 -10
- package/docs/MAP.md +6 -2
- package/docs/auditable-demo.md +2 -2
- package/docs/cli-reference.md +136 -0
- package/docs/dev-delivery-warrant.md +49 -4
- package/docs/engineering-housekeeper.md +138 -0
- package/docs/lifecycle-protocol.md +4 -2
- package/docs/node-api-reference.md +495 -281
- package/docs/publication-rehearsal.md +94 -0
- package/docs/release-governance.md +17 -2
- package/docs/release-tail-contract.md +160 -0
- package/docs/release-tail-provider-plane.md +120 -0
- package/docs/reusable-build-surface.md +11 -0
- package/package.json +11 -3
- package/packages/core/artifact-signing.js +61 -0
- package/packages/core/buildchain-agent-manuals.js +3 -0
- package/packages/core/buildchain-config.js +66 -6
- package/packages/core/buildchain-kfd-claims.js +1 -1
- package/packages/core/buildchain-publication-authority.js +5 -0
- package/packages/core/controller-evidence.js +2 -1
- package/packages/core/dev-delivery-warrant-cancellation.js +1 -0
- package/packages/core/dev-delivery-warrant-shadow.js +502 -0
- package/packages/core/dev-delivery-warrant.js +15 -6
- package/packages/core/diagnostics.js +8 -3
- package/packages/core/engineering-housekeeper-github-client.js +222 -0
- package/packages/core/engineering-housekeeper-github.js +501 -0
- package/packages/core/engineering-housekeeper.js +259 -0
- package/packages/core/index.js +42 -0
- package/packages/core/kfd-gate.js +45 -15
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper-repository.js +1 -0
- package/packages/core/paper-scaffold-content.js +21 -0
- package/packages/core/paper.js +28 -2
- package/packages/core/publication-rehearsal-projection.js +173 -0
- package/packages/core/publication-rehearsal-runtime.js +921 -0
- package/packages/core/release-passport.js +130 -20
- package/packages/core/release-tail-compatibility.js +60 -0
- package/packages/core/release-tail-provider-adapters.js +461 -0
- package/packages/core/release-tail-provider-plane.js +1228 -0
- package/scripts/assemble-publication-artifact-admission.mjs +1 -1
- package/scripts/assemble-self-publication-admission.mjs +2 -2
- package/scripts/audit-publication-control-plane.mjs +1 -1
- package/scripts/auditable-demo-bundle-verification.mjs +2 -3
- package/scripts/auditable-demo-platform.mjs +2 -2
- package/scripts/auditable-demo-renditions.mjs +1 -1
- package/scripts/auditable-demo.mjs +2 -2
- package/scripts/build-contract-core.mjs +8 -3
- package/scripts/build-standalone-binary.mjs +14 -3
- package/scripts/buildchain-cli-help.mjs +13 -0
- package/scripts/check-core-mechanism-inventory.mjs +347 -0
- package/scripts/check-inventory.mjs +10 -8
- package/scripts/check-maintainability.mjs +9 -2
- package/scripts/check-release-tail-contract.mjs +435 -0
- package/scripts/dev-delivery-warrant.mjs +31 -4
- package/scripts/dev-pr-auto-merge.mjs +30 -4
- package/scripts/dev-pr-delivery-warrant.mjs +50 -0
- package/scripts/engineering-housekeeper-workflow.mjs +394 -0
- package/scripts/generate-channel-promotion-workflow.mjs +10 -8
- package/scripts/generate-site-bundle.mjs +47 -4
- package/scripts/init-repo.mjs +26 -2
- package/scripts/inspect-artifact-signing-requests.mjs +6 -0
- package/scripts/materialize-self-release-candidate-version.mjs +137 -0
- package/scripts/publication-commit-evidence.mjs +69 -23
- package/scripts/release-candidate-resolver.mjs +16 -10
- package/scripts/release-tail.mjs +159 -0
- package/scripts/resume-from-candidate-run.mjs +123 -9
- package/scripts/seal-artifact-signing-requests.mjs +6 -0
- package/scripts/site-capability-metadata.mjs +13 -0
- package/scripts/v4-architecture.mjs +600 -0
- package/scripts/web-surface-core.mjs +8 -2
- package/scripts/workflow-call-contract.mjs +184 -5
|
@@ -146,6 +146,15 @@ function invariantSemanticPreimage(passport) {
|
|
|
146
146
|
return value;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function hasCompleteInvariantCoverage(value) {
|
|
150
|
+
const coverage = value?.coverage;
|
|
151
|
+
return Number.isInteger(coverage?.required)
|
|
152
|
+
&& coverage.required > 0
|
|
153
|
+
&& coverage.verified === coverage.required
|
|
154
|
+
&& [coverage.missing, coverage.falsified, coverage.unqualified]
|
|
155
|
+
.every((items) => Array.isArray(items) && items.length === 0);
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
function normalizeInvariantPassport(meta, index = 0) {
|
|
150
159
|
const value = meta?.value;
|
|
151
160
|
const label = `invariantPassportJsons[${index}]`;
|
|
@@ -165,11 +174,18 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
165
174
|
if (value.passportRoot !== expectedRoot) {
|
|
166
175
|
throw new Error(`${label}.passportRoot mismatch: expected ${expectedRoot}, got ${value.passportRoot}`);
|
|
167
176
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
const verifiedPassport = value.verdict === "verified" && value.coverage?.complete === true;
|
|
178
|
+
const scopedUnqualifiedClaim = value.verdict === "unqualified"
|
|
179
|
+
&& value.coverage?.complete === false
|
|
180
|
+
&& hasCompleteInvariantCoverage(value)
|
|
181
|
+
&& value.releaseClaims?.verdict === "unqualified"
|
|
182
|
+
&& /^sha256:[0-9a-f]{64}$/.test(optionalString(value.releaseClaims?.claimRoot))
|
|
183
|
+
&& Array.isArray(value.releaseClaims?.diagnostics)
|
|
184
|
+
&& value.releaseClaims.diagnostics.length > 0
|
|
185
|
+
&& Array.isArray(value.diagnostics)
|
|
186
|
+
&& value.diagnostics.length === 0;
|
|
187
|
+
if (!verifiedPassport && !scopedUnqualifiedClaim) {
|
|
188
|
+
throw new Error(`${label}.verdict must be verified, or unqualified only for complete invariant coverage with one explicit unqualified releaseClaims section`);
|
|
173
189
|
}
|
|
174
190
|
if (value.source?.dirty !== false) {
|
|
175
191
|
throw new Error(`${label}.source.dirty must be false`);
|
|
@@ -182,6 +198,9 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
182
198
|
: [];
|
|
183
199
|
if (platforms.length === 0) throw new Error(`${label}.coverage.platforms must be non-empty`);
|
|
184
200
|
if (!Array.isArray(value.residualRisk)) throw new Error(`${label}.residualRisk must be an array`);
|
|
201
|
+
const releaseClaimRisks = scopedUnqualifiedClaim
|
|
202
|
+
? value.releaseClaims.diagnostics.map((entry) => `Exit/provider claim ${optionalString(entry?.code) || "unqualified"}: ${optionalString(entry?.message) || "consumer claim remains unqualified"}`)
|
|
203
|
+
: [];
|
|
185
204
|
return {
|
|
186
205
|
path: optionalString(meta.path),
|
|
187
206
|
sha256: optionalString(meta.sha256),
|
|
@@ -194,7 +213,15 @@ function normalizeInvariantPassport(meta, index = 0) {
|
|
|
194
213
|
source: structuredClone(value.source),
|
|
195
214
|
coverage: structuredClone(value.coverage),
|
|
196
215
|
platforms,
|
|
197
|
-
|
|
216
|
+
admission: {
|
|
217
|
+
scope: "consumer-invariant-coverage",
|
|
218
|
+
result: "passed",
|
|
219
|
+
consumerVerdict: value.verdict,
|
|
220
|
+
releaseClaimsVerdict: optionalString(value.releaseClaims?.verdict),
|
|
221
|
+
},
|
|
222
|
+
releaseClaims: value.releaseClaims ? structuredClone(value.releaseClaims) : undefined,
|
|
223
|
+
diagnostics: Array.isArray(value.diagnostics) ? structuredClone(value.diagnostics) : [],
|
|
224
|
+
residualRisk: [...new Set([...value.residualRisk, ...releaseClaimRisks])].sort(),
|
|
198
225
|
};
|
|
199
226
|
}
|
|
200
227
|
|
|
@@ -533,7 +560,12 @@ function mergeAuthoritativeImpactBase(impact, basePassport = undefined) {
|
|
|
533
560
|
return merged;
|
|
534
561
|
}
|
|
535
562
|
|
|
536
|
-
function parseJsonCommandOutput({
|
|
563
|
+
function parseJsonCommandOutput({
|
|
564
|
+
command = "",
|
|
565
|
+
cwd = process.cwd(),
|
|
566
|
+
label = "command",
|
|
567
|
+
acceptNonzeroJson = undefined,
|
|
568
|
+
} = {}) {
|
|
537
569
|
const normalized = String(command || "").trim();
|
|
538
570
|
if (!normalized) {
|
|
539
571
|
return { value: undefined, path: "", sha256: "" };
|
|
@@ -547,7 +579,7 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
|
|
|
547
579
|
if (result.error) {
|
|
548
580
|
throw new Error(`${label} failed to start: ${result.error.message}`);
|
|
549
581
|
}
|
|
550
|
-
if (result.status !== 0) {
|
|
582
|
+
if (result.status !== 0 && typeof acceptNonzeroJson !== "function") {
|
|
551
583
|
const stderr = String(result.stderr || "").trim();
|
|
552
584
|
throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
|
|
553
585
|
}
|
|
@@ -561,10 +593,15 @@ function parseJsonCommandOutput({ command = "", cwd = process.cwd(), label = "co
|
|
|
561
593
|
} catch (error) {
|
|
562
594
|
throw new Error(`${label} output must be valid JSON: ${error.message}`, { cause: error });
|
|
563
595
|
}
|
|
596
|
+
if (result.status !== 0 && !acceptNonzeroJson({ status: result.status, value: parsed })) {
|
|
597
|
+
const stderr = String(result.stderr || "").trim();
|
|
598
|
+
throw new Error(`${label} exited with ${result.status}${stderr ? `: ${stderr.slice(-1000)}` : ""}`);
|
|
599
|
+
}
|
|
564
600
|
return {
|
|
565
601
|
value: parsed,
|
|
566
602
|
path: "",
|
|
567
603
|
sha256: sha256Text(stableJson(parsed)),
|
|
604
|
+
status: result.status,
|
|
568
605
|
};
|
|
569
606
|
}
|
|
570
607
|
|
|
@@ -879,6 +916,41 @@ function arrayOrSingleton(value) {
|
|
|
879
916
|
return value && typeof value === "object" ? [value] : [];
|
|
880
917
|
}
|
|
881
918
|
|
|
919
|
+
const KFD2_RESIDUAL_RISK_SCHEMA = "https://kfd.libkungfu.dev/schemas/kfd-2/trust-taxonomy.schema.json#/$defs/residualRisk";
|
|
920
|
+
|
|
921
|
+
function normalizeKfd2ResidualRisk(entry, { claim = {}, claimIndex = 0, riskIndex = 0 } = {}) {
|
|
922
|
+
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
923
|
+
return entry;
|
|
924
|
+
}
|
|
925
|
+
const reason = optionalString(entry).trim();
|
|
926
|
+
if (!reason) {
|
|
927
|
+
return entry;
|
|
928
|
+
}
|
|
929
|
+
const claimId = optionalString(claim.id || `claim-${claimIndex + 1}`)
|
|
930
|
+
.toLowerCase()
|
|
931
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
932
|
+
.replace(/^-+|-+$/g, "") || `claim-${claimIndex + 1}`;
|
|
933
|
+
const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility)
|
|
934
|
+
? claim.responsibility
|
|
935
|
+
: {};
|
|
936
|
+
return {
|
|
937
|
+
id: `${claimId}-residual-risk-${riskIndex + 1}`,
|
|
938
|
+
definedBy: KFD2_RESIDUAL_RISK_SCHEMA,
|
|
939
|
+
riskType: "manual-review-risk",
|
|
940
|
+
trustImpact: "downgrade-warning",
|
|
941
|
+
machineProvability: "not-machine-verifiable",
|
|
942
|
+
agentAction: "request-maintainer-review",
|
|
943
|
+
reason,
|
|
944
|
+
owner: optionalString(
|
|
945
|
+
responsibility.releaseDecisionOwner
|
|
946
|
+
|| responsibility.owner
|
|
947
|
+
|| responsibility.sourceOwner
|
|
948
|
+
|| responsibility.sourceContractOwner
|
|
949
|
+
|| "release maintainer",
|
|
950
|
+
),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
882
954
|
function normalizeKfd2Claim(raw = {}, index = 0) {
|
|
883
955
|
const claim = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
884
956
|
const sourceBindings = arrayOrEmpty(claim.sourceBindings || claim.source_bindings || claim.sources || claim.declaredSources);
|
|
@@ -892,10 +964,13 @@ function normalizeKfd2Claim(raw = {}, index = 0) {
|
|
|
892
964
|
? claim.audit_boundary
|
|
893
965
|
: {};
|
|
894
966
|
const responsibility = claim.responsibility && typeof claim.responsibility === "object" && !Array.isArray(claim.responsibility) ? claim.responsibility : {};
|
|
895
|
-
const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
967
|
+
const residualRisk = arrayOrSingleton(claim.residualRisk || claim.residual_risk).map((entry, riskIndex) => validateKfd2TrustTaxonomyEntry(
|
|
968
|
+
normalizeKfd2ResidualRisk(entry, { claim, claimIndex: index, riskIndex }),
|
|
969
|
+
{
|
|
970
|
+
kind: "residualRisk",
|
|
971
|
+
label: `kfd-2.claims[${index}].residualRisk[${riskIndex}]`,
|
|
972
|
+
},
|
|
973
|
+
));
|
|
899
974
|
const downgradeReasons = arrayOrSingleton(claim.downgradeReasons || claim.downgrade_reasons || claim.downgradeReason || claim.downgrade_reason)
|
|
900
975
|
.map((entry, reasonIndex) => validateKfd2TrustTaxonomyEntry(entry, {
|
|
901
976
|
kind: "downgradeReason",
|
|
@@ -1494,6 +1569,7 @@ export function collectGitHubReleasePassport({
|
|
|
1494
1569
|
outputDir = ".buildchain/release-passport",
|
|
1495
1570
|
assetsJson = "",
|
|
1496
1571
|
assetsDir = "",
|
|
1572
|
+
kfdArtifactSearchRoots = [],
|
|
1497
1573
|
releaseJson = "",
|
|
1498
1574
|
productName = "Buildchain",
|
|
1499
1575
|
packageName = "@kungfu-tech/buildchain",
|
|
@@ -1568,6 +1644,11 @@ export function collectGitHubReleasePassport({
|
|
|
1568
1644
|
.filter(Boolean)
|
|
1569
1645
|
.map((witnessJson) => parseJsonInputWithMeta(witnessJson, undefined, { cwd, label: "kfd3ArtifactWitnessJsons entry" }))
|
|
1570
1646
|
.filter((meta) => meta.value);
|
|
1647
|
+
const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
|
|
1648
|
+
command: kfd3ArtifactVerifyCommand,
|
|
1649
|
+
cwd,
|
|
1650
|
+
label: "KFD-3 artifact verify command",
|
|
1651
|
+
});
|
|
1571
1652
|
const kfdSupportMatrixMeta = parseJsonInputWithMeta(
|
|
1572
1653
|
kfdSupportMatrixJson,
|
|
1573
1654
|
undefined,
|
|
@@ -1578,11 +1659,6 @@ export function collectGitHubReleasePassport({
|
|
|
1578
1659
|
.map((gateJson) => parseJsonInputWithMeta(gateJson, undefined, { cwd, label: "kfdProductGateJsons entry" }))
|
|
1579
1660
|
.filter((meta) => meta.value);
|
|
1580
1661
|
const basePassportMeta = parseJsonInputWithMeta(basePassportJson, undefined, { cwd, label: "basePassportJson" });
|
|
1581
|
-
const kfd3ArtifactCommandMeta = parseJsonCommandOutput({
|
|
1582
|
-
command: kfd3ArtifactVerifyCommand,
|
|
1583
|
-
cwd,
|
|
1584
|
-
label: "KFD-3 artifact verify command",
|
|
1585
|
-
});
|
|
1586
1662
|
const invariantPassportMetas = (invariantPassportJsons || [])
|
|
1587
1663
|
.filter(Boolean)
|
|
1588
1664
|
.map((passportJson) => parseJsonInputWithMeta(passportJson, undefined, { cwd, label: "invariantPassportJsons entry" }))
|
|
@@ -1591,6 +1667,22 @@ export function collectGitHubReleasePassport({
|
|
|
1591
1667
|
command: invariantPassportCommand,
|
|
1592
1668
|
cwd,
|
|
1593
1669
|
label: "invariant passport command",
|
|
1670
|
+
acceptNonzeroJson: ({ status, value }) => {
|
|
1671
|
+
if (status !== 2) return false;
|
|
1672
|
+
try {
|
|
1673
|
+
const normalized = normalizeInvariantPassport(
|
|
1674
|
+
{ value, path: "", sha256: sha256Text(stableJson(value)) },
|
|
1675
|
+
invariantPassportMetas.length,
|
|
1676
|
+
);
|
|
1677
|
+
return normalized.verdict === "unqualified"
|
|
1678
|
+
&& normalized.coverage?.complete === false
|
|
1679
|
+
&& normalized.releaseClaims?.verdict === "unqualified"
|
|
1680
|
+
&& normalized.admission?.scope === "consumer-invariant-coverage"
|
|
1681
|
+
&& normalized.admission?.result === "passed";
|
|
1682
|
+
} catch {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1594
1686
|
});
|
|
1595
1687
|
if (invariantPassportCommandMeta.value) invariantPassportMetas.push(invariantPassportCommandMeta);
|
|
1596
1688
|
const invariantPassports = createInvariantPassportGate(invariantPassportMetas);
|
|
@@ -1664,6 +1756,7 @@ export function collectGitHubReleasePassport({
|
|
|
1664
1756
|
const kfd1 = createKfd1ReleaseGateEvidence({
|
|
1665
1757
|
cwd,
|
|
1666
1758
|
artifactRoot: assetsDir ? path.resolve(cwd, assetsDir) : "",
|
|
1759
|
+
artifactSearchRoots: kfdArtifactSearchRoots.map((root) => path.resolve(cwd, root)),
|
|
1667
1760
|
artifacts: assets,
|
|
1668
1761
|
witnesses: kfd1WitnessMetas.map((meta) => meta.value),
|
|
1669
1762
|
});
|
|
@@ -2092,7 +2185,17 @@ function validateReleaseEvidenceContracts({
|
|
|
2092
2185
|
for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
|
|
2093
2186
|
try {
|
|
2094
2187
|
const policy = normalizeGitHubArtifactAttestationPolicy(value);
|
|
2095
|
-
|
|
2188
|
+
const release = passport?.release || {};
|
|
2189
|
+
const acceptedSourceShas = new Set([String(release.sourceSha || "").toLowerCase()]);
|
|
2190
|
+
if (
|
|
2191
|
+
release.treeEquivalent === true
|
|
2192
|
+
&& release.builtSourceTreeSha
|
|
2193
|
+
&& release.builtSourceTreeSha === release.promotionChannelTreeSha
|
|
2194
|
+
&& release.builtSourceSha
|
|
2195
|
+
) {
|
|
2196
|
+
acceptedSourceShas.add(String(release.builtSourceSha).toLowerCase());
|
|
2197
|
+
}
|
|
2198
|
+
if (!acceptedSourceShas.has(policy.caller.sourceSha)) {
|
|
2096
2199
|
issues.push(issue(
|
|
2097
2200
|
"error",
|
|
2098
2201
|
`githubArtifactAttestations[${index}].caller.sourceSha`,
|
|
@@ -2490,8 +2593,15 @@ function validatePassportTrustSections({ passport, issues }) {
|
|
|
2490
2593
|
].filter(Boolean));
|
|
2491
2594
|
for (const [index, entry] of (section.passports || []).entries()) {
|
|
2492
2595
|
const prefix = `invariantPassports.passports[${index}]`;
|
|
2493
|
-
if (entry.
|
|
2494
|
-
|
|
2596
|
+
if (entry.admission?.scope !== "consumer-invariant-coverage" || entry.admission?.result !== "passed") {
|
|
2597
|
+
issues.push(issue("error", `${prefix}.admission`, `${prefix}.admission must record passed consumer invariant coverage`));
|
|
2598
|
+
}
|
|
2599
|
+
if (entry.verdict !== "verified" && !(entry.verdict === "unqualified" && entry.releaseClaims?.verdict === "unqualified")) {
|
|
2600
|
+
issues.push(issue("error", `${prefix}.verdict`, `${prefix}.verdict must be verified or preserve one explicit unqualified releaseClaims verdict`));
|
|
2601
|
+
}
|
|
2602
|
+
if (entry.coverage?.complete !== true && !hasCompleteInvariantCoverage(entry)) {
|
|
2603
|
+
issues.push(issue("error", `${prefix}.coverage`, `${prefix}.coverage must be complete or prove every required invariant coordinate verified`));
|
|
2604
|
+
}
|
|
2495
2605
|
if (entry.source?.dirty !== false) issues.push(issue("error", `${prefix}.source.dirty`, `${prefix}.source.dirty must be false`));
|
|
2496
2606
|
if (acceptedSourceShas.size > 0 && !acceptedSourceShas.has(entry.source?.revision)) {
|
|
2497
2607
|
issues.push(issue("error", `${prefix}.source.revision`, `${prefix}.source.revision must match a release source identity`));
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export const RELEASE_TAIL_COMPATIBILITY_CONTRACT =
|
|
2
|
+
"kungfu.buildchain.release-tail.compatibility-diagnostics/v1";
|
|
3
|
+
|
|
4
|
+
export const RELEASE_TAIL_LEGACY_HOOKS = Object.freeze({
|
|
5
|
+
"publication-gate-command": "declarative admission predicates",
|
|
6
|
+
"publication-consumer-qualification-command":
|
|
7
|
+
"declarative readback predicates",
|
|
8
|
+
"publish-command": "artifact.publish",
|
|
9
|
+
"lifecycle.publish": "artifact.publish",
|
|
10
|
+
"publication-commit-command": "signed-channel.commit",
|
|
11
|
+
"release-activation-command": "release.activate",
|
|
12
|
+
"release-passport-evidence-command": "released-evidence.synthesize",
|
|
13
|
+
"release-passport-attachment-command": "typed evidence requirements",
|
|
14
|
+
"verification-command": "the separate version-state verification contract",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function present(value) {
|
|
18
|
+
if (value === undefined || value === null || value === false) return false;
|
|
19
|
+
if (typeof value === "string") return value.trim() !== "";
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function diagnoseLegacyReleaseTailHooks(hooks = {}) {
|
|
24
|
+
if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
|
|
25
|
+
throw new Error("legacy release-tail hook diagnostics require an object");
|
|
26
|
+
}
|
|
27
|
+
const diagnostics = [];
|
|
28
|
+
for (const name of Object.keys(hooks).sort()) {
|
|
29
|
+
if (!present(hooks[name])) continue;
|
|
30
|
+
const replacement = RELEASE_TAIL_LEGACY_HOOKS[name];
|
|
31
|
+
if (!replacement) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
code: "release-tail-command-forbidden",
|
|
34
|
+
hook: name,
|
|
35
|
+
level: "error",
|
|
36
|
+
message: `unregistered executable release-tail hook '${name}' is forbidden`,
|
|
37
|
+
replacement: "none",
|
|
38
|
+
});
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
code: "release-tail-hook-deprecated",
|
|
43
|
+
hook: name,
|
|
44
|
+
level: "warning",
|
|
45
|
+
message: `legacy release-tail hook '${name}' is deprecated; use ${replacement}`,
|
|
46
|
+
replacement,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
schema: RELEASE_TAIL_COMPATIBILITY_CONTRACT,
|
|
51
|
+
compatible: diagnostics.every((entry) => entry.level !== "error"),
|
|
52
|
+
migrationWindow: {
|
|
53
|
+
startsAt: "train/v3/v3.0/release-tail-contract",
|
|
54
|
+
closesAt: "earlier-of-90-days-or-first-v3.2-stable",
|
|
55
|
+
maximumMinorLines: 2,
|
|
56
|
+
permanentEscapeHatch: false,
|
|
57
|
+
},
|
|
58
|
+
diagnostics,
|
|
59
|
+
};
|
|
60
|
+
}
|