@kungfu-tech/buildchain 3.0.2 → 3.0.3-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.
Files changed (52) hide show
  1. package/actions/report-buildchain-issue/README.md +1 -1
  2. package/contracts/buildchain-v2-residuals-v1.json +191 -0
  3. package/dist/site/buildchain-contract.json +58 -35
  4. package/dist/site/buildchain-site.json +33 -33
  5. package/dist/site/controller-registry.json +24 -3
  6. package/dist/site/kfd-claims.json +11 -6
  7. package/dist/site/kfd-upstream-aggregate.json +1 -1
  8. package/dist/site/manual-registry.json +7 -7
  9. package/dist/site/node-api-registry.json +6 -6
  10. package/dist/site/page-registry.json +22 -22
  11. package/dist/site/public-surface-audit.json +10 -5
  12. package/dist/site/publication-registry.json +4 -4
  13. package/dist/site/site-manifest.json +11 -11
  14. package/dist/site/workflow-registry.json +8 -3
  15. package/docs/auditable-demo.md +19 -1
  16. package/docs/consumer-issue-reporting.md +1 -1
  17. package/docs/dev-alpha-candidate-patrol.md +24 -10
  18. package/docs/github-governance-authority.md +23 -30
  19. package/docs/observed-evidence-patrol.md +28 -12
  20. package/docs/release-governance.md +8 -1
  21. package/docs/reusable-build-surface.md +124 -64
  22. package/docs/shifu-gate-profiles.md +7 -1
  23. package/docs/versioning.md +41 -21
  24. package/docs/web-surface-deployments.md +20 -1
  25. package/package.json +1 -1
  26. package/packages/core/artifact-signing.js +1 -0
  27. package/packages/core/buildchain-contract.js +1 -0
  28. package/packages/core/controller-evidence.js +2 -0
  29. package/packages/core/github-governance-authority.js +25 -17
  30. package/packages/core/publication-control-plane-audit.js +28 -0
  31. package/packages/core/release-passport.js +36 -27
  32. package/packages/core/stable-release-gate.js +4 -1
  33. package/scripts/artifact-signing-delegation.mjs +268 -0
  34. package/scripts/audit-github-governance.mjs +32 -13
  35. package/scripts/audit-publication-control-plane.mjs +17 -8
  36. package/scripts/auditable-demo.mjs +147 -2
  37. package/scripts/buildchain-patrol.mjs +1 -1
  38. package/scripts/check-inventory.mjs +1 -0
  39. package/scripts/dev-alpha-candidate-patrol.mjs +20 -0
  40. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  41. package/scripts/finalize-native-artifact-signing-result.mjs +114 -31
  42. package/scripts/gate-profile-core.mjs +6 -1
  43. package/scripts/inspect-artifact-signing-requests.mjs +45 -14
  44. package/scripts/observed-evidence.mjs +151 -33
  45. package/scripts/reconcile-github-governance.mjs +8 -1
  46. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  47. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  48. package/scripts/runtime-ref-core.mjs +13 -2
  49. package/scripts/seal-artifact-signing-requests.mjs +51 -6
  50. package/scripts/stable-candidate-qualification.mjs +8 -0
  51. package/scripts/verify-artifact-signing-results.mjs +26 -1
  52. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -22,6 +22,19 @@ const REQUIRED_ADAPTER_FILES = [
22
22
  "public-projection.json",
23
23
  "scene.json",
24
24
  ];
25
+ const OPTIONAL_ADAPTER_FILES = ["terminal-capture.json"];
26
+ const MAX_TERMINAL_CAPTURE_BYTES = 4 * 1024 * 1024;
27
+ const MAX_TERMINAL_CAPTURE_EVENTS = 10_000;
28
+ const TERMINAL_CAPTURE_NON_AUTHORITIES = [
29
+ "first-party-identity",
30
+ "system-identity",
31
+ "kfd-compliance",
32
+ "product-system-metadata",
33
+ "package-metadata",
34
+ "registry-history",
35
+ "scan-output",
36
+ "standalone-generation",
37
+ ];
25
38
 
