@kungfu-tech/buildchain 2.14.3 → 2.14.4-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 (37) hide show
  1. package/actions/promote-buildchain-ref/README.md +8 -0
  2. package/bin/buildchain.mjs +4 -0
  3. package/dist/site/buildchain-contract.json +39 -25
  4. package/dist/site/buildchain-site.json +26 -26
  5. package/dist/site/capability-registry.json +1 -1
  6. package/dist/site/controller-registry.json +28 -4
  7. package/dist/site/kfd-claims.json +24 -6
  8. package/dist/site/kfd-upstream-aggregate.json +1 -1
  9. package/dist/site/manual-registry.json +6 -6
  10. package/dist/site/node-api-registry.json +20 -7
  11. package/dist/site/page-registry.json +16 -16
  12. package/dist/site/public-surface-audit.json +11 -5
  13. package/dist/site/publication-registry.json +4 -4
  14. package/dist/site/release-passport-check-manifest.json +1 -0
  15. package/dist/site/release-provenance.json +1 -0
  16. package/dist/site/schemas/release-passport-v1.schema.json +3 -0
  17. package/dist/site/site-manifest.json +10 -10
  18. package/dist/site/workflow-registry.json +9 -3
  19. package/docs/MAP.md +1 -0
  20. package/docs/cli.md +7 -0
  21. package/docs/lifecycle-protocol.md +14 -0
  22. package/docs/release-governance.md +10 -1
  23. package/docs/release-passport.md +21 -0
  24. package/docs/versioning.md +1 -0
  25. package/package.json +2 -1
  26. package/packages/core/README.md +2 -0
  27. package/packages/core/anchored-version-material.js +281 -0
  28. package/packages/core/buildchain-config.js +68 -0
  29. package/packages/core/buildchain-contract.js +4 -0
  30. package/packages/core/controller-evidence.js +30 -4
  31. package/packages/core/index.js +6 -0
  32. package/packages/core/release-passport-contract.js +2 -0
  33. package/packages/core/release-passport.js +186 -1
  34. package/scripts/anchored-version-material.mjs +42 -0
  35. package/scripts/check-inventory.mjs +6 -0
  36. package/scripts/generate-site-bundle.mjs +1 -0
  37. package/scripts/stable-candidate-patrol.mjs +55 -6
