@kungfu-tech/buildchain 2.3.1-alpha.0 → 2.3.1-alpha.1

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.
@@ -1,8 +1,9 @@
1
1
  import crypto from "node:crypto";
2
- import { execSync } from "node:child_process";
2
+ import { execFileSync, execSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
6
7
  import {
7
8
  loadBuildchainConfig,
8
9
  normalizeLifecycleStage,
@@ -13,12 +14,32 @@ import {
13
14
  readBuildchainLogEvents,
14
15
  summarizeBuildchainLogEvents,
15
16
  } from "../packages/core/logging.js";
17
+ import {
18
+ BUILDCHAIN_DIAGNOSTICS_CONTRACT,
19
+ BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT,
20
+ BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT,
21
+ BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
22
+ createDiagnosticsArtifact,
23
+ summarizeLifecycleObservability,
24
+ writeDiagnosticsArtifact,
25
+ } from "../packages/core/diagnostics.js";
16
26
  import {
17
27
  createArtifactSummary,
18
28
  parseExpectedArtifactsJson,
19
29
  validateExpectedArtifacts,
20
30
  } from "./build-contract-core.mjs";
21
31
 
32
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
33
+ const buildchainCliCandidates = [
34
+ path.resolve(moduleDir, "..", "bin", "buildchain.mjs"),
35
+ path.resolve(moduleDir, "..", "..", "..", "bin", "buildchain.mjs"),
36
+ path.resolve(process.cwd(), ".buildchain", "runtime", "bin", "buildchain.mjs"),
37
+ ];
38
+
39
+ function resolveBuildchainCliPath() {
40
+ return buildchainCliCandidates.find((candidate) => fs.existsSync(candidate)) || buildchainCliCandidates[0];
41
+ }
42
+
22
43
  function sha256File(filePath) {
23
44
  const hash = crypto.createHash("sha256");
24
45
  hash.update(fs.readFileSync(filePath));
@@ -71,6 +92,224 @@ function collectArtifactFiles(root, patterns) {
71
92
  return [...files].sort();
72
93
  }
73
94
 
95
+ function readProcessSummaryArtifact(filePath) {
96
+ if (!filePath) {
97
+ return undefined;
98
+ }
99
+ if (!fs.existsSync(filePath)) {
100
+ throw new Error(`process summary file not found: ${filePath}`);
101
+ }
102
+ let artifact;
103
+ try {
104
+ artifact = JSON.parse(fs.readFileSync(filePath, "utf8"));
105
+ } catch (error) {
106
+ throw new Error(`failed to read process summary file ${filePath}: ${error.message}`);
107
+ }
108
+ if (artifact?.contract === BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT && artifact.summary) {
109
+ return {
110
+ artifact,
111
+ summary: artifact.summary,
112
+ samplesPath: artifact.samplesPath || "",
113
+ };
114
+ }
115
+ if (artifact?.contract === BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT) {
116
+ return {
117
+ artifact,
118
+ summary: artifact,
119
+ samplesPath: "",
120
+ };
121
+ }
122
+ throw new Error(`process summary file has unsupported contract: ${artifact?.contract || "unknown"}`);
123
+ }
124
+
125
+ function shellCommandArgs(command, shell) {
126
+ if (typeof shell === "string" && shell.trim()) {
127
+ return [shell, "-c", command];
128
+ }
129
+ if (process.platform === "win32") {
130
+ return [process.env.ComSpec || "cmd.exe", "/d", "/s", "/c", command];
131
+ }
132
+ return [process.env.SHELL || "/bin/sh", "-c", command];
133
+ }
134
+
135
+ function samplerPathForCwd(filePath, cwd) {
136
+ const relative = path.relative(cwd, filePath);
137
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
138
+ ? toPosix(relative)
139
+ : filePath;
140
+ }
141
+
142
+ function executeSampledShellCommand({
143
+ command,
144
+ cwd,
145
+ env,
146
+ shell,
147
+ timeout,
148
+ label,
149
+ processSummaryPath,
150
+ processSamplesPath,
151
+ processSampleIntervalMs,
152
+ requestedParallelism,
153
+ }) {
154
+ fs.mkdirSync(path.dirname(processSummaryPath), { recursive: true });
155
+ fs.mkdirSync(path.dirname(processSamplesPath), { recursive: true });
156
+ const args = [
157
+ resolveBuildchainCliPath(),
158
+ "sample",
159
+ "process-tree",
160
+ "--label",
161
+ label || "lifecycle",
162
+ "--interval-ms",
163
+ String(processSampleIntervalMs || 15000),
164
+ "--output",
165
+ samplerPathForCwd(processSamplesPath, cwd),
166
+ "--summary-output",
167
+ samplerPathForCwd(processSummaryPath, cwd),
168
+ ];
169
+ if (Number(requestedParallelism || 0) > 0) {
170
+ args.push("--requested-parallelism", String(Number(requestedParallelism)));
171
+ }
172
+ args.push("--", ...shellCommandArgs(command, shell));
173
+ execFileSync(process.execPath, args, {
174
+ cwd,
175
+ env,
176
+ stdio: "inherit",
177
+ timeout,
178
+ });
179
+ }
180
+
181
+ function stageCommandText(stage) {
182
+ if (stage.mode === "script") {
183
+ return stage.script;
184
+ }
185
+ if (process.platform === "win32") {
186
+ return stage.commands.join(" && ");
187
+ }
188
+ return ["set -e", ...stage.commands].join("\n");
189
+ }
190
+
191
+ function runLifecycleStageWithSampler({
192
+ cwd,
193
+ loadedConfig,
194
+ name,
195
+ env,
196
+ sampleProcessTree,
197
+ processSummaryPath,
198
+ processSamplesPath,
199
+ processSampleIntervalMs,
200
+ requestedParallelism,
201
+ }) {
202
+ if (!sampleProcessTree) {
203
+ return runLifecycleStage({ cwd, loadedConfig, name, env });
204
+ }
205
+ const lifecycle = loadedConfig?.config?.lifecycle || {};
206
+ const stage = lifecycle[name];
207
+ if (!stage) {
208
+ return false;
209
+ }
210
+ const stageEnv = {
211
+ ...process.env,
212
+ ...(lifecycle.env || {}),
213
+ ...(stage.env || {}),
214
+ ...(env || {}),
215
+ };
216
+ const timeout = stage.timeoutMinutes ? stage.timeoutMinutes * 60_000 : undefined;
217
+ let lastError;
218
+ for (let attempt = 1; attempt <= stage.retries; attempt += 1) {
219
+ try {
220
+ executeSampledShellCommand({
221
+ command: stageCommandText(stage),
222
+ cwd,
223
+ env: stageEnv,
224
+ shell: stage.shell || true,
225
+ timeout,
226
+ label: `lifecycle-${name}`,
227
+ processSummaryPath,
228
+ processSamplesPath,
229
+ processSampleIntervalMs,
230
+ requestedParallelism,
231
+ });
232
+ return true;
233
+ } catch (error) {
234
+ lastError = error;
235
+ if (attempt < stage.retries) {
236
+ console.log(`> lifecycle ${name || "stage"} failed, retry ${attempt + 1}/${stage.retries}`);
237
+ }
238
+ }
239
+ }
240
+ throw lastError;
241
+ }
242
+
243
+ function writeJsonlEvents(filePath, events = []) {
244
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
245
+ fs.writeFileSync(filePath, events.map((event) => JSON.stringify(event)).join("\n") + (events.length ? "\n" : ""));
246
+ }
247
+
248
+ function copyIfExists(sourcePath, targetPath) {
249
+ if (!sourcePath || !fs.existsSync(sourcePath)) {
250
+ return false;
251
+ }
252
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
253
+ fs.copyFileSync(sourcePath, targetPath);
254
+ return true;
255
+ }
256
+
257
+ function resolveLinkedFilePath({ linkedPath = "", workspace, cwd, fallbackDir }) {
258
+ if (!linkedPath) {
259
+ return "";
260
+ }
261
+ const candidates = path.isAbsolute(linkedPath)
262
+ ? [linkedPath]
263
+ : [
264
+ path.resolve(workspace, linkedPath),
265
+ path.resolve(cwd, linkedPath),
266
+ path.resolve(fallbackDir, linkedPath),
267
+ ];
268
+ return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0] || "";
269
+ }
270
+
271
+ function diagnosticsSidecarEntry({ kind, filePath, workspace, required = false }) {
272
+ if (!filePath || !fs.existsSync(filePath)) {
273
+ return null;
274
+ }
275
+ const stat = fs.statSync(filePath);
276
+ if (!stat.isFile()) {
277
+ return null;
278
+ }
279
+ return {
280
+ kind,
281
+ path: toPosix(path.relative(workspace, filePath)),
282
+ bytes: stat.size,
283
+ sha256: sha256File(filePath),
284
+ required: Boolean(required),
285
+ };
286
+ }
287
+
288
+ function writeDiagnosticsSidecarManifest(filePath, {
289
+ workspace,
290
+ artifactName,
291
+ platformId,
292
+ diagnosticsArtifactName = "",
293
+ files = [],
294
+ }) {
295
+ const entries = files
296
+ .map((entry) => diagnosticsSidecarEntry({ ...entry, workspace }))
297
+ .filter(Boolean);
298
+ const manifest = {
299
+ schemaVersion: 1,
300
+ contract: BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT,
301
+ generatedAt: new Date().toISOString(),
302
+ artifactName,
303
+ platformId,
304
+ ...(diagnosticsArtifactName ? { diagnosticsArtifactName } : {}),
305
+ fileCount: entries.length,
306
+ totalBytes: entries.reduce((sum, entry) => sum + entry.bytes, 0),
307
+ files: entries,
308
+ };
309
+ fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
310
+ return manifest;
311
+ }
312
+
74
313
  export function runLifecycle({
75
314
  cwd = process.cwd(),
76
315
  stageName = "",
@@ -78,20 +317,52 @@ export function runLifecycle({
78
317
  required = false,
79
318
  manifestPath = ".buildchain/artifacts/manifest.json",
80
319
  summaryPath = ".buildchain/artifacts/summary.json",
320
+ diagnosticsPath = "",
81
321
  artifactName = "buildchain-artifact",
322
+ manifestArtifactName = "",
323
+ diagnosticsArtifactName = "",
82
324
  platformId = os.platform(),
83
325
  platformName = platformId,
84
326
  artifactPaths = [],
85
327
  expectedArtifactsJson = "",
86
328
  workspace = process.cwd(),
87
329
  logPath = process.env.BUILDCHAIN_LOG_PATH || ".buildchain/logs/events.jsonl",
330
+ processSummaryPath = "",
331
+ processSamplesPath = ".buildchain/diagnostics/process-samples.jsonl",
332
+ sampleProcessTree = false,
333
+ processSampleIntervalMs = 15000,
334
+ requestedParallelism = 0,
335
+ processSummaryRequired = true,
88
336
  } = {}) {
89
337
  const resolvedCwd = path.resolve(cwd);
90
338
  const resolvedWorkspace = path.resolve(workspace);
91
339
  const resolvedManifestPath = path.resolve(resolvedWorkspace, manifestPath);
92
340
  const resolvedSummaryPath = path.resolve(resolvedWorkspace, summaryPath);
341
+ const resolvedDiagnosticsPath = path.resolve(
342
+ resolvedWorkspace,
343
+ diagnosticsPath || path.join(path.dirname(manifestPath), "diagnostics.json"),
344
+ );
93
345
  const resolvedLogPath = logPath ? path.resolve(resolvedWorkspace, logPath) : "";
346
+ const resolvedProcessSummaryPath = processSummaryPath || sampleProcessTree
347
+ ? path.resolve(resolvedWorkspace, processSummaryPath || ".buildchain/diagnostics/process-summary.json")
348
+ : "";
349
+ const resolvedProcessSamplesPath = processSamplesPath
350
+ ? path.resolve(resolvedWorkspace, processSamplesPath)
351
+ : path.resolve(resolvedWorkspace, ".buildchain/diagnostics/process-samples.jsonl");
94
352
  const relativeLogPath = resolvedLogPath ? toPosix(path.relative(resolvedWorkspace, resolvedLogPath)) : "";
353
+ const relativeProcessSummaryPath = resolvedProcessSummaryPath
354
+ ? toPosix(path.relative(resolvedWorkspace, resolvedProcessSummaryPath))
355
+ : "";
356
+ const relativeDiagnosticsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsPath));
357
+ const diagnosticsDir = path.dirname(resolvedDiagnosticsPath);
358
+ const resolvedDiagnosticsEventsPath = path.join(diagnosticsDir, "events.jsonl");
359
+ const resolvedDiagnosticsProcessSummaryPath = path.join(diagnosticsDir, "process-summary.json");
360
+ const resolvedDiagnosticsProcessSamplesPath = path.join(diagnosticsDir, "process-samples.jsonl");
361
+ const resolvedDiagnosticsManifestPath = path.join(diagnosticsDir, "diagnostics-manifest.json");
362
+ const relativeDiagnosticsEventsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsEventsPath));
363
+ const relativeDiagnosticsProcessSummaryPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSummaryPath));
364
+ const relativeDiagnosticsProcessSamplesPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSamplesPath));
365
+ const relativeDiagnosticsManifestPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsManifestPath));
95
366
  const logRunId = crypto.randomUUID();
