@kungfu-tech/buildchain 2.4.3-alpha.0 → 2.4.3

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.
@@ -57,7 +57,10 @@ first, restores the local working copies, and only then decides whether to
57
57
  publish, repair, or finalize.
58
58
 
59
59
  Every meaningful state transition is written to the durable ref before public
60
- release refs move. If the durable write fails, the action fails closed.
60
+ release refs move. Release-state GitHub API reads and writes use retry/backoff
61
+ for transient service failures such as HTTP 5xx responses, connection resets,
62
+ timeouts, and "other side closed" socket failures. If the durable write still
63
+ cannot be persisted after retries, the action fails closed.
61
64
 
62
65
  Durable release-state refs reserve their exact version even when the public exact
63
66
  tag was never created. If a later machine run sees a failed or repair-required
@@ -296,6 +299,9 @@ the transaction `release_sha`. Exact tags are accepted when they already point
296
299
  at the transaction release/material SHA or the finalized channel head. Floating
297
300
  channel tags and dev/alpha refs are then retried idempotently, and the
298
301
  transaction is marked `complete` only after those public refs are consistent.
302
+ Writing `complete` clears any stale `failure` value from earlier attempts, so
303
+ the durable `state.json` represents the successful final state instead of the
304
+ last transient error seen before a rerun.
299
305
  An exact tag at an unrelated SHA is still a material conflict and blocks
300
306
  recovery.
301
307
 
