@kungfu-tech/buildchain 2.5.5-alpha.3 → 2.5.5-alpha.5

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.
package/docs/cli.md CHANGED
@@ -276,10 +276,14 @@ platform packages with version, dist-tag, and digest evidence. Verification
276
276
  fails closed if supplied sections are internally incomplete or point to
277
277
  artifacts without matching evidence.
278
278
 
279
- `--impact-json` supplies the surface-aware impact ledger. When it includes
280
- `surfaceImpacts[]`, the verifier requires `versionImpact.final` to match the
281
- highest declared surface impact and copies `versionImpact` plus
282
- `surfaceImpacts` into `buildchain.release.json`. This lets
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
283
287
  `buildchain explain release --for agent --json` state why a release is patch,
284
288
  minor, or major instead of relying on file-path memory.
285
289
 
@@ -106,11 +106,15 @@ dist-tag, registry, role, platform, and digest, so agents do not need to stitch
106
106
  npm facts back together from the lower-level evidence files.
107
107
  `buildSummary`, `platformArtifactManifests`, and `distTagPromotion` preserve the
108
108
  build and npm dist-tag evidence chain in the same passport.
109
- `impact.json` can be supplied with `--impact-json`; when it contains
110
- `surfaceImpacts[]`, verification fails closed unless `versionImpact.final`
111
- matches the highest surface impact. For example, KFD-2 content can remain patch
112
- while an additive `registry.kind` field on the machine-consumed KFD registry
113
- schema records a minor `kfd-registry-schema` surface impact.
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.
114
118
 
115
119
  Verify a release passport:
116
120
 
@@ -46,6 +46,11 @@ The release passport records this as `surfaceImpacts[]` plus
46
46
  so an agent cannot silently label a release patch when one machine surface needs
47
47
  minor review.
48
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
+
49
54
  Example: a KFD document such as KFD-2 is content and remains patch, but adding a
50
55
  `kind` field to the machine-consumed KFD `registry.json` is an additive change
51
56
  to the `kfd-registry-schema` surface and therefore requires minor-impact
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.5.5-alpha.3",
3
+ "version": "2.5.5-alpha.5",
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",
@@ -59,6 +59,43 @@ function highestImpactLevel(levels = []) {
59
59
  return highest;
60
60
  }
61
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
+
62
99
  function stableJson(value) {
63
100
  if (Array.isArray(value)) {
64
101
  return `[${value.map(stableJson).join(",")}]`;
@@ -1132,6 +1169,10 @@ export function createReleaseCheckReport({
1132
1169
  const passportSurfaceImpacts = Array.isArray(passport?.surfaceImpacts) ? passport.surfaceImpacts : [];
1133
1170
  const declaredFinalImpact = normalizeImpactLevel(impact?.versionImpact?.final || impact?.classification);
1134
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
+ }
1135
1176
  if (surfaceImpacts.length > 0) {
1136
1177
  for (const [index, entry] of surfaceImpacts.entries()) {
1137
1178
  if (!entry.id) {
@@ -1167,6 +1208,7 @@ export function createReleaseCheckReport({
1167
1208
  checkedAt,
1168
1209
  ok,
1169
1210
  trust: ok ? "pass" : "fail",
1211
+ surfaceImpactRequirement: requiredSurfaceImpacts,
1170
1212
  completeness: {
1171
1213
  artifactCount: artifacts.length,
1172
1214
  evidenceArtifactCount: evidenceArtifacts.length,
@@ -1182,6 +1224,7 @@ export function createReleaseCheckReport({
1182
1224
  impactPresent: Boolean(impact),
1183
1225
  agentIndexPresent: Boolean(agentIndex),
1184
1226
  productMechanismPresent: Boolean(productMechanism),
1227
+ surfaceImpactsRequired: requiredSurfaceImpacts.required,
1185
1228
  surfaceImpactCount: surfaceImpacts.length,
1186
1229
  versionImpact: impact?.versionImpact?.final || "",
1187
1230
  },
@@ -1278,6 +1321,7 @@ export async function explainReleasePassport({ passportLocation, forAudience = "
1278
1321
  impact: {
1279
1322
  versionImpact: passport.versionImpact || {},
1280
1323
  surfaceImpacts: Array.isArray(passport.surfaceImpacts) ? passport.surfaceImpacts : [],
1324
+ surfaceImpactRequirement: report.surfaceImpactRequirement,
1281
1325
  breaking: Boolean(passport.versionImpact?.final === "major"),
1282
1326
  migrationRequired: Boolean(passport.versionImpact?.final === "major"),
1283
1327
  summary: passport.versionImpact?.rationale || "",
@@ -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,