@kungfu-tech/buildchain 2.12.1 → 2.12.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.
@@ -4,25 +4,31 @@ import path from "node:path";
4
4
 
5
5
  export const RUNNER_PRESETS = Object.freeze({
6
6
  "github-hosted": [
7
- { id: "linux-x64", name: "Linux x64", runner: '["ubuntu-24.04"]' },
8
- { id: "macos", name: "macOS", runner: '["macos-latest"]' },
9
- { id: "windows-x64", name: "Windows x64", runner: '["windows-2022"]' },
7
+ { id: "linux-x64", name: "Linux x64", platform: "linux", runner: '["ubuntu-24.04"]', capabilities: ["node"] },
8
+ { id: "macos", name: "macOS", platform: "macos", runner: '["macos-latest"]', capabilities: ["node"] },
9
+ { id: "windows-x64", name: "Windows x64", platform: "windows", runner: '["windows-2022"]', capabilities: ["node"] },
10
10
  ],
11
11
  "kungfu-v4-self-hosted": [
12
12
  {
13
13
  id: "linux-x64",
14
14
  name: "Linux x64",
15
+ platform: "linux",
15
16
  runner: '["self-hosted","Linux","X64","kungfu-build-v4-linux-x64"]',
17
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
16
18
  },
17
19
  {
18
20
  id: "macos-arm64",
19
21
  name: "macOS ARM64",
22
+ platform: "macos",
20
23
  runner: '["self-hosted","macOS","ARM64","kungfu-build-v4-macos-arm64"]',
24
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
21
25
  },
22
26
  {
23
27
  id: "windows-x64",
24
28
  name: "Windows x64",
29
+ platform: "windows",
25
30
  runner: '["self-hosted","Windows","X64","kungfu-build-v4-windows-x64"]',
31
+ capabilities: ["node", "native-toolchain", "product-artifacts", "rust"],
26
32
  },
27
33
  ],
28
34
  });
@@ -713,7 +719,18 @@ function normalizePlatform(platform, index) {
713
719
  throw new Error(`platforms-json[${index}].runner is required`);
714
720
  }
715
721
  parseJsonArray(runner, `platforms-json[${index}].runner`);
716
- return { id, name, runner };
722
+ const capabilities = platform?.capabilities === undefined
723
+ ? ["node"]
724
+ : parseJsonArray(JSON.stringify(platform.capabilities), `platforms-json[${index}].capabilities`)
725
+ .map((capability) => String(capability || "").trim());
726
+ if (capabilities.some((capability) => !capability) || new Set(capabilities).size !== capabilities.length) {
727
+ throw new Error(`platforms-json[${index}].capabilities must contain unique non-empty strings`);
728
+ }
729
+ const normalized = { id, name, runner };
730
+ if (platform?.platform !== undefined) normalized.platform = String(platform.platform || "").trim();
731
+ normalized.capabilities = capabilities.sort();
732
+ if (platform?.required === false) normalized.required = false;
733
+ return normalized;
717
734
  }
718
735
 
