@kungfu-tech/buildchain 2.5.5 → 2.5.6

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.
@@ -67,6 +67,7 @@ function usage() {
67
67
  [--trusted-publishing-json <json-or-path>]
68
68
  [--transaction-json <json-or-path>]
69
69
  [--anchor-manifest-json <json-or-path>]
70
+ [--impact-json <json-or-path>]
70
71
  [--build-summary-json <json-or-path>]
71
72
  [--platform-manifest-json <json-or-path>]...
72
73
  [--dist-tag-evidence-json <json-or-path>]
@@ -716,6 +717,7 @@ async function main(argv = process.argv.slice(2)) {
716
717
  trustedPublishingJson: readFlag(collectArgs, "trusted-publishing-json", ""),
717
718
  transactionJson: readFlag(collectArgs, "transaction-json", ""),
718
719
  anchorManifestJson: readFlag(collectArgs, "anchor-manifest-json", ""),
720
+ impactJson: readFlag(collectArgs, "impact-json", ""),
719
721
  buildSummaryJson: readFlag(collectArgs, "build-summary-json", ""),
720
722
  platformManifestJsons: readRepeatedFlag(collectArgs, "platform-manifest-json"),
721
723
  distTagEvidenceJson: readFlag(collectArgs, "dist-tag-evidence-json", ""),
package/docs/cli.md CHANGED
@@ -252,6 +252,7 @@ buildchain collect github-release \
252
252
  --publish-evidence-json .buildchain/release-evidence/v2.3.2/evidence.json \
253
253
  --transaction-json .buildchain/release-state/v2.3.2/state.json \
254
254
  --package-set-json package-set.json \
255
+ --impact-json impact.json \
255
256
  --trusted-publishing-json trusted-publishing.json \
256
257
  --anchor-manifest-json libnode.release.json \
257
258
  --build-summary-json .buildchain/artifacts/build-summary.json \
@@ -275,6 +276,17 @@ platform packages with version, dist-tag, and digest evidence. Verification
275
276
  fails closed if supplied sections are internally incomplete or point to
276
277
  artifacts without matching evidence.
277
278
 
279
+ `--impact-json` supplies the surface-aware impact ledger. Production release
280
+ passports (`release/*`) and major publish-gate passports require
281
+ `surfaceImpacts[]`; alpha, local, and legacy passport contexts keep it
282
+ optional. When `surfaceImpacts[]` is required or supplied, the verifier requires
283
+ each entry to include an id, impact, and rationale, and requires
284
+ `versionImpact.final` to match the highest declared surface impact. The
285
+ collector copies `versionImpact` plus `surfaceImpacts` into
286
+ `buildchain.release.json`. This lets
287
+ `buildchain explain release --for agent --json` state why a release is patch,
288
+ minor, or major instead of relying on file-path memory.
289
+
278
290
  Buildchain dogfoods its observability toolkit in this lane. The standalone
279
291
  builder writes API-generated events, while the workflow uses `buildchain mark`,
280
292
  `buildchain span`, `buildchain verify observability-log`, and `buildchain log
@@ -36,6 +36,10 @@ Additive passport sections:
36
36
 
37
37
  - `release`: exact tag, line, channel, source SHA, target ref, release SHA,
38
38
  release material SHA, publish tooling SHA, and durable release-state ref.
39
+ - `versionImpact`: final patch/minor/major classification, source, and
40
+ rationale.
41
+ - `surfaceImpacts`: per registered surface classification. The final impact is
42
+ the highest entry in this list.
39
43
  - `packageSet`: main package, platform packages, package-set order, registry,
40
44
  versions, dist-tags, and package digests.
41
45
  - `anchorManifest`: anchored/manual version manifest path, digest, and fields.
@@ -102,6 +106,15 @@ dist-tag, registry, role, platform, and digest, so agents do not need to stitch
102
106
  npm facts back together from the lower-level evidence files.
103
107
  `buildSummary`, `platformArtifactManifests`, and `distTagPromotion` preserve the
104
108
  build and npm dist-tag evidence chain in the same passport.
109
+ `impact.json` can be supplied with `--impact-json`. Production release
110
+ passports (`release/*`) and major publish-gate passports require
111
+ `surfaceImpacts[]`; alpha, local, and legacy passport contexts keep the field
112
+ optional. When `surfaceImpacts[]` is required or supplied, verification fails
113
+ closed unless each entry has an id, impact, and rationale, and
114
+ `versionImpact.final` matches the highest surface impact. For example, KFD-2
115
+ content can remain patch while an additive `registry.kind` field on the
116
+ machine-consumed KFD registry schema records a minor `kfd-registry-schema`
117
+ surface impact.
105
118
 
106
119
  Verify a release passport:
107
120
 
@@ -20,7 +20,8 @@ latest stable production patch for that minor line.
20
20
 
21
21
  ## Welded Surfaces
22
22
 
23
- These surfaces require at least a minor bump when first introduced:
23
+ These surfaces are classified independently; the final release impact is the
24
+ highest impact across the affected registered surfaces:
24
25
 
25
26
  - reusable workflow inputs, outputs, and artifact contracts;
26
27
  - public CLI command families and their machine-readable JSON shapes;
@@ -31,20 +32,40 @@ These surfaces require at least a minor bump when first introduced:
31
32
  ledger, and agent index files;
32
33
  - binary distribution shapes that users can install or automate against.
33
34
 
34
- Additive fields inside an existing welded surface can be patch releases when
35
- old consumers continue to work and validation remains stricter, not looser.
36
- Removing fields, changing meanings, weakening trust gates, or changing the
37
- expected ref flow requires a major bump.
35
+ For each surface:
36
+
37
+ - content, documentation, or implementation-only work that does not touch a
38
+ registered surface is patch;
39
+ - additive fields, new commands, new exports, new evidence sections, or new
40
+ registered surfaces are minor;
41
+ - removals, incompatible renames, changed meanings, newly required fields,
42
+ weakened trust gates, or changed ref flow are major.
43
+
44
+ The release passport records this as `surfaceImpacts[]` plus
45
+ `versionImpact.final`. The final impact must equal the highest surface impact,
46
+ so an agent cannot silently label a release patch when one machine surface needs
47
+ minor review.
48
+
49
+ `surfaceImpacts[]` is mandatory for production release passports (`release/*`)
50
+ and major publish-gate passports. Alpha, local, and legacy passport contexts
51
+ keep the field optional so temporary validation can proceed without pretending
52
+ to be a production release decision.
53
+
54
+ Example: a KFD document such as KFD-2 is content and remains patch, but adding a
55
+ `kind` field to the machine-consumed KFD `registry.json` is an additive change
56
+ to the `kfd-registry-schema` surface and therefore requires minor-impact
57
+ review. This avoids both false shortcuts: "new KFD means minor" and "all KFD
58
+ repository changes are patch".
38
59
 
39
60
  ## Decision Log
40
61
 
41
- | Date | Decision | Line | Reason |
42
- | --- | --- | --- | --- |
43
- | 2026-07-02 | Buildchain toolkit observability is a minor surface. | `v2.1` | It adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. |
44
- | 2026-07-02 | Release passport and binary distribution are a minor surface. | `v2.2` | They add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. |
45
- | 2026-07-02 | Web surface host mapping is a minor surface. | `v2.3` | It adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. |
46
- | 2026-07-03 | Infra contract lifecycle is a minor surface. | `v2.4` | It adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. |
47
- | 2026-07-04 | Scheduled integration governance is a minor surface. | `v2.5` | It adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. |
62
+ | Date | Action | Line | Faces | Class | Rationale | PR |
63
+ | --- | --- | --- | --- | --- | --- | --- |
64
+ | 2026-07-04 | open-minor | `v2.5` | scheduled-integration-governance | additive | Scheduled integration governance adds scheduled feature-branch discovery, conflict-free integration, reporting, and agent-visible governance automation for dev-line maintenance. | |
65
+ | 2026-07-03 | open-minor | `v2.4` | infra-contract-lifecycle | additive | Infra contract lifecycle adds the provider-neutral `infra-contract` CLI command family, project type, adapter capability contract, lifecycle evidence bundle, propagation evidence, CI evidence mode, and consumer-facing contract artifacts. | |
66
+ | 2026-07-02 | open-minor | `v2.3` | web-surface-host-mapping | additive | Web surface host mapping adds first-class multi-host surface bindings, reusable workflow URL outputs, per-surface deployment overrides, and an agent-readable fixture contract. | |
67
+ | 2026-07-02 | open-minor | `v2.2` | release-passport, binary-distribution | additive | Release passport and binary distribution add agent-readable release passport files, artifact evidence, impact ledger, agent index, GitHub Release collection and verification commands, and standalone binary assets. | |
68
+ | 2026-07-02 | open-minor | `v2.1` | logging-sdk, cli-observability, package-subpaths | additive | Buildchain toolkit observability adds the public logging SDK, CLI observability commands, and package subpaths that consumers can import. | |
48
69
 
49
70
  ## Runner Policy
50
71
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.5.5",
3
+ "version": "2.5.6",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -36,6 +36,66 @@ function optionalString(value) {
36
36
  return value === undefined || value === null ? "" : String(value);
37
37
  }
38
38
 
39
+ const VERSION_IMPACT_ORDER = new Map([
40
+ ["unknown", 0],
41
+ ["patch", 1],
42
+ ["minor", 2],
43
+ ["major", 3],
44
+ ]);
45
+
46
+ function normalizeImpactLevel(value, fallback = "unknown") {
47
+ const normalized = optionalString(value || fallback).toLowerCase();
48
+ return VERSION_IMPACT_ORDER.has(normalized) ? normalized : optionalString(value || fallback);
49
+ }
50
+
51
+ function highestImpactLevel(levels = []) {
52
+ let highest = "unknown";
53
+ for (const level of levels) {
54
+ const normalized = normalizeImpactLevel(level);
55
+ if ((VERSION_IMPACT_ORDER.get(normalized) ?? -1) > (VERSION_IMPACT_ORDER.get(highest) ?? -1)) {
56
+ highest = normalized;
57
+ }
58
+ }
59
+ return highest;
60
+ }
61
+
62
+ function surfaceImpactRequirement({ passport = {}, impact = {} } = {}) {
63
+ const release = passport?.release && typeof passport.release === "object" ? passport.release : {};
64
+ const impactRelease = impact?.release && typeof impact.release === "object" ? impact.release : {};
65
+ const channel = optionalString(release.channel || impactRelease.channel).toLowerCase();
66
+ const targetRef = optionalString(
67
+ release.targetRef ||
68
+ release.target_ref ||
69
+ impactRelease.targetRef ||
70
+ impactRelease.target_ref,
71
+ ).toLowerCase();
72
+ if (channel === "release" || targetRef.startsWith("release/")) {
73
+ return {
74
+ required: true,
75
+ type: "production-release",
76
+ reason: "production release passports require surfaceImpacts[] so agents can audit the final version impact",
77
+ channel,
78
+ targetRef,
79
+ };
80
+ }
81
+ if (channel === "major" || targetRef === "publish-gate/major" || targetRef === "major-gate") {
82
+ return {
83
+ required: true,
84
+ type: "major-gate",
85
+ reason: "major publish gates require surfaceImpacts[] so breaking-surface rationale is explicit",
86
+ channel,
87
+ targetRef,
88
+ };
89
+ }
90
+ return {
91
+ required: false,
92
+ type: channel || (targetRef ? "non-production-release" : "legacy"),
93
+ reason: "surfaceImpacts[] is optional for alpha, local, and legacy passport contexts",
94
+ channel,
95
+ targetRef,
96
+ };
97
+ }
98
+
39
99
  function stableJson(value) {
40
100
  if (Array.isArray(value)) {
41
101
  return `[${value.map(stableJson).join(",")}]`;
@@ -123,14 +183,21 @@ function defaultProductMechanism({ repository = "", productName = "Buildchain" }
123
183
  }
124
184
 
125
185
  function defaultImpact({ tag = "", line = "", decision = "unknown" } = {}) {
186
+ const finalImpact = normalizeImpactLevel(decision);
126
187
  return {
127
188
  schemaVersion: 1,
128
189
  contract: IMPACT_LEDGER_CONTRACT,
129
190
  release: { tag, line },
130
- classification: decision,
131
- breaking: false,
191
+ versionImpact: {
192
+ final: finalImpact,
193
+ source: "default",
194
+ rationale: "No surface impact classification was supplied.",
195
+ },
196
+ surfaceImpacts: [],
197
+ classification: finalImpact,
198
+ breaking: finalImpact === "major",
132
199
  security: false,
133
- migrationRequired: false,
200
+ migrationRequired: finalImpact === "major",
134
201
  summary: "No release impact summary was supplied.",
135
202
  recovery: {
136
203
  rollback: "Use the previous exact release tag or previous floating channel ref.",
@@ -339,6 +406,55 @@ function normalizePublishEvidence(value = undefined) {
339
406
  };
340
407
  }
341
408
 
409
+ function normalizeSurfaceImpact(entry = {}, index = 0) {
410
+ const id = nonEmptyString(entry.id || entry.surface || entry.surfaceId, `surfaceImpacts[${index}].id`);
411
+ const impact = normalizeImpactLevel(entry.impact || entry.classification || entry.versionImpact);
412
+ return {
413
+ id,
414
+ impact,
415
+ class: optionalString(entry.class || entry.changeClass || entry.change_class),
416
+ rationale: optionalString(entry.rationale || entry.reason),
417
+ source: optionalString(entry.source),
418
+ };
419
+ }
420
+
421
+ function normalizeImpactLedger(value = undefined, { tag = "", line = "", decision = "unknown" } = {}) {
422
+ if (!value) {
423
+ return defaultImpact({ tag, line, decision });
424
+ }
425
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
426
+ throw new Error("impact must be a JSON object");
427
+ }
428
+ const surfaceImpacts = Array.isArray(value.surfaceImpacts)
429
+ ? value.surfaceImpacts.map((entry, index) => normalizeSurfaceImpact(entry, index))
430
+ : [];
431
+ const finalImpact = normalizeImpactLevel(
432
+ value.versionImpact?.final ||
433
+ value.versionImpact ||
434
+ value.classification ||
435
+ decision,
436
+ );
437
+ return {
438
+ ...value,
439
+ schemaVersion: Number(value.schemaVersion || 1),
440
+ contract: value.contract || IMPACT_LEDGER_CONTRACT,
441
+ release: {
442
+ tag: optionalString(value.release?.tag || tag),
443
+ line: optionalString(value.release?.line || line),
444
+ ...(value.release && typeof value.release === "object" && !Array.isArray(value.release) ? value.release : {}),
445
+ },
446
+ versionImpact: {
447
+ final: finalImpact,
448
+ source: optionalString(value.versionImpact?.source || value.impactSource || "declared"),
449
+ rationale: optionalString(value.versionImpact?.rationale || value.rationale || value.summary),
450
+ },
451
+ surfaceImpacts,
452
+ classification: normalizeImpactLevel(value.classification || finalImpact),
453
+ breaking: value.breaking === undefined ? finalImpact === "major" : Boolean(value.breaking),
454
+ migrationRequired: value.migrationRequired === undefined ? finalImpact === "major" : Boolean(value.migrationRequired),
455
+ };
456
+ }
457
+
342
458
  function packageSetEntries(packageSet = undefined) {
343
459
  if (!packageSet) {
344
460
  return [];
@@ -503,6 +619,7 @@ export function createReleasePassport({
503
619
  distTagPromotionEvidence = undefined,
504
620
  release = {},
505
621
  publish = {},
622
+ impact = undefined,
506
623
  workflow = {},
507
624
  } = {}) {
508
625
  const normalizedTag = nonEmptyString(tag, "tag");
@@ -518,6 +635,7 @@ export function createReleasePassport({
518
635
  const normalizedDistTagPromotionEvidence = distTagPromotionEvidence
519
636
  ? normalizeEvidenceDocument(distTagPromotionEvidence, "distTagPromotionEvidence")
520
637
  : undefined;
638
+ const normalizedImpact = normalizeImpactLedger(impact, { tag: normalizedTag, line });
521
639
  const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
522
640
  const normalizedPublishSummary = normalizePublishSummary({
523
641
  packageSet: normalizedPackageSet,
@@ -618,6 +736,8 @@ export function createReleasePassport({
618
736
  ...(normalizedBuildSummary ? { buildSummary: normalizedBuildSummary } : {}),
619
737
  ...(normalizedPlatformArtifactManifests.length > 0 ? { platformArtifactManifests: normalizedPlatformArtifactManifests } : {}),
620
738
  ...(normalizedDistTagPromotionEvidence ? { distTagPromotion: normalizedDistTagPromotionEvidence } : {}),
739
+ versionImpact: normalizedImpact.versionImpact,
740
+ surfaceImpacts: normalizedImpact.surfaceImpacts,
621
741
  artifacts: [
622
742
  ...artifactEvidence.artifacts.map((asset) => ({
623
743
  group: "release",
@@ -680,6 +800,7 @@ export function collectGitHubReleasePassport({
680
800
  trustedPublishingJson = "",
681
801
  transactionJson = "",
682
802
  anchorManifestJson = "",
803
+ impactJson = "",
683
804
  buildSummaryJson = "",
684
805
  platformManifestJsons = [],
685
806
  distTagEvidenceJson = "",
@@ -695,6 +816,7 @@ export function collectGitHubReleasePassport({
695
816
  const trustedPublishing = parseJsonInput(trustedPublishingJson, undefined);
696
817
  const transactionMeta = parseJsonInputWithMeta(transactionJson, undefined);
697
818
  const anchorManifest = normalizeAnchorManifest(parseJsonInputWithMeta(anchorManifestJson, undefined));
819
+ const impactMeta = parseJsonInputWithMeta(impactJson, undefined);
698
820
  const buildSummaryMeta = parseJsonInputWithMeta(buildSummaryJson, undefined);
699
821
  const platformManifestMetas = (platformManifestJsons || [])
700
822
  .filter(Boolean)
@@ -710,7 +832,7 @@ export function collectGitHubReleasePassport({
710
832
  const resolvedOutputDir = path.resolve(cwd, outputDir);
711
833
  const productMechanism = defaultProductMechanism({ repository, productName });
712
834
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: resolvedTag, sourceSha, workflow });
713
- const impact = defaultImpact({ tag: resolvedTag, line, decision: "unknown" });
835
+ const impact = normalizeImpactLedger(impactMeta.value, { tag: resolvedTag, line, decision: "unknown" });
714
836
  const agentIndex = defaultAgentIndex({ tag: resolvedTag });
715
837
  const passport = createReleasePassport({
716
838
  cwd,
@@ -747,6 +869,7 @@ export function collectGitHubReleasePassport({
747
869
  : undefined,
748
870
  release: releaseExtra,
749
871
  publish,
872
+ impact,
750
873
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
751
874
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
752
875
  workflow,
@@ -1042,6 +1165,42 @@ export function createReleaseCheckReport({
1042
1165
  if (!passport?.runnerPolicy?.productionDefault) {
1043
1166
  issues.push(issue("warning", "runnerPolicy.productionDefault", "runner policy should record the production default"));
1044
1167
  }
1168
+ const surfaceImpacts = Array.isArray(impact?.surfaceImpacts) ? impact.surfaceImpacts : [];
1169
+ const passportSurfaceImpacts = Array.isArray(passport?.surfaceImpacts) ? passport.surfaceImpacts : [];
1170
+ const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
1171
+ const computedFinalImpact = highestImpactLevel(surfaceImpacts.map((entry) => entry.impact));
1172
+ const requiredSurfaceImpacts = surfaceImpactRequirement({ passport, impact });
1173
+ if (requiredSurfaceImpacts.required && surfaceImpacts.length === 0) {
1174
+ issues.push(issue("error", "impact.surfaceImpacts.required", "surfaceImpacts[] is required for this release passport type", requiredSurfaceImpacts));
1175
+ }
1176
+ if (surfaceImpacts.length > 0) {
1177
+ for (const [index, entry] of surfaceImpacts.entries()) {
1178
+ if (!entry.id) {
1179
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].id`, "surface impact id is required"));
1180
+ }
1181
+ if (!VERSION_IMPACT_ORDER.has(entry.impact)) {
1182
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].impact`, "surface impact must be patch, minor, or major"));
1183
+ }
1184
+ if (!entry.rationale) {
1185
+ issues.push(issue("error", `impact.surfaceImpacts[${index}].rationale`, "surface impact rationale is required"));
1186
+ }
1187
+ }
1188
+ if (!impact?.versionImpact?.rationale) {
1189
+ issues.push(issue("error", "impact.versionImpact.rationale", "version impact rationale is required when surface impacts are supplied"));
1190
+ }
1191
+ if (declaredFinalImpact !== computedFinalImpact) {
1192
+ issues.push(issue("error", "impact.versionImpact.final", "versionImpact.final must equal the highest surface impact", {
1193
+ declared: declaredFinalImpact,
1194
+ computed: computedFinalImpact,
1195
+ }));
1196
+ }
1197
+ if (stableJson(passportSurfaceImpacts) !== stableJson(surfaceImpacts)) {
1198
+ issues.push(issue("error", "passport.surfaceImpacts", "passport.surfaceImpacts must mirror impact.surfaceImpacts"));
1199
+ }
1200
+ }
1201
+ if (passport?.versionImpact?.final && impact?.versionImpact?.final && passport.versionImpact.final !== impact.versionImpact.final) {
1202
+ issues.push(issue("error", "passport.versionImpact.final", "passport.versionImpact.final must match impact.versionImpact.final"));
1203
+ }
1045
1204
  const ok = issues.every((entry) => entry.level !== "error");
1046
1205
  return {
1047
1206
  schemaVersion: 1,
@@ -1049,6 +1208,7 @@ export function createReleaseCheckReport({
1049
1208
  checkedAt,
1050
1209
  ok,
1051
1210
  trust: ok ? "pass" : "fail",
1211
+ surfaceImpactRequirement: requiredSurfaceImpacts,
1052
1212
  completeness: {
1053
1213
  artifactCount: artifacts.length,
1054
1214
  evidenceArtifactCount: evidenceArtifacts.length,
@@ -1064,6 +1224,9 @@ export function createReleaseCheckReport({
1064
1224
  impactPresent: Boolean(impact),
1065
1225
  agentIndexPresent: Boolean(agentIndex),
1066
1226
  productMechanismPresent: Boolean(productMechanism),
1227
+ surfaceImpactsRequired: requiredSurfaceImpacts.required,
1228
+ surfaceImpactCount: surfaceImpacts.length,
1229
+ versionImpact: impact?.versionImpact?.final || "",
1067
1230
  },
1068
1231
  issues,
1069
1232
  };
@@ -1156,8 +1319,12 @@ export async function explainReleasePassport({ passportLocation, forAudience = "
1156
1319
  artifactCount: report.completeness.artifactCount,
1157
1320
  runnerPolicy: passport.runnerPolicy,
1158
1321
  impact: {
1159
- breaking: false,
1160
- migrationRequired: false,
1322
+ versionImpact: passport.versionImpact || {},
1323
+ surfaceImpacts: Array.isArray(passport.surfaceImpacts) ? passport.surfaceImpacts : [],
1324
+ surfaceImpactRequirement: report.surfaceImpactRequirement,
1325
+ breaking: Boolean(passport.versionImpact?.final === "major"),
1326
+ migrationRequired: Boolean(passport.versionImpact?.final === "major"),
1327
+ summary: passport.versionImpact?.rationale || "",
1161
1328
  },
1162
1329
  recovery: passport.recovery,
1163
1330
  nextAction,
@@ -130,7 +130,9 @@ if (!coreIndexSource.includes("reportBuildchainIssue")) {
130
130
  throw new Error("packages/core/index.js must export reportBuildchainIssue");
131
131
  }
132
132
  for (const requiredSnippet of [
133
- "Release passport and binary distribution are a minor surface.",
133
+ "surfaceImpacts[]",
134
+ "versionImpact.final",
135
+ "kfd-registry-schema",
134
136
  "`v2.2`",
135
137
  "GitHub-hosted runners for production",
136
138
  "Self-hosted runners remain compatibility fixtures",
@@ -91,7 +91,7 @@ export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, r
91
91
  return candidates[0];
92
92
  }
93
93
 
94
- export function selectReleaseCandidateRun({ runs = [], pullRequest, workflowName = "" }) {
94
+ export function selectReleaseCandidateRuns({ runs = [], pullRequest, workflowName = "" }) {
95
95
  const prNumber = Number(pullRequest?.number || 0);
96
96
  const prHeadSha = String(pullRequest?.head?.sha || pullRequest?.headRefOid || "").trim();
97
97
  const prHeadBranch = normalizeBranch(pullRequest?.head?.ref || pullRequest?.headRefName || "");
@@ -111,7 +111,11 @@ export function selectReleaseCandidateRun({ runs = [], pullRequest, workflowName
111
111
  return matchesPr && matchesWorkflow && run.event === "pull_request" && run.status === "completed" && run.conclusion === "success";
112
112
  });
113
113
  candidates.sort((left, right) => Date.parse(right.updated_at || right.created_at || "") - Date.parse(left.updated_at || left.created_at || ""));
114
- return candidates[0];
114
+ return candidates;
115
+ }
116
+
117
+ export function selectReleaseCandidateRun({ runs = [], pullRequest, workflowName = "" }) {
118
+ return selectReleaseCandidateRuns({ runs, pullRequest, workflowName })[0];
115
119
  }
116
120
 
117
121
  function outputPath(filePath) {
@@ -375,24 +379,40 @@ export async function resolveReleaseCandidateArtifacts({
375
379
  fetchImpl,
376
380
  path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${encodeURIComponent(workflowFile)}/runs?event=pull_request&status=success&per_page=100`,
377
381
  });
378
- const run = selectReleaseCandidateRun({
382
+ const candidateRuns = selectReleaseCandidateRuns({
379
383
  runs: Array.isArray(runs.workflow_runs) ? runs.workflow_runs : [],
380
384
  pullRequest,
381
385
  workflowName,
382
386
  });
383
- if (!run) {
387
+ if (!candidateRuns.length) {
384
388
  throw new Error(`no successful ${workflowName} pull_request run found for channel PR #${pullRequest.number}`);
385
389
  }
386
- const artifactResponse = await githubJson({
387
- apiUrl,
388
- token,
389
- fetchImpl,
390
- path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${run.id}/artifacts?per_page=100`,
391
- });
392
- const selected = selectReleaseCandidateArtifacts({
393
- artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
394
- artifactName,
395
- });
390
+ let run;
391
+ let artifactResponse;
392
+ let selected;
393
+ const selectionErrors = [];
394
+ for (const candidateRun of candidateRuns) {
395
+ const candidateArtifactResponse = await githubJson({
396
+ apiUrl,
397
+ token,
398
+ fetchImpl,
399
+ path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${candidateRun.id}/artifacts?per_page=100`,
400
+ });
401
+ try {
402
+ selected = selectReleaseCandidateArtifacts({
403
+ artifacts: Array.isArray(candidateArtifactResponse.artifacts) ? candidateArtifactResponse.artifacts : [],
404
+ artifactName,
405
+ });
406
+ run = candidateRun;
407
+ artifactResponse = candidateArtifactResponse;
408
+ break;
409
+ } catch (error) {
410
+ selectionErrors.push(`run ${candidateRun.id}: ${error.message}`);
411
+ }
412
+ }
413
+ if (!run || !artifactResponse || !selected) {
414
+ throw new Error(`no successful ${workflowName} pull_request run for channel PR #${pullRequest.number} contained release-candidate artifacts: ${selectionErrors.join("; ")}`);
415
+ }
396
416
  const payloadArtifacts = selectPayloadArtifacts({
397
417
  artifacts: Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [],
398
418
  artifactName: selected.prefix,