@kungfu-tech/buildchain 2.0.18-alpha.0 → 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.
- package/bin/buildchain.mjs +208 -2
- package/docs/cli.md +64 -1
- package/package.json +2 -1
- package/packages/core/index.js +12 -0
- package/packages/core/logging.js +197 -0
- package/scripts/check-inventory.mjs +3 -0
- package/scripts/run-lifecycle-core.mjs +168 -12
package/bin/buildchain.mjs
CHANGED
|
@@ -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
|
|
172
|
-
|
|
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.
|
|
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
|
|
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": [
|
package/packages/core/index.js
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
128
|
+
const startedAt = Date.now();
|
|
129
|
+
userLog.info("lifecycle.command.start", {
|
|
130
|
+
attributes: {
|
|
131
|
+
commandSource,
|
|
132
|
+
stage: stageName || "command",
|
|
133
|
+
},
|
|
89
134
|
});
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
}
|