@kungfu-tech/buildchain 4.0.0 → 4.0.1-alpha.2

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 (69) hide show
  1. package/AGENTS.md +13 -5
  2. package/actions/promote-buildchain-ref/README.md +11 -6
  3. package/architecture/internal-capabilities.json +11 -2
  4. package/architecture/maintainability-policy.json +149 -17
  5. package/architecture/v4-floating-consumer-policy.json +75 -0
  6. package/architecture/v4-floating-consumer-policy.md +32 -0
  7. package/architecture/v4-runtime-ref-resume-authority.json +64 -0
  8. package/architecture/v4-stage-capsule-qualification.json +2 -2
  9. package/bin/internal/trust-release-release-handlers.mjs +5 -0
  10. package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +53 -0
  11. package/contracts/fixtures/v4-runtime-ref-resume-authority-v1/scenario.json +15 -0
  12. package/contracts/v4-floating-consumer-policy-receipt-v1.schema.json +105 -0
  13. package/contracts/v4-runtime-ref-resume-authority-v1.schema.json +235 -0
  14. package/dist/site/buildchain-contract.json +101 -31
  15. package/dist/site/buildchain-site.json +73 -27
  16. package/dist/site/capability-registry.json +3 -3
  17. package/dist/site/controller-registry.json +62 -6
  18. package/dist/site/kfd-claims.json +68 -13
  19. package/dist/site/kfd-upstream-aggregate.json +1 -1
  20. package/dist/site/manual-registry.json +6 -6
  21. package/dist/site/node-api-registry.json +799 -127
  22. package/dist/site/page-registry.json +63 -17
  23. package/dist/site/public-surface-audit.json +49 -17
  24. package/dist/site/publication-registry.json +4 -4
  25. package/dist/site/release-provenance.json +2 -0
  26. package/dist/site/site-manifest.json +10 -10
  27. package/dist/site/workflow-registry.json +46 -18
  28. package/docs/MAP.md +2 -0
  29. package/docs/dev-delivery-warrant.md +19 -1
  30. package/docs/lifecycle-protocol.md +41 -0
  31. package/docs/node-api-reference.md +207 -134
  32. package/docs/reusable-build-surface.md +41 -0
  33. package/docs/runtime-train-validation.md +10 -0
  34. package/docs/v4-runtime-ref-resume-authority.md +69 -0
  35. package/docs/versioning.md +1 -0
  36. package/package.json +6 -4
  37. package/packages/core/artifact-signing.js +61 -0
  38. package/packages/core/buildchain-config.js +66 -1
  39. package/packages/core/dev-delivery-proof.js +37 -3
  40. package/packages/core/dev-delivery-warrant.js +3 -3
  41. package/packages/core/index.js +2 -0
  42. package/packages/core/publication-authority.js +5 -5
  43. package/packages/core/release-candidate.js +79 -19
  44. package/packages/core/release-passport.js +239 -33
  45. package/packages/core/v4-floating-consumer-evidence.js +324 -0
  46. package/packages/core/v4-floating-consumer-policy.js +446 -0
  47. package/packages/core/v4-floating-consumer-release-passport.js +126 -0
  48. package/packages/core/v4-runtime-ref-resume-authority.js +625 -0
  49. package/packages/core/v4-runtime-selector-persistence.js +228 -0
  50. package/packages/core/workflow-yaml-contract.js +48 -0
  51. package/scripts/audit-publication-control-plane.mjs +31 -31
  52. package/scripts/check-inventory.mjs +1 -1
  53. package/scripts/check-v4-floating-consumer-policy-contract.mjs +198 -0
  54. package/scripts/check-v4-public-dogfood-contract.mjs +6 -11
  55. package/scripts/dev-delivery-source-proof-reuse.mjs +631 -0
  56. package/scripts/dev-pr-auto-merge.mjs +37 -48
  57. package/scripts/dev-pr-delivery-warrant.mjs +205 -2
  58. package/scripts/dev-pr-prequeue-guard.mjs +399 -0
  59. package/scripts/ensure-github-release.mjs +3 -3
  60. package/scripts/generate-channel-build-workflow.mjs +3 -0
  61. package/scripts/generate-channel-promotion-workflow.mjs +112 -12
  62. package/scripts/generate-release-candidate-passport.mjs +41 -33
  63. package/scripts/init-repo.mjs +5 -1
  64. package/scripts/inspect-artifact-signing-requests.mjs +6 -0
  65. package/scripts/npm-publish-transaction.mjs +101 -89
  66. package/scripts/resume-from-candidate-run.mjs +11 -14
  67. package/scripts/seal-artifact-signing-requests.mjs +6 -0
  68. package/scripts/site-capability-metadata.mjs +2 -0
  69. package/scripts/v4-consumer-policy.mjs +231 -0
