@kungfu-tech/buildchain 2.5.1-alpha.0 → 2.5.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.
- package/bin/buildchain.mjs +37 -2
- package/package.json +1 -1
- package/packages/core/diagnostics.js +54 -12
- package/scripts/run-lifecycle-core.mjs +50 -3
package/bin/buildchain.mjs
CHANGED
|
@@ -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: "
|
|
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.
|
|
3
|
+
"version": "2.5.1-alpha.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
|
|
668
|
-
|
|
669
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|