@kungfu-tech/buildchain 2.12.9-alpha.0 → 2.13.0-alpha.0
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/run-lifecycle/README.md +5 -0
- package/bin/buildchain.mjs +62 -0
- package/dist/site/buildchain-contract.json +33 -23
- package/dist/site/buildchain-site.json +66 -30
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +24 -0
- package/dist/site/controller-registry.json +11 -3
- package/dist/site/kfd-claims.json +64 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +7 -7
- package/dist/site/node-api-registry.json +19 -6
- package/dist/site/page-registry.json +55 -19
- package/dist/site/public-surface-audit.json +47 -10
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +11 -11
- package/dist/site/workflow-registry.json +7 -4
- package/docs/MAP.md +1 -0
- package/docs/artifact-verification-envelope.md +113 -0
- package/docs/cli.md +22 -0
- package/docs/release-candidate.md +5 -0
- package/docs/release-governance.md +7 -0
- package/docs/release-propagation.md +3 -0
- package/docs/reusable-build-surface.md +13 -0
- package/docs/versioning.md +1 -0
- package/package.json +2 -1
- package/packages/core/README.md +13 -0
- package/packages/core/artifact-passport.js +6 -1
- package/packages/core/artifact-verification-envelope.js +386 -0
- package/packages/core/buildchain-config.js +6 -2
- package/packages/core/controller-evidence.js +2 -3
- package/packages/core/index.js +10 -0
- package/packages/core/release-line-bootstrap.js +24 -0
- package/scripts/check-inventory.mjs +5 -0
- package/scripts/generate-site-bundle.mjs +4 -0
- package/scripts/release-candidate-resolver.mjs +52 -29
- package/scripts/run-lifecycle-core.mjs +50 -6
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT =
|
|
4
|
+
"kungfu-buildchain-artifact-verification-envelope";
|
|
5
|
+
export const ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT =
|
|
6
|
+
"kungfu-buildchain-artifact-verification-envelope-check";
|
|
7
|
+
export const KFX_ADMISSION_INPUTS_CONTRACT =
|
|
8
|
+
"kungfu-buildchain-kfx-admission-inputs";
|
|
9
|
+
|
|
10
|
+
const ARTIFACT_VERIFICATION_CONTRACT =
|
|
11
|
+
"kungfu-buildchain-artifact-verification";
|
|
12
|
+
const KFX_TRUST_INPUTS_SCHEMA = "kungfu.kfx-trust-inputs/v1";
|
|
13
|
+
const KFD_ASSESSMENT_SCHEMA = "kungfu.trust.assessment/v1";
|
|
14
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const ROOT_FIELDS = Object.freeze([
|
|
16
|
+
"packageRoot",
|
|
17
|
+
"sourceRoot",
|
|
18
|
+
"dependencyRoot",
|
|
19
|
+
"buildPlanRoot",
|
|
20
|
+
"toolchainRoot",
|
|
21
|
+
"artifactRoot",
|
|
22
|
+
"qualificationRoot",
|
|
23
|
+
"verifierRoot",
|
|
24
|
+
]);
|
|
25
|
+
const BINDING_FIELDS = Object.freeze([
|
|
26
|
+
"schema",
|
|
27
|
+
...ROOT_FIELDS,
|
|
28
|
+
"issuer",
|
|
29
|
+
"publisher",
|
|
30
|
+
"contractVersion",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function stableJson(value) {
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === "object") {
|
|
38
|
+
return `{${Object.keys(value)
|
|
39
|
+
.sort()
|
|
40
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
41
|
+
.join(",")}}`;
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function jsonClone(value) {
|
|
47
|
+
return JSON.parse(JSON.stringify(value));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function artifactVerificationEnvelopeDigest(value) {
|
|
51
|
+
return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function issue(code, message, details = {}) {
|
|
55
|
+
return { level: "error", code, message, details };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isObject(value) {
|
|
59
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isRoot(value) {
|
|
63
|
+
return SHA256_PATTERN.test(String(value || ""));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function nonEmptyString(value, label) {
|
|
67
|
+
const normalized = String(value || "").trim();
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
70
|
+
}
|
|
71
|
+
return normalized;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function nonNegativeInteger(value, label) {
|
|
75
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
76
|
+
throw new Error(`${label} must be a non-negative safe integer`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeBindings(value = {}) {
|
|
82
|
+
if (!isObject(value)) {
|
|
83
|
+
throw new Error("bindings must be an object");
|
|
84
|
+
}
|
|
85
|
+
const supplied = Object.keys(value).sort();
|
|
86
|
+
const expected = [...BINDING_FIELDS].sort();
|
|
87
|
+
if (stableJson(supplied) !== stableJson(expected)) {
|
|
88
|
+
throw new Error(`bindings must contain exactly: ${BINDING_FIELDS.join(", ")}`);
|
|
89
|
+
}
|
|
90
|
+
if (value.schema !== KFX_TRUST_INPUTS_SCHEMA) {
|
|
91
|
+
throw new Error(`bindings.schema must be ${KFX_TRUST_INPUTS_SCHEMA}`);
|
|
92
|
+
}
|
|
93
|
+
const normalized = { schema: KFX_TRUST_INPUTS_SCHEMA };
|
|
94
|
+
for (const field of ROOT_FIELDS) {
|
|
95
|
+
const root = String(value[field] || "");
|
|
96
|
+
if (!isRoot(root)) {
|
|
97
|
+
throw new Error(`bindings.${field} must be a canonical lowercase sha256 root`);
|
|
98
|
+
}
|
|
99
|
+
normalized[field] = root;
|
|
100
|
+
}
|
|
101
|
+
normalized.issuer = nonEmptyString(value.issuer, "bindings.issuer");
|
|
102
|
+
normalized.publisher = nonEmptyString(value.publisher, "bindings.publisher");
|
|
103
|
+
normalized.contractVersion = nonEmptyString(value.contractVersion, "bindings.contractVersion");
|
|
104
|
+
return normalized;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeRevocation(value = {}, { issuedAt } = {}) {
|
|
108
|
+
if (!isObject(value)) {
|
|
109
|
+
throw new Error("revocation must be an object");
|
|
110
|
+
}
|
|
111
|
+
const revoked = value.revoked === true;
|
|
112
|
+
const status = nonEmptyString(value.status || (revoked ? "revoked" : "active"), "revocation.status");
|
|
113
|
+
if (!new Set(["active", "revoked"]).has(status) || revoked !== (status === "revoked")) {
|
|
114
|
+
throw new Error("revocation.status must agree with revocation.revoked");
|
|
115
|
+
}
|
|
116
|
+
const checkedAt = nonNegativeInteger(value.checkedAt ?? issuedAt, "revocation.checkedAt");
|
|
117
|
+
const root = String(value.root || "");
|
|
118
|
+
if (!isRoot(root)) {
|
|
119
|
+
throw new Error("revocation.root must be a canonical lowercase sha256 root");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
status,
|
|
123
|
+
revoked,
|
|
124
|
+
checkedAt,
|
|
125
|
+
source: nonEmptyString(value.source, "revocation.source"),
|
|
126
|
+
root,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function envelopeRoot(value) {
|
|
131
|
+
const basis = jsonClone(value);
|
|
132
|
+
if (isObject(basis.envelope)) {
|
|
133
|
+
delete basis.envelope.root;
|
|
134
|
+
}
|
|
135
|
+
return artifactVerificationEnvelopeDigest(basis);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function recomputeKfdReportRoot(report) {
|
|
139
|
+
const basis = jsonClone(report);
|
|
140
|
+
delete basis.report_hash;
|
|
141
|
+
return artifactVerificationEnvelopeDigest(basis);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function verifyArtifactVerificationEnvelope({
|
|
145
|
+
envelope,
|
|
146
|
+
assessmentTime = Math.floor(Date.now() / 1000),
|
|
147
|
+
expectedEnvelopeRoot = "",
|
|
148
|
+
expectedIssuer = "",
|
|
149
|
+
expectedPublisher = "",
|
|
150
|
+
expectedContractVersion = "",
|
|
151
|
+
} = {}) {
|
|
152
|
+
const issues = [];
|
|
153
|
+
const at = Number.isSafeInteger(assessmentTime) && assessmentTime >= 0
|
|
154
|
+
? assessmentTime
|
|
155
|
+
: -1;
|
|
156
|
+
if (!isObject(envelope)) {
|
|
157
|
+
issues.push(issue("envelope.object", "artifact verification envelope must be an object"));
|
|
158
|
+
return {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
161
|
+
ok: false,
|
|
162
|
+
outcome: "fail",
|
|
163
|
+
assessmentTime,
|
|
164
|
+
issues,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const metadata = isObject(envelope.envelope) ? envelope.envelope : {};
|
|
169
|
+
if (
|
|
170
|
+
metadata.contract !== ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT ||
|
|
171
|
+
metadata.schemaVersion !== 1 ||
|
|
172
|
+
metadata.canonicalization !== "buildchain-stable-json/v1"
|
|
173
|
+
) {
|
|
174
|
+
issues.push(issue("envelope.contract", "artifact verification envelope contract metadata is invalid"));
|
|
175
|
+
}
|
|
176
|
+
const computedEnvelopeRoot = envelopeRoot(envelope);
|
|
177
|
+
if (!isRoot(metadata.root) || metadata.root !== computedEnvelopeRoot) {
|
|
178
|
+
issues.push(issue("envelope.root", "artifact verification envelope root does not match canonical content", {
|
|
179
|
+
expected: computedEnvelopeRoot,
|
|
180
|
+
actual: metadata.root || "",
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
if (expectedEnvelopeRoot && metadata.root !== expectedEnvelopeRoot) {
|
|
184
|
+
issues.push(issue("envelope.root.expected", "artifact verification envelope root does not match the pinned root"));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const verificationOk =
|
|
188
|
+
envelope.contract === ARTIFACT_VERIFICATION_CONTRACT &&
|
|
189
|
+
envelope.schemaVersion === 1 &&
|
|
190
|
+
envelope.outcome === "pass" &&
|
|
191
|
+
envelope.ok === true &&
|
|
192
|
+
envelope.trust === "pass" &&
|
|
193
|
+
envelope.passport?.verification?.ok === true &&
|
|
194
|
+
envelope.passport?.verification?.trust === "pass";
|
|
195
|
+
if (!verificationOk) {
|
|
196
|
+
issues.push(issue("verification.invalid", "base artifact verification must be a passing v1 report"));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let bindings;
|
|
200
|
+
try {
|
|
201
|
+
bindings = normalizeBindings(envelope.bindings);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
issues.push(issue("bindings.invalid", error.message));
|
|
204
|
+
}
|
|
205
|
+
if (bindings) {
|
|
206
|
+
const subjectRoot = String(envelope.subject?.digest || "");
|
|
207
|
+
const matchedRoot = String(envelope.match?.artifact?.digest || "");
|
|
208
|
+
if (
|
|
209
|
+
!isRoot(subjectRoot) ||
|
|
210
|
+
subjectRoot !== matchedRoot ||
|
|
211
|
+
bindings.artifactRoot !== subjectRoot
|
|
212
|
+
) {
|
|
213
|
+
issues.push(issue("bindings.artifactRoot", "artifact root must match the exact verified subject and passport artifact"));
|
|
214
|
+
}
|
|
215
|
+
if (expectedIssuer && bindings.issuer !== expectedIssuer) {
|
|
216
|
+
issues.push(issue("bindings.issuer", "issuer does not match the expected authority"));
|
|
217
|
+
}
|
|
218
|
+
if (expectedPublisher && bindings.publisher !== expectedPublisher) {
|
|
219
|
+
issues.push(issue("bindings.publisher", "publisher does not match the expected identity"));
|
|
220
|
+
}
|
|
221
|
+
if (expectedContractVersion && bindings.contractVersion !== expectedContractVersion) {
|
|
222
|
+
issues.push(issue("bindings.contractVersion", "verification contract version does not match the expected version"));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const issuedAt = envelope.issuedAt;
|
|
227
|
+
const expiresAt = envelope.expiresAt;
|
|
228
|
+
if (
|
|
229
|
+
at < 0 ||
|
|
230
|
+
!Number.isSafeInteger(issuedAt) ||
|
|
231
|
+
issuedAt < 0 ||
|
|
232
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
233
|
+
expiresAt <= issuedAt ||
|
|
234
|
+
at < issuedAt ||
|
|
235
|
+
at >= expiresAt
|
|
236
|
+
) {
|
|
237
|
+
issues.push(issue("lifecycle.invalid", "envelope must be active at assessmentTime with valid issuedAt/expiresAt bounds"));
|
|
238
|
+
}
|
|
239
|
+
const revocation = envelope.revocation;
|
|
240
|
+
if (
|
|
241
|
+
!isObject(revocation) ||
|
|
242
|
+
!isRoot(revocation.root) ||
|
|
243
|
+
!Number.isSafeInteger(revocation.checkedAt) ||
|
|
244
|
+
revocation.checkedAt < issuedAt ||
|
|
245
|
+
(at >= 0 && revocation.checkedAt > at) ||
|
|
246
|
+
!["active", "revoked"].includes(revocation.status) ||
|
|
247
|
+
revocation.revoked !== (revocation.status === "revoked") ||
|
|
248
|
+
envelope.revoked !== revocation.revoked
|
|
249
|
+
) {
|
|
250
|
+
issues.push(issue("lifecycle.revocation", "revocation binding is invalid or inconsistent"));
|
|
251
|
+
}
|
|
252
|
+
if (envelope.revoked === true) {
|
|
253
|
+
issues.push(issue("lifecycle.revoked", "artifact verification envelope has been revoked"));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const assessment = envelope.kfdAssessment;
|
|
257
|
+
const report = isObject(assessment?.report) ? assessment.report : {};
|
|
258
|
+
if (
|
|
259
|
+
!isObject(assessment) ||
|
|
260
|
+
assessment.schema !== KFD_ASSESSMENT_SCHEMA ||
|
|
261
|
+
assessment.state !== "fresh" ||
|
|
262
|
+
report.state !== "fresh"
|
|
263
|
+
) {
|
|
264
|
+
issues.push(issue("kfd.assessment.state", "KFD assessment must be a fresh ADR-0052 assessment"));
|
|
265
|
+
}
|
|
266
|
+
if (
|
|
267
|
+
!isRoot(assessment?.assessment_key) ||
|
|
268
|
+
report.assessment_key !== assessment?.assessment_key
|
|
269
|
+
) {
|
|
270
|
+
issues.push(issue("kfd.assessment.key", "KFD assessment key must be canonical and match the report"));
|
|
271
|
+
}
|
|
272
|
+
const computedReportRoot = isObject(report) ? recomputeKfdReportRoot(report) : "";
|
|
273
|
+
if (!isRoot(report.report_hash) || report.report_hash !== computedReportRoot) {
|
|
274
|
+
issues.push(issue("kfd.report.root", "KFD report hash does not match canonical report content", {
|
|
275
|
+
expected: computedReportRoot,
|
|
276
|
+
actual: report.report_hash || "",
|
|
277
|
+
}));
|
|
278
|
+
}
|
|
279
|
+
if (
|
|
280
|
+
!String(report.purpose || "").trim() ||
|
|
281
|
+
!isRoot(report.query_proof_root) ||
|
|
282
|
+
!isRoot(report.contract_world?.root) ||
|
|
283
|
+
!isRoot(report.policy?.root) ||
|
|
284
|
+
!Array.isArray(report.fact_surfaces) ||
|
|
285
|
+
report.fact_surfaces.length === 0 ||
|
|
286
|
+
report.fact_surfaces.some((surface) => !isRoot(surface?.root))
|
|
287
|
+
) {
|
|
288
|
+
issues.push(issue("kfd.report.bindings", "KFD report must bind purpose, query proof, contract world, policy, and fact-surface roots"));
|
|
289
|
+
}
|
|
290
|
+
if (bindings && bindings.qualificationRoot !== report.report_hash) {
|
|
291
|
+
issues.push(issue("bindings.qualificationRoot", "qualification root must equal the KFD report hash"));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const ok = issues.length === 0;
|
|
295
|
+
return {
|
|
296
|
+
schemaVersion: 1,
|
|
297
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
298
|
+
ok,
|
|
299
|
+
outcome: ok ? "pass" : "fail",
|
|
300
|
+
assessmentTime,
|
|
301
|
+
envelopeRoot: metadata.root || "",
|
|
302
|
+
bindings: bindings || undefined,
|
|
303
|
+
kfdAssessment: isObject(assessment)
|
|
304
|
+
? {
|
|
305
|
+
state: assessment.state || "",
|
|
306
|
+
assessmentKey: assessment.assessment_key || "",
|
|
307
|
+
reportRoot: report.report_hash || "",
|
|
308
|
+
}
|
|
309
|
+
: undefined,
|
|
310
|
+
issues,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function sealArtifactVerificationReport({
|
|
315
|
+
report,
|
|
316
|
+
bindings,
|
|
317
|
+
kfdAssessment,
|
|
318
|
+
issuedAt,
|
|
319
|
+
expiresAt,
|
|
320
|
+
revocation,
|
|
321
|
+
} = {}) {
|
|
322
|
+
if (!isObject(report)) {
|
|
323
|
+
throw new Error("report must be an artifact verification object");
|
|
324
|
+
}
|
|
325
|
+
const normalizedIssuedAt = nonNegativeInteger(issuedAt, "issuedAt");
|
|
326
|
+
const normalizedExpiresAt = nonNegativeInteger(expiresAt, "expiresAt");
|
|
327
|
+
if (normalizedExpiresAt <= normalizedIssuedAt) {
|
|
328
|
+
throw new Error("expiresAt must be greater than issuedAt");
|
|
329
|
+
}
|
|
330
|
+
const normalizedRevocation = normalizeRevocation(revocation, {
|
|
331
|
+
issuedAt: normalizedIssuedAt,
|
|
332
|
+
});
|
|
333
|
+
const result = {
|
|
334
|
+
...jsonClone(report),
|
|
335
|
+
issuedAt: normalizedIssuedAt,
|
|
336
|
+
expiresAt: normalizedExpiresAt,
|
|
337
|
+
revoked: normalizedRevocation.revoked,
|
|
338
|
+
revocation: normalizedRevocation,
|
|
339
|
+
bindings: normalizeBindings(bindings),
|
|
340
|
+
kfdAssessment: jsonClone(kfdAssessment),
|
|
341
|
+
envelope: {
|
|
342
|
+
schemaVersion: 1,
|
|
343
|
+
contract: ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
|
|
344
|
+
canonicalization: "buildchain-stable-json/v1",
|
|
345
|
+
root: "",
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
result.envelope.root = envelopeRoot(result);
|
|
349
|
+
const check = verifyArtifactVerificationEnvelope({
|
|
350
|
+
envelope: result,
|
|
351
|
+
assessmentTime: normalizedRevocation.checkedAt,
|
|
352
|
+
});
|
|
353
|
+
if (!check.ok) {
|
|
354
|
+
throw new Error(`artifact verification envelope is invalid: ${check.issues.map((entry) => entry.code).join(", ")}`);
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function projectArtifactVerificationEnvelopeToKfx({
|
|
360
|
+
envelope,
|
|
361
|
+
assessmentTime = Math.floor(Date.now() / 1000),
|
|
362
|
+
expectedEnvelopeRoot = "",
|
|
363
|
+
expectedIssuer = "",
|
|
364
|
+
expectedPublisher = "",
|
|
365
|
+
expectedContractVersion = "",
|
|
366
|
+
} = {}) {
|
|
367
|
+
const check = verifyArtifactVerificationEnvelope({
|
|
368
|
+
envelope,
|
|
369
|
+
assessmentTime,
|
|
370
|
+
expectedEnvelopeRoot,
|
|
371
|
+
expectedIssuer,
|
|
372
|
+
expectedPublisher,
|
|
373
|
+
expectedContractVersion,
|
|
374
|
+
});
|
|
375
|
+
if (!check.ok) {
|
|
376
|
+
throw new Error(`artifact verification envelope failed closed: ${check.issues.map((entry) => entry.code).join(", ")}`);
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
schemaVersion: 1,
|
|
380
|
+
contract: KFX_ADMISSION_INPUTS_CONTRACT,
|
|
381
|
+
envelopeRoot: envelope.envelope.root,
|
|
382
|
+
attestation: jsonClone(envelope),
|
|
383
|
+
trustInputs: jsonClone(envelope.bindings),
|
|
384
|
+
kfdAssessment: jsonClone(envelope.kfdAssessment),
|
|
385
|
+
};
|
|
386
|
+
}
|
|
@@ -1181,7 +1181,7 @@ export function getPublishContract(loadedConfig) {
|
|
|
1181
1181
|
return loadedConfig?.config?.publish;
|
|
1182
1182
|
}
|
|
1183
1183
|
|
|
1184
|
-
export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv }) {
|
|
1184
|
+
export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, stage, env: extraEnv, timeoutMinutes }) {
|
|
1185
1185
|
const lifecycle = loadedConfig?.config?.lifecycle || {};
|
|
1186
1186
|
const selected = stage || getLifecycleStage(loadedConfig, name);
|
|
1187
1187
|
if (!selected) {
|
|
@@ -1193,7 +1193,11 @@ export function runLifecycleStage({ cwd = process.cwd(), loadedConfig, name, sta
|
|
|
1193
1193
|
...(selected.env || {}),
|
|
1194
1194
|
...(extraEnv || {}),
|
|
1195
1195
|
};
|
|
1196
|
-
const
|
|
1196
|
+
const effectiveTimeoutMinutes = selected.timeoutMinutes ?? timeoutMinutes;
|
|
1197
|
+
if (effectiveTimeoutMinutes !== undefined && (!Number.isFinite(effectiveTimeoutMinutes) || effectiveTimeoutMinutes <= 0)) {
|
|
1198
|
+
throw new Error("lifecycle timeoutMinutes must be a positive number");
|
|
1199
|
+
}
|
|
1200
|
+
const timeout = effectiveTimeoutMinutes ? effectiveTimeoutMinutes * 60_000 : undefined;
|
|
1197
1201
|
const execOptions = {
|
|
1198
1202
|
cwd,
|
|
1199
1203
|
env,
|
|
@@ -344,7 +344,6 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
|
|
|
344
344
|
const issues = status === "passed"
|
|
345
345
|
? missingEvidence.map((kind) => `required controller evidence is missing: ${kind}`)
|
|
346
346
|
: [];
|
|
347
|
-
const qualifying = status === "passed" && plan.qualifying === true && issues.length === 0;
|
|
348
347
|
const receipt = {
|
|
349
348
|
schemaVersion: 1,
|
|
350
349
|
contract: BUILDCHAIN_CONTROLLER_EVIDENCE_CONTRACT,
|
|
@@ -354,11 +353,11 @@ export function createControllerReceipt({ plan, stages = [], evidence = [], reas
|
|
|
354
353
|
runtime: { ...plan.runtime },
|
|
355
354
|
planDigest: plan.digest,
|
|
356
355
|
status,
|
|
357
|
-
qualifying,
|
|
356
|
+
qualifying: status === "passed" && plan.qualifying === true && issues.length === 0,
|
|
358
357
|
stages: normalizedStages,
|
|
359
358
|
evidence: normalizedEvidence,
|
|
360
359
|
issues,
|
|
361
|
-
...(reason
|
|
360
|
+
...(reason ? {
|
|
362
361
|
reason: {
|
|
363
362
|
code: nonEmptyString(reason.code, "reason.code"),
|
|
364
363
|
summary: nonEmptyString(reason.summary, "reason.summary"),
|
package/packages/core/index.js
CHANGED
|
@@ -179,6 +179,16 @@ export {
|
|
|
179
179
|
verifyArtifactPassport,
|
|
180
180
|
} from "./artifact-passport.js";
|
|
181
181
|
|
|
182
|
+
export {
|
|
183
|
+
ARTIFACT_VERIFICATION_ENVELOPE_CHECK_CONTRACT,
|
|
184
|
+
ARTIFACT_VERIFICATION_ENVELOPE_CONTRACT,
|
|
185
|
+
KFX_ADMISSION_INPUTS_CONTRACT,
|
|
186
|
+
artifactVerificationEnvelopeDigest,
|
|
187
|
+
projectArtifactVerificationEnvelopeToKfx,
|
|
188
|
+
sealArtifactVerificationReport,
|
|
189
|
+
verifyArtifactVerificationEnvelope,
|
|
190
|
+
} from "./artifact-verification-envelope.js";
|
|
191
|
+
|
|
182
192
|
export {
|
|
183
193
|
BUILDCHAIN_CONTRACT_LOCK,
|
|
184
194
|
BUILDCHAIN_RUNTIME_CONTRACT_WORLD,
|
|
@@ -65,6 +65,29 @@ function writeJson(value) {
|
|
|
65
65
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
function updateReleaseImpactVersionState(cwd, plan) {
|
|
69
|
+
const filePath = path.join(cwd, ".buildchain", "release-impact.json");
|
|
70
|
+
if (!fs.existsSync(filePath)) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const current = readJsonIfExists(filePath);
|
|
74
|
+
if (!current || typeof current !== "object" || Array.isArray(current)) {
|
|
75
|
+
throw new Error(".buildchain/release-impact.json must be a JSON object");
|
|
76
|
+
}
|
|
77
|
+
const next = {
|
|
78
|
+
...current,
|
|
79
|
+
release: {
|
|
80
|
+
...(current.release && typeof current.release === "object" ? current.release : {}),
|
|
81
|
+
version: plan.initialVersion,
|
|
82
|
+
line: plan.line,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const content = writeJson(next);
|
|
86
|
+
if (content !== writeJson(current)) {
|
|
87
|
+
fs.writeFileSync(filePath, content);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
function discoverVersionStateFiles(cwd, loadedConfig) {
|
|
69
92
|
if (loadedConfig?.config?.version?.files?.length) {
|
|
70
93
|
return discoverConfiguredVersionStateFiles(cwd, loadedConfig);
|
|
@@ -212,6 +235,7 @@ export function writeReleaseLineBootstrapVersionState({
|
|
|
212
235
|
for (const file of changed) {
|
|
213
236
|
fs.writeFileSync(path.join(cwd, file.path), file.content);
|
|
214
237
|
}
|
|
238
|
+
updateReleaseImpactVersionState(cwd, plan);
|
|
215
239
|
const lifecycleVersionState =
|
|
216
240
|
getLifecycleStage(loadedConfig, "version-state") ||
|
|
217
241
|
getLifecycleStage(loadedConfig, "version_state");
|
|
@@ -21,6 +21,7 @@ const requiredPaths = [
|
|
|
21
21
|
".github/pull_request_template.md",
|
|
22
22
|
"bin/buildchain.mjs",
|
|
23
23
|
"packages/core/homebrew.js",
|
|
24
|
+
"packages/core/artifact-verification-envelope.js",
|
|
24
25
|
"packages/core/build-facts.js",
|
|
25
26
|
"packages/core/publication-package.js",
|
|
26
27
|
"packages/core/release-line-bootstrap.js",
|
|
@@ -239,6 +240,9 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
|
|
|
239
240
|
if (rootPackage.exports?.["./controller-evidence"] !== "./packages/core/controller-evidence.js") {
|
|
240
241
|
throw new Error("root package must export @kungfu-tech/buildchain/controller-evidence");
|
|
241
242
|
}
|
|
243
|
+
if (rootPackage.exports?.["./artifact-verification-envelope"] !== "./packages/core/artifact-verification-envelope.js") {
|
|
244
|
+
throw new Error("root package must export @kungfu-tech/buildchain/artifact-verification-envelope");
|
|
245
|
+
}
|
|
242
246
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
243
247
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
244
248
|
}
|
|
@@ -843,6 +847,7 @@ for (const requiredSnippet of [
|
|
|
843
847
|
"expected-publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
844
848
|
"publication-version: ${{ needs.publication-plan.outputs.version }}",
|
|
845
849
|
"name: Plan exact publication version",
|
|
850
|
+
"name: Install exact publication planning dependencies",
|
|
846
851
|
"DRY_RUN: ${{ inputs.dry-run }}",
|
|
847
852
|
"if (dryRun) {",
|
|
848
853
|
"Enforce Buildchain stable release canary gate",
|
|
@@ -444,6 +444,8 @@ function cliCommandMeta(id) {
|
|
|
444
444
|
"publication-artifact": { group: "reusable-build", purpose: "Generate publication artifact manifests, passports, and source bundles for paper/report repositories." },
|
|
445
445
|
"publication-artifact-manifest": { group: "reusable-build", purpose: "Write a site-consumable publication artifact manifest, publication passport, and source bundle." },
|
|
446
446
|
"publication-artifact-npm-package": { group: "reusable-build", purpose: "Synthesize the declared npm paper package from a publication artifact manifest, passport, registry, source bundle, and primary artifact." },
|
|
447
|
+
project: { group: "release-passport-trust", purpose: "Inspect read-only consumer projection command families." },
|
|
448
|
+
"project-kfx-admission": { group: "release-passport-trust", purpose: "Project one verified sealed envelope into direct KFX admission inputs without reconstructing evidence." },
|
|
447
449
|
"release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
|
|
448
450
|
"release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
|
|
449
451
|
"release-propagation": { group: "site-and-propagation", purpose: "Plan channel-preserving downstream release PRs and write exact upstream release locks." },
|
|
@@ -455,6 +457,7 @@ function cliCommandMeta(id) {
|
|
|
455
457
|
validate: { group: "getting-started", purpose: "Validate .buildchain/buildchain.toml and declared lifecycle surfaces." },
|
|
456
458
|
verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
|
|
457
459
|
"verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
|
|
460
|
+
"verify-artifact-envelope": { group: "release-passport-trust", purpose: "Verify exact roots, identity, lifecycle, revocation, and an existing KFD assessment in a sealed artifact envelope." },
|
|
458
461
|
"verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
|
|
459
462
|
"verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
|
|
460
463
|
"verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
|
|
@@ -485,6 +488,7 @@ function nodeApiMeta(exportName) {
|
|
|
485
488
|
"./publication-control-plane-audit": { group: "release-passport-trust", summary: "Read-only publication control-plane snapshot evaluation APIs." },
|
|
486
489
|
"./buildchain-publication-authority": { group: "release-passport-trust", summary: "Buildchain-owned closed-world publication authority descriptor registry." },
|
|
487
490
|
"./artifact-passport": { group: "release-passport-trust", summary: "Artifact passport digest and evidence helper APIs." },
|
|
491
|
+
"./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
|
|
488
492
|
"./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
|
|
489
493
|
"./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
|
|
490
494
|
"./stable-candidate-ledger": { group: "governance-versioning", summary: "Immutable alpha candidate ledger, qualification, revocation, selection, and exact stable source-lock APIs." },
|
|
@@ -353,6 +353,9 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
353
353
|
outputDir = ".buildchain/release-candidate",
|
|
354
354
|
fetchImpl = globalThis.fetch,
|
|
355
355
|
download = true,
|
|
356
|
+
waitSeconds = 600,
|
|
357
|
+
pollIntervalMs = 15000,
|
|
358
|
+
sleepImpl = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
356
359
|
} = {}) {
|
|
357
360
|
const repoInfo = splitRepository(repository);
|
|
358
361
|
const sha = assertSha(targetSha, "targetSha");
|
|
@@ -377,45 +380,63 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
377
380
|
if (!pullRequest) {
|
|
378
381
|
throw new Error(`no same-repository merged channel PR found for ${sha} into ${normalizedTarget}`);
|
|
379
382
|
}
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
|
|
388
|
-
pullRequest,
|
|
389
|
-
workflowName,
|
|
390
|
-
});
|
|
391
|
-
if (!candidateRuns.length) {
|
|
392
|
-
throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number}`);
|
|
383
|
+
const timeoutMs = Number(waitSeconds) * 1000;
|
|
384
|
+
const intervalMs = Number(pollIntervalMs);
|
|
385
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
|
|
386
|
+
throw new Error(`release-candidate waitSeconds must be a non-negative number, got ${waitSeconds}`);
|
|
387
|
+
}
|
|
388
|
+
if (!Number.isFinite(intervalMs) || intervalMs < 0) {
|
|
389
|
+
throw new Error(`release-candidate pollIntervalMs must be a non-negative number, got ${pollIntervalMs}`);
|
|
393
390
|
}
|
|
391
|
+
const deadline = Date.now() + timeoutMs;
|
|
394
392
|
let run;
|
|
395
393
|
let artifactResponse;
|
|
396
394
|
let selected;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
395
|
+
let selectionErrors = [];
|
|
396
|
+
let candidateRuns = [];
|
|
397
|
+
while (!selected) {
|
|
398
|
+
const runs = await githubJson({
|
|
400
399
|
apiUrl,
|
|
401
400
|
token,
|
|
402
401
|
fetchImpl,
|
|
403
|
-
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/
|
|
402
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
|
|
404
403
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
404
|
+
candidateRuns = selectReleaseCandidateRuns({
|
|
405
|
+
runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
|
|
406
|
+
pullRequest,
|
|
407
|
+
workflowName,
|
|
408
|
+
});
|
|
409
|
+
selectionErrors = [];
|
|
410
|
+
for (const candidateRun of candidateRuns) {
|
|
411
|
+
const candidateArtifactResponse = await githubJson({
|
|
412
|
+
apiUrl,
|
|
413
|
+
token,
|
|
414
|
+
fetchImpl,
|
|
415
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${candidateRun.id}/artifacts?per_page=100`,
|
|
409
416
|
});
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
417
|
+
try {
|
|
418
|
+
selected = selectReleaseCandidateArtifacts({
|
|
419
|
+
artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
|
|
420
|
+
artifactName,
|
|
421
|
+
});
|
|
422
|
+
run = candidateRun;
|
|
423
|
+
artifactResponse = candidateArtifactResponse;
|
|
424
|
+
break;
|
|
425
|
+
} catch (error) {
|
|
426
|
+
selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
|
|
427
|
+
}
|
|
415
428
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
429
|
+
if (selected) break;
|
|
430
|
+
if (Date.now() >= deadline) {
|
|
431
|
+
if (!candidateRuns.length) {
|
|
432
|
+
throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number} within ${waitSeconds}s`);
|
|
433
|
+
}
|
|
434
|
+
throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts within ${waitSeconds}s: ${selectionErrors.join("; ")}`);
|
|
435
|
+
}
|
|
436
|
+
console.log(
|
|
437
|
+
`> waiting for successful ${workflowName} release-candidate evidence for channel PR #${pullRequest.number}`,
|
|
438
|
+
);
|
|
439
|
+
await sleepImpl(intervalMs);
|
|
419
440
|
}
|
|
420
441
|
const payloadArtifacts = selectPayloadArtifacts({
|
|
421
442
|
artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
|
|
@@ -551,6 +572,8 @@ export async function resolveReleaseCandidateArtifactsCli() {
|
|
|
551
572
|
publishPackageMain: env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN"),
|
|
552
573
|
outputDir: env("BUILDCHAIN_RC_OUTPUT_DIR", ".buildchain/release-candidate"),
|
|
553
574
|
download: releaseCandidateDownloadEnabled(env("BUILDCHAIN_RC_DOWNLOAD", "true")),
|
|
575
|
+
waitSeconds: env("BUILDCHAIN_RC_WAIT_SECONDS", "600"),
|
|
576
|
+
pollIntervalMs: env("BUILDCHAIN_RC_POLL_INTERVAL_MS", "15000"),
|
|
554
577
|
});
|
|
555
578
|
writeGitHubOutputs({
|
|
556
579
|
"promote-only-release-candidate": String(result.enabled === true),
|