26
39
  function invariant(condition, message) {
27
40
  if (!condition) throw new Error(message);
@@ -155,6 +168,19 @@ function text(value, minimum, maximum, label) {
155
168
  return value;
156
169
  }
157
170
 
171
+ function decodeBase64(value, label) {
172
+ invariant(
173
+ typeof value === "string"
174
+ && value.length > 0
175
+ && value.length <= MAX_TERMINAL_CAPTURE_BYTES * 2
176
+ && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value),
177
+ `${label} must be canonical base64`,
178
+ );
179
+ const decoded = Buffer.from(value, "base64");
180
+ invariant(decoded.toString("base64") === value, `${label} must be canonical base64`);
181
+ return decoded;
182
+ }
183
+
158
184
  function validateScene(value) {
159
185
  exactKeys(
160
186
  value,
@@ -223,6 +249,81 @@ function validateProjection(value, scene, transcriptLineCount) {
223
249
  };
224
250
  }
225
251
 
252
+ function validateTerminalCapture(value, scene) {
253
+ exactKeys(
254
+ value,
255
+ [
256
+ "schema",
257
+ "command",
258
+ "dimensions",
259
+ "durationMs",
260
+ "encoding",
261
+ "events",
262
+ "completion",
263
+ "exitCode",
264
+ "authority",
265
+ ],
266
+ [],
267
+ "terminalCapture",
268
+ );
269
+ invariant(value.schema === "kungfu.terminal-capture/v1", "unsupported terminal capture schema");
270
+ text(value.command, 1, 160, "terminalCapture.command");
271
+ exactKeys(value.dimensions, ["columns", "rows"], [], "terminalCapture.dimensions");
272
+ integer(value.dimensions.columns, 80, 200, "terminalCapture.dimensions.columns");
273
+ integer(value.dimensions.rows, 24, 80, "terminalCapture.dimensions.rows");
274
+ const durationMs = integer(value.durationMs, 500, 60000, "terminalCapture.durationMs");
275
+ invariant(
276
+ durationMs <= scene.durationMs && scene.durationMs - durationMs <= 2000,
277
+ "terminal capture duration must end within two seconds of the scene",
278
+ );
279
+ invariant(value.encoding === "base64", "terminalCapture.encoding must be base64");
280
+ invariant(
281
+ Array.isArray(value.events)
282
+ && value.events.length > 0
283
+ && value.events.length <= MAX_TERMINAL_CAPTURE_EVENTS,
284
+ `terminalCapture.events must contain 1 through ${MAX_TERMINAL_CAPTURE_EVENTS} events`,
285
+ );
286
+ let previousAtMs = -1;
287
+ let totalBytes = 0;
288
+ for (const [index, event] of value.events.entries()) {
289
+ exactKeys(event, ["atMs", "data"], [], `terminalCapture.events[${index}]`);
290
+ const atMs = integer(event.atMs, 0, durationMs - 1, `terminalCapture.events[${index}].atMs`);
291
+ invariant(atMs >= previousAtMs, "terminal capture event timestamps must be monotonic");
292
+ invariant(index > 0 || atMs === 0, "the first terminal capture event must start at zero");
293
+ previousAtMs = atMs;
294
+ totalBytes += decodeBase64(event.data, `terminalCapture.events[${index}].data`).length;
295
+ invariant(totalBytes <= MAX_TERMINAL_CAPTURE_BYTES, "terminal capture exceeds the 4 MiB byte bound");
296
+ }
297
+ exactKeys(
298
+ value.completion,
299
+ ["schema", "status", "reportRoot", "eventCount"],
300
+ [],
301
+ "terminalCapture.completion",
302
+ );
303
+ invariant(
304
+ value.completion.schema === "kungfu.agent-work-lab.tui-autoplay/v1"
305
+ && value.completion.status === "qualified"
306
+ && DIGEST_PATTERN.test(value.completion.reportRoot),
307
+ "terminal capture completion sentinel is not a qualified Agent Work Lab autoplay",
308
+ );
309
+ integer(value.completion.eventCount, 1, 100_000, "terminalCapture.completion.eventCount");
310
+ invariant(value.exitCode === 0, "terminal capture exitCode must be zero");
311
+ exactKeys(value.authority, ["classification", "grants", "nonAuthorities"], [], "terminalCapture.authority");
312
+ invariant(
313
+ value.authority.classification === "volatile-terminal-observation",
314
+ "terminal capture authority classification must remain observation-only",
315
+ );
316
+ invariant(
317
+ Array.isArray(value.authority.grants) && value.authority.grants.length === 0,
318
+ "terminal capture must not grant authority",
319
+ );
320
+ invariant(
321
+ JSON.stringify(value.authority.nonAuthorities) === JSON.stringify(TERMINAL_CAPTURE_NON_AUTHORITIES),
322
+ "terminal capture must declare every identity and metadata non-authority",
323
+ );
324
+ return value;
325
+ }
326
+
226
327
  function validateSourceCoordinate(value) {
227
328
  exactKeys(
228
329
  value,
@@ -276,11 +377,21 @@ function validateAdapterOutput(output, strict = true) {
276
377
  scene,
277
378
  lines.length,
278
379
  );
380
+ const terminalCapturePath = path.join(output, "terminal-capture.json");
381
+ const terminalCapture = fs.existsSync(terminalCapturePath)
382
+ ? validateTerminalCapture(readJson(terminalCapturePath, "terminal capture"), scene)
383
+ : null;
279
384
  if (strict) {
280
- const allowed = new Set(REQUIRED_ADAPTER_FILES);
385
+ const allowed = new Set([...REQUIRED_ADAPTER_FILES, ...OPTIONAL_ADAPTER_FILES]);
281
386
  for (const member of listFiles(output)) invariant(allowed.has(member), `undeclared adapter output: ${member}`);
282
387
  }
283
- return { transcript: transcript.endsWith("\n") ? transcript : `${transcript}\n`, lines, scene, projection };
388
+ return {
389
+ transcript: transcript.endsWith("\n") ? transcript : `${transcript}\n`,
390
+ lines,
391
+ scene,
392
+ projection,
393
+ terminalCapture,
394
+ };
284
395
  }
285
396
 
286
397
  function parseArguments(argv) {
@@ -355,6 +466,9 @@ function runAdapter(values) {
355
466
  fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
356
467
  writeJson(path.join(output, "scene.json"), normalized.scene);
357
468
  writeJson(path.join(output, "public-projection.json"), normalized.projection);
469
+ if (normalized.terminalCapture) {
470
+ writeJson(path.join(output, "terminal-capture.json"), normalized.terminalCapture);
471
+ }
358
472
  writeJson(path.join(diagnostics, "adapter.json"), {
359
473
  schema: "buildchain.auditable-demo-adapter-execution/v1",
360
474
  path: adapterRelative,
@@ -885,6 +999,12 @@ function finalizeGate(values) {
885
999
  fs.writeFileSync(path.join(output, "complete-transcript.txt"), normalized.transcript);
886
1000
  writeJson(path.join(output, "scene.json"), normalized.scene);
887
1001
  writeJson(path.join(output, "public-projection.json"), normalized.projection);
1002
+ if (normalized.terminalCapture) {
1003
+ copyFile(
1004
+ path.join(adapterOutput, "terminal-capture.json"),
1005
+ path.join(output, "terminal-capture.json"),
1006
+ );
1007
+ }
888
1008
  copyFile(sourceCoordinatePath, path.join(output, "source-artifact.json"));
889
1009
  copyFile(path.join(diagnostics, "adapter.json"), path.join(output, "adapter.json"));
890
1010
  for (const name of listFiles(smokeOutput)) {
@@ -914,6 +1034,14 @@ function finalizeGate(values) {
914
1034
  transcript: sha256(readRegular(path.join(output, "complete-transcript.txt"), "transcript")),
915
1035
  projection: sha256(readRegular(path.join(output, "public-projection.json"), "projection")),
916
1036
  scene: sha256(readRegular(path.join(output, "scene.json"), "scene")),
1037
+ ...(normalized.terminalCapture
1038
+ ? {
1039
+ terminalCapture: {
1040
+ schema: normalized.terminalCapture.schema,
1041
+ root: sha256(readRegular(path.join(output, "terminal-capture.json"), "terminal capture")),
1042
+ },
1043
+ }
1044
+ : {}),
917
1045
  evidenceClass: normalized.projection.evidenceClass,
918
1046
  claimBoundary: normalized.projection.claimBoundary,
919
1047
  },
@@ -945,6 +1073,19 @@ function verifyGate(values) {
945
1073
  && receipt.qualifiedInputs?.scene === sha256(readRegular(path.join(bundle, "scene.json"), "scene")),
946
1074
  "gate qualified input roots mismatch",
947
1075
  );
1076
+ const qualifiedCapture = receipt.qualifiedInputs?.terminalCapture;
1077
+ invariant(
1078
+ Boolean(qualifiedCapture) === Boolean(normalized.terminalCapture),
1079
+ "gate terminal capture presence drifted",
1080
+ );
1081
+ if (normalized.terminalCapture) {
1082
+ invariant(
1083
+ qualifiedCapture.schema === normalized.terminalCapture.schema
1084
+ && qualifiedCapture.root
1085
+ === sha256(readRegular(path.join(bundle, "terminal-capture.json"), "terminal capture")),
1086
+ "gate terminal capture root mismatch",
1087
+ );
1088
+ }
948
1089
  invariant(receipt.qualifiedInputs.evidenceClass === normalized.projection.evidenceClass, "gate evidence class drifted");
949
1090
  }
950
1091
 
@@ -973,10 +1114,14 @@ function finalizeMedia(values) {
973
1114
  "--renderer-image": rendererImage,
974
1115
  "--source-sha": sourceSha,
975
1116
  });
1117
+ const terminalCapturePath = path.join(gateBundle, "terminal-capture.json");
976
1118
  const verifiedRenderer = verifyRendererOutput(renderOutput, rendererImage, {
977
1119
  scene: path.join(gateBundle, "scene.json"),
978
1120
  transcript: path.join(gateBundle, "complete-transcript.txt"),
979
1121
  projection: path.join(gateBundle, "public-projection.json"),
1122
+ ...(fs.existsSync(terminalCapturePath)
1123
+ ? { terminalCapture: terminalCapturePath }
1124
+ : {}),
980
1125
  }, {
981
1126
  mediaProfile,
982
1127
  inspectMedia: mediaInspection?.inspectMedia,
@@ -49,7 +49,7 @@ function normalizeRepository(value) {
49
49
  function normalizeTargetBranch(value) {
50
50
  const branch = String(value || process.env.GITHUB_REF_NAME || DEFAULT_TARGET_BRANCH).replace(/^refs\/heads\//, "");
51
51
  if (!/^dev\/v\d+\/v\d+\.\d+$/.test(branch)) {
52
- throw new Error(`target-branch must be a semver dev branch such as dev/v2/v2.N, got: ${branch || "<empty>"}`);
52
+ throw new Error(`target-branch must be a semver dev branch such as dev/v3/v3.N, got: ${branch || "<empty>"}`);
53
53
  }
54
54
  return branch;
55
55
  }
@@ -73,6 +73,7 @@ const requiredPaths = [
73
73
  "docs/auditable-demo.md",
74
74
  "contracts/auditable-demo-media-profiles-v1.json",
75
75
  "contracts/evidence/auditable-demo-web-delivery-v1.json",
76
+ "contracts/buildchain-v2-residuals-v1.json",
76
77
  "contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt",
77
78
  "contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json",
78
79
  "contracts/fixtures/auditable-demo-web-delivery-v1/scene.json",
@@ -65,6 +65,13 @@ function pullRequestBodyPrefix(value) {
65
65
  return normalized;
66
66
  }
67
67
 
68
+ function optionalExactSha(value, name) {
69
+ const normalized = text(value);
70
+ if (normalized && !EXACT_SHA.test(normalized))
71
+ throw new Error(`${name} must be an exact 40-character commit SHA`);
72
+ return normalized;
73
+ }
74
+
68
75
  function canonical(value) {
69
76
  if (Array.isArray(value)) return value.map(canonical);
70
77
  if (value && typeof value === "object") {
@@ -225,6 +232,11 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
225
232
  options.pullRequestBodyPrefix ??
226
233
  process.env.BUILDCHAIN_CHANNEL_PATROL_PR_BODY_PREFIX,
227
234
  ),
235
+ expectedSelectedSha: optionalExactSha(
236
+ options.expectedSelectedSha ??
237
+ process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_SELECTED_SHA,
238
+ "expectedSelectedSha",
239
+ ),
228
240
  createPullRequest,
229
241
  settlementAuthorized: bool(
230
242
  options.settlementAuthorized ??
@@ -526,6 +538,14 @@ export async function runDevAlphaCandidatePatrol(
526
538
  maxAgeSeconds: options.maxAgeSeconds,
527
539
  now: options.now,
528
540
  });
541
+ if (
542
+ options.expectedSelectedSha &&
543
+ decision.source.sha !== options.expectedSelectedSha
544
+ ) {
545
+ throw new Error(
546
+ `selected source changed between observation and settlement: expected ${options.expectedSelectedSha}, observed ${decision.source.sha}`,
547
+ );
548
+ }
529
549
  const openPullRequests = await client.listOpenPullRequests(
530
550
  options.targetBranch,
531
551
  );
@@ -17,6 +17,12 @@ function repository(value, label) {
17
17
  }
18
18
 
19
19
  const TRANSIENT_GITHUB_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
20
+ const DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF = "authority/v3/v3.0/artifact-signing";
21
+
22
+ export function resolveAuthorityDispatchRef(value) {
23
+ const ref = required(value, "authority ref");
24
+ return /^[0-9a-f]{40}$/u.test(ref) ? DEFAULT_ARTIFACT_SIGNING_AUTHORITY_REF : ref;
25
+ }
20
26
 
21
27
  function retryDelayMs(attempt) {
22
28
  return Math.min(1_000 * (2 ** (attempt - 1)), 10_000);
@@ -98,7 +104,7 @@ export async function dispatchArtifactSigningAuthority({
98
104
  const authToken = required(token, "Buildchain authority dispatch token");
99
105
  const authorityRepo = repository(authorityRepository, "authority repository");
100
106
  const sourceRepo = repository(sourceRepository, "source repository");
101
- const ref = required(authorityRef, "authority ref");
107
+ const ref = resolveAuthorityDispatchRef(authorityRef);
102
108
  const runtime = required(runtimeSha, "Buildchain runtime SHA");
103
109
  if (!/^[0-9a-f]{40}$/u.test(runtime)) throw new Error("Buildchain runtime SHA must be exact");
104
110
  const correlationId = `${required(sourceRunId, "source run ID")}-${required(sourceRunAttempt, "source run attempt")}-${runtime.slice(0, 12)}-${required(requestArtifact, "request artifact").replace(/[^A-Za-z0-9._-]+/gu, "-")}`;
@@ -4,8 +4,14 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
 
7
- import { createArtifactSigningReceipt, validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
8
- import { artifactSigningEvidenceDigest, createArtifactSigningResult } from "../packages/core/artifact-signing-result.js";
7
+ import {
8
+ createArtifactSigningReceipt,
9
+ validateArtifactSigningRequest,
10
+ } from "../packages/core/artifact-signing.js";
11
+ import {
12
+ artifactSigningEvidenceDigest,
13
+ createArtifactSigningResult,
14
+ } from "../packages/core/artifact-signing-result.js";
9
15
  import { writeGitHubOutputs } from "./build-contract-core.mjs";
10
16
 
11
17
  function required(value, label) {
@@ -21,7 +27,8 @@ function sha256File(filePath) {
21
27
  function resolveBelow(root, relative, label) {
22
28
  const target = path.resolve(root, required(relative, label));
23
29
  const rel = path.relative(path.resolve(root), target);
24
- if (rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${label} escapes its root`);
30
+ if (rel.startsWith("..") || path.isAbsolute(rel))
31
+ throw new Error(`${label} escapes its root`);
25
32
  return target;
26
33
  }
27
34
 
@@ -34,63 +41,139 @@ export function finalizeNativeArtifactSigningResult({
34
41
  checks = process.env.BUILDCHAIN_SIGNING_VERIFICATION_CHECKS,
35
42
  } = {}) {
36
43
  const requests = path.resolve(required(requestRoot, "signing request root"));
37
- const request = JSON.parse(fs.readFileSync(resolveBelow(requests, requestPath, "request path"), "utf8"));
44
+ const request = JSON.parse(
45
+ fs.readFileSync(
46
+ resolveBelow(requests, requestPath, "request path"),
47
+ "utf8",
48
+ ),
49
+ );
38
50
  const requestCheck = validateArtifactSigningRequest(request);
39
- if (!requestCheck.ok) throw new Error(`invalid signing request: ${requestCheck.issues.join(", ")}`);
51
+ if (!requestCheck.ok)
52
+ throw new Error(
53
+ `invalid signing request: ${requestCheck.issues.join(", ")}`,
54
+ );
40
55
  if (request.signature.semantics !== "native-platform-signature") {
41
- throw new Error("native result finalizer rejects non-native signature profiles");
56
+ throw new Error(
57
+ "native result finalizer rejects non-native signature profiles",
58
+ );
42
59
  }
43
60
  const payloadSource = path.resolve(required(signedPayload, "signed payload"));
44
- const evidenceSource = path.resolve(required(evidencePath, "signing evidence"));
45
- const resultDirectory = path.resolve(required(outputRoot, "signing result root"));
61
+ const evidenceSource = path.resolve(
62
+ required(evidencePath, "signing evidence"),
63
+ );
64
+ const resultDirectory = path.resolve(
65
+ required(outputRoot, "signing result root"),
66
+ );
46
67
  fs.mkdirSync(path.join(resultDirectory, "payload"), { recursive: true });
47
- const payloadPath = path.join(resultDirectory, "payload", path.basename(payloadSource));
68
+ const payloadPath = path.join(
69
+ resultDirectory,
70
+ "payload",
71
+ path.basename(payloadSource),
72
+ );
48
73
  fs.copyFileSync(payloadSource, payloadPath, fs.constants.COPYFILE_EXCL);
49
74
  const evidenceOutput = path.join(resultDirectory, "provider-evidence.json");
50
75
  fs.copyFileSync(evidenceSource, evidenceOutput, fs.constants.COPYFILE_EXCL);
51
76
  const evidenceDocument = JSON.parse(fs.readFileSync(evidenceOutput, "utf8"));
52
- if (evidenceDocument.status !== "passed" || evidenceDocument.provider !== request.signature.provider) {
53
- throw new Error("provider evidence does not prove the requested native signature");
77
+ if (
78
+ evidenceDocument.status !== "passed" ||
79
+ evidenceDocument.provider !== request.signature.provider
80
+ ) {
81
+ throw new Error(
82
+ "provider evidence does not prove the requested native signature",
83
+ );
54
84
  }
55
- const evidence = [{ kind: `${request.signature.profile}-verification`, path: "provider-evidence.json", digest: sha256File(evidenceOutput) }];
85
+ const evidence = [
86
+ {
87
+ kind: `${request.signature.profile}-verification`,
88
+ path: "provider-evidence.json",
89
+ digest: sha256File(evidenceOutput),
90
+ },
91
+ ];
56
92
  const payloadDigest = sha256File(payloadPath);
57
93
  const receipt = createArtifactSigningReceipt({
58
94
  request,
59
- result: { artifactDigest: payloadDigest, evidenceDigest: artifactSigningEvidenceDigest(evidence) },
60
- signatures: [{ kind: request.signature.profile, digest: evidence[0].digest }],
95
+ result: {
96
+ artifactDigest: payloadDigest,
97
+ evidenceDigest: artifactSigningEvidenceDigest(evidence),
98
+ },
99
+ signatures: [
100
+ { kind: request.signature.profile, digest: evidence[0].digest },
101
+ ],
61
102
  });
62
- fs.writeFileSync(path.join(resultDirectory, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`);
63
- const verificationChecks = String(checks || "").split(",").map((value) => value.trim()).filter(Boolean);
103
+ fs.writeFileSync(
104
+ path.join(resultDirectory, "receipt.json"),
105
+ `${JSON.stringify(receipt, null, 2)}\n`,
106
+ );
107
+ const verificationChecks = String(checks || "")
108
+ .split(",")
109
+ .map((value) => value.trim())
110
+ .filter(Boolean);
111
+ if (
112
+ verificationChecks.length === 0 &&
113
+ Array.isArray(evidenceDocument.checks)
114
+ ) {
115
+ verificationChecks.push(
116
+ ...evidenceDocument.checks
117
+ .map((value) => String(value).trim())
118
+ .filter(Boolean),
119
+ );
120
+ }
121
+ if (verificationChecks.length === 0)
122
+ throw new Error("provider evidence contains no verification checks");
64
123
  const result = createArtifactSigningResult({
65
124
  request,
66
125
  receipt,
67
- payload: { path: `payload/${path.basename(payloadPath)}`, bytes: fs.statSync(payloadPath).size, digest: payloadDigest },
126
+ payload: {
127
+ path: `payload/${path.basename(payloadPath)}`,
128
+ bytes: fs.statSync(payloadPath).size,
129
+ digest: payloadDigest,
130
+ },
68
131
  evidence,
69
- verification: { status: "passed", provider: request.signature.provider, checks: verificationChecks },
132
+ verification: {
133
+ status: "passed",
134
+ provider: request.signature.provider,
135
+ checks: verificationChecks,
136
+ },
70
137
  });
71
- fs.writeFileSync(path.join(resultDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
138
+ fs.writeFileSync(
139
+ path.join(resultDirectory, "result.json"),
140
+ `${JSON.stringify(result, null, 2)}\n`,
141
+ );
72
142
  const index = {
73
143
  schemaVersion: 1,
74
144
  contract: "kungfu-buildchain-artifact-signing-result-index/v1",
75
- results: [{
76
- id: request.artifact.id,
77
- requestDigest: request.digest,
78
- resultDigest: result.digest,
79
- result: "result.json",
80
- payload: result.artifact.path,
81
- receipt: "receipt.json",
82
- }],
145
+ results: [
146
+ {
147
+ id: request.artifact.id,
148
+ requestDigest: request.digest,
149
+ resultDigest: result.digest,
150
+ result: "result.json",
151
+ payload: result.artifact.path,
152
+ receipt: "receipt.json",
153
+ },
154
+ ],
83
155
  };
84
- fs.writeFileSync(path.join(resultDirectory, "index.json"), `${JSON.stringify(index, null, 2)}\n`);
85
- writeGitHubOutputs({ "result-root": resultDirectory, "result-index": path.join(resultDirectory, "index.json") });
156
+ fs.writeFileSync(
157
+ path.join(resultDirectory, "index.json"),
158
+ `${JSON.stringify(index, null, 2)}\n`,
159
+ );
160
+ writeGitHubOutputs({
161
+ "result-root": resultDirectory,
162
+ "result-index": path.join(resultDirectory, "index.json"),
163
+ });
86
164
  return index;
87
165
  }
88
166
 
89
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
167
+ if (
168
+ process.argv[1] &&
169
+ import.meta.url === pathToFileURL(process.argv[1]).href
170
+ ) {
90
171
  try {
91
172
  finalizeNativeArtifactSigningResult();
92
173
  } catch (error) {
93
- console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
174
+ console.error(
175
+ `::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
176
+ );
94
177
  process.exitCode = 1;
95
178
  }
96
179
  }
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
 
3
3
  export const GATE_MATRIX_CONTRACT = "buildchain.shifu-gate-matrix/v1";
4
4
  export const GATE_AGGREGATE_CONTRACT = "buildchain.shifu-gate-aggregate/v1";
5
+ export const GATE_JOB_CONTROL_PLANE_OVERHEAD_MINUTES = 30;
5
6
 
6
7
  function stableValue(value) {
7
8
  if (Array.isArray(value)) return value.map(stableValue);
@@ -265,7 +266,11 @@ export function createGateExecutionMatrix({
265
266
  planDigest: inspected.planDigest,
266
267
  timeoutMinutes: Math.max(
267
268
  1,
268
- Math.min(360, Math.ceil(timeoutSeconds / 60)),
269
+ Math.min(
270
+ 360,
271
+ Math.ceil(timeoutSeconds / 60) +
272
+ GATE_JOB_CONTROL_PLANE_OVERHEAD_MINUTES,
273
+ ),
269
274
  ),
270
275
  gates: inspected.gates.map((gate) => ({
271
276
  id: gate.id,
@@ -38,7 +38,8 @@ export function inspectArtifactSigningRequests({
38
38
  } = {}) {
39
39
  const root = path.resolve(required(inputRoot, "signing request root"));
40
40
  const indexes = walk(root, "index.json");
41
- if (indexes.length === 0) throw new Error("no artifact signing request indexes found");
41
+ if (indexes.length === 0)
42
+ throw new Error("no artifact signing request indexes found");
42
43
  const seen = new Set();
43
44
  const matrices = {
44
45
  detached: [],
@@ -47,41 +48,66 @@ export function inspectArtifactSigningRequests({
47
48
  };
48
49
  for (const indexPath of indexes) {
49
50
  const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
50
- if (index.contract !== "kungfu-buildchain-artifact-signing-request-index/v1") continue;
51
+ if (
52
+ index.contract !== "kungfu-buildchain-artifact-signing-request-index/v1"
53
+ )
54
+ continue;
51
55
  for (const entry of index.requests || []) {
52
56
  const requestPath = path.resolve(path.dirname(indexPath), entry.path);
53
57
  const relative = path.relative(root, requestPath);
54
58
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
55
- throw new Error("signing request path escapes the authority intake root");
59
+ throw new Error(
60
+ "signing request path escapes the authority intake root",
61
+ );
56
62
  }
57
63
  const request = JSON.parse(fs.readFileSync(requestPath, "utf8"));
58
64
  const check = validateArtifactSigningRequest(request);
59
65
  if (!check.ok || request.digest !== entry.digest) {
60
66
  throw new Error(`invalid artifact signing request: ${entry.id}`);
61
67
  }
62
- if (expectedRepository && request.source.repository !== expectedRepository) {
68
+ if (
69
+ expectedRepository &&
70
+ request.source.repository !== expectedRepository
71
+ ) {
63
72
  throw new Error("signing request source repository mismatch");
64
73
  }
65
74
  if (expectedRuntimeSha && request.runtime.sha !== expectedRuntimeSha) {
66
75
  throw new Error("signing request runtime SHA mismatch");
67
76
  }
68
77
  const key = `${request.source.sha}:${request.artifact.id}:${request.artifact.platform}`;
69
- if (seen.has(key)) throw new Error(`duplicate artifact signing request: ${key}`);
78
+ if (seen.has(key))
79
+ throw new Error(`duplicate artifact signing request: ${key}`);
70
80
  seen.add(key);
71
81
  const item = {
72
82
  id: request.artifact.id,
73
83
  slug: safeId(request.artifact.id),
74
84
  request: path.relative(root, requestPath).split(path.sep).join("/"),
75
- directory: path.relative(root, path.dirname(requestPath)).split(path.sep).join("/"),
76
- indexRoot: path.relative(root, path.dirname(indexPath)).split(path.sep).join("/") || ".",
85
+ directory: path
86
+ .relative(root, path.dirname(requestPath))
87
+ .split(path.sep)
88
+ .join("/"),
89
+ indexRoot:
90
+ path
91
+ .relative(root, path.dirname(indexPath))
92
+ .split(path.sep)
93
+ .join("/") || ".",
94
+ kind: request.artifact.kind,
95
+ transportFormat: request.artifact.transport?.format || "",
77
96
  };
78
- if (request.signature.profile === "detached-signature-v1") matrices.detached.push(item);
79
- else if (request.signature.profile === "apple-developer-id") matrices.macos.push(item);
80
- else if (request.signature.profile === "windows-authenticode") matrices.windows.push(item);
81
- else throw new Error(`unsupported signing authority profile: ${request.signature.profile}`);
97
+ if (request.signature.profile === "detached-signature-v1")
98
+ matrices.detached.push(item);
99
+ else if (request.signature.profile === "apple-developer-id")
100
+ matrices.macos.push(item);
101
+ else if (request.signature.profile === "windows-authenticode")
102
+ matrices.windows.push(item);
103
+ else
104
+ throw new Error(
105
+ `unsupported signing authority profile: ${request.signature.profile}`,
106
+ );
82
107
  }
83
108
  }
84
- for (const entries of Object.values(matrices)) entries.sort((a, b) => a.request.localeCompare(b.request));
109
+ for (const entries of Object.values(matrices))
110
+ entries.sort((a, b) => a.request.localeCompare(b.request));
85
111
  writeGitHubOutputs({
86
112
  "detached-matrix": JSON.stringify(matrices.detached),
87
113
  "macos-matrix": JSON.stringify(matrices.macos),
@@ -91,11 +117,16 @@ export function inspectArtifactSigningRequests({
91
117
  return matrices;
92
118
  }
93
119
 
94
- if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
120
+ if (
121
+ process.argv[1] &&
122
+ import.meta.url === pathToFileURL(process.argv[1]).href
123
+ ) {
95
124
  try {
96
125
  inspectArtifactSigningRequests();
97
126
  } catch (error) {
98
- console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
127
+ console.error(
128
+ `::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
129
+ );
99
130
  process.exitCode = 1;
100
131
  }
101
132
  }