@kungfu-tech/buildchain 3.0.2 → 3.0.3-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.
Files changed (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -130,6 +130,16 @@ function collectArtifactFiles(root, patterns) {
130
130
  return [...files].sort();
131
131
  }
132
132
 
133
+ function signingArtifactPathsForPlatform({ loadedConfig, cwd, platformId }) {
134
+ const declarations = loadedConfig?.config?.signing?.artifacts || [];
135
+ return declarations
136
+ .filter(
137
+ (entry) =>
138
+ entry.platforms.length === 0 || entry.platforms.includes(platformId),
139
+ )
140
+ .map((entry) => path.resolve(cwd, entry.path));
141
+ }
142
+
133
143
  function readProcessSummaryArtifact(filePath) {
134
144
  if (!filePath) {
135
145
  return undefined;
@@ -435,11 +445,35 @@ export function runLifecycle({
435
445
  const resolvedDiagnosticsProcessSamplesPath = path.join(diagnosticsDir, "process-samples.jsonl");
436
446
  const resolvedSourceCheckoutPath = path.resolve(resolvedWorkspace, ".buildchain/diagnostics/source-checkout.json");
437
447
  const resolvedDiagnosticsSourceCheckoutPath = path.join(diagnosticsDir, "source-checkout.json");
448
+ const resolvedCompilerCachePreparationPath = path.resolve(
449
+ resolvedWorkspace,
450
+ process.env.BUILDCHAIN_COMPILER_CACHE_PREPARATION_PATH ||
451
+ ".buildchain/diagnostics/compiler-cache-preparation.json",
452
+ );
453
+ const compilerCachePreparationRelative = path.relative(
454
+ resolvedWorkspace,
455
+ resolvedCompilerCachePreparationPath,
456
+ );
457
+ if (
458
+ compilerCachePreparationRelative.startsWith("..") ||
459
+ path.isAbsolute(compilerCachePreparationRelative)
460
+ ) {
461
+ throw new Error(
462
+ "BUILDCHAIN_COMPILER_CACHE_PREPARATION_PATH must remain inside the workflow workspace",
463
+ );
464
+ }
465
+ const resolvedDiagnosticsCompilerCachePreparationPath = path.join(
466
+ diagnosticsDir,
467
+ "compiler-cache-preparation.json",
468
+ );
438
469
  const resolvedDiagnosticsManifestPath = path.join(diagnosticsDir, "diagnostics-manifest.json");
439
470
  const relativeDiagnosticsEventsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsEventsPath));
440
471
  const relativeDiagnosticsProcessSummaryPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSummaryPath));
441
472
  const relativeDiagnosticsProcessSamplesPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSamplesPath));
442
473
  const relativeDiagnosticsSourceCheckoutPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsSourceCheckoutPath));
474
+ const relativeDiagnosticsCompilerCachePreparationPath = toPosix(
475
+ path.relative(resolvedWorkspace, resolvedDiagnosticsCompilerCachePreparationPath),
476
+ );
443
477
  const relativeDiagnosticsManifestPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsManifestPath));
444
478
  const logRunId = crypto.randomUUID();
