@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.
- package/AGENTS.md +11 -0
- package/CONTRIBUTING.md +12 -2
- package/README.md +21 -0
- package/actions/promote-buildchain-ref/README.md +39 -1
- package/actions/run-lifecycle/README.md +20 -0
- package/bin/buildchain.mjs +253 -8
- package/dist/site/cli-registry.json +5 -0
- package/dist/site/release-provenance.json +1 -0
- package/docs/MAP.md +4 -0
- package/docs/cli.md +125 -2
- package/docs/install.md +25 -1
- package/docs/publish-transaction.md +10 -0
- package/docs/release-passport.md +35 -0
- package/docs/reusable-build-surface.md +152 -3
- package/docs/toolkit-observability.md +235 -0
- package/docs/web-surface-deployments.md +35 -2
- package/fixtures/libnode-shaped/README.md +2 -1
- package/package.json +2 -1
- package/packages/core/README.md +22 -0
- package/packages/core/buildchain-config.js +42 -0
- package/packages/core/diagnostics.js +1392 -0
- package/packages/core/index.js +32 -0
- package/packages/core/logging.js +51 -0
- package/packages/core/release-passport.js +581 -13
- package/scripts/aggregate-build-summary.mjs +30 -0
- package/scripts/aggregate-diagnostics-summary.mjs +54 -0
- package/scripts/check-inventory.mjs +19 -0
- package/scripts/generate-site-bundle.mjs +1 -0
- package/scripts/init-repo.mjs +14 -0
- package/scripts/npm-publish-dry-run.mjs +6 -0
- package/scripts/run-lifecycle-core.mjs +391 -17
- package/scripts/run-lifecycle.mjs +16 -0
- package/scripts/runtime-ref-core.mjs +109 -0
- package/scripts/web-surface-core.mjs +8 -0
- package/scripts/web-surface.mjs +4 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
2
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import {
|
|
7
8
|
loadBuildchainConfig,
|
|
8
9
|
normalizeLifecycleStage,
|
|
@@ -13,12 +14,32 @@ import {
|
|
|
13
14
|
readBuildchainLogEvents,
|
|
14
15
|
summarizeBuildchainLogEvents,
|
|
15
16
|
} from "../packages/core/logging.js";
|
|
17
|
+
import {
|
|
18
|
+
BUILDCHAIN_DIAGNOSTICS_CONTRACT,
|
|
19
|
+
BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT,
|
|
20
|
+
BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT,
|
|
21
|
+
BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
|
|
22
|
+
createDiagnosticsArtifact,
|
|
23
|
+
summarizeLifecycleObservability,
|
|
24
|
+
writeDiagnosticsArtifact,
|
|
25
|
+
} from "../packages/core/diagnostics.js";
|
|
16
26
|
import {
|
|
17
27
|
createArtifactSummary,
|
|
18
28
|
parseExpectedArtifactsJson,
|
|
19
29
|
validateExpectedArtifacts,
|
|
20
30
|
} from "./build-contract-core.mjs";
|
|
21
31
|
|
|
32
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const buildchainCliCandidates = [
|
|
34
|
+
path.resolve(moduleDir, "..", "bin", "buildchain.mjs"),
|
|
35
|
+
path.resolve(moduleDir, "..", "..", "..", "bin", "buildchain.mjs"),
|
|
36
|
+
path.resolve(process.cwd(), ".buildchain", "runtime", "bin", "buildchain.mjs"),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function resolveBuildchainCliPath() {
|
|
40
|
+
return buildchainCliCandidates.find((candidate) => fs.existsSync(candidate)) || buildchainCliCandidates[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
function sha256File(filePath) {
|
|
23
44
|
const hash = crypto.createHash("sha256");
|
|
24
45
|
hash.update(fs.readFileSync(filePath));
|
|
@@ -71,6 +92,224 @@ function collectArtifactFiles(root, patterns) {
|
|
|
71
92
|
return [...files].sort();
|
|
72
93
|
}
|
|
73
94
|
|
|
95
|
+
function readProcessSummaryArtifact(filePath) {
|
|
96
|
+
if (!filePath) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (!fs.existsSync(filePath)) {
|
|
100
|
+
throw new Error(`process summary file not found: ${filePath}`);
|
|
101
|
+
}
|
|
102
|
+
let artifact;
|
|
103
|
+
try {
|
|
104
|
+
artifact = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`failed to read process summary file ${filePath}: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
if (artifact?.contract === BUILDCHAIN_PROCESS_SAMPLE_REPORT_CONTRACT && artifact.summary) {
|
|
109
|
+
return {
|
|
110
|
+
artifact,
|
|
111
|
+
summary: artifact.summary,
|
|
112
|
+
samplesPath: artifact.samplesPath || "",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (artifact?.contract === BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT) {
|
|
116
|
+
return {
|
|
117
|
+
artifact,
|
|
118
|
+
summary: artifact,
|
|
119
|
+
samplesPath: "",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
throw new Error(`process summary file has unsupported contract: ${artifact?.contract || "unknown"}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shellCommandArgs(command, shell) {
|
|
126
|
+
if (typeof shell === "string" && shell.trim()) {
|
|
127
|
+
return [shell, "-c", command];
|
|
128
|
+
}
|
|
129
|
+
if (process.platform === "win32") {
|
|
130
|
+
return [process.env.ComSpec || "cmd.exe", "/d", "/s", "/c", command];
|
|
131
|
+
}
|
|
132
|
+
return [process.env.SHELL || "/bin/sh", "-c", command];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function samplerPathForCwd(filePath, cwd) {
|
|
136
|
+
const relative = path.relative(cwd, filePath);
|
|
137
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative)
|
|
138
|
+
? toPosix(relative)
|
|
139
|
+
: filePath;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function executeSampledShellCommand({
|
|
143
|
+
command,
|
|
144
|
+
cwd,
|
|
145
|
+
env,
|
|
146
|
+
shell,
|
|
147
|
+
timeout,
|
|
148
|
+
label,
|
|
149
|
+
processSummaryPath,
|
|
150
|
+
processSamplesPath,
|
|
151
|
+
processSampleIntervalMs,
|
|
152
|
+
requestedParallelism,
|
|
153
|
+
}) {
|
|
154
|
+
fs.mkdirSync(path.dirname(processSummaryPath), { recursive: true });
|
|
155
|
+
fs.mkdirSync(path.dirname(processSamplesPath), { recursive: true });
|
|
156
|
+
const args = [
|
|
157
|
+
resolveBuildchainCliPath(),
|
|
158
|
+
"sample",
|
|
159
|
+
"process-tree",
|
|
160
|
+
"--label",
|
|
161
|
+
label || "lifecycle",
|
|
162
|
+
"--interval-ms",
|
|
163
|
+
String(processSampleIntervalMs || 15000),
|
|
164
|
+
"--output",
|
|
165
|
+
samplerPathForCwd(processSamplesPath, cwd),
|
|
166
|
+
"--summary-output",
|
|
167
|
+
samplerPathForCwd(processSummaryPath, cwd),
|
|
168
|
+
];
|
|
169
|
+
if (Number(requestedParallelism || 0) > 0) {
|
|
170
|
+
args.push("--requested-parallelism", String(Number(requestedParallelism)));
|
|
171
|
+
}
|
|
172
|
+
args.push("--", ...shellCommandArgs(command, shell));
|
|
173
|
+
execFileSync(process.execPath, args, {
|
|
174
|
+
cwd,
|
|
175
|
+
env,
|
|
176
|
+
stdio: "inherit",
|
|
177
|
+
timeout,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stageCommandText(stage) {
|
|
182
|
+
if (stage.mode === "script") {
|
|
183
|
+
return stage.script;
|
|
184
|
+
}
|
|
185
|
+
if (process.platform === "win32") {
|
|
186
|
+
return stage.commands.join(" && ");
|
|
187
|
+
}
|
|
188
|
+
return ["set -e", ...stage.commands].join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function runLifecycleStageWithSampler({
|
|
192
|
+
cwd,
|
|
193
|
+
loadedConfig,
|
|
194
|
+
name,
|
|
195
|
+
env,
|
|
196
|
+
sampleProcessTree,
|
|
197
|
+
processSummaryPath,
|
|
198
|
+
processSamplesPath,
|
|
199
|
+
processSampleIntervalMs,
|
|
200
|
+
requestedParallelism,
|
|
201
|
+
}) {
|
|
202
|
+
if (!sampleProcessTree) {
|
|
203
|
+
return runLifecycleStage({ cwd, loadedConfig, name, env });
|
|
204
|
+
}
|
|
205
|
+
const lifecycle = loadedConfig?.config?.lifecycle || {};
|
|
206
|
+
const stage = lifecycle[name];
|
|
207
|
+
if (!stage) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const stageEnv = {
|
|
211
|
+
...process.env,
|
|
212
|
+
...(lifecycle.env || {}),
|
|
213
|
+
...(stage.env || {}),
|
|
214
|
+
...(env || {}),
|
|
215
|
+
};
|
|
216
|
+
const timeout = stage.timeoutMinutes ? stage.timeoutMinutes * 60_000 : undefined;
|
|
217
|
+
let lastError;
|
|
218
|
+
for (let attempt = 1; attempt <= stage.retries; attempt += 1) {
|
|
219
|
+
try {
|
|
220
|
+
executeSampledShellCommand({
|
|
221
|
+
command: stageCommandText(stage),
|
|
222
|
+
cwd,
|
|
223
|
+
env: stageEnv,
|
|
224
|
+
shell: stage.shell || true,
|
|
225
|
+
timeout,
|
|
226
|
+
label: `lifecycle-${name}`,
|
|
227
|
+
processSummaryPath,
|
|
228
|
+
processSamplesPath,
|
|
229
|
+
processSampleIntervalMs,
|
|
230
|
+
requestedParallelism,
|
|
231
|
+
});
|
|
232
|
+
return true;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
lastError = error;
|
|
235
|
+
if (attempt < stage.retries) {
|
|
236
|
+
console.log(`> lifecycle ${name || "stage"} failed, retry ${attempt + 1}/${stage.retries}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
throw lastError;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function writeJsonlEvents(filePath, events = []) {
|
|
244
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
245
|
+
fs.writeFileSync(filePath, events.map((event) => JSON.stringify(event)).join("\n") + (events.length ? "\n" : ""));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function copyIfExists(sourcePath, targetPath) {
|
|
249
|
+
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
253
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function resolveLinkedFilePath({ linkedPath = "", workspace, cwd, fallbackDir }) {
|
|
258
|
+
if (!linkedPath) {
|
|
259
|
+
return "";
|
|
260
|
+
}
|
|
261
|
+
const candidates = path.isAbsolute(linkedPath)
|
|
262
|
+
? [linkedPath]
|
|
263
|
+
: [
|
|
264
|
+
path.resolve(workspace, linkedPath),
|
|
265
|
+
path.resolve(cwd, linkedPath),
|
|
266
|
+
path.resolve(fallbackDir, linkedPath),
|
|
267
|
+
];
|
|
268
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0] || "";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function diagnosticsSidecarEntry({ kind, filePath, workspace, required = false }) {
|
|
272
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
const stat = fs.statSync(filePath);
|
|
276
|
+
if (!stat.isFile()) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
kind,
|
|
281
|
+
path: toPosix(path.relative(workspace, filePath)),
|
|
282
|
+
bytes: stat.size,
|
|
283
|
+
sha256: sha256File(filePath),
|
|
284
|
+
required: Boolean(required),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function writeDiagnosticsSidecarManifest(filePath, {
|
|
289
|
+
workspace,
|
|
290
|
+
artifactName,
|
|
291
|
+
platformId,
|
|
292
|
+
diagnosticsArtifactName = "",
|
|
293
|
+
files = [],
|
|
294
|
+
}) {
|
|
295
|
+
const entries = files
|
|
296
|
+
.map((entry) => diagnosticsSidecarEntry({ ...entry, workspace }))
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
const manifest = {
|
|
299
|
+
schemaVersion: 1,
|
|
300
|
+
contract: BUILDCHAIN_DIAGNOSTICS_MANIFEST_CONTRACT,
|
|
301
|
+
generatedAt: new Date().toISOString(),
|
|
302
|
+
artifactName,
|
|
303
|
+
platformId,
|
|
304
|
+
...(diagnosticsArtifactName ? { diagnosticsArtifactName } : {}),
|
|
305
|
+
fileCount: entries.length,
|
|
306
|
+
totalBytes: entries.reduce((sum, entry) => sum + entry.bytes, 0),
|
|
307
|
+
files: entries,
|
|
308
|
+
};
|
|
309
|
+
fs.writeFileSync(filePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
310
|
+
return manifest;
|
|
311
|
+
}
|
|
312
|
+
|
|
74
313
|
export function runLifecycle({
|
|
75
314
|
cwd = process.cwd(),
|
|
76
315
|
stageName = "",
|
|
@@ -78,20 +317,52 @@ export function runLifecycle({
|
|
|
78
317
|
required = false,
|
|
79
318
|
manifestPath = ".buildchain/artifacts/manifest.json",
|
|
80
319
|
summaryPath = ".buildchain/artifacts/summary.json",
|
|
320
|
+
diagnosticsPath = "",
|
|
81
321
|
artifactName = "buildchain-artifact",
|
|
322
|
+
manifestArtifactName = "",
|
|
323
|
+
diagnosticsArtifactName = "",
|
|
82
324
|
platformId = os.platform(),
|
|
83
325
|
platformName = platformId,
|
|
84
326
|
artifactPaths = [],
|
|
85
327
|
expectedArtifactsJson = "",
|
|
86
328
|
workspace = process.cwd(),
|
|
87
329
|
logPath = process.env.BUILDCHAIN_LOG_PATH || ".buildchain/logs/events.jsonl",
|
|
330
|
+
processSummaryPath = "",
|
|
331
|
+
processSamplesPath = ".buildchain/diagnostics/process-samples.jsonl",
|
|
332
|
+
sampleProcessTree = false,
|
|
333
|
+
processSampleIntervalMs = 15000,
|
|
334
|
+
requestedParallelism = 0,
|
|
335
|
+
processSummaryRequired = true,
|
|
88
336
|
} = {}) {
|
|
89
337
|
const resolvedCwd = path.resolve(cwd);
|
|
90
338
|
const resolvedWorkspace = path.resolve(workspace);
|
|
91
339
|
const resolvedManifestPath = path.resolve(resolvedWorkspace, manifestPath);
|
|
92
340
|
const resolvedSummaryPath = path.resolve(resolvedWorkspace, summaryPath);
|
|
341
|
+
const resolvedDiagnosticsPath = path.resolve(
|
|
342
|
+
resolvedWorkspace,
|
|
343
|
+
diagnosticsPath || path.join(path.dirname(manifestPath), "diagnostics.json"),
|
|
344
|
+
);
|
|
93
345
|
const resolvedLogPath = logPath ? path.resolve(resolvedWorkspace, logPath) : "";
|
|
346
|
+
const resolvedProcessSummaryPath = processSummaryPath || sampleProcessTree
|
|
347
|
+
? path.resolve(resolvedWorkspace, processSummaryPath || ".buildchain/diagnostics/process-summary.json")
|
|
348
|
+
: "";
|
|
349
|
+
const resolvedProcessSamplesPath = processSamplesPath
|
|
350
|
+
? path.resolve(resolvedWorkspace, processSamplesPath)
|
|
351
|
+
: path.resolve(resolvedWorkspace, ".buildchain/diagnostics/process-samples.jsonl");
|
|
94
352
|
const relativeLogPath = resolvedLogPath ? toPosix(path.relative(resolvedWorkspace, resolvedLogPath)) : "";
|
|
353
|
+
const relativeProcessSummaryPath = resolvedProcessSummaryPath
|
|
354
|
+
? toPosix(path.relative(resolvedWorkspace, resolvedProcessSummaryPath))
|
|
355
|
+
: "";
|
|
356
|
+
const relativeDiagnosticsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsPath));
|
|
357
|
+
const diagnosticsDir = path.dirname(resolvedDiagnosticsPath);
|
|
358
|
+
const resolvedDiagnosticsEventsPath = path.join(diagnosticsDir, "events.jsonl");
|
|
359
|
+
const resolvedDiagnosticsProcessSummaryPath = path.join(diagnosticsDir, "process-summary.json");
|
|
360
|
+
const resolvedDiagnosticsProcessSamplesPath = path.join(diagnosticsDir, "process-samples.jsonl");
|
|
361
|
+
const resolvedDiagnosticsManifestPath = path.join(diagnosticsDir, "diagnostics-manifest.json");
|
|
362
|
+
const relativeDiagnosticsEventsPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsEventsPath));
|
|
363
|
+
const relativeDiagnosticsProcessSummaryPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSummaryPath));
|
|
364
|
+
const relativeDiagnosticsProcessSamplesPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsProcessSamplesPath));
|
|
365
|
+
const relativeDiagnosticsManifestPath = toPosix(path.relative(resolvedWorkspace, resolvedDiagnosticsManifestPath));
|
|
95
366
|
const logRunId = crypto.randomUUID();
|
|
96
367
|
const frameworkLog = createBuildchainLogger({
|
|
97
368
|
cwd: resolvedWorkspace,
|
|
@@ -130,23 +401,39 @@ export function runLifecycle({
|
|
|
130
401
|
attributes: {
|
|
131
402
|
commandSource,
|
|
132
403
|
stage: stageName || "command",
|
|
404
|
+
sampleProcessTree,
|
|
133
405
|
},
|
|
134
406
|
});
|
|
135
407
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
408
|
+
const commandEnv = {
|
|
409
|
+
...process.env,
|
|
410
|
+
...(resolvedLogPath
|
|
411
|
+
? {
|
|
412
|
+
BUILDCHAIN_LOG_PATH: resolvedLogPath,
|
|
413
|
+
BUILDCHAIN_LOG_RUN_ID: logRunId,
|
|
414
|
+
}
|
|
415
|
+
: {}),
|
|
416
|
+
};
|
|
417
|
+
if (sampleProcessTree) {
|
|
418
|
+
executeSampledShellCommand({
|
|
419
|
+
command,
|
|
420
|
+
cwd: resolvedCwd,
|
|
421
|
+
env: commandEnv,
|
|
422
|
+
shell: true,
|
|
423
|
+
label: `lifecycle-${stageName || "command"}`,
|
|
424
|
+
processSummaryPath: resolvedProcessSummaryPath,
|
|
425
|
+
processSamplesPath: resolvedProcessSamplesPath,
|
|
426
|
+
processSampleIntervalMs,
|
|
427
|
+
requestedParallelism,
|
|
428
|
+
});
|
|
429
|
+
} else {
|
|
430
|
+
execSync(command, {
|
|
431
|
+
cwd: resolvedCwd,
|
|
432
|
+
env: commandEnv,
|
|
433
|
+
shell: true,
|
|
434
|
+
stdio: "inherit",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
150
437
|
executed = true;
|
|
151
438
|
userLog.info("lifecycle.command.end", {
|
|
152
439
|
durationMs: Date.now() - startedAt,
|
|
@@ -162,6 +449,7 @@ export function runLifecycle({
|
|
|
162
449
|
attributes: lifecycleErrorAttributes(error, {
|
|
163
450
|
commandSource,
|
|
164
451
|
stage: stageName || "command",
|
|
452
|
+
sampleProcessTree,
|
|
165
453
|
}),
|
|
166
454
|
});
|
|
167
455
|
throw error;
|
|
@@ -173,10 +461,11 @@ export function runLifecycle({
|
|
|
173
461
|
attributes: {
|
|
174
462
|
commandSource,
|
|
175
463
|
stage: stageName,
|
|
464
|
+
sampleProcessTree,
|
|
176
465
|
},
|
|
177
466
|
});
|
|
178
467
|
try {
|
|
179
|
-
executed =
|
|
468
|
+
executed = runLifecycleStageWithSampler({
|
|
180
469
|
cwd: resolvedCwd,
|
|
181
470
|
loadedConfig,
|
|
182
471
|
name: stageName,
|
|
@@ -186,6 +475,11 @@ export function runLifecycle({
|
|
|
186
475
|
BUILDCHAIN_LOG_RUN_ID: logRunId,
|
|
187
476
|
}
|
|
188
477
|
: {},
|
|
478
|
+
sampleProcessTree,
|
|
479
|
+
processSummaryPath: resolvedProcessSummaryPath,
|
|
480
|
+
processSamplesPath: resolvedProcessSamplesPath,
|
|
481
|
+
processSampleIntervalMs,
|
|
482
|
+
requestedParallelism,
|
|
189
483
|
});
|
|
190
484
|
userLog.info("lifecycle.stage.end", {
|
|
191
485
|
durationMs: Date.now() - startedAt,
|
|
@@ -202,6 +496,7 @@ export function runLifecycle({
|
|
|
202
496
|
attributes: lifecycleErrorAttributes(error, {
|
|
203
497
|
commandSource,
|
|
204
498
|
stage: stageName,
|
|
499
|
+
sampleProcessTree,
|
|
205
500
|
}),
|
|
206
501
|
});
|
|
207
502
|
throw error;
|
|
@@ -218,6 +513,13 @@ export function runLifecycle({
|
|
|
218
513
|
throw new Error(`required lifecycle stage did not run: ${stageName || "command"}`);
|
|
219
514
|
}
|
|
220
515
|
|
|
516
|
+
const shouldReadProcessSummary = Boolean(
|
|
517
|
+
resolvedProcessSummaryPath
|
|
518
|
+
&& (fs.existsSync(resolvedProcessSummaryPath) || processSummaryRequired),
|
|
519
|
+
);
|
|
520
|
+
const processSummaryArtifact = shouldReadProcessSummary
|
|
521
|
+
? readProcessSummaryArtifact(resolvedProcessSummaryPath)
|
|
522
|
+
: undefined;
|
|
221
523
|
fs.mkdirSync(path.dirname(resolvedManifestPath), { recursive: true });
|
|
222
524
|
const scanStartedAt = Date.now();
|
|
223
525
|
const files = collectArtifactFiles(resolvedWorkspace, artifactPaths);
|
|
@@ -229,8 +531,9 @@ export function runLifecycle({
|
|
|
229
531
|
sha256: sha256File(file),
|
|
230
532
|
};
|
|
231
533
|
});
|
|
534
|
+
const artifactScanDurationMs = Date.now() - scanStartedAt;
|
|
232
535
|
frameworkLog.info("artifact.scan", {
|
|
233
|
-
durationMs:
|
|
536
|
+
durationMs: artifactScanDurationMs,
|
|
234
537
|
attributes: {
|
|
235
538
|
fileCount: manifestFiles.length,
|
|
236
539
|
},
|
|
@@ -279,6 +582,26 @@ export function runLifecycle({
|
|
|
279
582
|
: summarizeBuildchainLogEvents([...frameworkLog.events, ...userLog.events]),
|
|
280
583
|
},
|
|
281
584
|
};
|
|
585
|
+
const lifecycleObservability = summarizeLifecycleObservability({
|
|
586
|
+
events: resolvedLogPath ? readBuildchainLogEvents(resolvedLogPath) : [...frameworkLog.events, ...userLog.events],
|
|
587
|
+
logPath: relativeLogPath,
|
|
588
|
+
artifactScanDurationMs,
|
|
589
|
+
totalBytes: summary.totalBytes,
|
|
590
|
+
fileCount: summary.fileCount,
|
|
591
|
+
});
|
|
592
|
+
observability.lifecycle = lifecycleObservability;
|
|
593
|
+
observability.diagnostics = {
|
|
594
|
+
contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
|
|
595
|
+
path: relativeDiagnosticsPath,
|
|
596
|
+
manifestPath: relativeDiagnosticsManifestPath,
|
|
597
|
+
eventsPath: relativeDiagnosticsEventsPath,
|
|
598
|
+
};
|
|
599
|
+
if (relativeProcessSummaryPath) {
|
|
600
|
+
observability.process = {
|
|
601
|
+
contract: BUILDCHAIN_PROCESS_SAMPLE_SUMMARY_CONTRACT,
|
|
602
|
+
path: relativeProcessSummaryPath,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
282
605
|
const summaryWithObservability = {
|
|
283
606
|
...summary,
|
|
284
607
|
observability,
|
|
@@ -309,6 +632,57 @@ export function runLifecycle({
|
|
|
309
632
|
fs.writeFileSync(resolvedManifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
310
633
|
fs.mkdirSync(path.dirname(resolvedSummaryPath), { recursive: true });
|
|
311
634
|
fs.writeFileSync(resolvedSummaryPath, `${JSON.stringify(summaryWithObservability, null, 2)}\n`);
|
|
635
|
+
writeDiagnosticsArtifact(
|
|
636
|
+
resolvedDiagnosticsPath,
|
|
637
|
+
createDiagnosticsArtifact({
|
|
638
|
+
cwd: resolvedCwd,
|
|
639
|
+
logPath: resolvedLogPath,
|
|
640
|
+
artifactPaths,
|
|
641
|
+
lifecycleObservability,
|
|
642
|
+
processSummary: processSummaryArtifact?.summary,
|
|
643
|
+
links: {
|
|
644
|
+
artifactName,
|
|
645
|
+
platformId,
|
|
646
|
+
...(manifestArtifactName ? { manifestArtifactName } : {}),
|
|
647
|
+
...(diagnosticsArtifactName ? { diagnosticsArtifactName } : {}),
|
|
648
|
+
manifest: toPosix(path.relative(resolvedWorkspace, resolvedManifestPath)),
|
|
649
|
+
summary: toPosix(path.relative(resolvedWorkspace, resolvedSummaryPath)),
|
|
650
|
+
log: relativeLogPath,
|
|
651
|
+
diagnosticsManifest: relativeDiagnosticsManifestPath,
|
|
652
|
+
diagnosticsEvents: relativeDiagnosticsEventsPath,
|
|
653
|
+
...(relativeProcessSummaryPath ? { processSummary: relativeProcessSummaryPath } : {}),
|
|
654
|
+
...(processSummaryArtifact ? { diagnosticsProcessSummary: relativeDiagnosticsProcessSummaryPath } : {}),
|
|
655
|
+
...(processSummaryArtifact?.samplesPath ? { diagnosticsProcessSamples: relativeDiagnosticsProcessSamplesPath } : {}),
|
|
656
|
+
},
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
if (resolvedLogPath && fs.existsSync(resolvedLogPath)) {
|
|
660
|
+
copyIfExists(resolvedLogPath, resolvedDiagnosticsEventsPath);
|
|
661
|
+
} else {
|
|
662
|
+
writeJsonlEvents(resolvedDiagnosticsEventsPath, [...frameworkLog.events, ...userLog.events]);
|
|
663
|
+
}
|
|
664
|
+
if (processSummaryArtifact) {
|
|
665
|
+
copyIfExists(resolvedProcessSummaryPath, resolvedDiagnosticsProcessSummaryPath);
|
|
666
|
+
const resolvedSamplesPath = resolveLinkedFilePath({
|
|
667
|
+
linkedPath: processSummaryArtifact.samplesPath,
|
|
668
|
+
workspace: resolvedWorkspace,
|
|
669
|
+
cwd: resolvedCwd,
|
|
670
|
+
fallbackDir: path.dirname(resolvedProcessSummaryPath),
|
|
671
|
+
});
|
|
672
|
+
copyIfExists(resolvedSamplesPath, resolvedDiagnosticsProcessSamplesPath);
|
|
673
|
+
}
|
|
674
|
+
writeDiagnosticsSidecarManifest(resolvedDiagnosticsManifestPath, {
|
|
675
|
+
workspace: resolvedWorkspace,
|
|
676
|
+
artifactName,
|
|
677
|
+
platformId,
|
|
678
|
+
diagnosticsArtifactName,
|
|
679
|
+
files: [
|
|
680
|
+
{ kind: "diagnostics", filePath: resolvedDiagnosticsPath, required: true },
|
|
681
|
+
{ kind: "events", filePath: resolvedDiagnosticsEventsPath, required: true },
|
|
682
|
+
{ kind: "process-summary", filePath: resolvedDiagnosticsProcessSummaryPath },
|
|
683
|
+
{ kind: "process-samples", filePath: resolvedDiagnosticsProcessSamplesPath },
|
|
684
|
+
],
|
|
685
|
+
});
|
|
312
686
|
console.log(`buildchain_manifest=${path.relative(resolvedWorkspace, resolvedManifestPath)}`);
|
|
313
687
|
return manifest;
|
|
314
688
|
}
|
|
@@ -18,6 +18,14 @@ function parseList(value) {
|
|
|
18
18
|
.filter(Boolean);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function readBooleanArg(name, fallback = "false") {
|
|
22
|
+
return readArg(name, fallback) === "true";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readNumberArg(name, fallback) {
|
|
26
|
+
return Number(readArg(name, String(fallback)) || fallback);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
export function runLifecycleCli() {
|
|
22
30
|
return runLifecycle({
|
|
23
31
|
cwd: readArg("cwd", process.cwd()),
|
|
@@ -27,10 +35,18 @@ export function runLifecycleCli() {
|
|
|
27
35
|
manifestPath: readArg("manifest-path", ".buildchain/artifacts/manifest.json"),
|
|
28
36
|
summaryPath: readArg("summary-path", ".buildchain/artifacts/summary.json"),
|
|
29
37
|
artifactName: readArg("artifact-name", "buildchain-artifact"),
|
|
38
|
+
manifestArtifactName: readArg("manifest-artifact-name", ""),
|
|
39
|
+
diagnosticsArtifactName: readArg("diagnostics-artifact-name", ""),
|
|
30
40
|
platformId: readArg("platform-id", os.platform()),
|
|
31
41
|
platformName: readArg("platform-name", readArg("platform-id", os.platform())),
|
|
32
42
|
artifactPaths: parseList(process.env.BUILDCHAIN_ARTIFACT_PATHS || ""),
|
|
33
43
|
expectedArtifactsJson: process.env.BUILDCHAIN_EXPECTED_ARTIFACTS_JSON || "",
|
|
44
|
+
processSummaryPath: readArg("process-summary", process.env.BUILDCHAIN_PROCESS_SUMMARY_PATH || ""),
|
|
45
|
+
processSamplesPath: readArg("process-samples", process.env.BUILDCHAIN_PROCESS_SAMPLES_PATH || ".buildchain/diagnostics/process-samples.jsonl"),
|
|
46
|
+
sampleProcessTree: readBooleanArg("sample-process-tree", process.env.BUILDCHAIN_SAMPLE_PROCESS_TREE || "false"),
|
|
47
|
+
processSampleIntervalMs: readNumberArg("process-sample-interval-ms", process.env.BUILDCHAIN_PROCESS_SAMPLE_INTERVAL_MS || 15000),
|
|
48
|
+
requestedParallelism: readNumberArg("requested-parallelism", process.env.BUILDCHAIN_REQUESTED_PARALLELISM || 0),
|
|
49
|
+
processSummaryRequired: readBooleanArg("process-summary-required", process.env.BUILDCHAIN_PROCESS_SUMMARY_REQUIRED || "true"),
|
|
34
50
|
workspace: process.cwd(),
|
|
35
51
|
});
|
|
36
52
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const EXACT_SHA_RE = /^[0-9a-f]{40}$/i;
|
|
2
|
+
const TRAIN_REF_RE = /^train\/v\d+\/v\d+\.\d+\/[A-Za-z0-9._/-]+$/;
|
|
3
|
+
|
|
4
|
+
export function parseWorkflowShellRef(workflowRef = "", fallback = "v2", buildchainRepository = "kungfu-systems/buildchain") {
|
|
5
|
+
const value = String(workflowRef || "");
|
|
6
|
+
const expectedPrefix = `${buildchainRepository}/.github/workflows/`;
|
|
7
|
+
if (!value.startsWith(expectedPrefix)) {
|
|
8
|
+
return fallback;
|
|
9
|
+
}
|
|
10
|
+
const ref = value.split("@").pop()?.trim();
|
|
11
|
+
return ref || fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function classifyBuildchainRuntimeRef(ref = "") {
|
|
15
|
+
const value = String(ref || "").trim().replace(/^refs\/heads\//, "").replace(/^refs\/tags\//, "");
|
|
16
|
+
if (EXACT_SHA_RE.test(value)) {
|
|
17
|
+
return "exact-sha";
|
|
18
|
+
}
|
|
19
|
+
if (TRAIN_REF_RE.test(value)) {
|
|
20
|
+
return "train";
|
|
21
|
+
}
|
|
22
|
+
if (/^v\d+(?:\.\d+)?$/.test(value) || /^v\d+\.\d+\.\d+$/.test(value)) {
|
|
23
|
+
return "stable";
|
|
24
|
+
}
|
|
25
|
+
if (/^v\d+(?:\.\d+)?-alpha$/.test(value) || /^v\d+\.\d+\.\d+-alpha\.\d+$/.test(value)) {
|
|
26
|
+
return "alpha";
|
|
27
|
+
}
|
|
28
|
+
return "development";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeRequestedRuntimeRef(requestedRef = "") {
|
|
32
|
+
const requested = String(requestedRef || "").trim();
|
|
33
|
+
if (!requested) {
|
|
34
|
+
return { ref: "", fullRef: "", class: "", exactSha: false };
|
|
35
|
+
}
|
|
36
|
+
if (EXACT_SHA_RE.test(requested)) {
|
|
37
|
+
return { ref: requested, fullRef: requested, class: "exact-sha", exactSha: true };
|
|
38
|
+
}
|
|
39
|
+
const trainRef = requested.replace(/^refs\/heads\//, "");
|
|
40
|
+
if (!TRAIN_REF_RE.test(trainRef)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"buildchain-ref override must be train/vN/vN.M/<capability>, refs/heads/train/vN/vN.M/<capability>, or an exact 40-character SHA",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
ref: trainRef,
|
|
47
|
+
fullRef: `refs/heads/${trainRef}`,
|
|
48
|
+
class: "train",
|
|
49
|
+
exactSha: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveRuntimeSelection({
|
|
54
|
+
requestedRef = "",
|
|
55
|
+
workflowRef = "",
|
|
56
|
+
defaultStableRef = "v2",
|
|
57
|
+
buildchainRepository = "kungfu-systems/buildchain",
|
|
58
|
+
} = {}) {
|
|
59
|
+
const requested = String(requestedRef || "").trim();
|
|
60
|
+
const workflowShellRef = parseWorkflowShellRef(workflowRef, defaultStableRef, buildchainRepository);
|
|
61
|
+
if (!requested) {
|
|
62
|
+
return {
|
|
63
|
+
requestedRef: "",
|
|
64
|
+
runtimeRef: workflowShellRef || defaultStableRef,
|
|
65
|
+
runtimeFullRef: workflowShellRef || defaultStableRef,
|
|
66
|
+
runtimeClass: classifyBuildchainRuntimeRef(workflowShellRef || defaultStableRef),
|
|
67
|
+
runtimeOverride: false,
|
|
68
|
+
workflowShellRef: workflowShellRef || defaultStableRef,
|
|
69
|
+
rollbackRef: workflowShellRef || defaultStableRef,
|
|
70
|
+
trustDecision: "stable-default",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const normalized = normalizeRequestedRuntimeRef(requested);
|
|
74
|
+
return {
|
|
75
|
+
requestedRef: requested,
|
|
76
|
+
runtimeRef: normalized.ref,
|
|
77
|
+
runtimeFullRef: normalized.fullRef,
|
|
78
|
+
runtimeClass: normalized.class,
|
|
79
|
+
runtimeOverride: true,
|
|
80
|
+
workflowShellRef: workflowShellRef || defaultStableRef,
|
|
81
|
+
rollbackRef: workflowShellRef || defaultStableRef,
|
|
82
|
+
trustDecision: "override-requested",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function validateRuntimeOverrideTrust({
|
|
87
|
+
requestedRef = "",
|
|
88
|
+
eventName = "",
|
|
89
|
+
actorPermission = "",
|
|
90
|
+
} = {}) {
|
|
91
|
+
if (!String(requestedRef || "").trim()) {
|
|
92
|
+
return { ok: true, decision: "stable-default" };
|
|
93
|
+
}
|
|
94
|
+
if (eventName !== "workflow_dispatch") {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
decision: "rejected-untrusted-event",
|
|
98
|
+
reason: "buildchain-ref override is only allowed for trusted workflow_dispatch runs",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (!new Set(["admin", "maintain", "write"]).has(String(actorPermission || ""))) {
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
decision: "rejected-insufficient-permission",
|
|
105
|
+
reason: "buildchain-ref override requires actor write, maintain, or admin permission",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, decision: "override-accepted" };
|
|
109
|
+
}
|
|
@@ -584,6 +584,10 @@ export function planWebSurfaceDeploy({
|
|
|
584
584
|
sourceSha = "",
|
|
585
585
|
artifactHash = "",
|
|
586
586
|
artifactPath = "",
|
|
587
|
+
runtimeId = "",
|
|
588
|
+
configFingerprint = "",
|
|
589
|
+
rollbackPointer = "",
|
|
590
|
+
rollbackLimitations = "",
|
|
587
591
|
dryRun = true,
|
|
588
592
|
deployedAt = new Date().toISOString(),
|
|
589
593
|
} = {}) {
|
|
@@ -608,6 +612,10 @@ export function planWebSurfaceDeploy({
|
|
|
608
612
|
alias,
|
|
609
613
|
sourceSha,
|
|
610
614
|
artifactHash: resolvedArtifact.artifactHash,
|
|
615
|
+
runtimeId,
|
|
616
|
+
configFingerprint,
|
|
617
|
+
rollbackPointer,
|
|
618
|
+
rollbackLimitations,
|
|
611
619
|
deployedAt,
|
|
612
620
|
});
|
|
613
621
|
return {
|
package/scripts/web-surface.mjs
CHANGED
|
@@ -82,6 +82,10 @@ export function webSurfaceCli() {
|
|
|
82
82
|
sourceSha,
|
|
83
83
|
artifactHash: readArg("artifact-hash", process.env.BUILDCHAIN_WEB_SURFACE_ARTIFACT_HASH || ""),
|
|
84
84
|
artifactPath: readArg("artifact-path", process.env.BUILDCHAIN_WEB_SURFACE_ARTIFACT_PATH || ""),
|
|
85
|
+
runtimeId: readArg("runtime-id", process.env.BUILDCHAIN_RUNTIME_ID || ""),
|
|
86
|
+
configFingerprint: readArg("config-fingerprint", process.env.BUILDCHAIN_CONFIG_FINGERPRINT || ""),
|
|
87
|
+
rollbackPointer: readArg("rollback-pointer", process.env.BUILDCHAIN_ROLLBACK_REF || ""),
|
|
88
|
+
rollbackLimitations: readArg("rollback-limitations", process.env.BUILDCHAIN_ROLLBACK_LIMITATIONS || ""),
|
|
85
89
|
dryRun: readBooleanArg("dry-run", true),
|
|
86
90
|
});
|
|
87
91
|
const outputResult = mode === "manifest" ? result.manifest : result;
|