@kungfu-tech/buildchain 2.12.9-alpha.1 → 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.
Files changed (39) hide show
  1. package/actions/run-lifecycle/README.md +5 -0
  2. package/bin/buildchain.mjs +62 -0
  3. package/dist/site/buildchain-contract.json +33 -23
  4. package/dist/site/buildchain-site.json +72 -36
  5. package/dist/site/capability-registry.json +3 -3
  6. package/dist/site/cli-registry.json +24 -0
  7. package/dist/site/controller-registry.json +11 -3
  8. package/dist/site/kfd-claims.json +64 -13
  9. package/dist/site/kfd-upstream-aggregate.json +1 -1
  10. package/dist/site/manual-registry.json +9 -9
  11. package/dist/site/node-api-registry.json +19 -6
  12. package/dist/site/page-registry.json +59 -23
  13. package/dist/site/public-surface-audit.json +47 -10
  14. package/dist/site/publication-registry.json +4 -4
  15. package/dist/site/release-provenance.json +1 -0
  16. package/dist/site/site-manifest.json +13 -13
  17. package/dist/site/workflow-registry.json +7 -4
  18. package/docs/MAP.md +1 -0
  19. package/docs/artifact-verification-envelope.md +113 -0
  20. package/docs/cli.md +22 -0
  21. package/docs/install.md +1 -3
  22. package/docs/publish-transaction.md +1 -1
  23. package/docs/release-candidate.md +5 -0
  24. package/docs/release-governance.md +7 -5
  25. package/docs/release-propagation.md +3 -0
  26. package/docs/reusable-build-surface.md +13 -0
  27. package/docs/versioning.md +1 -0
  28. package/package.json +2 -1
  29. package/packages/core/README.md +13 -0
  30. package/packages/core/artifact-passport.js +6 -1
  31. package/packages/core/artifact-verification-envelope.js +386 -0
  32. package/packages/core/buildchain-config.js +6 -2
  33. package/packages/core/controller-evidence.js +2 -3
  34. package/packages/core/index.js +10 -0
  35. package/packages/core/release-line-bootstrap.js +24 -0
  36. package/scripts/check-inventory.mjs +5 -0
  37. package/scripts/generate-site-bundle.mjs +4 -0
  38. package/scripts/release-candidate-resolver.mjs +52 -29
  39. package/scripts/run-lifecycle-core.mjs +50 -6