96
367
  const frameworkLog = createBuildchainLogger({
97
368
  cwd: resolvedWorkspace,
@@ -130,23 +401,39 @@ export function runLifecycle({
130
401
  attributes: {
131
402
  commandSource,
132
403
  stage: stageName || "command",
404
+ sampleProcessTree,
133
405
  },
134
406
  });
135
407
  try {
136
- execSync(command, {
137
- cwd: resolvedCwd,
138
- env: {
139
- ...process.env,
140
- ...(resolvedLogPath
141
- ? {
142
- BUILDCHAIN_LOG_PATH: resolvedLogPath,
143
- BUILDCHAIN_LOG_RUN_ID: logRunId,
144
- }
145
- : {}),
146
- },
147
- shell: true,
148
- stdio: "inherit",
149
- });
408
+ const commandEnv = {
409
+ ...process.env,
410
+ ...(resolvedLogPath
411
+ ? {
412
+ BUILDCHAIN_LOG_PATH: resolvedLogPath,
413
+ BUILDCHAIN_LOG_RUN_ID: logRunId,
414
+ }
415
+ : {}),
416
+ };
417
+ if (sampleProcessTree) {
418
+ executeSampledShellCommand({
419
+ command,
420
+ cwd: resolvedCwd,
421
+ env: commandEnv,
422
+ shell: true,
423
+ label: `lifecycle-${stageName || "command"}`,
424
+ processSummaryPath: resolvedProcessSummaryPath,
425
+ processSamplesPath: resolvedProcessSamplesPath,
426
+ processSampleIntervalMs,
427
+ requestedParallelism,
428
+ });
429
+ } else {
430
+ execSync(command, {
431
+ cwd: resolvedCwd,
432
+ env: commandEnv,
433
+ shell: true,
434
+ stdio: "inherit",
435
+ });
436
+ }
150
437
  executed = true;
151
438
  userLog.info("lifecycle.command.end", {
152
439
  durationMs: Date.now() - startedAt,
@@ -162,6 +449,7 @@ export function runLifecycle({
162
449
  attributes: lifecycleErrorAttributes(error, {
163
450
  commandSource,
164
451
  stage: stageName || "command",
452
+ sampleProcessTree,
165
453
  }),
166
454
  });
167
455
  throw error;
@@ -173,10 +461,11 @@ export function runLifecycle({
173
461
  attributes: {
174
462
  commandSource,
175
463
  stage: stageName,
464
+ sampleProcessTree,
176
465
  },
177
466
  });
178
467
  try {
179
- executed = runLifecycleStage({
468
+ executed = runLifecycleStageWithSampler({
180
469
  cwd: resolvedCwd,
181
470
  loadedConfig,
182
471
  name: stageName,
@@ -186,6 +475,11 @@ export function runLifecycle({
186
475
  BUILDCHAIN_LOG_RUN_ID: logRunId,
187
476
  }
188
477
  : {},
478
+ sampleProcessTree,
479
+ processSummaryPath: resolvedProcessSummaryPath,
480
+ processSamplesPath: resolvedProcessSamplesPath,
481
+ processSampleIntervalMs,
482
+ requestedParallelism,
189
483
  });
190
484
  userLog.info("lifecycle.stage.end", {
191
485
  durationMs: Date.now() - startedAt,
@@ -202,6 +496,7 @@ export function runLifecycle({
202
496
  attributes: lifecycleErrorAttributes(error, {
203
497
  commandSource,
204
498
  stage: stageName,
499
+ sampleProcessTree,
205
500
  }),
206
501
  });
207
502
  throw error;
@@ -218,6 +513,13 @@ export function runLifecycle({
218
513
  throw new Error(`required lifecycle stage did not run: ${stageName || "command"}`);
219
514
  }
220
515
 
516
+ const shouldReadProcessSummary = Boolean(
517
+ resolvedProcessSummaryPath
518
+ && (fs.existsSync(resolvedProcessSummaryPath) || processSummaryRequired),
519
+ );
520
+ const processSummaryArtifact = shouldReadProcessSummary
521
+ ? readProcessSummaryArtifact(resolvedProcessSummaryPath)
522
+ : undefined;
221
523
  fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
222
524
  const scanStartedAt = Date.now();
223
525
  const files = collectArtifactFiles(resolvedWorkspace, artifactPaths);
@@ -229,8 +531,9 @@ export function runLifecycle({
229
531
  sha256: sha256File(file),
230
532
  };
231
533
  });
534
+ const artifactScanDurationMs = Date.now() - scanStartedAt;
232
535
  frameworkLog.info("artifact.scan", {
233
- durationMs: Date.now() - scanStartedAt,
536
+ durationMs: artifactScanDurationMs,
234
537
  attributes: {
235
538
  fileCount: manifestFiles.length,
236
539
  },
@@ -279,6 +582,26 @@ export function runLifecycle({
279
582
  : summarizeBuildchainLogEvents([...frameworkLog.events, ...userLog.events]),
280
583
  },
281
584
  };
585
+ const lifecycleObservability = summarizeLifecycleObservability({
586
+ events: resolvedLogPath ? readBuildchainLogEvents(resolvedLogPath) : [...frameworkLog.events, ...userLog.events],
587
+ logPath: relativeLogPath,
588
+ artifactScanDurationMs,
589
+ totalBytes: summary.totalBytes,
590
+ fileCount: summary.fileCount,
591
+ });
592
+ observability.lifecycle = lifecycleObservability;
593
+ observability.diagnostics = {
594
+ contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
595
+ path: relativeDiagnosticsPath,
596
+ manifestPath: relativeDiagnosticsManifestPath,
597
+ eventsPath: relativeDiagnosticsEventsPath,
598
+ };
599
+ if (relativeProcessSummaryPath) {
600
+ observability.process = {
601
+ contract: BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
602
+ path: relativeProcessSummaryPath,
603
+ };
604
+ }
282
605
  const summaryWithObservability = {
283
606
  ...summary,
284
607
  observability,
@@ -309,6 +632,57 @@ export function runLifecycle({
309
632
  fs.writeFileSync(resolvedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
310
633
  fs.mkdirSync(path.dirname(resolvedSummaryPath), { recursive: true });
311
634
  fs.writeFileSync(resolvedSummaryPath, `${JSON.stringify(summaryWithObservability, null, 2)}\n`);
635
+ writeDiagnosticsArtifact(
636
+ resolvedDiagnosticsPath,
637
+ createDiagnosticsArtifact({
638
+ cwd: resolvedCwd,
639
+ logPath: resolvedLogPath,
640
+ artifactPaths,
641
+ lifecycleObservability,
642
+ processSummary: processSummaryArtifact?.summary,
643
+ links: {
644
+ artifactName,
645
+ platformId,
646
+ ...(manifestArtifactName ? { manifestArtifactName } : {}),
647
+ ...(diagnosticsArtifactName ? { diagnosticsArtifactName } : {}),
648
+ manifest: toPosix(path.relative(resolvedWorkspace, resolvedManifestPath)),
649
+ summary: toPosix(path.relative(resolvedWorkspace, resolvedSummaryPath)),
650
+ log: relativeLogPath,
651
+ diagnosticsManifest: relativeDiagnosticsManifestPath,
652
+ diagnosticsEvents: relativeDiagnosticsEventsPath,
653
+ ...(relativeProcessSummaryPath ? { processSummary: relativeProcessSummaryPath } : {}),
654
+ ...(processSummaryArtifact ? { diagnosticsProcessSummary: relativeDiagnosticsProcessSummaryPath } : {}),
655
+ ...(processSummaryArtifact?.samplesPath ? { diagnosticsProcessSamples: relativeDiagnosticsProcessSamplesPath } : {}),
656
+ },
657
+ }),
658
+ );
659
+ if (resolvedLogPath && fs.existsSync(resolvedLogPath)) {
660
+ copyIfExists(resolvedLogPath, resolvedDiagnosticsEventsPath);
661
+ } else {
662
+ writeJsonlEvents(resolvedDiagnosticsEventsPath, [...frameworkLog.events, ...userLog.events]);
663
+ }
664
+ if (processSummaryArtifact) {
665
+ copyIfExists(resolvedProcessSummaryPath, resolvedDiagnosticsProcessSummaryPath);
666
+ const resolvedSamplesPath = resolveLinkedFilePath({
667
+ linkedPath: processSummaryArtifact.samplesPath,
668
+ workspace: resolvedWorkspace,
669
+ cwd: resolvedCwd,
670
+ fallbackDir: path.dirname(resolvedProcessSummaryPath),
671
+ });
672
+ copyIfExists(resolvedSamplesPath, resolvedDiagnosticsProcessSamplesPath);
673
+ }
674
+ writeDiagnosticsSidecarManifest(resolvedDiagnosticsManifestPath, {
675
+ workspace: resolvedWorkspace,
676
+ artifactName,
677
+ platformId,
678
+ diagnosticsArtifactName,
679
+ files: [
680
+ { kind: "diagnostics", filePath: resolvedDiagnosticsPath, required: true },
681
+ { kind: "events", filePath: resolvedDiagnosticsEventsPath, required: true },
682
+ { kind: "process-summary", filePath: resolvedDiagnosticsProcessSummaryPath },
683
+ { kind: "process-samples", filePath: resolvedDiagnosticsProcessSamplesPath },
684
+ ],
685
+ });
312
686
  console.log(`buildchain_manifest=${path.relative(resolvedWorkspace, resolvedManifestPath)}`);
313
687
  return manifest;
314
688
  }
@@ -18,6 +18,14 @@ function parseList(value) {
18
18
  .filter(Boolean);
19
19
  }
20
20
 
21
+ function readBooleanArg(name, fallback = "false") {
22
+ return readArg(name, fallback) === "true";
23
+ }
24
+
25
+ function readNumberArg(name, fallback) {
26
+ return Number(readArg(name, String(fallback)) || fallback);
27
+ }
28
+
21
29
  export function runLifecycleCli() {
22
30
  return runLifecycle({
23
31
  cwd: readArg("cwd", process.cwd()),
@@ -27,10 +35,18 @@ export function runLifecycleCli() {
27
35
  manifestPath: readArg("manifest-path", ".buildchain/artifacts/manifest.json"),
28
36
  summaryPath: readArg("summary-path", ".buildchain/artifacts/summary.json"),
29
37
  artifactName: readArg("artifact-name", "buildchain-artifact"),
38
+ manifestArtifactName: readArg("manifest-artifact-name", ""),
39
+ diagnosticsArtifactName: readArg("diagnostics-artifact-name", ""),
30
40
  platformId: readArg("platform-id", os.platform()),
31
41
  platformName: readArg("platform-name", readArg("platform-id", os.platform())),
32
42
  artifactPaths: parseList(process.env.BUILDCHAIN_ARTIFACT_PATHS || ""),
33
43
  expectedArtifactsJson: process.env.BUILDCHAIN_EXPECTED_ARTIFACTS_JSON || "",
44
+ processSummaryPath: readArg("process-summary", process.env.BUILDCHAIN_PROCESS_SUMMARY_PATH || ""),
45
+ processSamplesPath: readArg("process-samples", process.env.BUILDCHAIN_PROCESS_SAMPLES_PATH || ".buildchain/diagnostics/process-samples.jsonl"),
46
+ sampleProcessTree: readBooleanArg("sample-process-tree", process.env.BUILDCHAIN_SAMPLE_PROCESS_TREE || "false"),
47
+ processSampleIntervalMs: readNumberArg("process-sample-interval-ms", process.env.BUILDCHAIN_PROCESS_SAMPLE_INTERVAL_MS || 15000),
48
+ requestedParallelism: readNumberArg("requested-parallelism", process.env.BUILDCHAIN_REQUESTED_PARALLELISM || 0),
49
+ processSummaryRequired: readBooleanArg("process-summary-required", process.env.BUILDCHAIN_PROCESS_SUMMARY_REQUIRED || "true"),
34
50
  workspace: process.cwd(),
35
51
  });
36
52
  }
@@ -0,0 +1,109 @@
1
+ const EXACT_SHA_RE = /^[0-9a-f]{40}$/i;
2
+ const TRAIN_REF_RE = /^train\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
3
+
4
+ export function parseWorkflowShellRef(workflowRef = "", fallback = "v2", buildchainRepository = "kungfu-systems/buildchain") {
5
+ const value = String(workflowRef || "");
6
+ const expectedPrefix = `${buildchainRepository}/.github/workflows/`;
7
+ if (!value.startsWith(expectedPrefix)) {
8
+ return fallback;
9
+ }
10
+ const ref = value.split("@").pop()?.trim();
11
+ return ref || fallback;
12
+ }
13
+
14
+ export function classifyBuildchainRuntimeRef(ref = "") {
15
+ const value = String(ref || "").trim().replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "");
16
+ if (EXACT_SHA_RE.test(value)) {
17
+ return "exact-sha";
18
+ }
19
+ if (TRAIN_REF_RE.test(value)) {
20
+ return "train";
21
+ }
22
+ if (/^v\d+(?:\.\d+)?$/.test(value) || /^v\d+\.\d+\.\d+$/.test(value)) {
23
+ return "stable";
24
+ }
25
+ if (/^v\d+(?:\.\d+)?-alpha$/.test(value) || /^v\d+\.\d+\.\d+-alpha\.\d+$/.test(value)) {
26
+ return "alpha";
27
+ }
28
+ return "development";
29
+ }
30
+
31
+ export function normalizeRequestedRuntimeRef(requestedRef = "") {
32
+ const requested = String(requestedRef || "").trim();
33
+ if (!requested) {
34
+ return { ref: "", fullRef: "", class: "", exactSha: false };
35
+ }
36
+ if (EXACT_SHA_RE.test(requested)) {
37
+ return { ref: requested, fullRef: requested, class: "exact-sha", exactSha: true };
38
+ }
39
+ const trainRef = requested.replace(/^refs\/heads\//, "");
40
+ if (!TRAIN_REF_RE.test(trainRef)) {
41
+ throw new Error(
42
+ "buildchain-ref override must be train/vN/vN.M/<capability>, refs/heads/train/vN/vN.M/<capability>, or an exact 40-character SHA",
43
+ );
44
+ }
45
+ return {
46
+ ref: trainRef,
47
+ fullRef: `refs/heads/${trainRef}`,
48
+ class: "train",
49
+ exactSha: false,
50
+ };
51
+ }
52
+
53
+ export function resolveRuntimeSelection({
54
+ requestedRef = "",
55
+ workflowRef = "",
56
+ defaultStableRef = "v2",
57
+ buildchainRepository = "kungfu-systems/buildchain",
58
+ } = {}) {
59
+ const requested = String(requestedRef || "").trim();
60
+ const workflowShellRef = parseWorkflowShellRef(workflowRef, defaultStableRef, buildchainRepository);
61
+ if (!requested) {
62
+ return {
63
+ requestedRef: "",
64
+ runtimeRef: workflowShellRef || defaultStableRef,
65
+ runtimeFullRef: workflowShellRef || defaultStableRef,
66
+ runtimeClass: classifyBuildchainRuntimeRef(workflowShellRef || defaultStableRef),
67
+ runtimeOverride: false,
68
+ workflowShellRef: workflowShellRef || defaultStableRef,
69
+ rollbackRef: workflowShellRef || defaultStableRef,
70
+ trustDecision: "stable-default",
71
+ };
72
+ }
73
+ const normalized = normalizeRequestedRuntimeRef(requested);
74
+ return {
75
+ requestedRef: requested,
76
+ runtimeRef: normalized.ref,
77
+ runtimeFullRef: normalized.fullRef,
78
+ runtimeClass: normalized.class,
79
+ runtimeOverride: true,
80
+ workflowShellRef: workflowShellRef || defaultStableRef,
81
+ rollbackRef: workflowShellRef || defaultStableRef,
82
+ trustDecision: "override-requested",
83
+ };
84
+ }
85
+
86
+ export function validateRuntimeOverrideTrust({
87
+ requestedRef = "",
88
+ eventName = "",
89
+ actorPermission = "",
90
+ } = {}) {
91
+ if (!String(requestedRef || "").trim()) {
92
+ return { ok: true, decision: "stable-default" };
93
+ }
94
+ if (eventName !== "workflow_dispatch") {
95
+ return {
96
+ ok: false,
97
+ decision: "rejected-untrusted-event",
98
+ reason: "buildchain-ref override is only allowed for trusted workflow_dispatch runs",
99
+ };
100
+ }
101
+ if (!new Set(["admin", "maintain", "write"]).has(String(actorPermission || ""))) {
102
+ return {
103
+ ok: false,
104
+ decision: "rejected-insufficient-permission",
105
+ reason: "buildchain-ref override requires actor write, maintain, or admin permission",
106
+ };
107
+ }
108
+ return { ok: true, decision: "override-accepted" };
109
+ }
@@ -584,6 +584,10 @@ export function planWebSurfaceDeploy({
584
584
  sourceSha = "",
585
585
  artifactHash = "",
586
586
  artifactPath = "",
587
+ runtimeId = "",
588
+ configFingerprint = "",
589
+ rollbackPointer = "",
590
+ rollbackLimitations = "",
587
591
  dryRun = true,
588
592
  deployedAt = new Date().toISOString(),
589
593
  } = {}) {
@@ -608,6 +612,10 @@ export function planWebSurfaceDeploy({
608
612
  alias,
609
613
  sourceSha,
610
614
  artifactHash: resolvedArtifact.artifactHash,
615
+ runtimeId,
616
+ configFingerprint,
617
+ rollbackPointer,
618
+ rollbackLimitations,
611
619
  deployedAt,
612
620
  });
613
621
  return {
@@ -82,6 +82,10 @@ export function webSurfaceCli() {
82
82
  sourceSha,
83
83
  artifactHash: readArg("artifact-hash", process.env.BUILDCHAIN_WEB_SURFACE_ARTIFACT_HASH || ""),
84
84
  artifactPath: readArg("artifact-path", process.env.BUILDCHAIN_WEB_SURFACE_ARTIFACT_PATH || ""),
85
+ runtimeId: readArg("runtime-id", process.env.BUILDCHAIN_RUNTIME_ID || ""),
86
+ configFingerprint: readArg("config-fingerprint", process.env.BUILDCHAIN_CONFIG_FINGERPRINT || ""),
87
+ rollbackPointer: readArg("rollback-pointer", process.env.BUILDCHAIN_ROLLBACK_REF || ""),
88
+ rollbackLimitations: readArg("rollback-limitations", process.env.BUILDCHAIN_ROLLBACK_LIMITATIONS || ""),
85
89
  dryRun: readBooleanArg("dry-run", true),
86
90
  });
87
91
  const outputResult = mode === "manifest" ? result.manifest : result;