@@ -0,0 +1,228 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const V4_RUNTIME_PERSISTENCE_SCAN_CONTRACT =
6
+ "kungfu-buildchain-v4-runtime-persistence-scan/v1";
7
+
8
+ const EXACT_SHA = /^[0-9a-f]{40}$/u;
9
+ const TRAIN_V4_REF = /^train\/v4\/v4\.\d+\/[A-Za-z0-9._/-]+$/u;
10
+ const AUTHORITY_V4_REF = /^authority\/v4\/v4\.\d+\/[A-Za-z0-9._/-]+$/u;
11
+ const RUNTIME_SELECTOR =
12
+ /(?:buildchain[-_. ]?(?:ref|runtime)|runtime[-_. ]?sha|resume[-_. ]?buildchain)/iu;
13
+ const EXACT_SHA_LITERAL = /\b[0-9a-f]{40}\b/giu;
14
+ const EXTERNAL_AUTHORITY = Object.freeze([
15
+ ["oidc", /(?:id-token\s*:\s*write|oidc)/iu],
16
+ ["iam", /(?:role-to-assume|aws[-_. ]?(?:role|iam)|\biam\b)/iu],
17
+ ["repository-variable", /\$\{\{\s*vars\./u],
18
+ ["secret", /\$\{\{\s*secrets\./u],
19
+ ["environment", /\$\{\{\s*env\./u],
20
+ ]);
21
+
22
+ function stableJson(value) {
23
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
24
+ if (value && typeof value === "object") {
25
+ return `{${Object.keys(value)
26
+ .sort()
27
+ .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
28
+ .join(",")}}`;
29
+ }
30
+ return JSON.stringify(value);
31
+ }
32
+
33
+ function documentRoot(value) {
34
+ return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
35
+ }
36
+
37
+ function normalizeRef(value) {
38
+ return String(value || "")
39
+ .trim()
40
+ .replace(/^refs\/(?:heads|tags)\//u, "");
41
+ }
42
+
43
+ function transientSelector(value) {
44
+ const normalized = normalizeRef(value);
45
+ return (
46
+ EXACT_SHA.test(normalized.toLowerCase()) ||
47
+ TRAIN_V4_REF.test(normalized) ||
48
+ AUTHORITY_V4_REF.test(normalized) ||
49
+ /\$\{\{\s*(?:vars|secrets|env)\./u.test(value)
50
+ );
51
+ }
52
+
53
+ function listFiles(rootPath, relative, output) {
54
+ const directory = path.join(rootPath, relative);
55
+ if (!fs.existsSync(directory)) return;
56
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
57
+ const child = path.join(relative, entry.name);
58
+ if (entry.isDirectory()) listFiles(rootPath, child, output);
59
+ else if (entry.isFile() && /\.(?:json|toml|ya?ml)$/u.test(entry.name)) {
60
+ output.push(child.split(path.sep).join("/"));
61
+ }
62
+ }
63
+ }
64
+
65
+ function inputDefaultFailures(lines, sourcePath) {
66
+ const failures = [];
67
+ let current;
68
+ for (const [index, line] of lines.entries()) {
69
+ const key = line.match(/^(\s*)([a-z0-9-]+):\s*(?:#.*)?$/u);
70
+ if (key) {
71
+ const name = key[2];
72
+ if (RUNTIME_SELECTOR.test(name))
73
+ current = { indent: key[1].length, name };
74
+ else if (current && key[1].length <= current.indent) current = undefined;
75
+ }
76
+ if (!current) continue;
77
+ const defaultMatch = line.match(/^\s*default:\s*(.*?)\s*(?:#.*)?$/u);
78
+ if (!defaultMatch) continue;
79
+ const value = defaultMatch[1]
80
+ .replace(/^(?:["'])(.*)(?:["'])$/u, "$1")
81
+ .trim();
82
+ if (transientSelector(value)) {
83
+ failures.push({
84
+ code: "persistent-runtime-default",
85
+ path: sourcePath,
86
+ line: index + 1,
87
+ selector: current.name,
88
+ });
89
+ }
90
+ }
91
+ return failures;
92
+ }
93
+
94
+ function jsonSelectorFailures(value, sourcePath, trail = []) {
95
+ if (Array.isArray(value)) {
96
+ return value.flatMap((entry, index) =>
97
+ jsonSelectorFailures(entry, sourcePath, [...trail, index]),
98
+ );
99
+ }
100
+ if (!value || typeof value !== "object") return [];
101
+ return Object.entries(value).flatMap(([key, entry]) => {
102
+ const entryTrail = [...trail, key];
103
+ const selected =
104
+ (RUNTIME_SELECTOR.test(key) ||
105
+ trail.some((part) => RUNTIME_SELECTOR.test(String(part)))) &&
106
+ typeof entry === "string" &&
107
+ transientSelector(entry)
108
+ ? [
109
+ {
110
+ code: "persistent-runtime-json-value",
111
+ path: sourcePath,
112
+ jsonPointer: `/${entryTrail.map((part) => String(part).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`,
113
+ },
114
+ ]
115
+ : [];
116
+ return [
117
+ ...selected,
118
+ ...jsonSelectorFailures(entry, sourcePath, entryTrail),
119
+ ];
120
+ });
121
+ }
122
+
123
+ function inspectSource({
124
+ resolvedRoot,
125
+ sourcePath,
126
+ files,
127
+ failures,
128
+ authorityUsage,
129
+ }) {
130
+ const file = path.resolve(resolvedRoot, sourcePath);
131
+ if (!file.startsWith(`${resolvedRoot}${path.sep}`) || !fs.existsSync(file)) {
132
+ throw new Error(
133
+ `runtime persistence scan path is missing or escapes root: ${sourcePath}`,
134
+ );
135
+ }
136
+ const text = fs.readFileSync(file, "utf8");
137
+ const lines = text.split(/\r?\n/u);
138
+ files.push({ path: sourcePath, digest: documentRoot(text) });
139
+ failures.push(...inputDefaultFailures(lines, sourcePath));
140
+ if (sourcePath.endsWith(".json")) {
141
+ try {
142
+ failures.push(...jsonSelectorFailures(JSON.parse(text), sourcePath));
143
+ } catch (error) {
144
+ failures.push({
145
+ code: "runtime-selector-json-invalid",
146
+ path: sourcePath,
147
+ errorRoot: documentRoot(String(error?.message || error)),
148
+ });
149
+ }
150
+ }
151
+ for (const [index, line] of lines.entries()) {
152
+ if (RUNTIME_SELECTOR.test(line)) {
153
+ for (const match of line.matchAll(EXACT_SHA_LITERAL)) {
154
+ failures.push({
155
+ code: "persistent-runtime-exact-sha",
156
+ path: sourcePath,
157
+ line: index + 1,
158
+ selectorRoot: documentRoot(match[0].toLowerCase()),
159
+ });
160
+ }
161
+ if (/\$\{\{\s*(?:vars|secrets|env)\./u.test(line)) {
162
+ failures.push({
163
+ code: "persistent-runtime-external-indirection",
164
+ path: sourcePath,
165
+ line: index + 1,
166
+ });
167
+ }
168
+ }
169
+ for (const [authorityClass, pattern] of EXTERNAL_AUTHORITY) {
170
+ if (pattern.test(line)) {
171
+ authorityUsage.push({
172
+ class: authorityClass,
173
+ path: sourcePath,
174
+ line: index + 1,
175
+ });
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ export function scanV4RuntimeSelectorPersistence({
182
+ root: callerRoot = process.cwd(),
183
+ paths = undefined,
184
+ } = {}) {
185
+ const resolvedRoot = path.resolve(callerRoot);
186
+ const sourcePaths = [];
187
+ if (paths) sourcePaths.push(...paths);
188
+ else {
189
+ for (const relative of [
190
+ ".github/workflows",
191
+ ".github/actions",
192
+ ".buildchain",
193
+ ]) {
194
+ listFiles(resolvedRoot, relative, sourcePaths);
195
+ }
196
+ }
197
+ const excluded = new Set([
198
+ ".buildchain/contract-lock.json",
199
+ ".buildchain/alpha-contract-lock.json",
200
+ ]);
201
+ const files = [];
202
+ const failures = [];
203
+ const authorityUsage = [];
204
+ for (const sourcePath of [...new Set(sourcePaths)].sort()) {
205
+ if (!excluded.has(sourcePath)) {
206
+ inspectSource({
207
+ resolvedRoot,
208
+ sourcePath,
209
+ files,
210
+ failures,
211
+ authorityUsage,
212
+ });
213
+ }
214
+ }
215
+ const payload = {
216
+ schemaVersion: 1,
217
+ contract: V4_RUNTIME_PERSISTENCE_SCAN_CONTRACT,
218
+ status: failures.length === 0 ? "passed" : "rejected",
219
+ files,
220
+ authorityUsage: authorityUsage.sort((left, right) =>
221
+ stableJson(left).localeCompare(stableJson(right)),
222
+ ),
223
+ failures: failures.sort((left, right) =>
224
+ stableJson(left).localeCompare(stableJson(right)),
225
+ ),
226
+ };
227
+ return { ...payload, root: documentRoot(payload) };
228
+ }
@@ -263,6 +263,54 @@ export function parseWorkflowCallJobs(text) {
263
263
  .filter((job) => job.uses);
264
264
  }
265
265
 
266
+ function blockScalarIndent(lines, lineIndex, parentIndent) {
267
+ for (let index = lineIndex + 1; index < lines.length; index += 1) {
268
+ if (!lines[index].trim()) continue;
269
+ const indent = indentation(lines[index]);
270
+ return indent > parentIndent ? indent : null;
271
+ }
272
+ return null;
273
+ }
274
+
275
+ /**
276
+ * Enumerate semantic `uses` mapping nodes without mistaking shell/heredoc text
277
+ * inside YAML block scalars for workflow syntax. The returned scalar retains
278
+ * expression-vs-literal classification used by the reusable-call contract.
279
+ */
280
+ export function parseYamlUses(text) {
281
+ const lines = String(text || "").split(/\r?\n/);
282
+ const uses = [];
283
+ let block = null;
284
+ for (let index = 0; index < lines.length; index += 1) {
285
+ const line = lines[index];
286
+ const indent = indentation(line);
287
+ if (block) {
288
+ if (!line.trim()) continue;
289
+ if (indent >= block.contentIndent) continue;
290
+ block = null;
291
+ }
292
+ const mapping = line.match(
293
+ /^(\s*)(?:-\s+)?([A-Za-z0-9_.-]+):(?:\s*(.*))?$/,
294
+ );
295
+ if (!mapping) continue;
296
+ const value = stripComment(mapping[3] || "");
297
+ if (/^[>|][+-]?$/.test(value)) {
298
+ const contentIndent = blockScalarIndent(lines, index, indent);
299
+ if (contentIndent !== null) block = { contentIndent };
300
+ continue;
301
+ }
302
+ if (mapping[2] !== "uses") continue;
303
+ uses.push({
304
+ line: index + 1,
305
+ column: mapping[1].length + 1,
306
+ raw: value,
307
+ scalar: scalar(value),
308
+ value: unquote(value),
309
+ });
310
+ }
311
+ return uses;
312
+ }
313
+
266
314
  export function parseWorkflowDocument(text) {
267
315
  return {
268
316
  triggers: parseTriggers(String(text || "").split(/\r?\n/)),
@@ -16,12 +16,21 @@ function flag(name, fallback = "") {
16
16
  return index === -1 ? fallback : String(process.argv[index + 1] || "");
17
17
  }
18
18
 
19
- function commandJson(command, args, label) {
20
- const result = spawnSyncCommand(command, args, {
19
+ function commandJson(command, args, label, { publicReadFallback = false } = {}) {
20
+ const options = {
21
21
  encoding: "utf8",
22
22
  timeout: 60_000,
23
23
  maxBuffer: GITHUB_JSON_MAX_BUFFER,
24
- });
24
+ };
25
+ let result = spawnSyncCommand(command, args, options);
26
+ const fallbackToken = String(process.env.BUILDCHAIN_GITHUB_PUBLIC_READ_TOKEN || "");
27
+ const primaryToken = String(process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "");
28
+ if (result.status !== 0 && publicReadFallback && fallbackToken && fallbackToken !== primaryToken) {
29
+ result = spawnSyncCommand(command, args, {
30
+ ...options,
31
+ env: { ...process.env, GH_TOKEN: fallbackToken, GITHUB_TOKEN: fallbackToken },
32
+ });
33
+ }
25
34
  if (result.status !== 0) {
26
35
  const category = /401|E401|unauthorized/i.test(result.stderr) ? "unauthorized" : "unavailable";
27
36
  throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
@@ -33,18 +42,22 @@ function commandJson(command, args, label) {
33
42
  }
34
43
  }
35
44
 
36
- function githubJson(apiPath, label) {
37
- return commandJson("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], label);
45
+ function githubJson(apiPath, label, { publicReadFallback = false } = {}) {
46
+ return commandJson("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], label, { publicReadFallback });
47
+ }
48
+
49
+ function githubPublicJson(apiPath, label) {
50
+ return githubJson(apiPath, label, { publicReadFallback: true });
38
51
  }
39
52
 
40
- function githubJsonOptional(apiPath, label, fallback) {
53
+ function githubJsonOptional(apiPath, label, fallback, fallbackPattern = /404|not found/i) {
41
54
  const result = spawnSyncCommand("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
42
55
  encoding: "utf8",
43
56
  timeout: 60_000,
44
57
  maxBuffer: GITHUB_JSON_MAX_BUFFER,
45
58
  });
46
59
  if (result.status !== 0) {
47
- if (/404|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
60
+ if (fallbackPattern.test(`${result.stdout}\n${result.stderr}`)) return fallback;
48
61
  const category = /401|403|unauthorized|forbidden/i.test(`${result.stdout}\n${result.stderr}`) ? "unauthorized" : "unavailable";
49
62
  throw new Error(`${label} is ${category}; publication control-plane audit fails closed`);
50
63
  }
@@ -56,20 +69,7 @@ function githubJsonOptional(apiPath, label, fallback) {
56
69
  }
57
70
 
58
71
  function githubJsonReadLimited(apiPath, label, fallback) {
59
- const result = spawnSyncCommand("gh", ["api", apiPath, "-H", "Accept: application/vnd.github+json"], {
60
- encoding: "utf8",
61
- timeout: 60_000,
62
- maxBuffer: GITHUB_JSON_MAX_BUFFER,
63
- });
64
- if (result.status !== 0) {
65
- if (/401|403|404|unauthorized|forbidden|not found/i.test(`${result.stdout}\n${result.stderr}`)) return fallback;
66
- throw new Error(`${label} is unavailable; publication control-plane audit fails closed`);
67
- }
68
- try {
69
- return JSON.parse(result.stdout);
70
- } catch {
71
- throw new Error(`${label} did not return JSON; publication control-plane audit fails closed`);
72
- }
72
+ return githubJsonOptional(apiPath, label, fallback, /401|403|404|unauthorized|forbidden|not found/i);
73
73
  }
74
74
 
75
75
  function rulesetIncludesBranch(ruleset, branch, defaultBranch) {
@@ -218,7 +218,7 @@ function main() {
218
218
  if (!repository || !branch) throw new Error("--repository and --branch are required");
219
219
 
220
220
  const encodedWorkflow = workflowPath.split("/").map(encodeURIComponent).join("/");
221
- const workflowFile = githubJson(
221
+ const workflowFile = githubPublicJson(
222
222
  `repos/${workflowRepository}/contents/${encodedWorkflow}${workflowRef ? `?ref=${encodeURIComponent(workflowRef)}` : ""}`,
223
223
  "publication workflow source",
224
224
  );
@@ -231,8 +231,8 @@ function main() {
231
231
  !/^\s*[a-z-]+:\s*write\s*$/m.test(workflowHeader) &&
232
232
  !/permissions\s*:\s*write-all/i.test(workflowHeader);
233
233
 
234
- const repositoryState = githubJson(`repos/${repository}`, "repository metadata");
235
- const branchState = githubJson(`repos/${repository}/branches/${encodeURIComponent(branch)}`, "branch summary");
234
+ const repositoryState = githubPublicJson(`repos/${repository}`, "repository metadata");
235
+ const branchState = githubPublicJson(`repos/${repository}/branches/${encodeURIComponent(branch)}`, "branch summary");
236
236
  const exactTransactionSource = /^[0-9a-f]{40}$/.test(sourceSha);
237
237
  const protection = exactTransactionSource
238
238
  ? null
@@ -290,7 +290,7 @@ function main() {
290
290
  if (!/^[0-9a-f]{40}$/.test(sourceSha)) {
291
291
  throw new Error("--source-sha is required when detailed branch policy is not readable");
292
292
  }
293
- const sourceCommit = githubJson(`repos/${repository}/commits/${sourceSha}`, "source commit");
293
+ const sourceCommit = githubPublicJson(`repos/${repository}/commits/${sourceSha}`, "source commit");
294
294
  const sourceParents = Array.isArray(sourceCommit.parents) ? sourceCommit.parents.map((entry) => String(entry?.sha || "").toLowerCase()) : [];
295
295
  const sourceChangedPaths = Array.isArray(sourceCommit.files) ? sourceCommit.files.map((entry) => String(entry?.filename || "")) : [];
296
296
  const sourceMessage = String(sourceCommit.commit?.message || "").split("\n", 1)[0];
@@ -305,7 +305,7 @@ function main() {
305
305
  const branchHeadSha = String(branchState.commit?.sha || "").toLowerCase();
306
306
  const sourceComparison = sourceSha === branchHeadSha
307
307
  ? null
308
- : githubJson(
308
+ : githubPublicJson(
309
309
  `repos/${repository}/compare/${sourceSha}...${branchHeadSha}`,
310
310
  "source protected-branch lineage",
311
311
  );
@@ -313,7 +313,7 @@ function main() {
313
313
  sourceComparison?.status === "ahead" &&
314
314
  String(sourceComparison?.merge_base_commit?.sha || "").toLowerCase() === sourceSha
315
315
  );
316
- let pullRequests = githubJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "source pull-request lineage");
316
+ let pullRequests = githubPublicJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "source pull-request lineage");
317
317
  let mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
318
318
  entry?.merged_at &&
319
319
  (
@@ -324,7 +324,7 @@ function main() {
324
324
  entry.head?.repo?.full_name === repository
325
325
  );
326
326
  if (!mergedPullRequest && allowReleaseReconciliation) {
327
- const packageFile = githubJson(
327
+ const packageFile = githubPublicJson(
328
328
  `repos/${repository}/contents/package.json?ref=${encodeURIComponent(sourceSha)}`,
329
329
  "release reconciliation package metadata",
330
330
  );
@@ -340,7 +340,7 @@ function main() {
340
340
  });
341
341
  if (releaseReconciliation.qualifying) {
342
342
  authorizationSha = parentSha;
343
- pullRequests = githubJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "release parent pull-request lineage");
343
+ pullRequests = githubPublicJson(`repos/${repository}/commits/${authorizationSha}/pulls`, "release parent pull-request lineage");
344
344
  mergedPullRequest = (Array.isArray(pullRequests) ? pullRequests : []).find((entry) =>
345
345
  entry?.merged_at &&
346
346
  entry.merge_commit_sha === authorizationSha &&
@@ -350,7 +350,7 @@ function main() {
350
350
  }
351
351
  }
352
352
  const reviews = mergedPullRequest
353
- ? githubJson(`repos/${repository}/pulls/${mergedPullRequest.number}/reviews?per_page=100`, "source pull-request reviews")
353
+ ? githubPublicJson(`repos/${repository}/pulls/${mergedPullRequest.number}/reviews?per_page=100`, "source pull-request reviews")
354
354
  : [];
355
355
  const latestReviews = new Map();
356
356
  for (const review of Array.isArray(reviews) ? reviews : []) {
@@ -378,7 +378,7 @@ function main() {
378
378
  (prefixedRequiredStatusChecks.length === 1 ? prefixedRequiredStatusChecks[0] : requiredStatusCheck);
379
379
  const requiredStatusCheckMatchCount = exactRequiredStatusCheck ? 1 : prefixedRequiredStatusChecks.length;
380
380
  const checkRuns = /^[0-9a-f]{40}$/.test(pullRequestHeadSha)
381
- ? githubJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?check_name=${encodeURIComponent(resolvedRequiredStatusCheck)}&filter=latest&per_page=100`, "merged pull-request required check runs")
381
+ ? githubPublicJson(`repos/${repository}/commits/${pullRequestHeadSha}/check-runs?check_name=${encodeURIComponent(resolvedRequiredStatusCheck)}&filter=latest&per_page=100`, "merged pull-request required check runs")
382
382
  : { check_runs: [] };
383
383
  const requiredCheckSource = (requiredStatusCheckPolicy.checks || []).find((entry) =>
384
384
  entry.context === resolvedRequiredStatusCheck
@@ -1019,7 +1019,7 @@ for (const requiredSnippet of [
1019
1019
  "resume-buildchain-runtime-sha:",
1020
1020
  "github-release: true",
1021
1021
  "release-passport-buildchain-self-kfd: true",
1022
- 'artifact-patterns: "buildchain-package-*"',
1022
+ "artifact-patterns: ${{ startsWith(github.event.workflow_run.head_branch || inputs['target-ref'], 'alpha/') && 'buildchain-package-*' || '' }}",
1023
1023
  "release-passport-impact-json: .buildchain/release-impact.json",
1024
1024
  ]) {
1025
1025
  if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { parseYamlUses } from "../packages/core/workflow-yaml-contract.js";
6
+
7
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
9
+
10
+ function fail(message) {
11
+ throw new Error(`v4-floating-consumer-policy-contract: ${message}`);
12
+ }
13
+
14
+ export function workflowJobBlock(source, job) {
15
+ return (
16
+ source.match(
17
+ new RegExp(
18
+ `^ ${job}:\\n([\\s\\S]*?)(?=^ [a-z0-9_-]+:|(?![\\s\\S]))`,
19
+ "mu",
20
+ ),
21
+ )?.[1] || ""
22
+ );
23
+ }
24
+
25
+ export function assertTrustGatedJobs(source, jobs) {
26
+ for (const job of jobs) {
27
+ const block = workflowJobBlock(source, job);
28
+ if (!block.includes("- trust-gate")) {
29
+ fail(`.build.yml job ${job} is not directly gated by trust-gate`);
30
+ }
31
+ }
32
+ }
33
+
34
+ function jobDependsOn(source, job, dependency) {
35
+ const block = workflowJobBlock(source, job);
36
+ return new RegExp(
37
+ `^ needs:\\s*(?:${dependency}|\\[[^\\]]*\\b${dependency}\\b[^\\]]*\\])\\s*$`,
38
+ "mu",
39
+ ).test(block);
40
+ }
41
+
42
+ function assertOrdered(relative, markers) {
43
+ const source = read(relative);
44
+ let cursor = -1;
45
+ for (const marker of markers) {
46
+ const next = source.indexOf(marker, cursor + 1);
47
+ if (next < 0) fail(`${relative} is missing ${marker}`);
48
+ if (next <= cursor)
49
+ fail(`${relative} does not enforce ${markers.join(" -> ")}`);
50
+ cursor = next;
51
+ }
52
+ }
53
+
54
+ export function assertPromotionCertificationWiring(source) {
55
+ for (const marker of [
56
+ ".buildchain/runtime/promotion-shell/scripts/v4-consumer-policy.mjs certify",
57
+ '--caller-root "${{ github.workspace }}"',
58
+ '--stable-lock "${{ inputs.buildchain-stable-contract-lock-path }}"',
59
+ '--alpha-lock "${{ inputs.buildchain-alpha-contract-lock-path }}"',
60
+ "release-passport-v4-consumer-policy-certification-root: ${{ steps.v4-policy-certification.outputs.v4-consumer-policy-certification-root }}",
61
+ ]) {
62
+ if (!source.includes(marker)) {
63
+ fail(`promotion certification is missing ${marker}`);
64
+ }
65
+ }
66
+ }
67
+
68
+ function assertPersistedSelectors() {
69
+ const offenders = [];
70
+ const workflows = fs
71
+ .readdirSync(path.join(root, ".github/workflows"))
72
+ .filter((name) => /\.ya?ml$/u.test(name))
73
+ .sort();
74
+ for (const name of workflows) {
75
+ const relative = `.github/workflows/${name}`;
76
+ for (const node of parseYamlUses(read(relative))) {
77
+ const match = node.value.match(
78
+ /^kungfu-systems\/buildchain\/(.+)@(.+)$/u,
79
+ );
80
+ if (!match) continue;
81
+ const selector = match[2];
82
+ const isV4 =
83
+ selector === "v4" ||
84
+ selector === "v4-alpha" ||
85
+ /^v4(?:[./-]|$)/u.test(selector) ||
86
+ /^[0-9a-f]{40}$/iu.test(selector) ||
87
+ selector.includes("${{");
88
+ if (isV4 && !["v4", "v4-alpha"].includes(selector)) {
89
+ offenders.push(`${relative}:${node.line} @${selector}`);
90
+ }
91
+ }
92
+ }
93
+ if (offenders.length)
94
+ fail(
95
+ `persisted v4 selectors must be v4 or v4-alpha: ${offenders.join(", ")}`,
96
+ );
97
+ }
98
+
99
+ export function checkV4FloatingConsumerPolicyContract() {
100
+ const policy = JSON.parse(
101
+ read("architecture/v4-floating-consumer-policy.json"),
102
+ );
103
+ if (policy.contract !== "kungfu-buildchain-v4-floating-consumer-policy/v1") {
104
+ fail("architecture policy contract is missing");
105
+ }
106
+ if (
107
+ policy.contractLocks?.selectedLockMustBindResolvedWorkflowShell !== true
108
+ ) {
109
+ fail("contract lock must bind the visible workflow shell");
110
+ }
111
+ assertPersistedSelectors();
112
+ assertOrdered(".github/workflows/.build.yml", [
113
+ "Enforce v4 floating consumer policy",
114
+ "Validate consumer package manager contract",
115
+ ]);
116
+ assertTrustGatedJobs(read(".github/workflows/.build.yml"), [
117
+ "resolve-source",
118
+ "resolve-contract",
119
+ "controller-plan",
120
+ "artifact-transfer",
121
+ "build-native",
122
+ "build-linux-container",
123
+ "summarize",
124
+ ]);
125
+ assertOrdered(".github/workflows/publication-artifact.yml", [
126
+ "Enforce v4 floating consumer policy",
127
+ "Resolve controller identities",
128
+ "Install Buildchain runtime dependencies",
129
+ ]);
130
+ const stageCanary = read(".github/workflows/v4-stage-capsule-canary.yml");
131
+ if (
132
+ !stageCanary.includes("consumer-admission:") ||
133
+ !stageCanary.includes("needs: consumer-admission")
134
+ ) {
135
+ fail(
136
+ "Stage Capsule qualification is not transitively gated by consumer admission",
137
+ );
138
+ }
139
+ const promotion = read(".github/workflows/release-candidate-promote.yml");
140
+ if (
141
+ !promotion.includes("consumer-admission:") ||
142
+ !jobDependsOn(promotion, "alpha", "consumer-admission") ||
143
+ !jobDependsOn(promotion, "stable", "consumer-admission")
144
+ ) {
145
+ fail(
146
+ "release candidate promotion is not transitively gated by consumer admission",
147
+ );
148
+ }
149
+ assertPromotionCertificationWiring(
150
+ read(".github/workflows/.release-candidate-promote.yml"),
151
+ );
152
+ for (const [relative, marker] of [
153
+ ["packages/core/release-candidate.js", "consumerPolicy"],
154
+ ["packages/core/release-passport.js", "v4ConsumerPolicy"],
155
+ [
156
+ "actions/promote-buildchain-ref/action.yml",
157
+ "release-passport-v4-consumer-policy-certification-json",
158
+ ],
159
+ [
160
+ "actions/promote-buildchain-ref/action.yml",
161
+ "release-passport-v4-consumer-policy-certification-root",
162
+ ],
163
+ [
164
+ ".github/workflows/.release-candidate-promote.yml",
165
+ "v4-policy-certification",
166
+ ],
167
+ ]) {
168
+ if (!read(relative).includes(marker)) {
169
+ fail(
170
+ `${relative} does not bind v4 consumer policy evidence marker ${marker}`,
171
+ );
172
+ }
173
+ }
174
+ const agents = read("AGENTS.md");
175
+ for (const invariant of [
176
+ "source-persisted exact commit SHA",
177
+ "matching stable and alpha contract locks",
178
+ "trusted non-persistent runtime input",
179
+ ]) {
180
+ if (!agents.includes(invariant))
181
+ fail(`AGENTS.md is missing invariant: ${invariant}`);
182
+ }
183
+ return { ok: true, entrypoints: policy.scope.publicWorkflowEntrypoints };
184
+ }
185
+
186
+ if (
187
+ process.argv[1] &&
188
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
189
+ ) {
190
+ try {
191
+ process.stdout.write(
192
+ `${JSON.stringify(checkV4FloatingConsumerPolicyContract(), null, 2)}\n`,
193
+ );
194
+ } catch (error) {
195
+ console.error(error.message);
196
+ process.exitCode = 1;
197
+ }
198
+ }
@@ -10,9 +10,7 @@ const DEFAULT_ROOT = path.resolve(
10
10
  );
11
11
  const CALLER_PATH = ".github/workflows/v4-public-consumer-dogfood.yml";
12
12
  const REUSABLE_PATH = ".github/workflows/v4-stage-capsule-canary.yml";
13
- export const V4_PUBLIC_DOGFOOD_TRAIN_REF =
14
- "train/v4/v4.0/public-consumer-self-dogfood";
15
- const EXACT_SHA = /^[0-9a-f]{40}$/u;
13
+ export const V4_PUBLIC_DOGFOOD_ALPHA_REF = "v4-alpha";
16
14
  const PRIVATE_CONSUMER = ["buildchain", "self", "dogfood"].join("-");
17
15
  const PRIVATE_SHADOW = ["kungfu", "shadow"].join("-");
18
16
 
@@ -139,13 +137,8 @@ function assertArchitecture(root) {
139
137
  );
140
138
  const dogfood = architecture.publicConsumerDogfood;
141
139
  const validationRef = dogfood?.validationRef;
142
- if (
143
- validationRef !== V4_PUBLIC_DOGFOOD_TRAIN_REF &&
144
- !EXACT_SHA.test(String(validationRef))
145
- )
146
- fail(
147
- "architecture validationRef must be the exact public train or a protected commit SHA",
148
- );
140
+ if (validationRef !== V4_PUBLIC_DOGFOOD_ALPHA_REF)
141
+ fail("architecture validationRef must use the floating v4-alpha channel");
149
142
  const caller = read(root, CALLER_PATH);
150
143
  if (caller !== expectedV4PublicDogfoodWorkflow(validationRef))
151
144
  fail(`${CALLER_PATH} must remain the exact thin public consumer caller`);
@@ -168,7 +161,7 @@ function assertArchitecture(root) {
168
161
  dogfood.relativeOrSelfInvocationAllowed !== false ||
169
162
  dogfood.directQualificationInvocationAllowed !== false ||
170
163
  dogfood.candidateBranchOverrideAllowed !== false ||
171
- dogfood.recursionRecovery !== "public-train-ref-only"
164
+ dogfood.recursionRecovery !== "floating-selector-with-trusted-runtime-input"
172
165
  )
173
166
  fail(
174
167
  "architecture publicConsumerDogfood contract is incomplete or widened",
@@ -226,6 +219,8 @@ function assertPolicySources(root) {
226
219
  "No agent may add or restore a relative/self reusable-workflow call",
227
220
  "never solve recursion with an internal exception",
228
221
  "scripts/check-v4-public-dogfood-contract.mjs",
222
+ "source-persisted exact commit SHA",
223
+ "v4-alpha",
229
224
  ])
230
225
  if (!agents.includes(invariant))
231
226
  fail(`AGENTS.md is missing invariant: ${invariant}`);