@kungfu-tech/buildchain 3.0.6-alpha.1 → 3.0.6-alpha.3

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 (75) hide show
  1. package/README.md +4 -4
  2. package/actions/promote-buildchain-ref/README.md +8 -0
  3. package/bin/buildchain.mjs +13 -1
  4. package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
  5. package/contracts/release-candidate-recovery-v1.schema.json +102 -0
  6. package/dist/site/buildchain-contract.json +129 -34
  7. package/dist/site/buildchain-site.json +171 -45
  8. package/dist/site/capability-registry.json +5 -5
  9. package/dist/site/cli-registry.json +40 -4
  10. package/dist/site/controller-registry.json +56 -4
  11. package/dist/site/kfd-claims.json +184 -20
  12. package/dist/site/kfd-upstream-aggregate.json +1 -1
  13. package/dist/site/manual-registry.json +10 -10
  14. package/dist/site/node-api-registry.json +1242 -121
  15. package/dist/site/page-registry.json +157 -31
  16. package/dist/site/public-surface-audit.json +422 -21
  17. package/dist/site/publication-authority-registry.json +66 -1
  18. package/dist/site/publication-registry.json +4 -4
  19. package/dist/site/release-provenance.json +2 -0
  20. package/dist/site/site-manifest.json +14 -14
  21. package/dist/site/workflow-registry.json +197 -15
  22. package/docs/MAP.md +1 -0
  23. package/docs/auditable-demo.md +58 -11
  24. package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
  25. package/docs/cli-reference.md +154 -0
  26. package/docs/dev-alpha-candidate-patrol.md +13 -5
  27. package/docs/dev-delivery-warrant.md +158 -0
  28. package/docs/node-api-reference.md +96 -38
  29. package/docs/publication-authority.md +11 -0
  30. package/docs/publish-transaction.md +10 -1
  31. package/docs/release-candidate.md +106 -12
  32. package/docs/release-governance.md +52 -0
  33. package/docs/reusable-build-surface.md +11 -1
  34. package/docs/shifu-gate-profiles.md +12 -1
  35. package/docs/versioning.md +2 -0
  36. package/package.json +3 -1
  37. package/packages/core/buildchain-contract.js +30 -2
  38. package/packages/core/buildchain-publication-authority.js +3 -1
  39. package/packages/core/channel-candidate.js +2 -21
  40. package/packages/core/channel-promotion-baseline.js +199 -0
  41. package/packages/core/dev-delivery-candidate-identity.js +94 -0
  42. package/packages/core/dev-delivery-common.js +73 -0
  43. package/packages/core/dev-delivery-proof.js +252 -0
  44. package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
  45. package/packages/core/dev-delivery-warrant-settlement.js +73 -0
  46. package/packages/core/dev-delivery-warrant.js +591 -0
  47. package/packages/core/index.js +7 -0
  48. package/packages/core/release-candidate-recovery.js +386 -0
  49. package/scripts/auditable-demo-bundle-verification.mjs +148 -0
  50. package/scripts/auditable-demo-platform.mjs +86 -50
  51. package/scripts/auditable-demo-presentation.mjs +83 -0
  52. package/scripts/auditable-demo-renditions.mjs +264 -0
  53. package/scripts/auditable-demo.mjs +24 -30
  54. package/scripts/build-contract-core.mjs +58 -3
  55. package/scripts/buildchain-cli-help.mjs +8 -0
  56. package/scripts/buildchain-patrol.mjs +9 -0
  57. package/scripts/check-inventory.mjs +13 -2
  58. package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
  59. package/scripts/dev-delivery-proof.mjs +193 -0
  60. package/scripts/dev-delivery-warrant.mjs +426 -0
  61. package/scripts/dev-pr-auto-merge.mjs +488 -46
  62. package/scripts/dev-pr-delivery-warrant.mjs +209 -0
  63. package/scripts/gate-profile-core.mjs +24 -0
  64. package/scripts/generate-site-bundle.mjs +2 -2
  65. package/scripts/git-fetch-process-tree.mjs +142 -0
  66. package/scripts/lifecycle-substage-evidence.mjs +274 -0
  67. package/scripts/locked-source-checkout.mjs +6 -3
  68. package/scripts/release-candidate-resolver.mjs +35 -3
  69. package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
  70. package/scripts/resolve-build-contract.mjs +7 -0
  71. package/scripts/resume-from-candidate-run.mjs +498 -0
  72. package/scripts/route-offline-runners.mjs +1 -0
  73. package/scripts/run-lifecycle-core.mjs +9 -9
  74. package/scripts/shifu-gate-profile.mjs +10 -16
  75. package/scripts/site-capability-metadata.mjs +15 -0