445
479
  const frameworkLog = createBuildchainLogger({
@@ -622,9 +656,22 @@ export function runLifecycle({
622
656
  const sourceCheckoutArtifact = fs.existsSync(resolvedSourceCheckoutPath)
623
657
  ? JSON.parse(fs.readFileSync(resolvedSourceCheckoutPath, "utf8"))
624
658
  : undefined;
659
+ const compilerCachePreparationArtifact = fs.existsSync(resolvedCompilerCachePreparationPath)
660
+ ? JSON.parse(fs.readFileSync(resolvedCompilerCachePreparationPath, "utf8"))
661
+ : undefined;
625
662
  fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
626
663
  const scanStartedAt = Date.now();
627
- const files = collectArtifactFiles(resolvedWorkspace, artifactPaths);
664
+ const signingArtifactPaths = stageName === "build"
665
+ ? signingArtifactPathsForPlatform({
666
+ loadedConfig,
667
+ cwd: resolvedCwd,
668
+ platformId,
669
+ })
670
+ : [];
671
+ const files = collectArtifactFiles(resolvedWorkspace, [
672
+ ...artifactPaths,
673
+ ...signingArtifactPaths,
674
+ ]);
628
675
  const manifestFiles = files.map((file) => {
629
676
  const stat = fs.statSync(file);
630
677
  return {
@@ -743,6 +790,7 @@ export function runLifecycle({
743
790
  lifecycleObservability,
744
791
  processSummary: processSummaryArtifact?.summary,
745
792
  sourceCheckout: sourceCheckoutArtifact,
793
+ compilerCachePreparation: compilerCachePreparationArtifact,
746
794
  links: {
747
795
  artifactName,
748
796
  platformId,
@@ -757,6 +805,9 @@ export function runLifecycle({
757
805
  ...(processSummaryArtifact ? { diagnosticsProcessSummary: relativeDiagnosticsProcessSummaryPath } : {}),
758
806
  ...(processSummaryArtifact?.samplesPath ? { diagnosticsProcessSamples: relativeDiagnosticsProcessSamplesPath } : {}),
759
807
  ...(sourceCheckoutArtifact ? { sourceCheckout: relativeDiagnosticsSourceCheckoutPath } : {}),
808
+ ...(compilerCachePreparationArtifact
809
+ ? { compilerCachePreparation: relativeDiagnosticsCompilerCachePreparationPath }
810
+ : {}),
760
811
  },
761
812
  }),
762
813
  );
@@ -778,6 +829,12 @@ export function runLifecycle({
778
829
  if (sourceCheckoutArtifact) {
779
830
  copyIfExists(resolvedSourceCheckoutPath, resolvedDiagnosticsSourceCheckoutPath);
780
831
  }
832
+ if (compilerCachePreparationArtifact) {
833
+ copyIfExists(
834
+ resolvedCompilerCachePreparationPath,
835
+ resolvedDiagnosticsCompilerCachePreparationPath,
836
+ );
837
+ }
781
838
  writeDiagnosticsSidecarManifest(resolvedDiagnosticsManifestPath, {
782
839
  workspace: resolvedWorkspace,
783
840
  artifactName,
@@ -789,6 +846,10 @@ export function runLifecycle({
789
846
  { kind: "process-summary", filePath: resolvedDiagnosticsProcessSummaryPath },
790
847
  { kind: "process-samples", filePath: resolvedDiagnosticsProcessSamplesPath },
791
848
  { kind: "source-checkout", filePath: resolvedDiagnosticsSourceCheckoutPath },
849
+ {
850
+ kind: "compiler-cache-preparation",
851
+ filePath: resolvedDiagnosticsCompilerCachePreparationPath,
852
+ },
792
853
  ],
793
854
  });
794
855
  console.log(`buildchain_manifest=${path.relative(resolvedWorkspace, resolvedManifestPath)}`);
@@ -3,7 +3,7 @@ const TRAIN_REF_RE = /^train\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
3
3
  const AUTHORITY_REF_RE = /^authority\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
4
4
  const OFFICIAL_CHANNEL_REF_RE = /^v\d+(?:\.\d+)?(?:-alpha)?$/;
5
5
 
6
- export function parseWorkflowShellRef(workflowRef = "", fallback = "v2", buildchainRepository = "kungfu-systems/buildchain") {
6
+ export function parseWorkflowShellRef(workflowRef = "", fallback = "v3", buildchainRepository = "kungfu-systems/buildchain") {
7
7
  const value = String(workflowRef || "");
8
8
  const expectedPrefix = `${buildchainRepository}/.github/workflows/`;
9
9
  if (!value.startsWith(expectedPrefix)) {
@@ -80,7 +80,7 @@ export function normalizeRequestedRuntimeRef(requestedRef = "") {
80
80
  export function resolveRuntimeSelection({
81
81
  requestedRef = "",
82
82
  workflowRef = "",
83
- defaultStableRef = "v2",
83
+ defaultStableRef = "v3",
84
84
  buildchainRepository = "kungfu-systems/buildchain",
85
85
  } = {}) {
86
86
  const requested = String(requestedRef || "").trim();
@@ -98,7 +98,15 @@ export function resolveRuntimeSelection({
98
98
  };
99
99
  }
100
100
  const normalized = normalizeRequestedRuntimeRef(requested);
101
- const runtimeOverride = !normalized.officialChannel;
101
+ const sameRepositoryWorkflow = String(workflowRef || "").startsWith(
102
+ `${buildchainRepository}/.github/workflows/`,
103
+ );
104
+ const pinnedSelfRuntime =
105
+ sameRepositoryWorkflow &&
106
+ normalized.exactSha &&
107
+ EXACT_SHA_RE.test(workflowShellRef) &&
108
+ normalized.ref.toLowerCase() === workflowShellRef.toLowerCase();
109
+ const runtimeOverride = !normalized.officialChannel && !pinnedSelfRuntime;
102
110
  return {
103
111
  requestedRef: requested,
104
112
  runtimeRef: normalized.ref,
@@ -107,16 +115,23 @@ export function resolveRuntimeSelection({
107
115
  runtimeOverride,
108
116
  workflowShellRef: workflowShellRef || defaultStableRef,
109
117
  rollbackRef: workflowShellRef || defaultStableRef,
110
- trustDecision: normalized.officialChannel ? "official-channel" : "override-requested",
118
+ trustDecision: normalized.officialChannel
119
+ ? "official-channel"
120
+ : pinnedSelfRuntime
121
+ ? "pinned-self"
122
+ : "override-requested",
111
123
  };
112
124
  }
113
125
 
114
126
  export function validateRuntimeOverrideTrust({
115
127
  requestedRef = "",
116
128
  eventName = "",
129
+ eventAction = "",
117
130
  actorPermission = "",
118
131
  sameRepositoryPullRequest = false,
132
+ sameRepositoryWorkflow = false,
119
133
  pullRequestHeadSha = "",
134
+ workflowShellSha = "",
120
135
  } = {}) {
121
136
  if (!String(requestedRef || "").trim()) {
122
137
  return { ok: true, decision: "stable-default" };
@@ -126,6 +141,23 @@ export function validateRuntimeOverrideTrust({
126
141
  }
127
142
  const normalizedRequested = String(requestedRef || "").trim().toLowerCase();
128
143
  const normalizedHeadSha = String(pullRequestHeadSha || "").trim().toLowerCase();
144
+ const normalizedWorkflowShellSha = String(workflowShellSha || "").trim().toLowerCase();
145
+ if (
146
+ sameRepositoryWorkflow === true &&
147
+ EXACT_SHA_RE.test(normalizedRequested) &&
148
+ EXACT_SHA_RE.test(normalizedWorkflowShellSha) &&
149
+ normalizedRequested === normalizedWorkflowShellSha
150
+ ) {
151
+ return { ok: true, decision: "pinned-self" };
152
+ }
153
+ if (
154
+ eventName === "pull_request" &&
155
+ eventAction === "closed" &&
156
+ EXACT_SHA_RE.test(normalizedRequested) &&
157
+ normalizedRequested === normalizedWorkflowShellSha
158
+ ) {
159
+ return { ok: true, decision: "closed-release-pr-shell-runtime" };
160
+ }
129
161
  if (
130
162
  eventName === "pull_request" &&
131
163
  sameRepositoryPullRequest === true &&
@@ -52,6 +52,31 @@ function assertInside(root, target, label) {
52
52
  }
53
53
  }
54
54
 
55
+ function containsPath(root, target) {
56
+ const relative = path.relative(root, target);
57
+ return (
58
+ !relative || (!relative.startsWith("..") && !path.isAbsolute(relative))
59
+ );
60
+ }
61
+
62
+ function resetGeneratedOutputRoot({
63
+ workspace,
64
+ cwd,
65
+ outputRoot,
66
+ protectedPaths = [],
67
+ }) {
68
+ const protectedRoots = [workspace, cwd, ...protectedPaths];
69
+ for (const protectedPath of protectedRoots) {
70
+ if (containsPath(outputRoot, protectedPath)) {
71
+ throw new Error(
72
+ "signing request output must not contain the workspace, working directory, manifest, or a declared artifact",
73
+ );
74
+ }
75
+ }
76
+ fs.rmSync(outputRoot, { recursive: true, force: true });
77
+ fs.mkdirSync(outputRoot, { recursive: true });
78
+ }
79
+
55
80
  function walkSubject(subjectRoot) {
56
81
  const entries = [];
57
82
  function visit(current) {
@@ -208,18 +233,16 @@ function verifyLifecycleBinding({
208
233
  );
209
234
  const relativeSubject = toPosix(path.relative(workspace, subjectPath));
210
235
  const files = descriptor.entries.filter(
211
- (entry) => entry.type !== "directory",
236
+ (entry) => entry.type !== "directory" && entry.type !== "symlink",
212
237
  );
238
+ let matchedFiles = 0;
213
239
  for (const entry of files) {
214
- if (entry.type === "symlink") continue;
215
240
  const manifestPath = fs.statSync(subjectPath).isFile()
216
241
  ? relativeSubject
217
242
  : `${relativeSubject}/${entry.path}`;
218
243
  const observed = byPath.get(manifestPath);
219
- if (!observed)
220
- throw new Error(
221
- `signed artifact file is absent from lifecycle manifest: ${manifestPath}`,
222
- );
244
+ if (!observed) continue;
245
+ matchedFiles += 1;
223
246
  if (
224
247
  `sha256:${observed.sha256}` !== entry.digest ||
225
248
  Number(observed.size) !== entry.bytes
@@ -229,6 +252,15 @@ function verifyLifecycleBinding({
229
252
  );
230
253
  }
231
254
  }
255
+ // A signing declaration is an independent artifact inventory and must not
256
+ // require consumers to repeat its path in workflow artifact-paths. When the
257
+ // lifecycle manifest does cover the subject, require complete coverage so a
258
+ // truncated or stale manifest cannot weaken the exact descriptor binding.
259
+ if (matchedFiles !== 0 && matchedFiles !== files.length) {
260
+ throw new Error(
261
+ `signed artifact is only partially represented in lifecycle manifest: ${relativeSubject}`,
262
+ );
263
+ }
232
264
  }
233
265
 
234
266
  export function sealArtifactSigningRequests({
@@ -255,8 +287,12 @@ export function sealArtifactSigningRequests({
255
287
  );
256
288
  const resolvedOutputRoot = path.resolve(resolvedWorkspace, outputRoot);
257
289
  assertInside(resolvedWorkspace, resolvedOutputRoot, "signing request output");
258
- fs.mkdirSync(resolvedOutputRoot, { recursive: true });
259
290
  if (selected.length === 0) {
291
+ resetGeneratedOutputRoot({
292
+ workspace: resolvedWorkspace,
293
+ cwd: resolvedCwd,
294
+ outputRoot: resolvedOutputRoot,
295
+ });
260
296
  const index = { schemaVersion: 1, contract: INDEX_CONTRACT, requests: [] };
261
297
  const indexPath = path.join(resolvedOutputRoot, "index.json");
262
298
  fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
@@ -274,8 +310,7 @@ export function sealArtifactSigningRequests({
274
310
  assertInside(resolvedWorkspace, resolvedManifest, "artifact manifest");
275
311
  const manifest = JSON.parse(fs.readFileSync(resolvedManifest, "utf8"));
276
312
  const platform = normalizePlatform(manifest.platform?.os || process.platform);
277
- const requests = [];
278
- for (const declaration of selected) {
313
+ const prepared = selected.map((declaration) => {
279
314
  const subjectPath = path.resolve(resolvedCwd, declaration.path);
280
315
  assertInside(
281
316
  resolvedWorkspace,
@@ -288,7 +323,7 @@ export function sealArtifactSigningRequests({
288
323
  );
289
324
  const realSubject = fs.realpathSync(subjectPath);
290
325
  assertInside(
291
- resolvedWorkspace,
326
+ fs.realpathSync(resolvedWorkspace),
292
327
  realSubject,
293
328
  `signing artifact ${declaration.id}`,
294
329
  );
@@ -299,8 +334,25 @@ export function sealArtifactSigningRequests({
299
334
  subjectPath,
300
335
  descriptor,
301
336
  });
302
- const id = safeId(declaration.id);
303
- const kind = inferKind(subjectPath, declaration.kind);
337
+ return {
338
+ declaration,
339
+ subjectPath,
340
+ descriptor,
341
+ id: safeId(declaration.id),
342
+ kind: inferKind(subjectPath, declaration.kind),
343
+ };
344
+ });
345
+ resetGeneratedOutputRoot({
346
+ workspace: resolvedWorkspace,
347
+ cwd: resolvedCwd,
348
+ outputRoot: resolvedOutputRoot,
349
+ protectedPaths: [
350
+ resolvedManifest,
351
+ ...prepared.map((entry) => entry.subjectPath),
352
+ ],
353
+ });
354
+ const requests = [];
355
+ for (const { declaration, subjectPath, descriptor, id, kind } of prepared) {
304
356
  const transport = archiveSubject({
305
357
  subjectPath,
306
358
  outputRoot: resolvedOutputRoot,
@@ -41,6 +41,29 @@ function optionalSha(value, label) {
41
41
  return normalized;
42
42
  }
43
43
 
44
+ export function resolveStableCandidateQualificationCandidate({ eventName, inputCandidateSha = "", selfDogfoodEvidence } = {}) {
45
+ if (text(eventName) === "workflow_dispatch") {
46
+ return optionalSha(inputCandidateSha, "candidate SHA");
47
+ }
48
+ if (text(eventName) !== "workflow_run") {
49
+ throw new Error(`qualification candidate resolver does not admit event ${eventName || "<empty>"}`);
50
+ }
51
+ const evidence = selfDogfoodEvidence;
52
+ if (
53
+ evidence?.contract !== "kungfu-buildchain-alpha-self-dogfood"
54
+ || evidence?.status !== "passed"
55
+ || evidence?.observed?.alpha?.ref !== "v3-alpha"
56
+ ) {
57
+ throw new Error("self-dogfood evidence is not a passing v3-alpha observation");
58
+ }
59
+ const observedSha = optionalSha(evidence.observed.alpha.sha, "observed v3-alpha SHA");
60
+ const expectedSha = optionalSha(evidence.observed.alpha.expectedSha, "expected v3-alpha SHA");
61
+ if (!observedSha || observedSha !== expectedSha) {
62
+ throw new Error("self-dogfood evidence does not bind observed and expected v3-alpha SHAs");
63
+ }
64
+ return observedSha;
65
+ }
66
+
44
67
  function optionalRef(value, label) {
45
68
  const normalized = text(value);
46
69
  if (
@@ -91,8 +114,12 @@ async function ensureWorkflowEvidence({ client, repository, workflowFile, workfl
91
114
  let run = await client.findWorkflowRun({ repository, workflowFile, workflowName, headSha, runName, sourceSha });
92
115
  if (successful(run)) return { state: "existing", run };
93
116
 
117
+ let notBefore = "";
118
+ let excludeRunId = "";
94
119
  if (!active(run)) {
95
120
  if (options.dryRun) return { state: "planned", run };
121
+ notBefore = text(run?.created_at);
122
+ excludeRunId = text(run?.id);
96
123
  await client.dispatchWorkflow({ repository, workflowFile, ref, inputs: runName ? { buildchain_ref: headSha } : {} });
97
124
  }
98
125
 
@@ -104,6 +131,8 @@ async function ensureWorkflowEvidence({ client, repository, workflowFile, workfl
104
131
  headSha,
105
132
  runName,
106
133
  sourceSha,
134
+ notBefore,
135
+ excludeRunId,
107
136
  attempts: options.pollAttempts,
108
137
  intervalMs: options.pollIntervalMs,
109
138
  });
@@ -220,6 +249,8 @@ export function createGitHubQualificationClient({
220
249
  || (query.runName && (run.display_title === query.runName || run.name === query.runName))
221
250
  )
222
251
  && (!query.sourceSha || run.head_sha === query.sourceSha)
252
+ && (!query.notBefore || String(run.created_at || "") >= query.notBefore)
253
+ && (!query.excludeRunId || String(run.id || "") !== query.excludeRunId)
223
254
  ))
224
255
  .sort((left, right) => String(right.created_at).localeCompare(String(left.created_at)))[0];
225
256
  }
@@ -267,6 +298,25 @@ export function createGitHubQualificationClient({
267
298
  }
268
299
 
269
300
  async function main() {
301
+ if (bool(process.env.BUILDCHAIN_QUALIFICATION_RESOLVE_CANDIDATE, false)) {
302
+ let selfDogfoodEvidence;
303
+ if (text(process.env.BUILDCHAIN_QUALIFICATION_EVENT_NAME) === "workflow_run") {
304
+ const fs = await import("node:fs");
305
+ const evidencePath = text(process.env.BUILDCHAIN_QUALIFICATION_SELF_DOGFOOD_EVIDENCE);
306
+ selfDogfoodEvidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
307
+ }
308
+ const sha = resolveStableCandidateQualificationCandidate({
309
+ eventName: process.env.BUILDCHAIN_QUALIFICATION_EVENT_NAME,
310
+ inputCandidateSha: process.env.BUILDCHAIN_QUALIFICATION_INPUT_CANDIDATE_SHA,
311
+ selfDogfoodEvidence,
312
+ });
313
+ process.stdout.write(`${sha}\n`);
314
+ if (process.env.GITHUB_OUTPUT) {
315
+ const fs = await import("node:fs");
316
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `sha=${sha}\n`);
317
+ }
318
+ return;
319
+ }
270
320
  const result = await runStableCandidateQualification();
271
321
  const output = `${JSON.stringify(result, null, 2)}\n`;
272
322
  process.stdout.write(output);
@@ -27,11 +27,36 @@ function resolveBelow(root, relative, label) {
27
27
  return target;
28
28
  }
29
29
 
30
+ function walk(root, name, output = []) {
31
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
32
+ const child = path.join(root, entry.name);
33
+ if (entry.isDirectory()) walk(child, name, output);
34
+ else if (entry.isFile() && entry.name === name) output.push(child);
35
+ }
36
+ return output;
37
+ }
38
+
39
+ function resolveRequestRoot(root) {
40
+ const direct = path.join(root, "index.json");
41
+ if (fs.existsSync(direct)) return root;
42
+ const candidates = walk(root, "index.json").filter((candidate) => {
43
+ try {
44
+ return JSON.parse(fs.readFileSync(candidate, "utf8")).contract === "kungfu-buildchain-artifact-signing-request-index/v1";
45
+ } catch {
46
+ return false;
47
+ }
48
+ });
49
+ if (candidates.length !== 1) {
50
+ throw new Error(`expected exactly one artifact signing request index, found ${candidates.length}`);
51
+ }
52
+ return path.dirname(candidates[0]);
53
+ }
54
+
30
55
  export function verifyArtifactSigningResults({
31
56
  requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
32
57
  resultRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
33
58
  } = {}) {
34
- const requests = path.resolve(required(requestRoot, "signing request root"));
59
+ const requests = resolveRequestRoot(path.resolve(required(requestRoot, "signing request root")));
35
60
  const results = path.resolve(required(resultRoot, "signing result root"));
36
61
  const requestIndex = JSON.parse(
37
62
  fs.readFileSync(path.join(requests, "index.json"), "utf8"),