@@ -0,0 +1,281 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import {
6
+ discoverConfiguredDerivedVersionMaterial,
7
+ discoverConfiguredVersionStateFiles,
8
+ getLifecycleStage,
9
+ getVersionStrategy,
10
+ loadBuildchainConfig,
11
+ loadConfiguredAnchorManifest,
12
+ runLifecycleStage,
13
+ } from "./buildchain-config.js";
14
+
15
+ export const ANCHORED_VERSION_MATERIAL_CONTRACT =
16
+ "kungfu-buildchain-anchored-version-material/v1";
17
+
18
+ function git(cwd, args, { buffer = false } = {}) {
19
+ return execFileSync("git", args, {
20
+ cwd,
21
+ encoding: buffer ? undefined : "utf8",
22
+ stdio: ["ignore", "pipe", "pipe"],
23
+ });
24
+ }
25
+
26
+ function sha256(content) {
27
+ return `sha256:${crypto.createHash("sha256").update(content).digest("hex")}`;
28
+ }
29
+
30
+ function stableJson(value) {
31
+ if (Array.isArray(value)) {
32
+ return `[${value.map(stableJson).join(",")}]`;
33
+ }
34
+ if (value && typeof value === "object") {
35
+ return `{${Object.keys(value)
36
+ .sort()
37
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
38
+ .join(",")}}`;
39
+ }
40
+ return JSON.stringify(value);
41
+ }
42
+
43
+ function normalizeTargetChannel(targetChannel, targetRef) {
44
+ if (targetChannel) {
45
+ return targetChannel;
46
+ }
47
+ if (String(targetRef || "").startsWith("release/")) {
48
+ return "release";
49
+ }
50
+ if (String(targetRef || "").startsWith("alpha/")) {
51
+ return "alpha";
52
+ }
53
+ return "";
54
+ }
55
+
56
+ function resolveLatestAlphaRef(cwd, targetRef) {
57
+ const match = String(targetRef || "").match(/^release\/v(\d+)\/v\1\.(\d+)$/);
58
+ if (!match) {
59
+ throw new Error(
60
+ `anchored derived material preflight requires release/vN/vN.M target ref, got ${targetRef || "<empty>"}`,
61
+ );
62
+ }
63
+ const prefix = `v${match[1]}.${match[2]}.`;
64
+ const tags = git(cwd, [
65
+ "tag",
66
+ "--list",
67
+ `${prefix}*-alpha.*`,
68
+ "--sort=-version:refname",
69
+ ])
70
+ .trim()
71
+ .split(/\r?\n/)
72
+ .filter(Boolean);
73
+ if (tags.length === 0) {
74
+ throw new Error(
75
+ `anchored derived material preflight found no exact alpha tag matching ${prefix}*-alpha.*`,
76
+ );
77
+ }
78
+ return tags[0];
79
+ }
80
+
81
+ function gitStatus(cwd) {
82
+ return git(cwd, ["status", "--porcelain", "--untracked-files=all"]).trim();
83
+ }
84
+
85
+ function fileAtRef(cwd, ref, filePath) {
86
+ try {
87
+ const content = git(cwd, ["show", `${ref}:${filePath}`], { buffer: true });
88
+ return {
89
+ present: true,
90
+ sha256: sha256(content),
91
+ bytes: content.length,
92
+ };
93
+ } catch {
94
+ return {
95
+ present: false,
96
+ sha256: "",
97
+ bytes: 0,
98
+ };
99
+ }
100
+ }
101
+
102
+ function pathEvidence(cwd, ref, paths) {
103
+ return paths.map((filePath) => ({
104
+ path: filePath,
105
+ ...fileAtRef(cwd, ref, filePath),
106
+ }));
107
+ }
108
+
109
+ function readPackageVersion(cwd) {
110
+ const packagePath = path.join(cwd, "package.json");
111
+ if (!fs.existsSync(packagePath)) {
112
+ return "";
113
+ }
114
+ const value = JSON.parse(fs.readFileSync(packagePath, "utf8"));
115
+ return typeof value.version === "string" ? value.version : "";
116
+ }
117
+
118
+ export function createAnchoredVersionMaterialEvidence({
119
+ cwd = process.cwd(),
120
+ targetChannel = "",
121
+ targetRef = "",
122
+ alphaRef = "",
123
+ releaseRef = "HEAD",
124
+ runLifecycle = true,
125
+ } = {}) {
126
+ const resolvedCwd = path.resolve(cwd);
127
+ const loadedConfig = loadBuildchainConfig(resolvedCwd);
128
+ const versionStrategy = getVersionStrategy(loadedConfig);
129
+ const derivedFiles = loadedConfig
130
+ ? discoverConfiguredDerivedVersionMaterial(resolvedCwd, loadedConfig)
131
+ : [];
132
+ const channel = normalizeTargetChannel(targetChannel, targetRef);
133
+ const base = {
134
+ schemaVersion: 1,
135
+ contract: ANCHORED_VERSION_MATERIAL_CONTRACT,
136
+ applicable: false,
137
+ targetChannel: channel,
138
+ targetRef,
139
+ versionStrategy,
140
+ };
141
+ if (
142
+ !loadedConfig ||
143
+ versionStrategy.strategy !== "anchored" ||
144
+ versionStrategy.next !== "manual" ||
145
+ derivedFiles.length === 0
146
+ ) {
147
+ return {
148
+ ...base,
149
+ reason: "anchored-derived-version-material-not-configured",
150
+ };
151
+ }
152
+ if (channel !== "release") {
153
+ return {
154
+ ...base,
155
+ reason: "target-channel-is-not-release",
156
+ };
157
+ }
158
+
159
+ const initialStatus = gitStatus(resolvedCwd);
160
+ if (initialStatus) {
161
+ throw new Error(
162
+ `anchored derived material preflight requires a clean checkout: ${initialStatus}`,
163
+ );
164
+ }
165
+
166
+ const anchorManifest = loadConfiguredAnchorManifest(resolvedCwd, loadedConfig);
167
+ const version = anchorManifest?.fields?.npmVersion || readPackageVersion(resolvedCwd);
168
+ const lifecycleEnv = {
169
+ BUILDCHAIN_VERSION: version,
170
+ BUILDCHAIN_VERSION_STRATEGY: versionStrategy.strategy,
171
+ BUILDCHAIN_VERSION_NEXT: versionStrategy.next,
172
+ ...(anchorManifest
173
+ ? {
174
+ BUILDCHAIN_ANCHOR_MANIFEST: anchorManifest.path,
175
+ BUILDCHAIN_ANCHOR_MANIFEST_JSON: JSON.stringify(anchorManifest.fields),
176
+ }
177
+ : {}),
178
+ };
179
+ if (runLifecycle) {
180
+ const installStage = getLifecycleStage(loadedConfig, "install");
181
+ if (installStage) {
182
+ runLifecycleStage({
183
+ cwd: resolvedCwd,
184
+ loadedConfig,
185
+ name: "install",
186
+ stage: installStage,
187
+ env: lifecycleEnv,
188
+ });
189
+ }
190
+ const versionStateStage =
191
+ getLifecycleStage(loadedConfig, "version-state") ||
192
+ getLifecycleStage(loadedConfig, "version_state");
193
+ runLifecycleStage({
194
+ cwd: resolvedCwd,
195
+ loadedConfig,
196
+ name: "version-state",
197
+ stage: versionStateStage,
198
+ env: lifecycleEnv,
199
+ });
200
+ runLifecycleStage({
201
+ cwd: resolvedCwd,
202
+ loadedConfig,
203
+ name: "verify",
204
+ stage: getLifecycleStage(loadedConfig, "verify"),
205
+ env: lifecycleEnv,
206
+ });
207
+ }
208
+ const derivedStatus = gitStatus(resolvedCwd);
209
+ if (derivedStatus) {
210
+ throw new Error(
211
+ `anchored derived version material is stale or hand-edited; derivation changed the committed tree: ${derivedStatus}`,
212
+ );
213
+ }
214
+
215
+ const resolvedAlphaRef = alphaRef || resolveLatestAlphaRef(resolvedCwd, targetRef);
216
+ const versionFiles = discoverConfiguredVersionStateFiles(resolvedCwd, loadedConfig)
217
+ .map((file) => file.path);
218
+ const derivedPaths = derivedFiles.map((file) => file.path);
219
+ const allowedPaths = [
220
+ ...new Set([
221
+ ...versionFiles,
222
+ ...(anchorManifest?.path ? [anchorManifest.path] : []),
223
+ ...derivedPaths,
224
+ ]),
225
+ ].sort();
226
+ const changedPaths = git(resolvedCwd, [
227
+ "diff",
228
+ "--name-only",
229
+ resolvedAlphaRef,
230
+ releaseRef,
231
+ "--",
232
+ ])
233
+ .trim()
234
+ .split(/\r?\n/)
235
+ .filter(Boolean)
236
+ .sort();
237
+ const allowed = new Set(allowedPaths);
238
+ const unexpectedPaths = changedPaths.filter((filePath) => !allowed.has(filePath));
239
+ if (unexpectedPaths.length > 0) {
240
+ throw new Error(
241
+ `anchored release tree changed undeclared paths: ${unexpectedPaths.join(", ")}`,
242
+ );
243
+ }
244
+
245
+ const alphaCommit = git(resolvedCwd, ["rev-parse", resolvedAlphaRef]).trim();
246
+ const alphaTree = git(resolvedCwd, ["rev-parse", `${resolvedAlphaRef}^{tree}`]).trim();
247
+ const releaseCommit = git(resolvedCwd, ["rev-parse", releaseRef]).trim();
248
+ const releaseTree = git(resolvedCwd, ["rev-parse", `${releaseRef}^{tree}`]).trim();
249
+ const evidence = {
250
+ ...base,
251
+ applicable: true,
252
+ reason: "verified",
253
+ version,
254
+ alpha: {
255
+ ref: resolvedAlphaRef,
256
+ commit: alphaCommit,
257
+ tree: alphaTree,
258
+ material: pathEvidence(resolvedCwd, resolvedAlphaRef, allowedPaths),
259
+ },
260
+ release: {
261
+ ref: releaseRef,
262
+ commit: releaseCommit,
263
+ tree: releaseTree,
264
+ material: pathEvidence(resolvedCwd, releaseRef, allowedPaths),
265
+ },
266
+ allowedPaths,
267
+ versionFiles,
268
+ manifest: anchorManifest?.path || "",
269
+ derivedPaths,
270
+ changedPaths,
271
+ lifecycle: {
272
+ install: Boolean(getLifecycleStage(loadedConfig, "install")),
273
+ derivation: "version-state",
274
+ verification: "verify",
275
+ },
276
+ };
277
+ return {
278
+ ...evidence,
279
+ digest: sha256(stableJson(evidence)),
280
+ };
281
+ }
@@ -220,6 +220,7 @@ export function normalizeBuildchainConfig(config) {
220
220
  }