@@ -7,10 +7,7 @@ import os from "node:os";
7
7
  import path from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { fileURLToPath } from "node:url";
10
- import {
11
- validateRenditionSet,
12
- validateTerminalCapture,
13
- } from "./auditable-demo-renditions.mjs";
10
+ import { readRendererManifest, validateRendererCompositionInputs, validateRenditionSet, validateTerminalCapture } from "./auditable-demo-renditions.mjs";
14
11
 
15
12
  const UTF8 = new TextDecoder("utf-8", { fatal: true });
16
13
  const IMAGE_PATTERN = /^[a-z0-9][a-z0-9./_-]*@sha256:[0-9a-f]{64}$/;
@@ -130,7 +127,7 @@ function writeChecksums(root, checksumName = "checksums.sha256") {
130
127
  return sha256(Buffer.from(bytes));
131
128
  }
132
129
 
133
- function verifyChecksums(root, checksumName = "checksums.sha256") {
130
+ function verifyChecksums(root, checksumName = "checksums.sha256", options = {}) {
134
131
  const bytes = readRegular(path.join(root, checksumName), checksumName);
135
132
  const text = decodeUtf8(bytes, checksumName);
136
133
  invariant(text.endsWith("\n"), `${checksumName} must end with a newline`);
@@ -143,8 +140,11 @@ function verifyChecksums(root, checksumName = "checksums.sha256") {
143
140
  const target = resolveInside(root, member, "checksum member");
144
141
  invariant(!declared.has(member), `duplicate checksum member: ${member}`);
145
142
  declared.add(member);
143
+ const maximumBytes = options.allowLongFormRendererManifest && member === "manifest.json"
144
+ ? 32 * 1024 * 1024
145
+ : MAX_BUNDLE_MEMBER_BYTES;
146
146
  invariant(
147
- sha256(readRegular(target, member, MAX_BUNDLE_MEMBER_BYTES)).slice(7) === match[1],
147
+ sha256(readRegular(target, member, maximumBytes)).slice(7) === match[1],
148
148
  `checksum mismatch: ${member}`,
149
149
  );
150
150
  }
@@ -190,7 +190,7 @@ function validateScene(value) {
190
190
  exactKeys(
191
191
  value,
192
192
  ["schema", "id", "width", "height", "fps", "durationMs", "title"],
193
- ["durationClass", "commandLabel", "background", "accent"],
193
+ ["durationClass", "compositionMode", "commandLabel", "background", "accent"],
194
194
  "scene",
195
195
  );
196
196
  invariant(value.schema === "build-images.demo-scene/v1", "unsupported scene schema");
@@ -199,6 +199,11 @@ function validateScene(value) {
199
199
  integer(value.height, 360, 1080, "scene.height");
200
200
  const durationClass = value.durationClass ?? "standard";
201
201
  invariant(durationClass === "standard" || durationClass === "long-form", "scene.durationClass is invalid");
202
+ const compositionMode = value.compositionMode ?? "presentation-framed";
203
+ invariant(
204
+ compositionMode === "presentation-framed" || compositionMode === "terminal-fill",
205
+ "scene.compositionMode is invalid",
206
+ );
202
207
  const maximumDurationMs = durationClass === "long-form" ? LONG_FORM_MAX_DURATION_MS : STANDARD_MAX_DURATION_MS;
203
208
  const maximumFps = durationClass === "long-form" ? LONG_FORM_MAX_FPS : 30;
204
209
  integer(value.fps, 1, maximumFps, "scene.fps");
@@ -216,6 +221,7 @@ function validateScene(value) {
216
221
  height: value.height,
217
222
  fps: value.fps,
218
223
  ...(value.durationClass === undefined ? {} : { durationClass }),
224
+ compositionMode,
219
225
  durationMs: value.durationMs,
220
226
  title: value.title,
221
227
  commandLabel: value.commandLabel ?? "",
@@ -674,8 +680,7 @@ function inspectRendererMedia(values) {
674
680
  const rendererImage = required(values, "--renderer-image");
675
681
  invariant(IMAGE_PATTERN.test(rendererImage), "renderer image must use an immutable sha256 coordinate");
676
682
  invariant(!fs.existsSync(output), "media inspection output must not already exist");
677
- const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
678
- invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
683
+ const { manifest } = readRendererManifest(path.join(renderOutput, "manifest.json"), RENDITION_VALIDATION_HELPERS);
679
684
  invariant(manifest.renderer?.image === rendererImage, "renderer manifest image coordinate mismatch");
680
685
  const members = Object.keys(manifest.outputs || {})
681
686
  .filter((name) => name !== "media-probe.json")
@@ -739,7 +744,7 @@ function qualifyMediaFixture(values) {
739
744
  sha256(readRegular(path.join(renderOutput, name), `fixture ${name}`)),
740
745
  ]),
741
746
  ),
742
- rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
747
+ rendererManifestRoot: sha256(readRendererManifest(path.join(renderOutput, "manifest.json"), RENDITION_VALIDATION_HELPERS).bytes),
743
748
  qualification: verified.qualification,
744
749
  };
745
750
  writeJson(output, { ...body, evidenceRoot: semanticRoot(body) });
@@ -979,9 +984,8 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
979
984
  "public-projection.json",
980
985
  "scene.json",
981
986
  ];
982
- verifyChecksums(renderOutput);
983
- const manifest = readJson(path.join(renderOutput, "manifest.json"), "renderer manifest");
984
- invariant(manifest.schema === "build-images.auditable-demo-render/v1", "unexpected renderer manifest schema");
987
+ verifyChecksums(renderOutput, "checksums.sha256", { allowLongFormRendererManifest: true });
988
+ const { manifest } = readRendererManifest(path.join(renderOutput, "manifest.json"), RENDITION_VALIDATION_HELPERS);
985
989
  invariant(manifest.renderer?.image === expectedImage, "renderer manifest image coordinate mismatch");
986
990
  const outputNames = Object.keys(manifest.outputs || {}).sort();
987
991
  invariant(outputNames.includes("media-probe.json"), "renderer manifest must declare media-probe.json");
@@ -999,21 +1003,11 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
999
1003
  const observed = manifest.inputs?.[key]?.root;
1000
1004
  invariant(observed === sha256(readRegular(filePath, `${key} input`)), `renderer ${key} input root mismatch`);
1001
1005
  }
1002
- if (expectedInputs.renditionSet) {
1003
- invariant(
1004
- manifest.derivation?.policy === "independent-native-frame-sets/v1",
1005
- "renderer did not use independent native frame sets",
1006
- );
1007
- invariant(
1008
- Array.isArray(manifest.inputs?.renditions)
1009
- && manifest.inputs.renditions.length === 2
1010
- && manifest.inputs.renditions[0]?.role === "primary"
1011
- && manifest.inputs.renditions[1]?.role === "responsive"
1012
- && manifest.inputs.renditions[0]?.terminalCapture?.root
1013
- !== manifest.inputs.renditions[1]?.terminalCapture?.root,
1014
- "renderer native rendition inputs are not independently bound",
1015
- );
1016
- }
1006
+ const composition = validateRendererCompositionInputs(
1007
+ manifest,
1008
+ expectedInputs,
1009
+ RENDITION_VALIDATION_HELPERS,
1010
+ );
1017
1011
  const probe = readJson(path.join(renderOutput, "media-probe.json"), "media probe");
1018
1012
  invariant(probe.schema === "build-images.demo-media-probe/v1" && probe.passed === true, "renderer media probe failed");
1019
1013
  const qualification = qualifyRendererOutput(
@@ -1024,7 +1018,7 @@ function verifyRendererOutput(renderOutput, expectedImage, expectedInputs, optio
1024
1018
  options.inspectMedia || inspectMediaFile,
1025
1019
  options.inspectionRoot || "",
1026
1020
  );
1027
- return { manifest, probe, qualification };
1021
+ return { manifest, probe, qualification, composition };
1028
1022
  }
1029
1023
 
1030
1024
  function renditionInputRoots(root, renditions) {
@@ -1254,7 +1248,7 @@ function finalizeMedia(values) {
1254
1248
  sourceSha,
1255
1249
  qualifiedGateRoot: gateRoot,
1256
1250
  rendererImage,
1257
- rendererManifestRoot: sha256(readRegular(path.join(renderOutput, "manifest.json"), "renderer manifest")),
1251
+ rendererManifestRoot: sha256(readRendererManifest(path.join(renderOutput, "manifest.json"), RENDITION_VALIDATION_HELPERS).bytes),
1258
1252
  };
1259
1253
  const mediaReceipt = selectedProfile.mode === "archive"
1260
1254
  ? { schema: "buildchain.auditable-demo-media/v1", ...commonReceipt }
@@ -3,7 +3,6 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { macosJitRunnerLabel } from "./aws-macos-jit-core.mjs";
5
5
  import { windowsJitRunnerLabel } from "./aws-windows-jit-core.mjs";
6
-
7
6
  export const RUNNER_PRESETS = Object.freeze({
8
7
  "github-hosted": [
9
8
  { id: "linux-x64", name: "Linux x64", platform: "linux", runner: '["ubuntu-24.04"]', capabilities: ["node"] },
@@ -768,6 +767,55 @@ function platformIsLinux(platform) {
768
767
  );
769
768
  }
770
769
 
770
+ function platformUsesGitHubHostedRunner(platform, index) {
771
+ if (platform?.githubHosted !== undefined) {
772
+ if (typeof platform.githubHosted !== "boolean") {
773
+ throw new Error(
774
+ `platforms-json[${index}].githubHosted must be a boolean`,
775
+ );
776
+ }
777
+ return platform.githubHosted;
778
+ }
779
+ if (String(platform?.provider || "").trim()) {
780
+ return false;
781
+ }
782
+ const runnerLabels = parseJsonArray(
783
+ String(platform?.runner || "[]"),
784
+ `platforms-json[${index}].runner`,
785
+ ).map((label) => String(label || "").toLowerCase());
786
+ if (runnerLabels.includes("self-hosted") || runnerLabels.length !== 1) {
787
+ return false;
788
+ }
789
+ return /^(ubuntu-(latest|20\.04|22\.04|24\.04|24\.04-arm)|windows-(latest|2019|2022|2025|11-arm)|macos-(latest|13|14|15|26)(-intel)?)$/.test(
790
+ runnerLabels[0],
791
+ );
792
+ }
793
+
794
+ function bindRunnerHosting(platforms) {
795
+ return platforms.map((platform, index) => ({
796
+ ...platform,
797
+ githubHosted: platformUsesGitHubHostedRunner(platform, index),
798
+ }));
799
+ }
800
+
801
+ function runnerHostingSummary(platforms) {
802
+ const githubHostedPlatforms = platforms.filter(
803
+ (platform) => platform.githubHosted,
804
+ );
805
+ const relayPlatforms = platforms.filter((platform) => !platform.githubHosted);
806
+ return {
807
+ githubHostedPlatforms,
808
+ githubHostedPlatformsJson: JSON.stringify(githubHostedPlatforms),
809
+ githubHostedPlatformIdsJson: JSON.stringify(
810
+ githubHostedPlatforms.map((platform) => platform.id),
811
+ ),
812
+ githubHostedPlatformCount: githubHostedPlatforms.length,
813
+ relayPlatforms,
814
+ relayPlatformsJson: JSON.stringify(relayPlatforms),
815
+ relayPlatformCount: relayPlatforms.length,
816
+ };
817
+ }
818
+
771
819
  function normalizePlatform(platform, index) {
772
820
  const id = String(platform?.id || "").trim();
773
821
  const name = String(platform?.name || id).trim();
@@ -793,7 +841,9 @@ function normalizePlatform(platform, index) {
793
841
  if (platform?.platform !== undefined) normalized.platform = String(platform.platform || "").trim();
794
842
  if (platform?.provider !== undefined) normalized.provider = String(platform.provider || "").trim();
795
843
  if (platform?.project !== undefined) normalized.project = String(platform.project || "").trim();
844
+ if (platform?.githubHosted !== undefined) normalized.githubHosted = platform.githubHosted;
796
845
  normalized.capabilities = capabilities.sort();
846
+ if (platform?.environment !== undefined) normalized.environment = platform.environment;
797
847
  if (platform?.required === false) normalized.required = false;
798
848
  return normalized;
799
849
  }
@@ -813,8 +863,10 @@ export function resolveRunnerMatrix({
813
863
  linuxContainerImage,
814
864
  });
815
865
  if (customPlatformsJson) {
816
- const platforms = parseJsonArray(customPlatformsJson, "platforms-json").map(
817
- normalizePlatform,
866
+ const platforms = bindRunnerHosting(
867
+ parseJsonArray(customPlatformsJson, "platforms-json").map(
868
+ normalizePlatform,
869
+ ),
818
870
  );
819
871
  if (platforms.length === 0) {
820
872
  throw new Error("platforms-json must include at least one platform");
@@ -838,6 +890,7 @@ export function resolveRunnerMatrix({
838
890
  containerPlatformsJson: JSON.stringify(containerPlatforms),
839
891
  containerPlatformCount: containerPlatforms.length,
840
892
  linuxContainer,
893
+ ...runnerHostingSummary(platforms),
841
894
  };
842
895
  }
843
896
 
@@ -875,6 +928,7 @@ export function resolveRunnerMatrix({
875
928
  runner: JSON.stringify(["self-hosted", "macOS", "ARM64", runnerLabel]),
876
929
  }));
877
930
  }
931
+ resolvedPlatforms = bindRunnerHosting(resolvedPlatforms);
878
932
  const containerPlatforms = linuxContainer.enabled
879
933
  ? resolvedPlatforms.filter(platformIsLinux)
880
934
  : [];
@@ -894,6 +948,7 @@ export function resolveRunnerMatrix({
894
948
  containerPlatformsJson: JSON.stringify(containerPlatforms),
895
949
  containerPlatformCount: containerPlatforms.length,
896
950
  linuxContainer,
951
+ ...runnerHostingSummary(resolvedPlatforms),
897
952
  };
898
953
  }
899
954
 
@@ -108,10 +108,18 @@ export const BUILDCHAIN_USAGE = `Usage:
108
108
  buildchain inspect release --passport <file-or-url> [--json]
109
109
  buildchain inspect artifact <subject> [--passport <file-or-url>] [--npm-registry <url>] [--json]
110
110
  buildchain doctor [--cwd <dir>] [--require-publish-source-lock] [--json]
111
+ buildchain dev pr-admit --repository <owner/repo> --branch <dev/vN/vN.M>
112
+ --pull-request <n> --expected-head <sha>
113
+ [--execute] [--output <file>] [--json]
111
114
  buildchain dev merge-queue --repository <owner/repo> --branch <dev/vN/vN.M>
112
115
  [--from-config | --workflow <required-workflow.yml>...] [--cwd <dir>]
113
116
  [--check-response-timeout-minutes <n>]
114
117
  [--max-entries-to-build <n>] [--apply]
118
+ buildchain dev warrant <submit|select|heartbeat|recover|close|cancel-queued|observe>
119
+ --repository <owner/repo> --branch <dev/vN/vN.M>
120
+ [--execute] [--output <file>] [--json]
121
+ buildchain dev proof <source|verify-source|classify|replay|integration|verify-integration>
122
+ [--output <file>] [--json]
115
123
  buildchain log <info|warn|error> --event <name> [--phase <phase>]
116
124
  [--component <name>] [--source <name>] [--attribute key=value]...
117
125
  [--path <jsonl>] [--json]
@@ -194,6 +194,15 @@ export async function runBuildchainPatrol(optionsInput = {}, clientInput) {
194
194
  }
195
195
 
196
196
  result.summary.plannedCount = result.planned.length;
197
+ const admissionActions = result.actions.flatMap((action) => action.result?.actions || []);
198
+ result.runKind = "cadence-patrol";
199
+ result.outcome = result.summary.evaluatedCount === 0
200
+ ? "no-op-no-candidates"
201
+ : admissionActions.length === 0
202
+ ? "no-op-all-skipped"
203
+ : "actions-present";
204
+ result.noOp = admissionActions.length === 0;
205
+ result.qualification = false;
197
206
  result.ok = result.inspections.every((inspection) => inspection.ok !== false);
198
207
  return result;
199
208
  }
@@ -74,6 +74,7 @@ const requiredPaths = [
74
74
  "docs/shifu-gate-profiles.md",
75
75
  "docs/auditable-demo.md",
76
76
  "contracts/auditable-demo-scenario-v1.schema.json",
77
+ "contracts/release-candidate-recovery-v1.schema.json",
77
78
  "contracts/auditable-demo-media-profiles-v1.json",
78
79
  "contracts/evidence/auditable-demo-web-delivery-v1.json",
79
80
  "contracts/evidence/auditable-demo-responsive-web-delivery-v1.json",
@@ -117,6 +118,7 @@ const requiredPaths = [
117
118
  "scripts/publication-commit-evidence.mjs",
118
119
  "scripts/publication-reproducibility.mjs",
119
120
  "scripts/release-candidate-resolver.mjs",
121
+ "scripts/resume-from-candidate-run.mjs",
120
122
  "scripts/buildchain-patrol.mjs",
121
123
  "scripts/observed-evidence.mjs",
122
124
  "scripts/workflow-friction-report.mjs",
@@ -134,9 +136,11 @@ const requiredPaths = [
134
136
  ".github/actionlint.yaml",
135
137
  ".github/workflows/self-hosted-runner-smoke.yml",
136
138
  ".github/workflows/buildchain-ref-promotion.yml",
139
+ ".github/workflows/buildchain-candidate-recovery-dogfood-failure.yml",
137
140
  ".github/workflows/release-line-bootstrap.yml",
138
141
  ".github/workflows/release-governance-reconcile.yml",
139
142
  ".github/workflows/dev-pr-auto-merge.yml",
143
+ ".github/workflows/buildchain-dev-delivery.yml",
140
144
  ".github/workflows/buildchain-patrol.yml",
141
145
  ".github/workflows/patrol-daily.yml",
142
146
  ".github/workflows/patrol-weekly.yml",
@@ -983,10 +987,12 @@ for (const requiredSnippet of [
983
987
  "github.event.workflow_run.event == 'push'",
984
988
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
985
989
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
986
- "target-sha: ${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }}",
990
+ "resume-candidate-run-id:",
991
+ "resume-expected-source-tree:",
992
+ "resume-buildchain-runtime-sha:",
987
993
  "github-release: true",
988
994
  "release-passport-buildchain-self-kfd: true",
989
- "publish-required-artifacts-json: \"[]\"",
995
+ "artifact-patterns: ${{ inputs['resume-candidate-run-id'] != '' && 'buildchain-package-*' || '' }}",
990
996
  "release-passport-impact-json: .buildchain/release-impact.json",
991
997
  ]) {
992
998
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -1040,6 +1046,11 @@ for (const requiredSnippet of [
1040
1046
  "BUILDCHAIN_STABLE_RELEASE_POLICY: .buildchain/stable-release-policy.json",
1041
1047
  "Consumer has no binary-distribution.yml; standalone binary dispatch is not applicable.",
1042
1048
  "gh workflow view binary-distribution.yml",
1049
+ "resume-candidate-run-id:",
1050
+ "node .buildchain/runtime/scripts/resume-from-candidate-run.mjs",
1051
+ "publish-sealed-bundle-root: ${{ steps.rc.outputs.publish-sealed-bundle-root }}",
1052
+ "BUILDCHAIN_EXPECTED_TRANSACTION_ID: ${{ inputs.resume-transaction-id }}",
1053
+ "if: ${{ inputs.resume-candidate-run-id == '' }}",
1043
1054
  ]) {
1044
1055
  if (!releaseCandidatePromoteWorkflow.includes(requiredSnippet)) {
1045
1056
  throw new Error(`release candidate promote workflow missing KFD gate pass-through: ${requiredSnippet}`);
@@ -5,6 +5,10 @@ import fs from "node:fs";
5
5
  import path from "node:path";
6
6
  import crypto from "node:crypto";
7
7
  import { decideChannelCandidate } from "../packages/core/channel-candidate.js";
8
+ import {
9
+ resolveManagedPromotionBaseline,
10
+ resolvePatrolSourceInputs,
11
+ } from "../packages/core/channel-promotion-baseline.js";
8
12
 
9
13
  export const DEV_ALPHA_CANDIDATE_STATE_SCHEMA =
10
14
  "kungfu-buildchain-dev-alpha-candidate-state/v1";
@@ -301,8 +305,7 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
301
305
  false,
302
306
  ),
303
307
  mergeMethod: autoMergeMethod(
304
- options.mergeMethod ??
305
- process.env.BUILDCHAIN_CHANNEL_PATROL_MERGE_METHOD,
308
+ options.mergeMethod ?? process.env.BUILDCHAIN_CHANNEL_PATROL_MERGE_METHOD,
306
309
  ),
307
310
  dryRun: bool(
308
311
  options.dryRun ?? process.env.BUILDCHAIN_CHANNEL_PATROL_DRY_RUN,
@@ -321,7 +324,10 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
321
324
  function latestWorkflowEvidence(runs, workflowPathValue, sourceSha) {
322
325
  const matching = runs
323
326
  .filter(
324
- (run) => run.path === workflowPathValue && run.head_sha === sourceSha,
327
+ (run) =>
328
+ run.conclusion !== "cancelled" &&
329
+ run.path === workflowPathValue &&
330
+ run.head_sha === sourceSha,
325
331
  )
326
332
  .sort((left, right) => Number(right.id) - Number(left.id));
327
333
  if (matching.length === 0)
@@ -356,7 +362,9 @@ function workflowEvidenceIsFreshAndSuccessful(run, { now, maxAgeSeconds }) {
356
362
 
357
363
  function latestRunsBySha(runs, workflowPathValue) {
358
364
  const latest = new Map();
359
- for (const run of runs.filter((row) => row.path === workflowPathValue)) {
365
+ for (const run of runs.filter(
366
+ (row) => row.path === workflowPathValue && row.conclusion !== "cancelled",
367
+ )) {
360
368
  const current = latest.get(run.head_sha);
361
369
  if (!current || Number(run.id) > Number(current.id))
362
370
  latest.set(run.head_sha, run);
@@ -620,52 +628,26 @@ export async function runDevAlphaCandidatePatrol(
620
628
  repository: options.repository,
621
629
  token: process.env.GITHUB_TOKEN,
622
630
  });
623
- const [observedSourceHeadSha, targetSha] = await Promise.all([
624
- client.resolveBranch(options.sourceBranch),
625
- client.resolveBranch(options.targetBranch),
626
- ]);
627
- const headComparison = await client.compare(targetSha, observedSourceHeadSha);
628
631
  const requiredWorkflowPaths = [
629
632
  options.devWorkflowPath,
630
633
  options.alphaWorkflowPath,
631
634
  ];
632
- let sourceSha = observedSourceHeadSha;
633
- let comparison = headComparison;
634
- let workflowEvidence = [];
635
- let skippedNewerCommitCount = 0;
636
- let qualificationError;
637
- if (
638
- headComparison.status === "ahead" &&
639
- Number(headComparison.ahead_by) > 0
640
- ) {
641
- const [sourceHistory, ...workflowRunSets] = await Promise.all([
642
- client.listBranchHistory(options.sourceBranch, targetSha),
643
- ...requiredWorkflowPaths.map((workflow) =>
644
- client.listCompletedWorkflowRuns(workflow, options.sourceBranch),
645
- ),
646
- ]);
647
- try {
648
- const selected = selectLatestQualifiedSource({
649
- sourceHistory,
650
- workflowRunsByPath: new Map(
651
- requiredWorkflowPaths.map((workflow, index) => [
652
- workflow,
653
- workflowRunSets[index],
654
- ]),
655
- ),
656
- requiredWorkflowPaths,
657
- now: options.now,
658
- maxAgeSeconds: options.maxAgeSeconds,
659
- });
660
- sourceSha = selected.sourceSha;
661
- skippedNewerCommitCount = selected.skippedNewerCommitCount;
662
- workflowEvidence = selected.workflowEvidence;
663
- if (sourceSha !== observedSourceHeadSha)
664
- comparison = await client.compare(targetSha, sourceSha);
665
- } catch (error) {
666
- qualificationError = error;
667
- }
668
- }
635
+ const {
636
+ observedSourceHeadSha,
637
+ targetSha,
638
+ headComparison,
639
+ targetBaseline,
640
+ sourceSha,
641
+ comparison,
642
+ workflowEvidence,
643
+ skippedNewerCommitCount,
644
+ qualificationError,
645
+ } = await resolvePatrolSourceInputs({
646
+ client,
647
+ options,
648
+ requiredWorkflowPaths,
649
+ selectQualifiedSource: selectLatestQualifiedSource,
650
+ });
669
651
  const decision = qualificationError
670
652
  ? blockedCandidateDecision({
671
653
  options,
@@ -682,9 +664,12 @@ export async function runDevAlphaCandidatePatrol(
682
664
  targetSha,
683
665
  comparison: { status: comparison.status, aheadBy: comparison.ahead_by },
684
666
  selection: {
685
- mode: "latest-qualified-source-ancestor",
667
+ mode: targetBaseline
668
+ ? "latest-qualified-source-after-managed-promotion"
669
+ : "latest-qualified-source-ancestor",
686
670
  observedSourceHeadSha,
687
671
  skippedNewerCommitCount,
672
+ ...(targetBaseline ? { targetBaseline } : {}),
688
673
  },
689
674
  workflowEvidence,
690
675
  requiredWorkflowPaths,
@@ -927,7 +912,12 @@ export async function runDevAlphaCandidatePatrol(
927
912
  }
928
913
  }
929
914
  }
930
- const autoMerge = await armCandidateAutoMerge(options, controllerState, pullRequest, client);
915
+ const autoMerge = await armCandidateAutoMerge(
916
+ options,
917
+ controllerState,
918
+ pullRequest,
919
+ client,
920
+ );
931
921
  return {
932
922
  schema: "kungfu-buildchain-dev-alpha-candidate-patrol/v1",
933
923
  dryRun: options.dryRun,
@@ -1015,6 +1005,13 @@ export function createGitHubChannelCandidateClient({
1015
1005
  async compare(baseSha, headSha) {
1016
1006
  return api(`/repos/${owner}/${repo}/compare/${baseSha}...${headSha}`);
1017
1007
  },
1008
+ resolveManagedPromotionBaseline: (targetSha, targetBranch) =>
1009
+ resolveManagedPromotionBaseline({
1010
+ api: (requestPath) => api(`/repos/${owner}/${repo}${requestPath}`),
1011
+ targetSha,
1012
+ targetBranch,
1013
+ parseCandidate: managedCandidateFromPullRequest,
1014
+ }),
1018
1015
  async listCompletedWorkflowRuns(workflowPathValue, sourceBranch) {
1019
1016
  const runs = [];
1020
1017
  for (let page = 1; page <= 10; page += 1) {