@@ -158,18 +158,24 @@ writeDiagnosticsArtifact(".buildchain/artifacts/diagnostics.json", {
158
158
  `collectCacheDiagnostics()` includes package-manager/workspace context, selected
159
159
  cache directory stats, and compiler-cache stats from `ccache --show-stats
160
160
  --json` plus `sccache --show-stats --stats-format json` when those tools are
161
- present. Missing cache tools are recorded as unavailable instead of failing the
162
- diagnostics artifact. Call `collectCompilerCacheDiagnostics()` directly when a
163
- consumer script only needs compiler cache data.
161
+ present. If a ccache build does not support JSON stats, Buildchain falls back to
162
+ plain `ccache --show-stats` and parses the text counters. Missing cache tools
163
+ are recorded as unavailable instead of failing the diagnostics artifact. Call
164
+ `collectCompilerCacheDiagnostics()` directly when a consumer script only needs
165
+ compiler cache data. Native diagnostics also expose `compilerCaches` and
166
+ `nativeCacheDirs` as top-level fields in each diagnostics artifact and aggregate
167
+ summary, so reviewers do not have to dig through nested cache sections first.
164
168
 
165
169
  Process samples are intentionally summarized before they become long-lived
166
170
  artifacts. The summary records requested parallelism, the source of that value
167
- (`command`, `env:MAKEFLAGS`, `env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`),
168
- observed active process concurrency, elapsed sample time, total sampled CPU,
169
- and conservative command categories such as `compiler`, `archive`, `linker`,
170
- `build-tool`, and `cache`. This lets native projects distinguish "we asked for
171
+ (`command`, `process-tree`, `env:MAKEFLAGS`,
172
+ `env:CMAKE_BUILD_PARALLEL_LEVEL`, or `explicit`), observed active process
173
+ concurrency, elapsed sample time, total sampled CPU, and conservative command
174
+ categories such as `compiler`, `archive`, `linker`, `build-tool`, and `cache`.
175
+ The sampler detects common `make -j N`, `ninja -j N`, CMake/MSBuild/Xcode job
176
+ flags, and MAKEFLAGS. This lets native projects distinguish "we asked for
171
177
  `make -j20`" from "the build graph only kept two active compiler or archive
172
- children busy during the sampled window" without storing full command lines.
178
+ children busy during the sampled window" without storing environment dumps.
173
179
 
174
180
  Native repositories can opt into a reusable diagnostics profile in
175
181
  `buildchain.toml`:
@@ -304,12 +310,15 @@ buildchain sample process-tree \
304
310
  make -j20
305
311
  ```
306
312
 
307
- The JSONL sample file stores timestamped process-tree snapshots with command
308
- basenames, CPU percentages, elapsed time, and requested parallelism context. The
309
- summary file records observed concurrency, total sampled CPU, command
310
- categories, and top command basenames. This is intended for diagnosing
311
- low-utilization tails such as archive/link phases without logging full command
312
- lines or environment dumps.
313
+ The JSONL sample file stores timestamped process-tree snapshots with full
314
+ redacted command lines, command basenames, CPU percentages when available,
315
+ elapsed time, and requested parallelism context. Unix and macOS snapshots read
316
+ the full `ps args` field instead of truncated `comm` names. Windows snapshots
317
+ read `Win32_Process` command lines through PowerShell so the sampler no longer
318
+ returns an empty process set on Windows runners. The summary file records
319
+ observed concurrency, total sampled CPU, command categories, and top command
320
+ basenames. This is intended for diagnosing low-utilization tails such as
321
+ archive/link phases without logging full environment dumps.
313
322
 
314
323
  The lifecycle observability summary is stage-wide, not just final-step timing:
315
324
  when install and build write to the same Buildchain log, the final platform
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.4.3-alpha.0",
3
+ "version": "2.4.3",
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",
@@ -497,26 +497,70 @@ function parseJsonDiagnostics(value = "") {
497
497
  }
498
498
  }
499
499
 
500
- function collectCompilerCacheTool({ command, args, cwd, runCommand }) {
501
- try {
502
- const output = runCommand(command, args, { cwd, timeoutMs: 5000 });
503
- const text = String(output || "").trim();
504
- const stats = parseJsonDiagnostics(text);
505
- return {
506
- available: true,
507
- command,
508
- format: stats ? "json" : "text",
509
- stats: stats || {},
510
- rawBytes: Buffer.byteLength(text, "utf8"),
511
- parseError: stats ? "" : "stats output was not JSON",
512
- };
513
- } catch (error) {
514
- return {
515
- available: false,
516
- command,
517
- error: error?.code || error?.message || "stats command failed",
518
- };
500
+ function parseTextStats(value = "") {
501
+ const stats = {};
502
+ for (const line of String(value || "").split(/\r?\n/)) {
503
+ const trimmed = line.trim();
504
+ if (!trimmed) {
505
+ continue;
506
+ }
507
+ const match = trimmed.match(/^(.+?)(?:\s{2,}|\t+|:\s+)([-+]?\d+(?:\.\d+)?)(?:\s|$)/);
508
+ if (!match) {
509
+ continue;
510
+ }
511
+ const key = match[1]
512
+ .trim()
513
+ .toLowerCase()
514
+ .replace(/[^a-z0-9]+/g, "_")
515
+ .replace(/^_+|_+$/g, "");
516
+ if (!key) {
517
+ continue;
518
+ }
519
+ const number = Number(match[2]);
520
+ stats[key] = Number.isFinite(number) ? number : match[2];
519
521
  }
522
+ return stats;
523
+ }
524
+
525
+ function collectCompilerCacheTool({ command, attempts, cwd, runCommand }) {
526
+ let lastError;
527
+ for (const attempt of attempts) {
528
+ const args = attempt.args || [];
529
+ const expectedFormat = attempt.format || "";
530
+ try {
531
+ const output = runCommand(command, args, { cwd, timeoutMs: 5000 });
532
+ const text = String(output || "").trim();
533
+ const jsonStats = parseJsonDiagnostics(text);
534
+ if (jsonStats) {
535
+ return {
536
+ available: true,
537
+ command,
538
+ args,
539
+ format: "json",
540
+ stats: jsonStats,
541
+ rawBytes: Buffer.byteLength(text, "utf8"),
542
+ parseError: "",
543
+ };
544
+ }
545
+ const textStats = parseTextStats(text);
546
+ return {
547
+ available: true,
548
+ command,
549
+ args,
550
+ format: "text",
551
+ stats: textStats,
552
+ rawBytes: Buffer.byteLength(text, "utf8"),
553
+ parseError: Object.keys(textStats).length ? "" : `${expectedFormat || "stats"} output was not parseable`,
554
+ };
555
+ } catch (error) {
556
+ lastError = error;
557
+ }
558
+ }
559
+ return {
560
+ available: false,
561
+ command,
562
+ error: lastError?.code || lastError?.message || "stats command failed",
563
+ };
520
564
  }
521
565
 
522
566
  export function collectCompilerCacheDiagnostics({
@@ -527,13 +571,18 @@ export function collectCompilerCacheDiagnostics({
527
571
  return {
528
572
  ccache: collectCompilerCacheTool({
529
573
  command: "ccache",
530
- args: ["--show-stats", "--json"],
574
+ attempts: [
575
+ { args: ["--show-stats", "--json"], format: "json" },
576
+ { args: ["--show-stats"], format: "text" },
577
+ ],
531
578
  cwd: resolvedCwd,
532
579
  runCommand,
533
580
  }),
534
581
  sccache: collectCompilerCacheTool({
535
582
  command: "sccache",
536
- args: ["--show-stats", "--stats-format", "json"],
583
+ attempts: [
584
+ { args: ["--show-stats", "--stats-format", "json"], format: "json" },
585
+ ],
537
586
  cwd: resolvedCwd,
538
587
  runCommand,
539
588
  }),
@@ -578,41 +627,21 @@ export function redactDiagnosticsValue(key, value, pattern = DEFAULT_SECRET_KEY_
578
627
  return value;
579
628
  }
580
629
 
581
- export function collectProcessTreeSnapshot({ rootPid = process.pid, cwd = process.cwd() } = {}) {
582
- const pid = String(rootPid || process.pid);
583
- if (process.platform === "win32") {
584
- return { rootPid: pid, platform: process.platform, processes: [] };
585
- }
586
- let lines = [];
587
- try {
588
- lines = execFileSync("ps", ["-axo", "pid=,ppid=,pcpu=,comm=,args="], {
589
- cwd,
590
- encoding: "utf8",
591
- stdio: ["ignore", "pipe", "ignore"],
592
- timeout: 5000,
593
- }).split(/\r?\n/).filter(Boolean);
594
- } catch {
595
- return { rootPid: pid, platform: process.platform, processes: [] };
596
- }
597
- const rows = lines.map((line) => {
598
- const match = line.trim().match(/^(\d+)\s+(\d+)\s+([0-9.]+)\s+(\S+)(?:\s+(.*))?$/);
599
- const args = match?.[5] || "";
600
- return match
601
- ? {
602
- pid: match[1],
603
- ppid: match[2],
604
- cpu: Number(match[3]),
605
- command: path.basename(match[4]),
606
- commandLine: redactCommandLine(args),
607
- }
608
- : undefined;
609
- }).filter(Boolean);
630
+ function processCommandFromLine(commandLine = "", fallback = "") {
631
+ const firstToken = shellishTokens(commandLine)[0] || fallback;
632
+ return String(firstToken || "")
633
+ .replace(/[()]/g, "")
634
+ .split(/[\\/]/)
635
+ .pop();
636
+ }
637
+
638
+ function normalizeProcessRows({ rootPid, platform, rows = [] }) {
610
639
  const byParent = new Map();
611
640
  for (const row of rows) {
612
641
  byParent.set(row.ppid, [...(byParent.get(row.ppid) || []), row]);
613
642
  }
614
643
  const seen = new Set();
615
- const stack = [pid];
644
+ const stack = [String(rootPid)];
616
645
  const processes = [];
617
646
  while (stack.length) {
618
647
  const current = stack.pop();
@@ -625,11 +654,80 @@ export function collectProcessTreeSnapshot({ rootPid = process.pid, cwd = proces
625
654
  stack.push(child.pid);
626
655
  }
627
656
  }
628
- return { rootPid: pid, platform: process.platform, processes };
657
+ return { rootPid: String(rootPid), platform, processes };
658
+ }
659
+
660
+ function collectWindowsProcessRows({ cwd, runCommand }) {
661
+ const script = [
662
+ "$ErrorActionPreference = 'Stop'",
663
+ "Get-CimInstance Win32_Process |",
664
+ "Select-Object ProcessId,ParentProcessId,Name,CommandLine |",
665
+ "ConvertTo-Json -Compress",
666
+ ].join("; ");
667
+ const output = runCommand("powershell", ["-NoProfile", "-Command", script], {
668
+ cwd,
669
+ timeoutMs: 5000,
670
+ });
671
+ const parsed = JSON.parse(String(output || "[]"));
672
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
673
+ return entries.map((entry) => {
674
+ const commandLine = redactCommandLine(entry.CommandLine || entry.Name || "");
675
+ return {
676
+ pid: String(entry.ProcessId || ""),
677
+ ppid: String(entry.ParentProcessId || ""),
678
+ cpu: 0,
679
+ command: processCommandFromLine(commandLine, entry.Name || ""),
680
+ commandLine,
681
+ };
682
+ }).filter((entry) => entry.pid && entry.ppid);
683
+ }
684
+
685
+ export function collectProcessTreeSnapshot({
686
+ rootPid = process.pid,
687
+ cwd = process.cwd(),
688
+ platform = process.platform,
689
+ runCommand = defaultDiagnosticCommandRunner,
690
+ } = {}) {
691
+ const pid = String(rootPid || process.pid);
692
+ if (platform === "win32") {
693
+ try {
694
+ return normalizeProcessRows({
695
+ rootPid: pid,
696
+ platform,
697
+ rows: collectWindowsProcessRows({ cwd, runCommand }),
698
+ });
699
+ } catch {
700
+ return { rootPid: pid, platform, processes: [] };
701
+ }
702
+ }
703
+ let lines = [];
704
+ try {
705
+ lines = runCommand("ps", ["-axo", "pid=,ppid=,pcpu=,args="], {
706
+ cwd,
707
+ timeoutMs: 5000,
708
+ }).split(/\r?\n/).filter(Boolean);
709
+ } catch {
710
+ return { rootPid: pid, platform, processes: [] };
711
+ }
712
+ const rows = lines.map((line) => {
713
+ const match = line.trim().match(/^(\d+)\s+(\d+)\s+([0-9.]+)\s+(.*)$/);
714
+ const args = match?.[4] || "";
715
+ const commandLine = redactCommandLine(args);
716
+ return match
717
+ ? {
718
+ pid: match[1],
719
+ ppid: match[2],
720
+ cpu: Number(match[3]),
721
+ command: processCommandFromLine(commandLine),
722
+ commandLine,
723
+ }
724
+ : undefined;
725
+ }).filter(Boolean);
726
+ return normalizeProcessRows({ rootPid: pid, platform, rows });
629
727
  }
630
728
 
631
729
  export function classifyProcessCommand(command = "") {
632
- const name = path.basename(String(command || "")).toLowerCase();
730
+ const name = String(command || "").split(/[\\/]/).pop().toLowerCase();
633
731
  if (/^(ccache|sccache)$/.test(name)) {
634
732
  return "cache";
635
733
  }
@@ -657,7 +755,12 @@ function firstPositiveInteger(value) {
657
755
  }
658
756
 
659
757
  function commandName(value = "") {
660
- return path.basename(String(value || "").replace(/[()]/g, "").replace(/\.exe$/i, "")).toLowerCase();
758
+ return String(value || "")
759
+ .replace(/[()]/g, "")
760
+ .replace(/\.exe$/i, "")
761
+ .split(/[\\/]/)
762
+ .pop()
763
+ .toLowerCase();
661
764
  }
662
765
 
663
766
  function detectParallelismFromTokens(tokens = []) {
@@ -945,6 +1048,8 @@ export function createDiagnosticsArtifact({
945
1048
  const events = logPath ? readBuildchainLogEvents(logPath) : [];
946
1049
  const loadedConfig = loadBuildchainConfig(resolvedCwd);
947
1050
  const nativeProfile = getNativeDiagnosticsProfile(loadedConfig);
1051
+ const native = collectNativeDiagnostics({ cwd: resolvedCwd, profile: nativeProfile });
1052
+ const cache = collectCacheDiagnostics({ cwd: resolvedCwd, cacheDirs });
948
1053
  return {
949
1054
  schemaVersion: 1,
950
1055
  contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
@@ -953,8 +1058,10 @@ export function createDiagnosticsArtifact({
953
1058
  buildchain: collectBuildchainDiagnostics({ cwd: resolvedCwd, artifactPaths }),
954
1059
  runner: collectRunnerDiagnostics(),
955
1060
  tools: collectToolDiagnostics({ cwd: resolvedCwd }),
956
- cache: collectCacheDiagnostics({ cwd: resolvedCwd, cacheDirs }),
957
- native: collectNativeDiagnostics({ cwd: resolvedCwd, profile: nativeProfile }),
1061
+ cache,
1062
+ native,
1063
+ compilerCaches: native.compilerCaches || cache.compilerCaches || {},
1064
+ nativeCacheDirs: native.cacheDirs || [],
958
1065
  git: collectGitDiagnostics({ cwd: resolvedCwd }),
959
1066
  lifecycleObservability: lifecycleObservability || summarizeLifecycleObservability({ events, logPath }),
960
1067
  process: processSummary || summarizeProcessSamples({ samples: processSamples, requestedParallelism }),
@@ -1353,6 +1460,12 @@ export function summarizeDiagnosticsArtifacts(inputs = []) {
1353
1460
  runnerDetails: compactRunnerDetails(entry.runner || {}),
1354
1461
  tools: compactToolSummary(entry.tools || {}, entry.native?.tools || {}),
1355
1462
  cache: compactCacheSummary(entry.cache || {}, entry.native || {}),
1463
+ compilerCaches: compactCompilerCacheSummary(entry.compilerCaches || entry.native?.compilerCaches || entry.cache?.compilerCaches || {}),
1464
+ nativeCacheDirs: Array.isArray(entry.nativeCacheDirs)
1465
+ ? entry.nativeCacheDirs
1466
+ : Array.isArray(entry.native?.cacheDirs)
1467
+ ? entry.native.cacheDirs
1468
+ : [],
1356
1469
  process: entry.process || {},
1357
1470
  diagnosticsContract,
1358
1471
  ...(diagnosticsManifest ? { diagnosticsManifest } : {}),
@@ -164,6 +164,9 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
164
164
  throw new Error(`cannot transition release transaction from ${currentState} to ${nextState}`);
165
165
  }
166
166
  }
167
+ const nextFailure = nextState === "complete"
168
+ ? optionalString(metadata.failure ?? "")
169
+ : optionalString(metadata.failure ?? record.failure);
167
170
  return {
168
171
  ...record,
169
172
  previous_state: currentState === nextState ? record.previous_state || "" : currentState,
@@ -171,7 +174,7 @@ export function transitionReleaseTransaction(record, nextState, metadata = {}) {
171
174
  actor: optionalString(metadata.actor ?? record.actor),
172
175
  run_id: optionalString(metadata.runId ?? record.run_id),
173
176
  superseded_by: optionalString(metadata.supersededBy ?? record.superseded_by),
174
- failure: optionalString(metadata.failure ?? record.failure),
177
+ failure: nextFailure,
175
178
  updated_at: nowIso(),
176
179
  };
177
180
  }