719
736
  export function resolveRunnerMatrix({
@@ -35,6 +35,7 @@ const requiredPaths = [
35
35
  "docs/product-mechanism.md",
36
36
  "docs/readme-badges.md",
37
37
  "docs/release-passport.md",
38
+ "docs/shifu-gate-profiles.md",
38
39
  "docs/release-propagation.md",
39
40
  "docs/site-bundle-contract.md",
40
41
  "docs/toolkit-observability.md",
@@ -48,6 +49,7 @@ const requiredPaths = [
48
49
  "scripts/generate-site-bundle.mjs",
49
50
  "scripts/generate-buildchain-kfd-witnesses.mjs",
50
51
  "scripts/generate-release-candidate-passport.mjs",
52
+ "scripts/shifu-gate-profile.mjs",
51
53
  "scripts/artifact-relay-s3.mjs",
52
54
  "scripts/npm-publish-dry-run.mjs",
53
55
  "scripts/npm-publish-transaction.mjs",
@@ -84,6 +86,7 @@ const requiredPaths = [
84
86
  ".github/workflows/binary-distribution.yml",
85
87
  ".github/workflows/verify.yml",
86
88
  ".github/workflows/.build.yml",
89
+ ".github/workflows/.gate-profile.yml",
87
90
  ".github/workflows/build.yml",
88
91
  ".github/workflows/build-surface-fixture.yml",
89
92
  ".github/workflows/candidate-lab.yml",
@@ -193,6 +196,7 @@ const actionlintConfig = fs.readFileSync(
193
196
  for (const requiredSnippet of [
194
197
  ".github/workflows/build.yml:",
195
198
  ".github/workflows/.build.yml:",
199
+ ".github/workflows/.gate-profile.yml:",
196
200
  'property "workflow_ref" is not defined in object type',
197
201
  ]) {
198
202
  if (!actionlintConfig.includes(requiredSnippet)) {
@@ -0,0 +1,413 @@
1
+ import crypto from "node:crypto";
2
+
3
+ export const GATE_MATRIX_CONTRACT = "buildchain.shifu-gate-matrix/v1";
4
+ export const GATE_AGGREGATE_CONTRACT = "buildchain.shifu-gate-aggregate/v1";
5
+
6
+ function stableValue(value) {
7
+ if (Array.isArray(value)) return value.map(stableValue);
8
+ if (value && typeof value === "object") {
9
+ return Object.fromEntries(
10
+ Object.keys(value)
11
+ .sort()
12
+ .map((key) => [key, stableValue(value[key])]),
13
+ );
14
+ }
15
+ return value;
16
+ }
17
+
18
+ export function stableJson(value) {
19
+ return JSON.stringify(stableValue(value));
20
+ }
21
+
22
+ export function sha256(value) {
23
+ return `sha256:${crypto.createHash("sha256").update(stableJson(value)).digest("hex")}`;
24
+ }
25
+
26
+ export function shifuGateRunPlanDigest(plan) {
27
+ return sha256({
28
+ registryDigest: plan.registry.digest,
29
+ profile: plan.profile,
30
+ platform: plan.platform,
31
+ includeAdvisory: plan.includeAdvisory,
32
+ explicitGates: plan.explicitGates,
33
+ qualifying: plan.qualifying,
34
+ groups: plan.groups.map((group) => ({
35
+ index: group.index,
36
+ gates: group.gates.map((gate) => ({
37
+ id: gate.id,
38
+ mode: gate.mode,
39
+ selectedBy: gate.selectedBy,
40
+ dependencies: gate.dependencies,
41
+ actionId: gate.actionId,
42
+ definitionDigest: gate.definitionDigest,
43
+ })),
44
+ })),
45
+ skipped: plan.skipped,
46
+ unsupported: plan.unsupported,
47
+ });
48
+ }
49
+
50
+ function assertObject(value, label) {
51
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
52
+ throw new Error(`${label} must be an object`);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ function uniqueStrings(value, label) {
58
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
59
+ const normalized = value.map((item) => String(item || "").trim());
60
+ if (normalized.some((item) => !item)) {
61
+ throw new Error(`${label} must contain non-empty strings`);
62
+ }
63
+ if (new Set(normalized).size !== normalized.length) {
64
+ throw new Error(`${label} must not contain duplicates`);
65
+ }
66
+ return normalized;
67
+ }
68
+
69
+ function inferShifuPlatform(platform) {
70
+ const explicit = String(platform.platform || "")
71
+ .trim()
72
+ .toLowerCase();
73
+ if (explicit) return explicit;
74
+ const probe =
75
+ `${platform.id || ""} ${platform.name || ""} ${platform.runner || ""}`.toLowerCase();
76
+ if (probe.includes("windows")) return "windows";
77
+ if (probe.includes("macos") || probe.includes("mac os")) return "macos";
78
+ if (probe.includes("linux") || probe.includes("ubuntu")) return "linux";
79
+ throw new Error(
80
+ `platform ${platform.id || "(unknown)"} requires a platform field`,
81
+ );
82
+ }
83
+
84
+ export function normalizeGatePlatform(platform, index = 0) {
85
+ assertObject(platform, `platforms[${index}]`);
86
+ const id = String(platform.id || "").trim();
87
+ const name = String(platform.name || id).trim();
88
+ const runner = String(platform.runner || "").trim();
89
+ if (!id) throw new Error(`platforms[${index}].id is required`);
90
+ if (!/^[A-Za-z0-9._-]+$/.test(id)) {
91
+ throw new Error(
92
+ `platforms[${index}].id may only contain letters, numbers, dot, underscore, and hyphen`,
93
+ );
94
+ }
95
+ if (!name) throw new Error(`platforms[${index}].name is required`);
96
+ if (!runner) throw new Error(`platforms[${index}].runner is required`);
97
+ let labels;
98
+ try {
99
+ labels = JSON.parse(runner);
100
+ } catch (error) {
101
+ throw new Error(
102
+ `platforms[${index}].runner must be valid JSON: ${error.message}`,
103
+ );
104
+ }
105
+ if (
106
+ !Array.isArray(labels) ||
107
+ labels.length === 0 ||
108
+ labels.some((label) => typeof label !== "string" || !label)
109
+ ) {
110
+ throw new Error(
111
+ `platforms[${index}].runner must be a non-empty JSON string array`,
112
+ );
113
+ }
114
+ return {
115
+ id,
116
+ name,
117
+ platform: inferShifuPlatform(platform),
118
+ runner,
119
+ capabilities: uniqueStrings(
120
+ platform.capabilities || ["node"],
121
+ `platforms[${index}].capabilities`,
122
+ ).sort(),
123
+ required: platform.required !== false,
124
+ };
125
+ }
126
+
127
+ export function inspectGatePlan(plan, { profile, platform } = {}) {
128
+ assertObject(plan, "Shifu gate plan");
129
+ if (plan.schema !== "shifu.gate-plan/v1") {
130
+ throw new Error(
131
+ `unsupported Shifu gate plan schema: ${plan.schema || "(missing)"}`,
132
+ );
133
+ }
134
+ if (profile && plan.profile !== profile) {
135
+ throw new Error(
136
+ `Shifu gate plan profile mismatch: expected ${profile}, got ${plan.profile || "(missing)"}`,
137
+ );
138
+ }
139
+ if (platform && plan.platform !== platform) {
140
+ throw new Error(
141
+ `Shifu gate plan platform mismatch: expected ${platform}, got ${plan.platform || "(missing)"}`,
142
+ );
143
+ }
144
+ const registry = assertObject(plan.registry, "Shifu gate plan registry");
145
+ if (
146
+ !registry.projectId ||
147
+ !/^sha256:[0-9a-f]{64}$/.test(registry.digest || "")
148
+ ) {
149
+ throw new Error(
150
+ "Shifu gate plan registry requires projectId and sha256 digest",
151
+ );
152
+ }
153
+ if (!Array.isArray(plan.groups))
154
+ throw new Error("Shifu gate plan groups must be an array");
155
+ const gates = plan.groups.flatMap((group, groupIndex) => {
156
+ if (group.index !== groupIndex || !Array.isArray(group.gates)) {
157
+ throw new Error(
158
+ "Shifu gate plan groups must use contiguous indexes and gate arrays",
159
+ );
160
+ }
161
+ return group.gates.map((gate) => ({ ...gate, group: group.index }));
162
+ });
163
+ const ids = gates.map((gate) => String(gate.id || ""));
164
+ if (ids.some((id) => !id) || new Set(ids).size !== ids.length) {
165
+ throw new Error("Shifu gate plan gate ids must be non-empty and unique");
166
+ }
167
+ for (const gate of gates) {
168
+ if (!["required", "advisory"].includes(gate.mode)) {
169
+ throw new Error(
170
+ `planned gate ${gate.id} has unsupported mode ${gate.mode}`,
171
+ );
172
+ }
173
+ if (
174
+ !/^sha256:[0-9a-f]{64}$/.test(gate.actionId || "") ||
175
+ !/^sha256:[0-9a-f]{64}$/.test(gate.definitionDigest || "")
176
+ ) {
177
+ throw new Error(
178
+ `planned gate ${gate.id} requires actionId and definitionDigest sha256 digests`,
179
+ );
180
+ }
181
+ }
182
+ const requiredCapabilities = [
183
+ ...new Set(gates.flatMap((gate) => gate.runner?.capabilities || [])),
184
+ ].sort();
185
+ return {
186
+ plan,
187
+ gates,
188
+ requiredCapabilities,
189
+ planDigest: shifuGateRunPlanDigest(plan),
190
+ };
191
+ }
192
+
193
+ export function createGateExecutionMatrix({
194
+ profile,
195
+ includeAdvisory = false,
196
+ platforms,
197
+ plans,
198
+ }) {
199
+ if (!profile) throw new Error("gate profile is required");
200
+ const normalizedPlatforms = platforms.map(normalizeGatePlatform);
201
+ const planMap =
202
+ plans instanceof Map ? plans : new Map(Object.entries(plans || {}));
203
+ const entries = [];
204
+ const omitted = [];
205
+ let registryIdentity;
206
+
207
+ for (const platform of normalizedPlatforms) {
208
+ const inspected = inspectGatePlan(planMap.get(platform.id), {
209
+ profile,
210
+ platform: platform.platform,
211
+ });
212
+ const identity = `${inspected.plan.registry.projectId}\u0000${inspected.plan.registry.digest}`;
213
+ if (registryIdentity && registryIdentity !== identity) {
214
+ throw new Error(
215
+ "all Shifu gate plans must use the same project and registry digest",
216
+ );
217
+ }
218
+ registryIdentity = identity;
219
+ const missingCapabilities = inspected.requiredCapabilities.filter(
220
+ (capability) =>
221
+ capability !== "node" && !platform.capabilities.includes(capability),
222
+ );
223
+ const unsupportedRequired = (inspected.plan.unsupported || []).filter(
224
+ (item) => item.mode === "required",
225
+ );
226
+ const reasons = [];
227
+ if (inspected.plan.ok !== true || inspected.plan.qualifying !== true)
228
+ reasons.push("plan is not qualifying");
229
+ if (unsupportedRequired.length)
230
+ reasons.push(
231
+ `required gates unsupported: ${unsupportedRequired.map((item) => item.id).join(", ")}`,
232
+ );
233
+ if (missingCapabilities.length)
234
+ reasons.push(
235
+ `runner capabilities missing: ${missingCapabilities.join(", ")}`,
236
+ );
237
+ if (reasons.length) {
238
+ omitted.push({
239
+ id: platform.id,
240
+ platform: platform.platform,
241
+ required: platform.required,
242
+ reasons,
243
+ });
244
+ if (platform.required)
245
+ throw new Error(
246
+ `required gate platform ${platform.id} cannot run: ${reasons.join("; ")}`,
247
+ );
248
+ continue;
249
+ }
250
+ const timeoutSeconds = inspected.gates.reduce(
251
+ (total, gate) =>
252
+ total + Math.max(0, Number(gate.cost?.timeoutSeconds || 0)),
253
+ 0,
254
+ );
255
+ entries.push({
256
+ id: platform.id,
257
+ name: platform.name,
258
+ platform: platform.platform,
259
+ runner: platform.runner,
260
+ capabilities: platform.capabilities,
261
+ required: platform.required,
262
+ profile,
263
+ includeAdvisory: Boolean(includeAdvisory),
264
+ registry: inspected.plan.registry,
265
+ planDigest: inspected.planDigest,
266
+ timeoutMinutes: Math.max(
267
+ 1,
268
+ Math.min(360, Math.ceil(timeoutSeconds / 60)),
269
+ ),
270
+ gates: inspected.gates.map((gate) => ({
271
+ id: gate.id,
272
+ mode: gate.mode,
273
+ group: gate.group,
274
+ actionId: gate.actionId,
275
+ definitionDigest: gate.definitionDigest,
276
+ })),
277
+ skipped: inspected.plan.skipped || [],
278
+ unsupported: inspected.plan.unsupported || [],
279
+ });
280
+ }
281
+ if (entries.length === 0)
282
+ throw new Error("gate profile matrix has no runnable platforms");
283
+ const base = {
284
+ contract: GATE_MATRIX_CONTRACT,
285
+ profile,
286
+ includeAdvisory: Boolean(includeAdvisory),
287
+ registry: entries[0].registry,
288
+ entries,
289
+ omitted,
290
+ };
291
+ return { ...base, digest: sha256(base) };
292
+ }
293
+
294
+ function resultKey(platformId, gateId) {
295
+ return `${platformId}\u0000${gateId}`;
296
+ }
297
+
298
+ function validateExecution(entry, execution, sourceSha) {
299
+ const issues = [];
300
+ const receipt = execution?.receipt;
301
+ const validation = execution?.validation;
302
+ if (!receipt) return { issues: ["receipt is missing"], gateRows: [] };
303
+ if (!validation) issues.push("Shifu receipt validation is missing");
304
+ else {
305
+ if (validation.schema !== "shifu.gate-receipt-validation/v1")
306
+ issues.push("receipt validation schema is unsupported");
307
+ if (validation.valid !== true) issues.push("receipt is invalid");
308
+ if (validation.current !== true) issues.push("receipt is stale");
309
+ if (validation.qualifying !== true)
310
+ issues.push("receipt is non-qualifying");
311
+ }
312
+ if (receipt.schema !== "shifu.gate-receipt/v1")
313
+ issues.push("receipt schema is unsupported");
314
+ if (receipt.source?.sha !== sourceSha)
315
+ issues.push("receipt source SHA does not match the locked source");
316
+ if (receipt.source?.dirty !== false) issues.push("receipt source is dirty");
317
+ if (receipt.selection?.profile !== entry.profile)
318
+ issues.push("receipt profile does not match the matrix");
319
+ if (receipt.environment?.platform !== entry.platform)
320
+ issues.push("receipt platform does not match the matrix");
321
+ if (
322
+ receipt.registry?.digest !== entry.registry.digest ||
323
+ receipt.registry?.projectId !== entry.registry.projectId
324
+ ) {
325
+ issues.push("receipt registry identity does not match the matrix");
326
+ }
327
+ if (receipt.plan?.digest !== entry.planDigest)
328
+ issues.push("receipt plan digest does not match the matrix");
329
+ if (receipt.ok !== true || receipt.qualifying !== true)
330
+ issues.push("receipt did not qualify");
331
+ const results = new Map(
332
+ (receipt.results || []).map((result) => [result.gateId, result]),
333
+ );
334
+ const gateRows = entry.gates.map((expected) => {
335
+ const actual = results.get(expected.id);
336
+ const gateIssues = [];
337
+ if (!actual) gateIssues.push("result is missing");
338
+ else {
339
+ if (actual.policyMode !== expected.mode)
340
+ gateIssues.push("policy mode mismatch");
341
+ if (actual.actionId !== expected.actionId)
342
+ gateIssues.push("action digest mismatch");
343
+ if (actual.definitionDigest !== expected.definitionDigest)
344
+ gateIssues.push("definition digest mismatch");
345
+ if (expected.mode === "required" && actual.status !== "pass")
346
+ gateIssues.push(`required status is ${actual.status}`);
347
+ if (expected.mode === "required" && actual.attempted !== true)
348
+ gateIssues.push("required action was not attempted");
349
+ }
350
+ if (gateIssues.length)
351
+ issues.push(`${expected.id}: ${gateIssues.join(", ")}`);
352
+ return {
353
+ key: resultKey(entry.id, expected.id),
354
+ platformId: entry.id,
355
+ gateId: expected.id,
356
+ mode: expected.mode,
357
+ status: actual?.status || "missing",
358
+ attempted: actual?.attempted === true,
359
+ definitionDigest: expected.definitionDigest,
360
+ actionId: expected.actionId,
361
+ evidence: actual?.evidence || null,
362
+ artifacts: actual?.artifacts || [],
363
+ issues: gateIssues,
364
+ };
365
+ });
366
+ return { issues, gateRows };
367
+ }
368
+
369
+ export function createGateAggregate({ matrix, sourceSha, executions }) {
370
+ if (matrix?.contract !== GATE_MATRIX_CONTRACT)
371
+ throw new Error("unsupported gate matrix contract");
372
+ if (!/^[0-9a-f]{40}$/i.test(sourceSha || ""))
373
+ throw new Error("gate aggregate requires a 40-character source SHA");
374
+ const executionMap =
375
+ executions instanceof Map
376
+ ? executions
377
+ : new Map(Object.entries(executions || {}));
378
+ const receipts = [];
379
+ const gates = [];
380
+ const issues = [];
381
+ for (const entry of matrix.entries) {
382
+ const execution = executionMap.get(entry.id);
383
+ const validated = validateExecution(entry, execution, sourceSha);
384
+ issues.push(...validated.issues.map((issue) => `${entry.id}: ${issue}`));
385
+ gates.push(...validated.gateRows);
386
+ receipts.push({
387
+ platformId: entry.id,
388
+ platform: entry.platform,
389
+ runId: execution?.receipt?.runId || "",
390
+ status: execution?.receipt?.status || "missing",
391
+ qualifying:
392
+ execution?.receipt?.qualifying === true &&
393
+ execution?.validation?.qualifying === true,
394
+ integrityDigest: execution?.receipt?.integrity?.digest || "",
395
+ issues: validated.issues,
396
+ });
397
+ }
398
+ const base = {
399
+ contract: GATE_AGGREGATE_CONTRACT,
400
+ profile: matrix.profile,
401
+ sourceSha,
402
+ registry: matrix.registry,
403
+ matrixDigest: matrix.digest,
404
+ status: issues.length ? "fail" : "pass",
405
+ ok: issues.length === 0,
406
+ qualifying: issues.length === 0,
407
+ receipts,
408
+ gates,
409
+ omitted: matrix.omitted,
410
+ issues,
411
+ };
412
+ return { ...base, digest: sha256(base) };
413
+ }
@@ -25,6 +25,8 @@ export function generateReleaseCandidatePassportCli() {
25
25
  "BUILDCHAIN_RC_VERSION",
26
26
  buildSummary.publishSource?.consumerVersion || "",
27
27
  );
28
+ const gateAggregateJson = env("BUILDCHAIN_GATE_PROFILE_AGGREGATE_JSON");
29
+ const gateAggregate = gateAggregateJson ? JSON.parse(gateAggregateJson) : undefined;
28
30
  const passport = createReleaseCandidatePassport({
29
31
  repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
30
32
  pullRequest: {
@@ -46,6 +48,7 @@ export function generateReleaseCandidatePassportCli() {
46
48
  version: env("BUILDCHAIN_RUNTIME_VERSION"),
47
49
  workflowShellRef: env("BUILDCHAIN_WORKFLOW_SHELL_REF", buildSummary.runtime?.workflowShellRef || ""),
48
50
  },
51
+ gateAggregate,
49
52
  workflow: {
50
53
  name: env("GITHUB_WORKFLOW"),
51
54
  runId: env("GITHUB_RUN_ID", buildSummary.git?.runId || ""),
@@ -73,6 +76,7 @@ export function generateReleaseCandidatePassportCli() {
73
76
  source: passport.source,
74
77
  candidateHash: passport.candidateHash,
75
78
  platformCount: passport.platformMatrix.length,
79
+ gateProfileEvidence: passport.gateProfileEvidence,
76
80
  }),
77
81
  });
78
82
  return passport;