221
221
  validateWebSurfaceConfig(normalized);
222
222
  validateInfraContractConfig(normalized);
223
+ validateAnchoredDerivedVersionMaterialConfig(normalized);
223
224
  return normalized;
224
225
  }
225
226
 
@@ -1087,6 +1088,42 @@ function validateInfraContractConfig(config) {
1087
1088
  }
1088
1089
  }
1089
1090
 
1091
+ function validateAnchoredDerivedVersionMaterialConfig(config) {
1092
+ const derivedFiles = config.version?.derivedFiles || [];
1093
+ if (derivedFiles.length === 0) {
1094
+ return;
1095
+ }
1096
+ if (config.version.strategy !== "anchored" || config.version.next !== "manual") {
1097
+ throw new Error(
1098
+ "version.derived_files requires version.strategy = \"anchored\" and version.next = \"manual\"",
1099
+ );
1100
+ }
1101
+ const versionState =
1102
+ config.lifecycle?.["version-state"] ||
1103
+ config.lifecycle?.version_state;
1104
+ if (!versionState) {
1105
+ throw new Error(
1106
+ "version.derived_files requires lifecycle.version-state to regenerate declared material",
1107
+ );
1108
+ }
1109
+ if (!config.lifecycle?.verify) {
1110
+ throw new Error(
1111
+ "version.derived_files requires lifecycle.verify to validate regenerated material",
1112
+ );
1113
+ }
1114
+ const versionPaths = new Set((config.version.files || []).map((file) => file.path));
1115
+ if (config.version.manifest) {
1116
+ versionPaths.add(config.version.manifest);
1117
+ }
1118
+ for (const filePath of derivedFiles) {
1119
+ if (versionPaths.has(filePath)) {
1120
+ throw new Error(
1121
+ `version.derived_files must not repeat version.files or version.manifest: ${filePath}`,
1122
+ );
1123
+ }
1124
+ }
1125
+ }
1126
+
1090
1127
  function validateWebSurfaceSurfaces(config) {
1091
1128
  const surfaces = config.surfaces || {};
1092
1129
  for (const [name, surface] of Object.entries(surfaces)) {
@@ -1129,6 +1166,13 @@ function normalizeVersionSection(version) {
1129
1166
  if (strategy === "anchored" && next !== "manual") {
1130
1167
  throw new Error('version.strategy = "anchored" requires version.next = "manual"');
1131
1168
  }
1169
+ const derivedFiles = normalizeStringArray(
1170
+ version.derived_files,
1171
+ "version.derived_files",
1172
+ ).map((filePath) => posixPath(filePath));
1173
+ if (new Set(derivedFiles).size !== derivedFiles.length) {
1174
+ throw new Error("version.derived_files must not contain duplicate paths");
1175
+ }
1132
1176
  return {
1133
1177
  required: version.required === undefined ? false : Boolean(version.required),
1134
1178
  strategy,
@@ -1137,6 +1181,7 @@ function normalizeVersionSection(version) {
1137
1181
  ? undefined
1138
1182
  : posixPath(assertString(version.manifest, "version.manifest")),
1139
1183
  files: files.map((file, index) => normalizeVersionFile(file, index)),
1184
+ derivedFiles,
1140
1185
  };
1141
1186
  }
1142
1187
 
@@ -1319,6 +1364,27 @@ export function discoverConfiguredVersionStateFiles(cwd = process.cwd(), loadedC
1319
1364
  return files;
1320
1365
  }
1321
1366
 
1367
+ export function discoverConfiguredDerivedVersionMaterial(
1368
+ cwd = process.cwd(),
1369
+ loadedConfig = loadBuildchainConfig(cwd),
1370
+ ) {
1371
+ const configured = loadedConfig?.config?.version?.derivedFiles || [];
1372
+ return configured.map((filePath) => {
1373
+ const absolutePath = path.join(cwd, filePath);
1374
+ if (!fs.existsSync(absolutePath)) {
1375
+ throw new Error(`Configured derived version material does not exist: ${filePath}`);
1376
+ }
1377
+ if (!fs.statSync(absolutePath).isFile()) {
1378
+ throw new Error(`Configured derived version material must be a file: ${filePath}`);
1379
+ }
1380
+ return {
1381
+ path: filePath,
1382
+ filePath: absolutePath,
1383
+ content: fs.readFileSync(absolutePath),
1384
+ };
1385
+ });
1386
+ }
1387
+
1322
1388
  function summarizeManifestFields(content) {
1323
1389
  assertPlainObject(content, "version.manifest content");
1324
1390
  return Object.fromEntries(
@@ -1499,6 +1565,8 @@ export function validateBuildchainConfig(
1499
1565
  key: file.key,
1500
1566
  pattern: file.pattern?.source,
1501
1567
  })),
1568
+ derivedVersionMaterial: discoverConfiguredDerivedVersionMaterial(cwd, loadedConfig)
1569
+ .map((file) => ({ path: file.path })),
1502
1570
  lifecycleStages,
1503
1571
  publish: loadedConfig.config.publish,
1504
1572
  release: loadedConfig.config.release,
@@ -193,6 +193,8 @@ export function createBuildchainContractWorld({
193
193
  "release-passport-kfd-3-prebuild-witness-jsons",
194
194
  "release-passport-kfd-3-artifact-witness-jsons",
195
195
  "release-passport-kfd-3-artifact-verify-command",
196
+ "release-passport-invariant-passport-jsons",
197
+ "release-passport-invariant-passport-command",
196
198
  "buildchain-contract-lock-path",
197
199
  "buildchain-contract-drift-issue-mode",
198
200
  "github-release",
@@ -319,6 +321,8 @@ export function createBuildchainContractWorld({
319
321
  "release-passport-kfd-3-prebuild-witness-jsons",
320
322
  "release-passport-kfd-3-artifact-witness-jsons",
321
323
  "release-passport-kfd-3-artifact-verify-command",
324
+ "release-passport-invariant-passport-jsons",
325
+ "release-passport-invariant-passport-command",
322
326
  "github-release",
323
327
  "github-release-title",
324
328
  "github-release-notes",
@@ -23,8 +23,20 @@ const CONTROLLER_SPECS = [
23
23
  workflowId: ".build",
24
24
  version: 1,
25
25
  capabilities: ["source-lock", "lifecycle-build", "lifecycle-verify", "artifact-admission"],
26
- stages: ["resolve-runtime", "resolve-source", "build", "verify", "aggregate"],
27
- evidence: ["platform-manifests", "build-summary", "controller-receipt"],
26
+ stages: [
27
+ "resolve-runtime",
28
+ "resolve-source",
29
+ "anchored-release-preflight",
30
+ "build",
31
+ "verify",
32
+ "aggregate",
33
+ ],
34
+ optionalStages: ["anchored-release-preflight"],
35
+ evidence: [
36
+ "platform-manifests",
37
+ "build-summary",
38
+ "controller-receipt",
39
+ ],
28
40
  },
29
41
  {
30
42
  id: "build-channel-router",
@@ -83,8 +95,22 @@ const CONTROLLER_SPECS = [
83
95
  workflowId: "release-propagation",
84
96
  version: 1,
85
97
  capabilities: ["release-propagation-plan", "downstream-lock", "pull-request-handoff"],
86
- stages: ["resolve-runtime", "plan", "write-lock", "open-pr", "aggregate"],
87
- optionalStages: ["open-pr"],
98
+ stages: [
99
+ "resolve-runtime",
100
+ "plan",
101
+ "write-lock",
102
+ "prepare-consumer",
103
+ "refresh-badges",
104
+ "verify-consumer",
105
+ "open-pr",
106
+ "aggregate",
107
+ ],
108
+ optionalStages: [
109
+ "prepare-consumer",
110
+ "refresh-badges",
111
+ "verify-consumer",
112
+ "open-pr",
113
+ ],
88
114
  evidence: ["propagation-plan", "propagation-lock", "controller-receipt"],
89
115
  },
90
116
  ];
@@ -1,4 +1,5 @@
1
1
  export {
2
+ discoverConfiguredDerivedVersionMaterial,
2
3
  discoverConfiguredVersionStateFiles,
3
4
  getLifecycleStage,
4
5
  getNativeDiagnosticsProfile,
@@ -14,6 +15,11 @@ export {
14
15
  validateBuildchainConfig,
15
16
  } from "./buildchain-config.js";
16
17
 
18
+ export {
19
+ ANCHORED_VERSION_MATERIAL_CONTRACT,
20
+ createAnchoredVersionMaterialEvidence,
21
+ } from "./anchored-version-material.js";
22
+
17
23
  export {
18
24
  STABLE_CANDIDATE_LEDGER_CONTRACT,
19
25
  STABLE_CANDIDATE_STATES,
@@ -87,6 +87,7 @@ export const RELEASE_PASSPORT_SCHEMA = {
87
87
  surfaceImpacts: { type: "array", items: OBJECT },
88
88
  packageSet: OBJECT,
89
89
  anchorManifest: OBJECT,
90
+ versionMaterial: OBJECT,
90
91
  trustedPublishing: OBJECT,
91
92
  transaction: OBJECT,
92
93
  surfaceTimestampPolicy: OBJECT,
@@ -111,6 +112,7 @@ const BUILDCHAIN_AGGREGATION_FIELDS = [
111
112
  "surfaceImpacts",
112
113
  "packageSet",
113
114
  "anchorManifest",
115
+ "versionMaterial",
114
116
  "trustedPublishing",
115
117
  "transaction",
116
118
  "surfaceTimestampPolicy",