@kungfu-tech/buildchain 2.0.17 → 2.1.0-alpha.0

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,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from "node:child_process";
3
+ import crypto from "node:crypto";
3
4
  import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -7,6 +8,12 @@ import { initBuildchainRepo } from "../scripts/init-repo.mjs";
7
8
  import { npmPublishDryRun } from "../scripts/npm-publish-dry-run.mjs";
8
9
  import { runLifecycle } from "../scripts/run-lifecycle-core.mjs";
9
10
  import { validateBuildchainConfig } from "../packages/core/buildchain-config.js";
11
+ import { detectPackageManager } from "../packages/core/package-manager.js";
12
+ import {
13
+ createBuildchainLogger,
14
+ defaultBuildchainLogPath,
15
+ summarizeBuildchainLogEvents,
16
+ } from "../packages/core/logging.js";
10
17
  import {
11
18
  explainReleaseLineDryRun,
12
19
  formatReleaseLineDryRun,
@@ -29,9 +36,21 @@ function usage() {
29
36
  [--dist-tag <tag>] [--skip-npm-publish-dry-run] [--json]
30
37
  buildchain release --dry-run --target-ref <ref> [--sha <sha>] [--source-ref <ref>]
31
38
  [--tags <comma-list>] [--json]
39
+ buildchain release explain --target-ref <ref> [--sha <sha>] [--source-ref <ref>]
40
+ [--tags <comma-list>] [--json]
32
41
  buildchain release dry-run --target-ref <ref> [--sha <sha>] [--source-ref <ref>]
33
42
  [--tags <comma-list>] [--json]
34
43
  buildchain release <inspect|recover|finalize|abort> ...
44
+ buildchain transaction inspect ...
45
+ buildchain doctor [--cwd <dir>] [--json]
46
+ buildchain log <info|warn|error> --event <name> [--phase <phase>]
47
+ [--component <name>] [--source <name>] [--attribute key=value]...
48
+ [--path <jsonl>] [--json]
49
+ buildchain log summary [--path <jsonl>] [--json]
50
+ buildchain mark --event <name> [--phase <phase>] [--component <name>]
51
+ [--attribute key=value]... [--path <jsonl>] [--json]
52
+ buildchain span --event <name> [--phase <phase>] [--component <name>]
53
+ [--path <jsonl>] -- <command> [args...]
35
54
  buildchain web-surface ...
36
55
  buildchain publish-source <lock|manifest|verify-lock> ...
37
56
  buildchain build-contract ...
@@ -42,6 +61,7 @@ Examples:
42
61
  buildchain lifecycle run build --artifact-path dist --artifact-name "{repo}-{version}-{platform}"
43
62
  buildchain npm dry-run --json
44
63
  buildchain release --dry-run --target-ref alpha/v2/v2.0
64
+ buildchain span --event native.build -- cmake --build build
45
65
  `;
46
66
  }
47
67
 
@@ -68,6 +88,76 @@ function readRepeatedFlag(args, name) {
68
88
  return values;
69
89
  }
70
90
 
91
+ function readAttributes(args) {
92
+ const attributes = {};
93
+ for (const value of readRepeatedFlag(args, "attribute")) {
94
+ const separator = value.indexOf("=");
95
+ if (separator === -1) {
96
+ attributes[value] = true;
97
+ } else {
98
+ attributes[value.slice(0, separator)] = value.slice(separator + 1);
99
+ }
100
+ }
101
+ return attributes;
102
+ }
103
+
104
+ function defaultCliLogPath(args) {
105
+ return readFlag(args, "path", process.env.BUILDCHAIN_LOG_PATH || defaultBuildchainLogPath({ cwd: process.cwd() }));
106
+ }
107
+
108
+ function cliLogger(args, defaults = {}) {
109
+ return createBuildchainLogger({
110
+ cwd: process.cwd(),
111
+ path: defaultCliLogPath(args),
112
+ console: !readBooleanFlag(args, "quiet"),
113
+ source: readFlag(args, "source", defaults.source || "user"),
114
+ component: readFlag(args, "component", defaults.component || "cli"),
115
+ phase: readFlag(args, "phase", defaults.phase || ""),
116
+ });
117
+ }
118
+
119
+ function checkStatus(ok, id, message, details = {}) {
120
+ return { id, status: ok ? "pass" : "fail", message, details };
121
+ }
122
+
123
+ function runDoctor({ cwd = process.cwd() } = {}) {
124
+ const resolvedCwd = path.resolve(cwd);
125
+ const checks = [];
126
+ checks.push(checkStatus(fs.existsSync(resolvedCwd), "cwd.exists", "working directory exists", { cwd: resolvedCwd }));
127
+ try {
128
+ const validation = validateBuildchainConfig(resolvedCwd);
129
+ checks.push(checkStatus(true, "config.valid", "buildchain.toml is valid", {
130
+ projectType: validation.project.type,
131
+ lifecycleStages: validation.lifecycleStages.map((stage) => stage.name),
132
+ }));
133
+ } catch (error) {
134
+ checks.push(checkStatus(false, "config.valid", error.message));
135
+ }
136
+ try {
137
+ const manager = detectPackageManager(resolvedCwd);
138
+ checks.push(checkStatus(true, "package-manager.detected", `package manager: ${manager.name}`, manager));
139
+ } catch (error) {
140
+ checks.push(checkStatus(false, "package-manager.detected", error.message));
141
+ }
142
+ const git = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
143
+ cwd: resolvedCwd,
144
+ encoding: "utf8",
145
+ });
146
+ checks.push(checkStatus(git.status === 0 && git.stdout.trim() === "true", "git.repository", "directory is a git repository"));
147
+ const workflowPath = path.join(resolvedCwd, ".github", "workflows", "build.yml");
148
+ checks.push(checkStatus(fs.existsSync(workflowPath), "workflow.build", "reusable workflow caller exists", {
149
+ path: ".github/workflows/build.yml",
150
+ }));
151
+ return {
152
+ schemaVersion: 1,
153
+ contract: "kungfu-buildchain-doctor",
154
+ cwd: resolvedCwd,
155
+ ok: checks.every((check) => check.status === "pass"),
156
+ checks,
157
+ docsUrl: "https://buildchain.libkungfu.dev/docs/cli",
158
+ };
159
+ }
160
+
71
161
  function runScript(scriptName, args) {
72
162
  const scriptPath = path.join(root, "scripts", scriptName);
73
163
  const result = spawnSync(process.execPath, [scriptPath, ...args], {
@@ -127,6 +217,111 @@ async function main(argv = process.argv.slice(2)) {
127
217
  return;
128
218
  }
129
219
 
220
+ if (command === "doctor") {
221
+ const result = runDoctor({ cwd: readFlag(args, "cwd", process.cwd()) });
222
+ if (readBooleanFlag(args, "json")) {
223
+ printJson(result);
224
+ } else {
225
+ process.stdout.write(`buildchain doctor: ${result.ok ? "ok" : "failed"}\n`);
226
+ for (const check of result.checks) {
227
+ process.stdout.write(`- ${check.status}: ${check.id}: ${check.message}\n`);
228
+ }
229
+ }
230
+ return;
231
+ }
232
+
233
+ if (command === "log") {
234
+ const [levelOrSubcommand = "info", ...logArgs] = args;
235
+ if (levelOrSubcommand === "summary") {
236
+ const logPath = defaultCliLogPath(logArgs);
237
+ const summary = summarizeBuildchainLogEvents({ path: logPath });
238
+ if (readBooleanFlag(logArgs, "json")) {
239
+ printJson(summary);
240
+ } else {
241
+ process.stdout.write(`buildchain log summary: ${summary.eventCount} events\n`);
242
+ process.stdout.write(`sources: ${Object.keys(summary.sources).join(", ") || "none"}\n`);
243
+ process.stdout.write(`phases: ${Object.keys(summary.phases).join(", ") || "none"}\n`);
244
+ }
245
+ return;
246
+ }
247
+ if (!["info", "warn", "error"].includes(levelOrSubcommand)) {
248
+ throw new Error("usage: buildchain log <info|warn|error> --event <name>");
249
+ }
250
+ const eventName = readFlag(logArgs, "event", "");
251
+ if (!eventName) {
252
+ throw new Error("buildchain log requires --event <name>");
253
+ }
254
+ const logger = cliLogger(logArgs);
255
+ const event = logger.emit(levelOrSubcommand, eventName, {
256
+ message: readFlag(logArgs, "message", ""),
257
+ attributes: readAttributes(logArgs),
258
+ });
259
+ if (readBooleanFlag(logArgs, "json")) {
260
+ printJson(event);
261
+ }
262
+ return;
263
+ }
264
+
265
+ if (command === "mark") {
266
+ const eventName = readFlag(args, "event", "");
267
+ if (!eventName) {
268
+ throw new Error("buildchain mark requires --event <name>");
269
+ }
270
+ const logger = cliLogger(args);
271
+ const event = logger.mark(eventName, {
272
+ message: readFlag(args, "message", ""),
273
+ attributes: readAttributes(args),
274
+ });
275
+ if (readBooleanFlag(args, "json")) {
276
+ printJson(event);
277
+ }
278
+ return;
279
+ }
280
+
281
+ if (command === "span") {
282
+ const separator = args.indexOf("--");
283
+ const spanArgs = separator === -1 ? args : args.slice(0, separator);
284
+ const commandArgs = separator === -1 ? [] : args.slice(separator + 1);
285
+ const eventName = readFlag(spanArgs, "event", "");
286
+ if (!eventName || commandArgs.length === 0) {
287
+ throw new Error("usage: buildchain span --event <name> -- <command> [args...]");
288
+ }
289
+ const logger = cliLogger(spanArgs);
290
+ const spanId = crypto.randomUUID();
291
+ const startedAt = Date.now();
292
+ logger.info(`${eventName}.start`, {
293
+ spanId,
294
+ message: readFlag(spanArgs, "message", ""),
295
+ attributes: readAttributes(spanArgs),
296
+ });
297
+ const result = spawnSync(commandArgs[0], commandArgs.slice(1), {
298
+ cwd: process.cwd(),
299
+ env: process.env,
300
+ stdio: "inherit",
301
+ });
302
+ const durationMs = Date.now() - startedAt;
303
+ if (result.error || result.status !== 0) {
304
+ logger.error(`${eventName}.error`, {
305
+ spanId,
306
+ durationMs,
307
+ message: result.error?.message || `command exited with ${result.status ?? "signal"}`,
308
+ attributes: {
309
+ ...readAttributes(spanArgs),
310
+ status: result.status ?? "",
311
+ signal: result.signal || "",
312
+ },
313
+ });
314
+ process.exitCode = result.status ?? 1;
315
+ return;
316
+ }
317
+ logger.info(`${eventName}.end`, {
318
+ spanId,
319
+ durationMs,
320
+ attributes: readAttributes(spanArgs),
321
+ });
322
+ return;
323
+ }
324
+
130
325
  if (command === "lifecycle") {
131
326
  const [subcommand, stageName = "", ...lifecycleArgs] = args;
132
327
  if (subcommand !== "run" || !stageName) {
@@ -140,6 +335,7 @@ async function main(argv = process.argv.slice(2)) {
140
335
  artifactName: readFlag(lifecycleArgs, "artifact-name", "buildchain-artifact"),
141
336
  artifactPaths,
142
337
  expectedArtifactsJson: readFlag(lifecycleArgs, "expected-artifacts-json", ""),
338
+ logPath: readFlag(lifecycleArgs, "log-path", process.env.BUILDCHAIN_LOG_PATH || ".buildchain/logs/events.jsonl"),
143
339
  workspace: process.cwd(),
144
340
  });
145
341
  printJson(manifest);
@@ -168,8 +364,9 @@ async function main(argv = process.argv.slice(2)) {
168
364
  }
169
365
 
170
366
  if (command === "release") {
171
- const releaseArgs = args[0] === "dry-run" ? args.slice(1) : args;
172
- if (args[0] === "dry-run" || readBooleanFlag(args, "dry-run")) {
367
+ const explainMode = args[0] === "dry-run" || args[0] === "explain";
368
+ const releaseArgs = explainMode ? args.slice(1) : args;
369
+ if (explainMode || readBooleanFlag(args, "dry-run")) {
173
370
  const plan = explainReleaseLineDryRun({
174
371
  cwd: readFlag(releaseArgs, "cwd", process.cwd()),
175
372
  targetRef: readFlag(releaseArgs, "target-ref", ""),
@@ -190,6 +387,15 @@ async function main(argv = process.argv.slice(2)) {
190
387
  return;
191
388
  }
192
389
 
390
+ if (command === "transaction") {
391
+ const [subcommand = "inspect", ...transactionArgs] = args;
392
+ if (subcommand !== "inspect") {
393
+ throw new Error("usage: buildchain transaction inspect ...");
394
+ }
395
+ runScript("release-transaction.mjs", ["inspect", ...transactionArgs]);
396
+ return;
397
+ }
398
+
193
399
  if (command === "web-surface") {
194
400
  runScript("web-surface.mjs", args);
195
401
  return;
package/docs/cli.md CHANGED
@@ -58,6 +58,54 @@ buildchain lifecycle run build \
58
58
  --artifact-name "{repo}-{version}-{platform}"
59
59
  ```
60
60
 
61
+ Lifecycle runs also write a Buildchain observability JSONL log at
62
+ `.buildchain/logs/events.jsonl` by default. Framework events use
63
+ `source=buildchain`; consumer lifecycle commands use `source=user`. This lets a
64
+ maintainer tell apart time spent inside Buildchain's artifact/manifest
65
+ framework from time spent in the repository's own build, test, packaging, or
66
+ publish commands. The artifact manifest and summary embed the observability
67
+ summary for that lifecycle run id, so uploaded artifacts preserve the timing
68
+ facts without mixing in older JSONL events.
69
+
70
+ `buildchain log`, `buildchain mark`, and `buildchain span` expose the same event
71
+ protocol to repository scripts:
72
+
73
+ ```bash
74
+ buildchain mark --event configure.ready --phase configure --attribute target=release
75
+ buildchain span --event native.build --phase build -- cmake --build build
76
+ buildchain log warn --event cache.miss --component conan --attribute token=hidden
77
+ buildchain log summary --json
78
+ ```
79
+
80
+ During `buildchain lifecycle run`, child processes receive
81
+ `BUILDCHAIN_LOG_PATH` and `BUILDCHAIN_LOG_RUN_ID`. A shell, Python, CMake, Conan,
82
+ or JavaScript helper can call `buildchain mark` or `buildchain span` mid-build
83
+ and have those events grouped into the same lifecycle summary.
84
+
85
+ The event protocol is JSONL and is also available from the SDK:
86
+
87
+ ```js
88
+ import { createBuildchainLogger } from "@kungfu-tech/buildchain/logging";
89
+
90
+ const logger = createBuildchainLogger({ source: "user", component: "native-build" });
91
+ logger.mark("configure.ready", { phase: "configure" });
92
+ ```
93
+
94
+ Secret-looking attribute keys such as `token`, `password`, `secret`,
95
+ `authorization`, `cookie`, and `private-key` are redacted before they are written.
96
+ Full command strings are not recorded by `span`; scripts should provide stable
97
+ event names and safe attributes instead.
98
+
99
+ `buildchain doctor` checks repository readiness before remote side effects:
100
+
101
+ ```bash
102
+ buildchain doctor --json
103
+ ```
104
+
105
+ It validates `buildchain.toml`, package-manager detection, Git repository state,
106
+ and the reusable workflow caller. The JSON result is shaped for future
107
+ `buildchain.libkungfu.dev` fact ingestion.
108
+
61
109
  `buildchain release`, `buildchain web-surface`, `buildchain publish-source`,
62
110
  and `buildchain build-contract` route to the same scripts used by Buildchain's
63
111
  GitHub Actions workflows. This keeps local inspection and CI behavior on the
@@ -70,6 +118,7 @@ maintainer opens or merges a channel PR:
70
118
  buildchain release --dry-run --target-ref alpha/v2/v2.0
71
119
  buildchain release --dry-run --target-ref release/v2/v2.0 --sha <verified-sha>
72
120
  buildchain release dry-run --target-ref publish-gate/major --source-ref release/v2/v2.0
121
+ buildchain release explain --target-ref alpha/v2/v2.1 --json
73
122
  ```
74
123
 
75
124
  This is a Buildchain-level dry-run, not an npm dry-run. It explains the legal
@@ -77,7 +126,21 @@ source branch, exact release or alpha tags, floating tags, channel branches,
77
126
  version-state files, governance checks, and publish transaction behavior that
78
127
  would apply if the corresponding PR merge were promoted. It does not move
79
128
  branches, move tags, edit files, publish npm packages, or run lifecycle publish
80
- commands. Pass `--json` for a machine-readable plan.
129
+ commands. `release explain` is the same explanation surface with a clearer name.
130
+ Pass `--json` for a machine-readable plan.
131
+
132
+ `buildchain transaction inspect` is the top-level recovery inspection command
133
+ for the publish transaction state:
134
+
135
+ ```bash
136
+ buildchain transaction inspect --version v2.1.0-alpha.0
137
+ ```
138
+
139
+ It reads or locally initializes the durable transaction record and validates
140
+ available publish evidence. Remote durable refs and public Git ref finalization
141
+ remain owned by `actions/promote-buildchain-ref`; the CLI inspection surface is
142
+ for preflight and recovery reasoning before a maintainer reruns or resumes a
143
+ promotion.
81
144
 
82
145
  `buildchain npm dry-run` verifies the package shape before a release tag exists:
83
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kungfu-tech/buildchain",
3
- "version": "2.0.17",
3
+ "version": "2.1.0-alpha.0",
4
4
  "private": false,
5
5
  "description": "Kungfu Buildchain reusable workflows, release governance, and CLI tooling.",
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
+ "./logging": "./packages/core/logging.js",
16
17
  "./package.json": "./package.json"
17
18
  },
18
19
  "files": [
@@ -39,3 +39,15 @@ export {
39
39
  explainReleaseLineDryRun,
40
40
  formatReleaseLineDryRun,
41
41
  } from "./release-line-dry-run.js";
42
+
43
+ export {
44
+ BUILDCHAIN_LOG_EVENT_CONTRACT,
45
+ BUILDCHAIN_LOG_SUMMARY_CONTRACT,
46
+ appendBuildchainLogEvent,
47
+ createBuildchainLogger,
48
+ defaultBuildchainLogPath,
49
+ normalizeBuildchainLogEvent,
50
+ readBuildchainLogEvents,
51
+ redactBuildchainLogAttributes,
52
+ summarizeBuildchainLogEvents,
53
+ } from "./logging.js";
@@ -0,0 +1,197 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export const BUILDCHAIN_LOG_EVENT_CONTRACT = "kungfu-buildchain-log-event";
6
+ export const BUILDCHAIN_LOG_SUMMARY_CONTRACT = "kungfu-buildchain-log-summary";
7
+
8
+ const SECRET_KEY_PATTERN =
9
+ /(authorization|cookie|credential|password|passwd|private[_-]?key|secret|token|api[_-]?key)/i;
10
+
11
+ function compactObject(value) {
12
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined && entry !== ""));
13
+ }
14
+
15
+ function scalarValue(value) {
16
+ if (value === null || value === undefined) {
17
+ return "";
18
+ }
19
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
20
+ return value;
21
+ }
22
+ return JSON.stringify(value);
23
+ }
24
+
25
+ export function redactBuildchainLogAttributes(attributes = {}) {
26
+ const redacted = {};
27
+ for (const [key, value] of Object.entries(attributes || {})) {
28
+ redacted[key] = SECRET_KEY_PATTERN.test(key) ? "[REDACTED]" : scalarValue(value);
29
+ }
30
+ return redacted;
31
+ }
32
+
33
+ export function defaultBuildchainLogPath({ cwd = process.cwd() } = {}) {
34
+ return path.join(cwd, ".buildchain", "logs", "events.jsonl");
35
+ }
36
+
37
+ export function normalizeBuildchainLogEvent(input = {}, defaults = {}) {
38
+ const timestamp = input.timestamp || new Date().toISOString();
39
+ return compactObject({
40
+ schemaVersion: 1,
41
+ contract: BUILDCHAIN_LOG_EVENT_CONTRACT,
42
+ timestamp,
43
+ level: input.level || defaults.level || "info",
44
+ source: input.source || defaults.source || "buildchain",
45
+ component: input.component || defaults.component || "cli",
46
+ event: input.event || defaults.event || "event",
47
+ phase: input.phase || defaults.phase,
48
+ spanId: input.spanId || defaults.spanId,
49
+ parentSpanId: input.parentSpanId || defaults.parentSpanId,
50
+ durationMs: Number.isFinite(input.durationMs) ? Math.round(input.durationMs) : undefined,
51
+ message: input.message || defaults.message,
52
+ attributes: redactBuildchainLogAttributes({
53
+ ...(defaults.attributes || {}),
54
+ ...(input.attributes || {}),
55
+ }),
56
+ });
57
+ }
58
+
59
+ export function appendBuildchainLogEvent(filePath, event) {
60
+ if (!filePath) {
61
+ return;
62
+ }
63
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
64
+ fs.appendFileSync(filePath, `${JSON.stringify(event)}\n`);
65
+ }
66
+
67
+ export function readBuildchainLogEvents(filePath) {
68
+ if (!filePath || !fs.existsSync(filePath)) {
69
+ return [];
70
+ }
71
+ return fs
72
+ .readFileSync(filePath, "utf8")
73
+ .split(/\r?\n/)
74
+ .filter(Boolean)
75
+ .map((line) => JSON.parse(line));
76
+ }
77
+
78
+ function groupSummary(events, field) {
79
+ const groups = {};
80
+ for (const event of events) {
81
+ const key = event[field] || "unknown";
82
+ const current = groups[key] || { count: 0, durationMs: 0 };
83
+ current.count += 1;
84
+ current.durationMs += Number(event.durationMs || 0);
85
+ groups[key] = current;
86
+ }
87
+ return Object.fromEntries(
88
+ Object.entries(groups).sort(([left], [right]) => left.localeCompare(right)),
89
+ );
90
+ }
91
+
92
+ export function summarizeBuildchainLogEvents(input = {}) {
93
+ const events = Array.isArray(input)
94
+ ? input
95
+ : typeof input === "string"
96
+ ? readBuildchainLogEvents(input)
97
+ : readBuildchainLogEvents(input.path);
98
+ const errorCount = events.filter((event) => event.level === "error").length;
99
+ const warningCount = events.filter((event) => event.level === "warn").length;
100
+ return {
101
+ schemaVersion: 1,
102
+ contract: BUILDCHAIN_LOG_SUMMARY_CONTRACT,
103
+ generatedAt: new Date().toISOString(),
104
+ eventCount: events.length,
105
+ warningCount,
106
+ errorCount,
107
+ durationMs: events.reduce((sum, event) => sum + Number(event.durationMs || 0), 0),
108
+ sources: groupSummary(events, "source"),
109
+ phases: groupSummary(events, "phase"),
110
+ components: groupSummary(events, "component"),
111
+ };
112
+ }
113
+
114
+ export function createBuildchainLogger(options = {}) {
115
+ const cwd = path.resolve(options.cwd || process.cwd());
116
+ const runningInActions = process.env.GITHUB_ACTIONS === "true";
117
+ const resolvedPath =
118
+ options.path === false
119
+ ? ""
120
+ : options.path || process.env.BUILDCHAIN_LOG_PATH || (runningInActions ? defaultBuildchainLogPath({ cwd }) : "");
121
+ const consoleEnabled = options.console ?? !resolvedPath;
122
+ const defaults = {
123
+ source: options.source || "buildchain",
124
+ component: options.component || "cli",
125
+ phase: options.phase || "",
126
+ attributes: {
127
+ ...(process.env.BUILDCHAIN_LOG_RUN_ID
128
+ ? { buildchainLogRunId: process.env.BUILDCHAIN_LOG_RUN_ID }
129
+ : {}),
130
+ ...(options.attributes || {}),
131
+ },
132
+ };
133
+ const inMemoryEvents = [];
134
+
135
+ function emit(level, eventName, details = {}) {
136
+ const event = normalizeBuildchainLogEvent(
137
+ {
138
+ ...details,
139
+ level,
140
+ event: eventName,
141
+ },
142
+ defaults,
143
+ );
144
+ try {
145
+ appendBuildchainLogEvent(resolvedPath, event);
146
+ } catch (error) {
147
+ if (options.strict) {
148
+ throw error;
149
+ }
150
+ }
151
+ inMemoryEvents.push(event);
152
+ if (consoleEnabled) {
153
+ const message = event.message ? ` ${event.message}` : "";
154
+ process.stderr.write(`[buildchain] ${event.level} ${event.event}${message}\n`);
155
+ }
156
+ return event;
157
+ }
158
+
159
+ async function span(eventName, details = {}, callback = async () => undefined) {
160
+ const spanId = details.spanId || crypto.randomUUID();
161
+ const startedAt = Date.now();
162
+ emit("info", `${eventName}.start`, { ...details, spanId });
163
+ try {
164
+ const result = await callback({ spanId });
165
+ emit("info", `${eventName}.end`, {
166
+ ...details,
167
+ spanId,
168
+ durationMs: Date.now() - startedAt,
169
+ });
170
+ return result;
171
+ } catch (error) {
172
+ emit("error", `${eventName}.error`, {
173
+ ...details,
174
+ spanId,
175
+ durationMs: Date.now() - startedAt,
176
+ message: error.message,
177
+ attributes: {
178
+ ...(details.attributes || {}),
179
+ errorName: error.name,
180
+ },
181
+ });
182
+ throw error;
183
+ }
184
+ }
185
+
186
+ return {
187
+ path: resolvedPath,
188
+ events: inMemoryEvents,
189
+ emit,
190
+ info: (eventName, details) => emit("info", eventName, details),
191
+ warn: (eventName, details) => emit("warn", eventName, details),
192
+ error: (eventName, details) => emit("error", eventName, details),
193
+ mark: (eventName, details) => emit("info", eventName, details),
194
+ span,
195
+ summary: () => summarizeBuildchainLogEvents(resolvedPath ? { path: resolvedPath } : inMemoryEvents),
196
+ };
197
+ }
@@ -55,6 +55,9 @@ if (rootPackage.bin?.buildchain !== "./bin/buildchain.mjs") {
55
55
  if (rootPackage.exports?.["."] !== "./packages/core/index.js") {
56
56
  throw new Error("root package must export packages/core/index.js");
57
57
  }
58
+ if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
59
+ throw new Error("root package must export @kungfu-tech/buildchain/logging");
60
+ }
58
61
  if (rootPackage.publishConfig?.access !== "public") {
59
62
  throw new Error("root package publishConfig.access must be public");
60
63
  }
@@ -8,6 +8,11 @@ import {
8
8
  normalizeLifecycleStage,
9
9
  runLifecycleStage,
10
10
  } from "../packages/core/buildchain-config.js";
11
+ import {
12
+ createBuildchainLogger,
13
+ readBuildchainLogEvents,
14
+ summarizeBuildchainLogEvents,
15
+ } from "../packages/core/logging.js";
11
16
  import {
12
17
  createArtifactSummary,
13
18
  parseExpectedArtifactsJson,
@@ -46,6 +51,15 @@ function manifestPathFor(root, filePath) {
46
51
  return toPosix(path.resolve(filePath));
47
52
  }
48
53
 
54
+ function lifecycleErrorAttributes(error, extra = {}) {
55
+ return {
56
+ ...extra,
57
+ errorName: error.name,
58
+ status: error.status ?? "",
59
+ signal: error.signal || "",
60
+ };
61
+ }
62
+
49
63
  function collectArtifactFiles(root, patterns) {
50
64
  const files = new Set();
51
65
  for (const pattern of patterns) {
@@ -70,38 +84,142 @@ export function runLifecycle({
70
84
  artifactPaths = [],
71
85
  expectedArtifactsJson = "",
72
86
  workspace = process.cwd(),
87
+ logPath = process.env.BUILDCHAIN_LOG_PATH || ".buildchain/logs/events.jsonl",
73
88
  } = {}) {
74
89
  const resolvedCwd = path.resolve(cwd);
75
90
  const resolvedWorkspace = path.resolve(workspace);
76
91
  const resolvedManifestPath = path.resolve(resolvedWorkspace, manifestPath);
77
92
  const resolvedSummaryPath = path.resolve(resolvedWorkspace, summaryPath);
93
+ const resolvedLogPath = logPath ? path.resolve(resolvedWorkspace, logPath) : "";
94
+ const relativeLogPath = resolvedLogPath ? toPosix(path.relative(resolvedWorkspace, resolvedLogPath)) : "";
95
+ const logRunId = crypto.randomUUID();
96
+ const frameworkLog = createBuildchainLogger({
97
+ cwd: resolvedWorkspace,
98
+ path: resolvedLogPath || false,
99
+ console: false,
100
+ source: "buildchain",
101
+ component: "lifecycle",
102
+ phase: stageName || "lifecycle",
103
+ attributes: { buildchainLogRunId: logRunId },
104
+ });
105
+ const userLog = createBuildchainLogger({
106
+ cwd: resolvedWorkspace,
107
+ path: resolvedLogPath || false,
108
+ console: false,
109
+ source: "user",
110
+ component: "lifecycle",
111
+ phase: stageName || "lifecycle",
112
+ attributes: { buildchainLogRunId: logRunId },
113
+ });
78
114
  const loadedConfig = loadBuildchainConfig(resolvedCwd);
79
115
  let commandSource = "none";
80
116
  let executed = false;
81
117
 
118
+ frameworkLog.info("lifecycle.start", {
119
+ attributes: {
120
+ stage: stageName,
121
+ artifactName,
122
+ platformId,
123
+ },
124
+ });
125
+
82
126
  if (command.trim()) {
83
127
  commandSource = "workflow-input";
84
- execSync(command, {
85
- cwd: resolvedCwd,
86
- env: process.env,
87
- shell: true,
88
- stdio: "inherit",
128
+ const startedAt = Date.now();
129
+ userLog.info("lifecycle.command.start", {
130
+ attributes: {
131
+ commandSource,
132
+ stage: stageName || "command",
133
+ },
89
134
  });
90
- executed = true;
135
+ 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
+ });
150
+ executed = true;
151
+ userLog.info("lifecycle.command.end", {
152
+ durationMs: Date.now() - startedAt,
153
+ attributes: {
154
+ commandSource,
155
+ stage: stageName || "command",
156
+ },
157
+ });
158
+ } catch (error) {
159
+ userLog.error("lifecycle.command.error", {
160
+ durationMs: Date.now() - startedAt,
161
+ message: "lifecycle command failed",
162
+ attributes: lifecycleErrorAttributes(error, {
163
+ commandSource,
164
+ stage: stageName || "command",
165
+ }),
166
+ });
167
+ throw error;
168
+ }
91
169
  } else if (stageName) {
92
170
  commandSource = "buildchain.toml";
93
- executed = runLifecycleStage({
94
- cwd: resolvedCwd,
95
- loadedConfig,
96
- name: stageName,
171
+ const startedAt = Date.now();
172
+ userLog.info("lifecycle.stage.start", {
173
+ attributes: {
174
+ commandSource,
175
+ stage: stageName,
176
+ },
97
177
  });
178
+ try {
179
+ executed = runLifecycleStage({
180
+ cwd: resolvedCwd,
181
+ loadedConfig,
182
+ name: stageName,
183
+ env: resolvedLogPath
184
+ ? {
185
+ BUILDCHAIN_LOG_PATH: resolvedLogPath,
186
+ BUILDCHAIN_LOG_RUN_ID: logRunId,
187
+ }
188
+ : {},
189
+ });
190
+ userLog.info("lifecycle.stage.end", {
191
+ durationMs: Date.now() - startedAt,
192
+ attributes: {
193
+ commandSource,
194
+ stage: stageName,
195
+ executed,
196
+ },
197
+ });
198
+ } catch (error) {
199
+ userLog.error("lifecycle.stage.error", {
200
+ durationMs: Date.now() - startedAt,
201
+ message: "lifecycle stage failed",
202
+ attributes: lifecycleErrorAttributes(error, {
203
+ commandSource,
204
+ stage: stageName,
205
+ }),
206
+ });
207
+ throw error;
208
+ }
98
209
  }
99
210
 
100
211
  if (required && !executed) {
212
+ frameworkLog.error("lifecycle.required-missing", {
213
+ attributes: {
214
+ stage: stageName || "command",
215
+ commandSource,
216
+ },
217
+ });
101
218
  throw new Error(`required lifecycle stage did not run: ${stageName || "command"}`);
102
219
  }
103
220
 
104
221
  fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
222
+ const scanStartedAt = Date.now();
105
223
  const files = collectArtifactFiles(resolvedWorkspace, artifactPaths);
106
224
  const manifestFiles = files.map((file) => {
107
225
  const stat = fs.statSync(file);
@@ -111,6 +229,12 @@ export function runLifecycle({
111
229
  sha256: sha256File(file),
112
230
  };
113
231
  });
232
+ frameworkLog.info("artifact.scan", {
233
+ durationMs: Date.now() - scanStartedAt,
234
+ attributes: {
235
+ fileCount: manifestFiles.length,
236
+ },
237
+ });
114
238
  const platform = {
115
239
  id: platformId,
116
240
  name: platformName,
@@ -128,6 +252,37 @@ export function runLifecycle({
128
252
  files: manifestFiles,
129
253
  summary,
130
254
  });
255
+ frameworkLog.info("artifact.manifest.write", {
256
+ attributes: {
257
+ manifestPath: toPosix(path.relative(resolvedWorkspace, resolvedManifestPath)),
258
+ summaryPath: toPosix(path.relative(resolvedWorkspace, resolvedSummaryPath)),
259
+ },
260
+ });
261
+ frameworkLog.info("lifecycle.end", {
262
+ attributes: {
263
+ stage: stageName,
264
+ executed,
265
+ fileCount: manifestFiles.length,
266
+ },
267
+ });
268
+ const observability = {
269
+ log: {
270
+ contract: "kungfu-buildchain-log-event",
271
+ runId: logRunId,
272
+ path: relativeLogPath,
273
+ summary: resolvedLogPath
274
+ ? summarizeBuildchainLogEvents(
275
+ readBuildchainLogEvents(resolvedLogPath).filter(
276
+ (event) => event.attributes?.buildchainLogRunId === logRunId,
277
+ ),
278
+ )
279
+ : summarizeBuildchainLogEvents([...frameworkLog.events, ...userLog.events]),
280
+ },
281
+ };
282
+ const summaryWithObservability = {
283
+ ...summary,
284
+ observability,
285
+ };
131
286
  const manifest = {
132
287
  schemaVersion: 1,
133
288
  contract: "kungfu-buildchain-artifact",
@@ -145,14 +300,15 @@ export function runLifecycle({
145
300
  commandSource,
146
301
  executed,
147
302
  },
148
- summary,
303
+ observability,
304
+ summary: summaryWithObservability,
149
305
  expectedArtifacts,
150
306
  files: manifestFiles,
151
307
  };
152
308
 
153
309
  fs.writeFileSync(resolvedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
154
310
  fs.mkdirSync(path.dirname(resolvedSummaryPath), { recursive: true });
155
- fs.writeFileSync(resolvedSummaryPath, `${JSON.stringify(summary, null, 2)}\n`);
311
+ fs.writeFileSync(resolvedSummaryPath, `${JSON.stringify(summaryWithObservability, null, 2)}\n`);
156
312
  console.log(`buildchain_manifest=${path.relative(resolvedWorkspace, resolvedManifestPath)}`);
157
313
  return manifest;
158
314
  }