@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.
@@ -60,6 +60,20 @@ await logger.span("native.compile", {
60
60
  await compile();
61
61
  });
62
62
 
63
+ logger.spanSync("native.archive", {
64
+ phase: "build",
65
+ attributes: { tool: "libtool" },
66
+ }, () => {
67
+ archiveStaticLibraries();
68
+ });
69
+
70
+ logger.spawnSync("native.build", "make", ["-j20"], {
71
+ stdio: "inherit",
72
+ }, {
73
+ phase: "build",
74
+ attributes: { requestedJobs: 20 },
75
+ });
76
+
63
77
  const report = verifyBuildchainLogEvents({
64
78
  path: logger.path,
65
79
  minEvents: 3,
@@ -80,6 +94,227 @@ Use the API when a build script has internal stages that are invisible to the
80
94
  outer workflow. Keep secret values out of attributes; known sensitive keys are
81
95
  redacted, but callers should still avoid logging private material.
82
96
 
97
+ CommonJS scripts should import Buildchain's ESM surfaces dynamically:
98
+
99
+ ```js
100
+ const { createBuildchainLogger } = await import("@kungfu-tech/buildchain/logging");
101
+ const { collectRunnerDiagnostics } = await import("@kungfu-tech/buildchain/diagnostics");
102
+ ```
103
+
104
+ ## Diagnostics API
105
+
106
+ The diagnostics surface collects local, non-telemetry build facts that are
107
+ useful when a native build is slow or flaky:
108
+
109
+ ```js
110
+ import {
111
+ collectBuildchainDiagnostics,
112
+ collectCacheDiagnostics,
113
+ collectCompilerCacheDiagnostics,
114
+ collectRunnerDiagnostics,
115
+ collectToolDiagnostics,
116
+ detectRequestedParallelism,
117
+ startProcessSampler,
118
+ summarizeDiagnosticsArtifacts,
119
+ summarizeLifecycleObservability,
120
+ summarizeProcessSamples,
121
+ validateAnchoredPackageRelease,
122
+ writeDiagnosticsArtifact,
123
+ } from "@kungfu-tech/buildchain/diagnostics";
124
+
125
+ const lifecycleObservability = summarizeLifecycleObservability({
126
+ logPath: ".buildchain/logs/events.jsonl",
127
+ });
128
+ const buildCommand = "make";
129
+ const buildArgs = ["-j20"];
130
+ const requestedParallelism = detectRequestedParallelism({
131
+ command: buildCommand,
132
+ args: buildArgs,
133
+ });
134
+ const processSampler = startProcessSampler({
135
+ intervalMs: 15000,
136
+ label: "native-build",
137
+ command: buildCommand,
138
+ args: buildArgs,
139
+ });
140
+ // Run the long native build while the sampler is active.
141
+ const processSummary = summarizeProcessSamples({
142
+ requestedParallelism: requestedParallelism.value,
143
+ samples: processSampler.stop(),
144
+ });
145
+ const cacheDiagnostics = collectCacheDiagnostics({ cwd: process.cwd() });
146
+
147
+ writeDiagnosticsArtifact(".buildchain/artifacts/diagnostics.json", {
148
+ contract: "consumer-build-diagnostics",
149
+ buildchain: collectBuildchainDiagnostics({ cwd: process.cwd() }),
150
+ runner: collectRunnerDiagnostics(),
151
+ tools: collectToolDiagnostics({ cwd: process.cwd() }),
152
+ cache: cacheDiagnostics,
153
+ lifecycleObservability,
154
+ process: processSummary,
155
+ });
156
+ ```
157
+
158
+ `collectCacheDiagnostics()` includes package-manager/workspace context, selected
159
+ cache directory stats, and compiler-cache stats from `ccache --show-stats
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.
164
+
165
+ Process samples are intentionally summarized before they become long-lived
166
+ 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
+ `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.
173
+
174
+ Native repositories can opt into a reusable diagnostics profile in
175
+ `buildchain.toml`:
176
+
177
+ ```toml
178
+ [diagnostics.native]
179
+ enabled = true
180
+ sample_process_tree = true
181
+ compiler_cache = "auto"
182
+ expected_tools = ["ccache", "sccache", "clang", "cl", "cmake", "ninja"]
183
+ artifact_dirs = ["build", "dist", "build/Release"]
184
+ cache_dirs = [".ccache", ".sccache"]
185
+ ```
186
+
187
+ When enabled, diagnostics artifacts include the normalized profile, selected
188
+ tool versions, compiler-cache stats, and configured artifact/cache directory
189
+ stats. The profile is data-driven: Buildchain does not assume a specific
190
+ project such as libnode. The reusable build workflow also exposes
191
+ `sample-process-tree`, which wraps the build lifecycle with the process sampler
192
+ and carries the generated summary into the final diagnostics artifact. Custom
193
+ workflows can call `buildchain sample process-tree` directly when they need a
194
+ different command boundary.
195
+
196
+ Anchored/manual package projects can also run one higher-level release-shape
197
+ check instead of assembling lower-level config calls:
198
+
199
+ ```js
200
+ const anchoredReport = validateAnchoredPackageRelease({
201
+ cwd: process.cwd(),
202
+ requireManifest: true,
203
+ requirePackageSetOrder: "platforms-first-main-last",
204
+ requireTrustedPublishing: true,
205
+ });
206
+
207
+ if (!anchoredReport.ok) {
208
+ throw new Error("Anchored package release contract failed");
209
+ }
210
+ ```
211
+
212
+ In an actual publish job, make that check source-lock aware:
213
+
214
+ ```js
215
+ const publishReady = validateAnchoredPackageRelease({
216
+ cwd: process.cwd(),
217
+ requirePublishGateSourceLock: true,
218
+ });
219
+ ```
220
+
221
+ The source-lock inputs are read from `BUILDCHAIN_PUBLISH_SOURCE_REF`,
222
+ `BUILDCHAIN_PUBLISH_SOURCE_SHA`, and `BUILDCHAIN_PUBLISH_SOURCE_LOCKED`, which
223
+ the reusable build workflow emits after resolving `publish-source-ref`. That
224
+ turns direct channel-branch publication from `alpha/*` or `release/*` into a
225
+ hard failure, while `publish-gate/alpha/<line>/<version>` and
226
+ `publish-gate/release/<line>/<version>` also validate the requested consumer
227
+ version against configured version files and the anchor manifest.
228
+
229
+ `buildchain lifecycle run` writes this small diagnostics artifact next to the
230
+ platform manifest by default. The per-platform diagnostics upload includes the
231
+ compact `diagnostics.json`, `diagnostics-manifest.json`, lifecycle
232
+ `events.jsonl`, and, when process sampling is enabled, copied
233
+ `process-summary.json` and `process-samples.jsonl` sidecars. The sidecar
234
+ manifest records each uploaded diagnostics file with its relative path, byte
235
+ count, and sha256 hash. It is intended to stay small enough to download without
236
+ fetching large binary packages, and it should not include full environment dumps
237
+ or secret-looking values.
238
+
239
+ Use `summarizeDiagnosticsArtifacts()` when a matrix build uploads one
240
+ diagnostics artifact per platform. The summary keeps each platform's lifecycle
241
+ stage table, adds a lifecycle total duration, carries the top slow spans, and
242
+ aggregates warning/error counts plus the slowest platforms. Per-platform rows
243
+ also include compact runner facts, checked tool versions/missing tools, package
244
+ manager/cache directory details, compiler cache availability, and a compact
245
+ process sampler view with requested parallelism, observed max active processes,
246
+ the ratio between them, sample count, process categories, and top sampled
247
+ command basenames. That gives release reviewers a small cross-platform timing,
248
+ runner, tool, cache, and concurrency view without downloading full build outputs
249
+ or process sidecars.
250
+
251
+ The reusable build workflow writes that rollup as `diagnostics-summary.json` and
252
+ uploads it in a separate aggregate diagnostics summary artifact. Consumers can
253
+ read the `build-diagnostics-summary-artifact` output when they need only timing,
254
+ warning/error, runner, cache, and process-sampler context instead of the build
255
+ summary or binary artifacts. Per-platform rows keep the diagnostics `links`
256
+ object, including the binary artifact name, manifest artifact name, diagnostics
257
+ artifact name, platform id, diagnostics sidecar manifest path, manifest path,
258
+ summary path, and process sidecar paths when present. They also keep compact
259
+ runner/tool/cache summaries so reviewers can tell whether a slow row ran on the
260
+ expected runner, missed an expected tool, or lacked useful compiler-cache stats.
261
+ When the downloaded platform diagnostics include a sibling
262
+ `diagnostics-manifest.json`, `summarizeDiagnosticsArtifacts()` carries a compact
263
+ `diagnosticsManifest` section for that platform and verifies the manifest's
264
+ `diagnostics.json` byte count and sha256. Missing or mismatched sidecar manifests
265
+ increment `diagnosticsManifestWarningCount`, so reviewers can distinguish
266
+ diagnostics sidecar drift from lifecycle warnings or build failures.
267
+ The same summary carries `diagnosticsContract` per platform and aggregates
268
+ `diagnosticsContractWarningCount` when a downloaded `diagnostics.json` does not
269
+ match `BUILDCHAIN_DIAGNOSTICS_CONTRACT`.
270
+
271
+ The diagnostics SDK also exports stable contract constants such as
272
+ `BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT`,
273
+ `BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT`,
274
+ `BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT`,
275
+ `BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT`, and
276
+ `BUILDCHAIN_ANCHORED_PACKAGE_RELEASE_VALIDATION_CONTRACT` from
277
+ `@kungfu-tech/buildchain/diagnostics`; consumers should compare against those
278
+ constants instead of hardcoding contract strings.
279
+
280
+ The CLI exposes the same aggregation for shell and workflow steps:
281
+
282
+ ```bash
283
+ buildchain diagnostics summary \
284
+ .buildchain/artifacts/linux-x64/diagnostics.json \
285
+ .buildchain/artifacts/macos-arm64/diagnostics.json \
286
+ --output .buildchain/artifacts/diagnostics-summary.json \
287
+ --json
288
+ ```
289
+
290
+ Omit `--json` when the workflow log should show a compact platform table with
291
+ lifecycle stages, artifact scan/upload time, total time, requested jobs,
292
+ observed active processes, warnings, and errors.
293
+
294
+ For long native build commands, the CLI can also sample the child process tree
295
+ while preserving the wrapped command's exit code:
296
+
297
+ ```bash
298
+ buildchain sample process-tree \
299
+ --label native-build \
300
+ --interval-ms 15000 \
301
+ --output .buildchain/diagnostics/process-samples.jsonl \
302
+ --summary-output .buildchain/diagnostics/process-summary.json \
303
+ -- \
304
+ make -j20
305
+ ```
306
+
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
+
314
+ The lifecycle observability summary is stage-wide, not just final-step timing:
315
+ when install and build write to the same Buildchain log, the final platform
316
+ manifest can show both stages and the slowest spans.
317
+
83
318
  ## CLI Logging
84
319
 
85
320
  ```bash
@@ -356,7 +356,6 @@ jobs:
356
356
  web-surface:
357
357
  uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2
358
358
  with:
359
- buildchain-ref: v2
360
359
  build-command: npm run build
361
360
  verify-command: npm run check
362
361
  artifact-path: dist
@@ -371,6 +370,41 @@ The reusable workflow maps GitHub events to Buildchain web-surface semantics:
371
370
  | `push` to `main` | validate, build, verify, and plan `staging` from the merged `main` SHA |
372
371
  | `workflow_dispatch` with `production-approved = true` | plan `production` and enter the configured GitHub Environment gate |
373
372
 
373
+ The optional `buildchain-ref` input is empty by default. Empty keeps the
374
+ web-surface run on the stable Buildchain runtime selected by the reusable
375
+ workflow ref, normally `@v2`. A trusted maintainer can expose a
376
+ `workflow_dispatch` input and pass it through for one-off train validation.
377
+ See [`runtime-train-validation.md`](runtime-train-validation.md) for the shared
378
+ train protocol and notification template:
379
+
380
+ ```yaml
381
+ on:
382
+ workflow_dispatch:
383
+ inputs:
384
+ buildchain-ref:
385
+ description: "Temporary Buildchain runtime ref for trusted manual validation"
386
+ required: false
387
+ default: ""
388
+
389
+ jobs:
390
+ web-surface:
391
+ uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2
392
+ with:
393
+ buildchain-ref: ${{ inputs.buildchain-ref || '' }}
394
+ build-command: pnpm run build
395
+ verify-command: pnpm run check
396
+ artifact-path: dist
397
+ ```
398
+
399
+ Only trusted `workflow_dispatch` runs by repository actors with write,
400
+ maintain, or admin permission may use a non-empty runtime override. Train refs
401
+ such as `train/v2/v2.3/site-source-of-truth` are temporary validation refs, not
402
+ stable production dependencies or pending merge targets. They may remain for a
403
+ retention window after release as a fast-use and rollback channel, with old
404
+ trains handled by periodic Buildchain cleanup. The web-surface deployment
405
+ manifest records the resolved runtime SHA as `runtimeId` and the stable
406
+ rollback ref as `rollbackPointer`.
407
+
374
408
  The workflow deliberately plans and emits manifests by default. Live mutation is
375
409
  opt-in per channel:
376
410
 
@@ -384,7 +418,6 @@ jobs:
384
418
  web-surface:
385
419
  uses: kungfu-systems/buildchain/.github/workflows/.web-surface.yml@v2
386
420
  with:
387
- buildchain-ref: v2
388
421
  build-command: pnpm run build
389
422
  verify-command: pnpm run check
390
423
  artifact-path: dist
@@ -7,7 +7,8 @@ It is intentionally small, but it keeps the important shape:
7
7
 
8
8
  - `package.json` is the package version-state file;
9
9
  - `libnode.release.json` is the explicit upstream anchor manifest;
10
- - `buildchain.toml` declares `install`, `build`, and `verify` lifecycle stages;
10
+ - `buildchain.toml` declares `install`, `build`, `verify`, and `publish`
11
+ lifecycle stages;
11
12
  - `version.strategy = "anchored"` and `version.next = "manual"` tell
12
13
  Buildchain to validate the current anchor instead of deriving the next Node
13
14
  anchor automatically;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.3.1-alpha.0",
3
+ "version": "2.3.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",
@@ -13,6 +13,7 @@
13
13
  "exports": {
14
14
  ".": "./packages/core/index.js",
15
15
  "./core": "./packages/core/index.js",
16
+ "./diagnostics": "./packages/core/diagnostics.js",
16
17
  "./logging": "./packages/core/logging.js",
17
18
  "./release-passport": "./packages/core/release-passport.js",
18
19
  "./site/buildchain-site.json": "./dist/site/buildchain-site.json",
@@ -10,6 +10,8 @@ Current shared surfaces:
10
10
  - lifecycle stage normalization and execution;
11
11
  - config validation for release-package and `web-surface` projects.
12
12
  - toolkit observability logging through `@kungfu-tech/buildchain/logging`;
13
+ - toolkit diagnostics and native profile collection through
14
+ `@kungfu-tech/buildchain/diagnostics`;
13
15
  - release passport creation and verification through
14
16
  `@kungfu-tech/buildchain/release-passport`.
15
17
 
@@ -28,6 +30,26 @@ await logger.span("native.package", { phase: "package" }, packageArtifacts);
28
30
  The CLI remains the right surface for GitHub Actions steps, shell scripts, and
29
31
  non-JavaScript build tools.
30
32
 
33
+ Diagnostics consumers should import the published subpath and compare stable
34
+ contracts through the exported constants instead of hardcoding JSON contract
35
+ names:
36
+
37
+ ```js
38
+ import {
39
+ BUILDCHAIN_DIAGNOSTICS_CONTRACT,
40
+ BUILDCHAIN_DIAGNOSTICS_SUMMARY_CONTRACT,
41
+ collectRunnerDiagnostics,
42
+ summarizeDiagnosticsArtifacts,
43
+ } from "@kungfu-tech/buildchain/diagnostics";
44
+ ```
45
+
46
+ CommonJS scripts can use dynamic imports for the same package surfaces:
47
+
48
+ ```js
49
+ const { createBuildchainLogger } = await import("@kungfu-tech/buildchain/logging");
50
+ const { collectRunnerDiagnostics } = await import("@kungfu-tech/buildchain/diagnostics");
51
+ ```
52
+
31
53
  Web-surface validation stays in core because both local scripts and GitHub
32
54
  Actions need the same fail-closed interpretation of project, channel, deploy,
33
55
  retention, and staging security declarations.
@@ -13,6 +13,7 @@ const SUPPORTED_PROJECT_TYPES = new Set(["package", "web-surface"]);
13
13
  const SUPPORTED_PUBLISH_MODES = new Set(["publish-final-version", "promote-existing-version"]);
14
14
  const SUPPORTED_PUBLISH_AUTH = new Set(["trusted-publishing", "npm-token"]);
15
15
  const SUPPORTED_PACKAGE_SET_ORDER = new Set(["as-provided", "platforms-first-main-last"]);
16
+ const SUPPORTED_NATIVE_COMPILER_CACHE = new Set(["auto", "ccache", "sccache", "none"]);
16
17
  const WEB_SURFACE_CHANNELS = ["preview", "staging", "production"];
17
18
  const SUPPORTED_CHANNEL_VISIBILITY = new Set(["ephemeral", "protected", "public", "internal"]);
18
19
  const SUPPORTED_ACCESS_CONTROL = new Set(["none", "managed-network", "edge-basic-auth", "oidc", "app-auth"]);
@@ -125,10 +126,51 @@ export function normalizeBuildchainConfig(config) {
125
126
  if (normalized.security !== undefined) {
126
127
  normalized.security = normalizeSecuritySection(normalized.security);
127
128
  }
129
+ if (normalized.diagnostics !== undefined) {
130
+ normalized.diagnostics = normalizeDiagnosticsSection(normalized.diagnostics);
131
+ }
128
132
  validateWebSurfaceConfig(normalized);
129
133
  return normalized;
130
134
  }
131
135
 
136
+ function normalizeDiagnosticsSection(diagnostics) {
137
+ assertPlainObject(diagnostics, "diagnostics");
138
+ const normalized = { ...diagnostics };
139
+ if (diagnostics.native !== undefined) {
140
+ normalized.native = normalizeNativeDiagnosticsProfile(diagnostics.native);
141
+ }
142
+ return normalized;
143
+ }
144
+
145
+ function normalizeNativeDiagnosticsProfile(native) {
146
+ assertPlainObject(native, "diagnostics.native");
147
+ const compilerCache = native.compiler_cache === undefined
148
+ ? "auto"
149
+ : assertString(native.compiler_cache, "diagnostics.native.compiler_cache");
150
+ if (!SUPPORTED_NATIVE_COMPILER_CACHE.has(compilerCache)) {
151
+ throw new Error("diagnostics.native.compiler_cache must be one of auto, ccache, sccache, or none");
152
+ }
153
+ return {
154
+ enabled: optionalBoolean(native.enabled, false),
155
+ sampleProcessTree: optionalBoolean(native.sample_process_tree, false),
156
+ compilerCache,
157
+ expectedTools: normalizeStringArray(native.expected_tools, "diagnostics.native.expected_tools"),
158
+ artifactDirs: normalizeStringArray(native.artifact_dirs, "diagnostics.native.artifact_dirs").map(posixPath),
159
+ cacheDirs: normalizeStringArray(native.cache_dirs, "diagnostics.native.cache_dirs").map(posixPath),
160
+ };
161
+ }
162
+
163
+ export function getNativeDiagnosticsProfile(loadedConfig) {
164
+ return loadedConfig?.config?.diagnostics?.native || {
165
+ enabled: false,
166
+ sampleProcessTree: false,
167
+ compilerCache: "auto",
168
+ expectedTools: [],
169
+ artifactDirs: [],
170
+ cacheDirs: [],
171
+ };
172
+ }
173
+
132
174
  function normalizePublishSection(publish) {
133
175
  assertPlainObject(publish, "publish");
134
176
  const mode = publish.mode === undefined