@kungfu-tech/buildchain 2.5.1-alpha.0 → 2.5.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.
@@ -311,6 +311,21 @@ function packageVersion() {
311
311
  return packageJson.version;
312
312
  }
313
313
 
314
+ function createTailBuffer(limit = 64 * 1024) {
315
+ let value = "";
316
+ return {
317
+ append(chunk) {
318
+ value += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk || "");
319
+ if (value.length > limit) {
320
+ value = value.slice(value.length - limit);
321
+ }
322
+ },
323
+ text() {
324
+ return value;
325
+ },
326
+ };
327
+ }
328
+
314
329
  async function runProcessTreeSample(sampleArgs = []) {
315
330
  const separator = sampleArgs.indexOf("--");
316
331
  const optionArgs = separator === -1 ? sampleArgs : sampleArgs.slice(0, separator);
@@ -326,10 +341,20 @@ async function runProcessTreeSample(sampleArgs = []) {
326
341
  const outputPath = readFlag(optionArgs, "output", ".buildchain/diagnostics/process-samples.jsonl");
327
342
  const summaryOutputPath = readFlag(optionArgs, "summary-output", ".buildchain/diagnostics/process-summary.json");
328
343
  const startedAt = Date.now();
344
+ const stdoutTail = createTailBuffer();
345
+ const stderrTail = createTailBuffer();
329
346
  const child = spawn(command, args, {
330
347
  cwd: process.cwd(),
331
348
  env: process.env,
332
- stdio: "inherit",
349
+ stdio: ["ignore", "pipe", "pipe"],
350
+ });
351
+ child.stdout?.on("data", (chunk) => {
352
+ stdoutTail.append(chunk);
353
+ process.stdout.write(chunk);
354
+ });
355
+ child.stderr?.on("data", (chunk) => {
356
+ stderrTail.append(chunk);
357
+ process.stderr.write(chunk);
333
358
  });
334
359
  const sampler = startProcessSampler({
335
360
  rootPid: child.pid || process.pid,
@@ -345,7 +370,7 @@ async function runProcessTreeSample(sampleArgs = []) {
345
370
  });
346
371
  const result = await new Promise((resolve) => {
347
372
  child.on("error", (error) => resolve({ error, status: 1, signal: "" }));
348
- child.on("close", (status, signal) => resolve({ status, signal: signal || "" }));
373
+ child.on("close", (status, signal) => resolve({ status: status ?? 0, signal: signal || "" }));
349
374
  });
350
375
  const samples = sampler.stop();
351
376
  const summary = summarizeProcessSamples({
@@ -366,6 +391,16 @@ async function runProcessTreeSample(sampleArgs = []) {
366
391
  signal: result.signal || "",
367
392
  error: result.error?.message || "",
368
393
  },
394
+ wrappedCommand: {
395
+ command,
396
+ args,
397
+ rootPid: child.pid || 0,
398
+ exitCode: result.status ?? 0,
399
+ signal: result.signal || "",
400
+ error: result.error?.message || "",
401
+ stdoutTail: stdoutTail.text(),
402
+ stderrTail: stderrTail.text(),
403
+ },
369
404
  durationMs: Date.now() - startedAt,
370
405
  samplesPath: outputPath,
371
406
  summaryPath: summaryOutputPath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.5.1-alpha.0",
3
+ "version": "2.5.1",
4
4
  "private": false,
5
5
  "description": "Buildchain Release Passport, release governance, CLI toolkit, and site facts.",
6
6
  "repository": "https://github.com/kungfu-systems/buildchain",
@@ -637,7 +637,9 @@ function processCommandFromLine(commandLine = "", fallback = "") {
637
637
 
638
638
  function normalizeProcessRows({ rootPid, platform, rows = [] }) {
639
639
  const byParent = new Map();
640
+ const byPid = new Map();
640
641
  for (const row of rows) {
642
+ byPid.set(row.pid, row);
641
643
  byParent.set(row.ppid, [...(byParent.get(row.ppid) || []), row]);
642
644
  }
643
645
  const seen = new Set();
@@ -654,20 +656,32 @@ function normalizeProcessRows({ rootPid, platform, rows = [] }) {
654
656
  stack.push(child.pid);
655
657
  }
656
658
  }
657
- return { rootPid: String(rootPid), platform, processes };
659
+ return { rootPid: String(rootPid), platform, rootProcess: byPid.get(String(rootPid)) || undefined, processes };
658
660
  }
659
661
 
660
662
  function collectWindowsProcessRows({ cwd, runCommand }) {
661
663
  const script = [
662
- "$ErrorActionPreference = 'Stop'",
664
+ "$ErrorActionPreference = 'Stop';",
663
665
  "Get-CimInstance Win32_Process |",
664
666
  "Select-Object ProcessId,ParentProcessId,Name,CommandLine |",
665
667
  "ConvertTo-Json -Compress",
666
- ].join("; ");
667
- const output = runCommand("powershell", ["-NoProfile", "-Command", script], {
668
- cwd,
669
- timeoutMs: 5000,
670
- });
668
+ ].join(" ");
669
+ const errors = [];
670
+ let output = "";
671
+ for (const command of ["powershell.exe", "powershell", "pwsh"]) {
672
+ try {
673
+ output = runCommand(command, ["-NoProfile", "-NonInteractive", "-Command", script], {
674
+ cwd,
675
+ timeoutMs: 5000,
676
+ });
677
+ break;
678
+ } catch (error) {
679
+ errors.push(`${command}: ${error.message}`);
680
+ }
681
+ }
682
+ if (!output) {
683
+ throw new Error(`Windows process sampler unavailable: ${errors.join("; ") || "no process query output"}`);
684
+ }
671
685
  const parsed = JSON.parse(String(output || "[]"));
672
686
  const entries = Array.isArray(parsed) ? parsed : [parsed];
673
687
  return entries.map((entry) => {
@@ -696,8 +710,14 @@ export function collectProcessTreeSnapshot({
696
710
  platform,
697
711
  rows: collectWindowsProcessRows({ cwd, runCommand }),
698
712
  });
699
- } catch {
700
- return { rootPid: pid, platform, processes: [] };
713
+ } catch (error) {
714
+ return {
715
+ rootPid: pid,
716
+ platform,
717
+ processes: [],
718
+ samplerUnavailable: true,
719
+ samplerUnavailableReason: error.message,
720
+ };
701
721
  }
702
722
  }
703
723
  let lines = [];
@@ -706,8 +726,14 @@ export function collectProcessTreeSnapshot({
706
726
  cwd,
707
727
  timeoutMs: 5000,
708
728
  }).split(/\r?\n/).filter(Boolean);
709
- } catch {
710
- return { rootPid: pid, platform, processes: [] };
729
+ } catch (error) {
730
+ return {
731
+ rootPid: pid,
732
+ platform,
733
+ processes: [],
734
+ samplerUnavailable: true,
735
+ samplerUnavailableReason: error.message,
736
+ };
711
737
  }
712
738
  const rows = lines.map((line) => {
713
739
  const match = line.trim().match(/^(\d+)\s+(\d+)\s+([0-9.]+)\s+(.*)$/);
@@ -904,6 +930,7 @@ export function summarizeProcessSamples({
904
930
  activeCpuThreshold = 0.1,
905
931
  } = {}) {
906
932
  const normalizedSamples = Array.isArray(samples) ? samples : [];
933
+ const unavailableSamples = normalizedSamples.filter((sample) => sample?.samplerUnavailable);
907
934
  const activeCounts = [];
908
935
  const cpuTotals = [];
909
936
  const categoryMax = {};
@@ -972,6 +999,15 @@ export function summarizeProcessSamples({
972
999
  schemaVersion: 1,
973
1000
  contract: BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
974
1001
  sampleCount: normalizedSamples.length,
1002
+ sampler: {
1003
+ unavailable: unavailableSamples.length > 0,
1004
+ unavailableSamples: unavailableSamples.length,
1005
+ unavailableReasons: [...new Set(
1006
+ unavailableSamples
1007
+ .map((sample) => String(sample.samplerUnavailableReason || "").trim())
1008
+ .filter(Boolean),
1009
+ )].slice(0, 5),
1010
+ },
975
1011
  activeCpuThreshold: threshold,
976
1012
  requestedParallelism: requested,
977
1013
  requestedParallelismSource: requestedSource,
@@ -1023,10 +1059,16 @@ export function startProcessSampler({
1023
1059
  return snapshot;
1024
1060
  };
1025
1061
  sample();
1026
- const timer = setInterval(sample, Math.max(1000, Number(intervalMs || 15000)));
1062
+ const interval = Math.max(1000, Number(intervalMs || 15000));
1063
+ const earlyDelay = Math.min(1000, interval);
1064
+ const earlyTimer = interval > earlyDelay ? setTimeout(sample, earlyDelay) : undefined;
1065
+ const timer = setInterval(sample, interval);
1027
1066
  return {
1028
1067
  samples,
1029
1068
  stop() {
1069
+ if (earlyTimer) {
1070
+ clearTimeout(earlyTimer);
1071
+ }
1030
1072
  clearInterval(timer);
1031
1073
  return samples;
1032
1074
  },
@@ -1,5 +1,5 @@
1
1
  import crypto from "node:crypto";
2
- import { execFileSync, execSync } from "node:child_process";
2
+ import { execFileSync, execSync, spawnSync } 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";
@@ -73,12 +73,28 @@ function manifestPathFor(root, filePath) {
73
73
  }
74
74
 
75
75
  function lifecycleErrorAttributes(error, extra = {}) {
76
- return {
76
+ const attributes = {
77
77
  ...extra,
78
78
  errorName: error.name,
79
79
  status: error.status ?? "",
80
80
  signal: error.signal || "",
81
81
  };
82
+ if (error.stdoutTail) {
83
+ attributes.stdoutTail = error.stdoutTail;
84
+ }
85
+ if (error.stderrTail) {
86
+ attributes.stderrTail = error.stderrTail;
87
+ }
88
+ if (error.wrappedCommand) {
89
+ attributes.wrappedCommand = error.wrappedCommand;
90
+ attributes.wrappedCommandExitCode = error.wrappedCommand.exitCode ?? "";
91
+ attributes.wrappedCommandSignal = error.wrappedCommand.signal || "";
92
+ attributes.wrappedCommandError = error.wrappedCommand.error || "";
93
+ }
94
+ if (error.samplerUnavailable !== undefined) {
95
+ attributes.samplerUnavailable = error.samplerUnavailable;
96
+ }
97
+ return attributes;
82
98
  }
83
99
 
84
100
  function collectArtifactFiles(root, patterns) {
@@ -122,6 +138,30 @@ function readProcessSummaryArtifact(filePath) {
122
138
  throw new Error(`process summary file has unsupported contract: ${artifact?.contract || "unknown"}`);
123
139
  }
124
140
 
141
+ function readOptionalProcessSummaryArtifact(filePath) {
142
+ try {
143
+ return readProcessSummaryArtifact(filePath);
144
+ } catch {
145
+ return undefined;
146
+ }
147
+ }
148
+
149
+ function attachProcessSampleFailureEvidence(error, processSummaryPath) {
150
+ const artifact = readOptionalProcessSummaryArtifact(processSummaryPath)?.artifact;
151
+ const wrappedCommand = artifact?.wrappedCommand;
152
+ if (wrappedCommand) {
153
+ error.wrappedCommand = wrappedCommand;
154
+ error.stdoutTail = wrappedCommand.stdoutTail || "";
155
+ error.stderrTail = wrappedCommand.stderrTail || "";
156
+ error.status = wrappedCommand.exitCode ?? error.status;
157
+ error.signal = wrappedCommand.signal || error.signal || "";
158
+ }
159
+ if (artifact?.summary?.sampler) {
160
+ error.samplerUnavailable = Boolean(artifact.summary.sampler.unavailable);
161
+ }
162
+ return error;
163
+ }
164
+
125
165
  function shellCommandArgs(command, shell) {
126
166
  if (typeof shell === "string" && shell.trim()) {
127
167
  return [shell, "-c", command];
@@ -170,12 +210,19 @@ function executeSampledShellCommand({
170
210
  args.push("--requested-parallelism", String(Number(requestedParallelism)));
171
211
  }
172
212
  args.push("--", ...shellCommandArgs(command, shell));
173
- execFileSync(process.execPath, args, {
213
+ const result = spawnSync(process.execPath, args, {
174
214
  cwd,
175
215
  env,
176
216
  stdio: "inherit",
177
217
  timeout,
178
218
  });
219
+ if (result.error || result.status !== 0 || result.signal) {
220
+ const error = result.error || new Error(`sampled lifecycle command failed with status ${result.status ?? ""}`);
221
+ error.status = result.status ?? error.status ?? 1;
222
+ error.signal = result.signal || error.signal || "";
223
+ attachProcessSampleFailureEvidence(error, processSummaryPath);
224
+ throw error;
225
+ }
179
226
  }
180
227
 
181
228
  function stageCommandText(stage) {