@@ -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 runs = await githubJson({
381
- apiUrl,
382
- token,
383
- fetchImpl,
384
- path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
385
- });
386
- const candidateRuns = selectReleaseCandidateRuns({
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
- const selectionErrors = [];
398
- for (const candidateRun of candidateRuns) {
399
- const candidateArtifactResponse = await githubJson({
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/runs/${candidateRun.id}/artifacts?per_page=100`,
402
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
404
403
  });
405
- try {
406
- selected = selectReleaseCandidateArtifacts({
407
- artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
408
- artifactName,
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
- run = candidateRun;
411
- artifactResponse = candidateArtifactResponse;
412
- break;
413
- } catch (error) {
414
- selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
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
- if (!run || !artifactResponse || !selected) {
418
- throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts: ${selectionErrors.join("; ")}`);
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),
@@ -97,6 +97,28 @@ function lifecycleErrorAttributes(error, extra = {}) {
97
97
  return attributes;
98
98
  }
99
99
 
100
+ function describeLifecycleTimeout(error, {
101
+ timeoutMinutes,
102
+ stageName,
103
+ platformId,
104
+ platformName,
105
+ }) {
106
+ if (
107
+ !timeoutMinutes ||
108
+ (error?.code !== "ETIMEDOUT" && error?.signal !== "SIGTERM")
109
+ ) {
110
+ return error;
111
+ }
112
+ const timeoutError = new Error(
113
+ `lifecycle ${stageName || "command"} timed out after ${timeoutMinutes} minute(s) on ${platformName} (${platformId})`,
114
+ { cause: error },
115
+ );
116
+ timeoutError.name = "LifecycleTimeoutError";
117
+ timeoutError.code = "ETIMEDOUT";
118
+ timeoutError.signal = error?.signal || "";
119
+ return timeoutError;
120
+ }
121
+
100
122
  function collectArtifactFiles(root, patterns) {
101
123
  const files = new Set();
102
124
  for (const pattern of patterns) {
@@ -245,9 +267,10 @@ function runLifecycleStageWithSampler({
245
267
  processSamplesPath,
246
268
  processSampleIntervalMs,
247
269
  requestedParallelism,
270
+ timeoutMinutes,
248
271
  }) {
249
272
  if (!sampleProcessTree) {
250
- return runLifecycleStage({ cwd, loadedConfig, name, env });
273
+ return runLifecycleStage({ cwd, loadedConfig, name, env, timeoutMinutes });
251
274
  }
252
275
  const lifecycle = loadedConfig?.config?.lifecycle || {};
253
276
  const stage = lifecycle[name];
@@ -260,7 +283,8 @@ function runLifecycleStageWithSampler({
260
283
  ...(stage.env || {}),
261
284
  ...(env || {}),
262
285
  };
263
- const timeout = stage.timeoutMinutes ? stage.timeoutMinutes * 60_000 : undefined;
286
+ const effectiveTimeoutMinutes = stage.timeoutMinutes ?? timeoutMinutes;
287
+ const timeout = effectiveTimeoutMinutes ? effectiveTimeoutMinutes * 60_000 : undefined;
264
288
  let lastError;
265
289
  for (let attempt = 1; attempt <= stage.retries; attempt += 1) {
266
290
  try {
@@ -362,6 +386,7 @@ export function runLifecycle({
362
386
  stageName = "",
363
387
  command = "",
364
388
  required = false,
389
+ timeoutMinutes,
365
390
  manifestPath = ".buildchain/artifacts/manifest.json",
366
391
  summaryPath = ".buildchain/artifacts/summary.json",
367
392
  diagnosticsPath = "",
@@ -381,6 +406,9 @@ export function runLifecycle({
381
406
  requestedParallelism = 0,
382
407
  processSummaryRequired = true,
383
408
  } = {}) {
409
+ if (timeoutMinutes !== undefined && (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0)) {
410
+ throw new Error("lifecycle timeoutMinutes must be a positive number");
411
+ }
384
412
  const resolvedCwd = path.resolve(cwd);
385
413
  const resolvedWorkspace = path.resolve(workspace);
386
414
  const resolvedManifestPath = path.resolve(resolvedWorkspace, manifestPath);
@@ -449,6 +477,7 @@ export function runLifecycle({
449
477
  const lifecycle = loadedConfig?.config?.lifecycle || {};
450
478
  const configuredStage = stageName ? lifecycle[stageName] : undefined;
451
479
  const commandShell = configuredStage?.shell || true;
480
+ const effectiveCommandTimeoutMinutes = configuredStage?.timeoutMinutes ?? timeoutMinutes;
452
481
  const startedAt = Date.now();
453
482
  userLog.info("lifecycle.command.start", {
454
483
  attributes: {
@@ -480,6 +509,7 @@ export function runLifecycle({
480
509
  processSamplesPath: resolvedProcessSamplesPath,
481
510
  processSampleIntervalMs,
482
511
  requestedParallelism,
512
+ timeout: effectiveCommandTimeoutMinutes ? effectiveCommandTimeoutMinutes * 60_000 : undefined,
483
513
  });
484
514
  } else {
485
515
  execSync(command, {
@@ -487,6 +517,7 @@ export function runLifecycle({
487
517
  env: commandEnv,
488
518
  shell: commandShell,
489
519
  stdio: "inherit",
520
+ timeout: effectiveCommandTimeoutMinutes ? effectiveCommandTimeoutMinutes * 60_000 : undefined,
490
521
  });
491
522
  }
492
523
  executed = true;
@@ -498,16 +529,22 @@ export function runLifecycle({
498
529
  },
499
530
  });
500
531
  } catch (error) {
532
+ const lifecycleError = describeLifecycleTimeout(error, {
533
+ timeoutMinutes: effectiveCommandTimeoutMinutes,
534
+ stageName,
535
+ platformId,
536
+ platformName,
537
+ });
501
538
  userLog.error("lifecycle.command.error", {
502
539
  durationMs: Date.now() - startedAt,
503
540
  message: "lifecycle command failed",
504
- attributes: lifecycleErrorAttributes(error, {
541
+ attributes: lifecycleErrorAttributes(lifecycleError, {
505
542
  commandSource,
506
543
  stage: stageName || "command",
507
544
  sampleProcessTree,
508
545
  }),
509
546
  });
510
- throw error;
547
+ throw lifecycleError;
511
548
  }
512
549
  } else if (stageName) {
513
550
  commandSource = "buildchain.toml";
@@ -535,6 +572,7 @@ export function runLifecycle({
535
572
  processSamplesPath: resolvedProcessSamplesPath,
536
573
  processSampleIntervalMs,
537
574
  requestedParallelism,
575
+ timeoutMinutes,
538
576
  });
539
577
  userLog.info("lifecycle.stage.end", {
540
578
  durationMs: Date.now() - startedAt,
@@ -545,16 +583,22 @@ export function runLifecycle({
545
583
  },
546
584
  });
547
585
  } catch (error) {
586
+ const lifecycleError = describeLifecycleTimeout(error, {
587
+ timeoutMinutes: loadedConfig?.config?.lifecycle?.[stageName]?.timeoutMinutes ?? timeoutMinutes,
588
+ stageName,
589
+ platformId,
590
+ platformName,
591
+ });
548
592
  userLog.error("lifecycle.stage.error", {
549
593
  durationMs: Date.now() - startedAt,
550
594
  message: "lifecycle stage failed",
551
- attributes: lifecycleErrorAttributes(error, {
595
+ attributes: lifecycleErrorAttributes(lifecycleError, {
552
596
  commandSource,
553
597
  stage: stageName,
554
598
  sampleProcessTree,
555
599
  }),
556
600
  });
557
- throw error;
601
+ throw lifecycleError;
558
602
  }
559
603
  }
560
604