@kungfu-tech/buildchain 2.1.1-alpha.0 → 2.2.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.
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { createBuildchainLogger } from "../packages/core/logging.js";
9
+
10
+ function readArg(name, fallback = "") {
11
+ const index = process.argv.indexOf(`--${name}`);
12
+ if (index === -1) {
13
+ return fallback;
14
+ }
15
+ return process.argv[index + 1] || "";
16
+ }
17
+
18
+ function timed(logger, eventName, details, callback) {
19
+ const spanId = crypto.randomUUID();
20
+ const startedAt = Date.now();
21
+ logger.info(`${eventName}.start`, { ...details, spanId });
22
+ try {
23
+ const result = callback();
24
+ logger.info(`${eventName}.end`, {
25
+ ...details,
26
+ spanId,
27
+ durationMs: Date.now() - startedAt,
28
+ });
29
+ return result;
30
+ } catch (error) {
31
+ logger.error(`${eventName}.error`, {
32
+ ...details,
33
+ spanId,
34
+ durationMs: Date.now() - startedAt,
35
+ message: error.message,
36
+ attributes: {
37
+ ...(details.attributes || {}),
38
+ errorName: error.name,
39
+ },
40
+ });
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ function run(command, args, options = {}) {
46
+ const { logger, event = "process.run", phase = "process", attributes = {}, ...spawnOptions } = options;
47
+ const runCommand = () => {
48
+ const result = spawnSync(command, args, {
49
+ stdio: "inherit",
50
+ shell: false,
51
+ ...spawnOptions,
52
+ });
53
+ if (result.error) {
54
+ throw result.error;
55
+ }
56
+ if (result.status !== 0) {
57
+ throw new Error(`${command} ${args.join(" ")} exited with ${result.status}`);
58
+ }
59
+ };
60
+ if (logger) {
61
+ return timed(logger, event, {
62
+ phase,
63
+ attributes: {
64
+ command,
65
+ args: args.join(" "),
66
+ ...attributes,
67
+ },
68
+ }, runCommand);
69
+ }
70
+ return runCommand();
71
+ }
72
+
73
+ function relativePath(cwd, targetPath) {
74
+ return path.relative(cwd, targetPath).split(path.sep).join("/");
75
+ }
76
+
77
+ function writeLogSummary(logger, cwd, outputDir, archiveBase) {
78
+ if (!logger.path) {
79
+ return "";
80
+ }
81
+ const summaryPath = path.join(outputDir, `${archiveBase}.log-summary.json`);
82
+ const summary = logger.summary();
83
+ fs.writeFileSync(summaryPath, `${JSON.stringify(summary, null, 2)}\n`);
84
+ return relativePath(cwd, summaryPath);
85
+ }
86
+
87
+ function readLogPath(value) {
88
+ if (value === false || value === "false") {
89
+ return false;
90
+ }
91
+ if (typeof value === "string" && value) {
92
+ return value;
93
+ }
94
+ return undefined;
95
+ }
96
+
97
+ function noopLogger() {
98
+ return {
99
+ path: "",
100
+ info: () => undefined,
101
+ error: () => undefined,
102
+ summary: () => ({
103
+ schemaVersion: 1,
104
+ contract: "kungfu-buildchain-log-summary",
105
+ eventCount: 0,
106
+ warningCount: 0,
107
+ errorCount: 0,
108
+ durationMs: 0,
109
+ sources: {},
110
+ phases: {},
111
+ components: {},
112
+ }),
113
+ };
114
+ }
115
+
116
+ function platformTriple() {
117
+ const platform = process.platform;
118
+ const arch = process.arch;
119
+ if (platform === "darwin") {
120
+ return arch === "arm64" ? "aarch64-apple-darwin" : "x86_64-apple-darwin";
121
+ }
122
+ if (platform === "win32") {
123
+ return "x86_64-pc-windows-msvc";
124
+ }
125
+ if (platform === "linux") {
126
+ return arch === "arm64" ? "aarch64-unknown-linux-gnu" : "x86_64-unknown-linux-gnu";
127
+ }
128
+ return `${arch}-${platform}`;
129
+ }
130
+
131
+ function copyNodeBinary(destination) {
132
+ fs.copyFileSync(process.execPath, destination);
133
+ if (process.platform !== "win32") {
134
+ fs.chmodSync(destination, 0o755);
135
+ }
136
+ }
137
+
138
+ function packageVersion(cwd) {
139
+ const packageJson = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
140
+ return packageJson.version;
141
+ }
142
+
143
+ function bundleCli({ cwd, tempDir, version, logger }) {
144
+ const outDir = path.join(tempDir, "bundle");
145
+ const configPath = path.join(tempDir, "tsup.config.mjs");
146
+ fs.writeFileSync(configPath, `export default {
147
+ entry: {
148
+ buildchain: ${JSON.stringify(path.join(cwd, "bin", "buildchain.mjs"))},
149
+ },
150
+ format: ["cjs"],
151
+ platform: "node",
152
+ target: "node24",
153
+ outDir: ${JSON.stringify(outDir)},
154
+ clean: true,
155
+ silent: true,
156
+ splitting: false,
157
+ sourcemap: false,
158
+ dts: false,
159
+ shims: true,
160
+ noExternal: ["smol-toml"],
161
+ define: {
162
+ "process.env.BUILDCHAIN_EMBEDDED_PACKAGE_VERSION": ${JSON.stringify(JSON.stringify(version || packageVersion(cwd)))},
163
+ },
164
+ };
165
+ `);
166
+ run("pnpm", ["exec", "tsup", "--config", configPath], {
167
+ cwd,
168
+ logger,
169
+ event: "standalone.cli-bundle.create",
170
+ phase: "prepare",
171
+ attributes: {
172
+ outDir,
173
+ },
174
+ });
175
+ return path.join(outDir, "buildchain.cjs");
176
+ }
177
+
178
+ function postjectArgs(binaryPath, blobPath) {
179
+ const args = [
180
+ "--yes",
181
+ "postject",
182
+ binaryPath,
183
+ "NODE_SEA_BLOB",
184
+ blobPath,
185
+ "--sentinel-fuse",
186
+ "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
187
+ ];
188
+ if (process.platform === "darwin") {
189
+ args.push("--macho-segment-name", "NODE_SEA");
190
+ }
191
+ return args;
192
+ }
193
+
194
+ export function buildStandaloneBinary({
195
+ cwd = process.cwd(),
196
+ outputDir = "dist/binary",
197
+ name = "buildchain",
198
+ version = "",
199
+ packageManagerInstall = false,
200
+ logPath = undefined,
201
+ } = {}) {
202
+ const resolvedOutputDir = path.resolve(cwd, outputDir);
203
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-sea-"));
204
+ const triple = platformTriple();
205
+ const archiveBase = `${name}-${triple}`;
206
+ const logger = createBuildchainLogger({
207
+ cwd,
208
+ path: readLogPath(logPath),
209
+ source: "buildchain",
210
+ component: "standalone-binary",
211
+ phase: "binary",
212
+ attributes: {
213
+ name,
214
+ version,
215
+ platform: triple,
216
+ outputDir: relativePath(cwd, resolvedOutputDir),
217
+ },
218
+ }) || noopLogger();
219
+ logger.info("standalone.build.requested", {
220
+ attributes: {
221
+ packageManagerInstall,
222
+ node: process.version,
223
+ },
224
+ });
225
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
226
+ if (packageManagerInstall) {
227
+ run("pnpm", ["install", "--frozen-lockfile"], {
228
+ cwd,
229
+ logger,
230
+ event: "standalone.dependencies.install",
231
+ phase: "setup",
232
+ });
233
+ }
234
+ const blobPath = path.join(tempDir, "sea-prep.blob");
235
+ const configPath = path.join(tempDir, "sea-config.json");
236
+ const bundledCliPath = bundleCli({
237
+ cwd,
238
+ tempDir,
239
+ version,
240
+ logger,
241
+ });
242
+ timed(logger, "standalone.sea-config.write", {
243
+ phase: "prepare",
244
+ attributes: {
245
+ configPath,
246
+ blobPath,
247
+ bundledCliPath,
248
+ },
249
+ }, () => {
250
+ fs.writeFileSync(configPath, `${JSON.stringify({
251
+ main: bundledCliPath,
252
+ mainFormat: "commonjs",
253
+ output: blobPath,
254
+ disableExperimentalSEAWarning: true,
255
+ useSnapshot: false,
256
+ useCodeCache: false,
257
+ }, null, 2)}\n`);
258
+ });
259
+ run(process.execPath, ["--experimental-sea-config", configPath], {
260
+ cwd,
261
+ logger,
262
+ event: "standalone.sea-blob.create",
263
+ phase: "prepare",
264
+ });
265
+ const extension = process.platform === "win32" ? ".exe" : "";
266
+ const binaryPath = path.join(resolvedOutputDir, `${name}${extension}`);
267
+ timed(logger, "standalone.node.copy", {
268
+ phase: "prepare",
269
+ attributes: {
270
+ source: process.execPath,
271
+ destination: relativePath(cwd, binaryPath),
272
+ },
273
+ }, () => copyNodeBinary(binaryPath));
274
+ if (process.platform === "darwin") {
275
+ timed(logger, "standalone.codesign.remove", {
276
+ phase: "sign",
277
+ attributes: {
278
+ binary: relativePath(cwd, binaryPath),
279
+ },
280
+ }, () => {
281
+ spawnSync("codesign", ["--remove-signature", binaryPath], { stdio: "ignore" });
282
+ });
283
+ }
284
+ run("npx", postjectArgs(binaryPath, blobPath), {
285
+ cwd,
286
+ logger,
287
+ event: "standalone.sea-blob.inject",
288
+ phase: "package",
289
+ });
290
+ if (process.platform === "darwin") {
291
+ run("codesign", ["--sign", "-", binaryPath], {
292
+ logger,
293
+ event: "standalone.codesign.adhoc",
294
+ phase: "sign",
295
+ });
296
+ }
297
+ const archiveName = process.platform === "win32" ? `${archiveBase}.zip` : `${archiveBase}.tar.gz`;
298
+ const archivePath = path.join(resolvedOutputDir, archiveName);
299
+ if (process.platform === "win32") {
300
+ run("powershell", [
301
+ "-NoProfile",
302
+ "-Command",
303
+ `Compress-Archive -Path '${binaryPath.replaceAll("'", "''")}' -DestinationPath '${archivePath.replaceAll("'", "''")}' -Force`,
304
+ ], {
305
+ logger,
306
+ event: "standalone.archive.create",
307
+ phase: "archive",
308
+ });
309
+ } else {
310
+ run("tar", ["-czf", archivePath, "-C", resolvedOutputDir, path.basename(binaryPath)], {
311
+ logger,
312
+ event: "standalone.archive.create",
313
+ phase: "archive",
314
+ });
315
+ }
316
+ const manifest = {
317
+ schemaVersion: 1,
318
+ contract: "kungfu-buildchain-standalone-binary",
319
+ name,
320
+ version,
321
+ platform: triple,
322
+ binary: relativePath(cwd, binaryPath),
323
+ archive: relativePath(cwd, archivePath),
324
+ node: process.version,
325
+ observability: {
326
+ eventLog: logger.path ? relativePath(cwd, logger.path) : "",
327
+ summary: logger.path ? relativePath(cwd, path.join(resolvedOutputDir, `${archiveBase}.log-summary.json`)) : "",
328
+ },
329
+ };
330
+ timed(logger, "standalone.manifest.write", {
331
+ phase: "evidence",
332
+ attributes: {
333
+ manifest: `${archiveBase}.json`,
334
+ },
335
+ }, () => {
336
+ fs.writeFileSync(path.join(resolvedOutputDir, `${archiveBase}.json`), `${JSON.stringify(manifest, null, 2)}\n`);
337
+ });
338
+ logger.info("standalone.build.complete", {
339
+ attributes: {
340
+ archive: manifest.archive,
341
+ binary: manifest.binary,
342
+ },
343
+ });
344
+ writeLogSummary(logger, cwd, resolvedOutputDir, archiveBase);
345
+ return manifest;
346
+ }
347
+
348
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
349
+ try {
350
+ const result = buildStandaloneBinary({
351
+ cwd: path.resolve(readArg("cwd", process.cwd())),
352
+ outputDir: readArg("output-dir", "dist/binary"),
353
+ name: readArg("name", "buildchain"),
354
+ version: readArg("version", ""),
355
+ packageManagerInstall: process.argv.includes("--install"),
356
+ logPath: readArg("log-path", ""),
357
+ });
358
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
359
+ } catch (error) {
360
+ console.error(`buildchain binary: ${error.message}`);
361
+ process.exitCode = 1;
362
+ }
363
+ }
@@ -16,7 +16,10 @@ const requiredPaths = [
16
16
  "bin/buildchain.mjs",
17
17
  "docs/MAP.md",
18
18
  "docs/cli.md",
19
+ "docs/release-passport.md",
20
+ "docs/versioning.md",
19
21
  "scripts/release-line-dry-run.mjs",
22
+ "scripts/build-standalone-binary.mjs",
20
23
  "scripts/npm-publish-dry-run.mjs",
21
24
  "scripts/npm-publish-transaction.mjs",
22
25
  "docs/migration-inventory.md",
@@ -28,6 +31,7 @@ const requiredPaths = [
28
31
  ".github/workflows/self-hosted-runner-smoke.yml",
29
32
  ".github/workflows/buildchain-ref-promotion.yml",
30
33
  ".github/workflows/npm-publish.yml",
34
+ ".github/workflows/binary-distribution.yml",
31
35
  ".github/workflows/verify.yml",
32
36
  ".github/workflows/.build.yml",
33
37
  ".github/workflows/build-surface-fixture.yml",
@@ -58,6 +62,9 @@ if (rootPackage.exports?.["."] !== "./packages/core/index.js") {
58
62
  if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
59
63
  throw new Error("root package must export @kungfu-tech/buildchain/logging");
60
64
  }
65
+ if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-passport.js") {
66
+ throw new Error("root package must export @kungfu-tech/buildchain/release-passport");
67
+ }
61
68
  if (rootPackage.publishConfig?.access !== "public") {
62
69
  throw new Error("root package publishConfig.access must be public");
63
70
  }
@@ -70,13 +77,29 @@ for (const expectedFile of ["bin/", "scripts/*.mjs", "packages/core/", "docs/MAP
70
77
  }
71
78
  }
72
79
  const cliSource = fs.readFileSync(path.join(root, "bin/buildchain.mjs"), "utf8");
80
+ const coreIndexSource = fs.readFileSync(path.join(root, "packages/core/index.js"), "utf8");
81
+ const versioningDoc = fs.readFileSync(path.join(root, "docs/versioning.md"), "utf8");
73
82
  if (!cliSource.startsWith("#!/usr/bin/env node")) {
74
83
  throw new Error("bin/buildchain.mjs must be executable with a node shebang");
75
84
  }
76
85
  if (commonJsSourcePattern.test(cliSource)) {
77
86
  throw new Error("bin/buildchain.mjs must use ESM syntax");
78
87
  }
88
+ if (!coreIndexSource.includes("verifyBuildchainLogEvents")) {
89
+ throw new Error("packages/core/index.js must export verifyBuildchainLogEvents");
90
+ }
91
+ for (const requiredSnippet of [
92
+ "Release passport and binary distribution are a minor surface.",
93
+ "`v2.2`",
94
+ "GitHub-hosted runners for production",
95
+ "Self-hosted runners remain compatibility fixtures",
96
+ ]) {
97
+ if (!versioningDoc.includes(requiredSnippet)) {
98
+ throw new Error(`versioning doc missing required snippet: ${requiredSnippet}`);
99
+ }
100
+ }
79
101
  const releaseLineDryRunScript = fs.readFileSync(path.join(root, "scripts/release-line-dry-run.mjs"), "utf8");
102
+ const standaloneBinaryScript = fs.readFileSync(path.join(root, "scripts/build-standalone-binary.mjs"), "utf8");
80
103
  for (const requiredSnippet of [
81
104
  "explainReleaseLineDryRun",
82
105
  "formatReleaseLineDryRun",
@@ -89,8 +112,30 @@ for (const requiredSnippet of [
89
112
  if (commonJsSourcePattern.test(releaseLineDryRunScript)) {
90
113
  throw new Error("scripts/release-line-dry-run.mjs must use ESM syntax");
91
114
  }
115
+ for (const requiredSnippet of [
116
+ "../packages/core/logging.js",
117
+ "standalone.cli-bundle.create",
118
+ "BUILDCHAIN_EMBEDDED_PACKAGE_VERSION",
119
+ "noExternal: [\"smol-toml\"]",
120
+ "--macho-segment-name",
121
+ "mainFormat: \"commonjs\"",
122
+ "standalone.sea-blob.create",
123
+ "standalone.sea-blob.inject",
124
+ "standalone.archive.create",
125
+ "standalone.build.complete",
126
+ ".log-summary.json",
127
+ ]) {
128
+ if (!standaloneBinaryScript.includes(requiredSnippet)) {
129
+ throw new Error(`standalone binary script missing observability dogfood snippet: ${requiredSnippet}`);
130
+ }
131
+ }
132
+ if (commonJsSourcePattern.test(standaloneBinaryScript)) {
133
+ throw new Error("scripts/build-standalone-binary.mjs must use ESM syntax");
134
+ }
92
135
  const npmPublishWorkflow = fs.readFileSync(path.join(root, ".github/workflows/npm-publish.yml"), "utf8");
93
136
  const buildchainRefPromotionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/buildchain-ref-promotion.yml"), "utf8");
137
+ const binaryDistributionWorkflow = fs.readFileSync(path.join(root, ".github/workflows/binary-distribution.yml"), "utf8");
138
+ const selfHostedRunnerSmokeWorkflow = fs.readFileSync(path.join(root, ".github/workflows/self-hosted-runner-smoke.yml"), "utf8");
94
139
  const npmDryRunScript = fs.readFileSync(path.join(root, "scripts/npm-publish-dry-run.mjs"), "utf8");
95
140
  const npmPublishTransactionScript = fs.readFileSync(path.join(root, "scripts/npm-publish-transaction.mjs"), "utf8");
96
141
  for (const requiredSnippet of [
@@ -147,6 +192,32 @@ if (commonJsSourcePattern.test(npmPublishTransactionScript)) {
147
192
  if (/runs-on:\s*self-hosted/.test(npmPublishWorkflow)) {
148
193
  throw new Error("npm publish workflow must use GitHub-hosted runners for trusted publishing");
149
194
  }
195
+ for (const requiredSnippet of [
196
+ "name: Binary Distribution",
197
+ "ubuntu-24.04",
198
+ "macos-latest",
199
+ "windows-2022",
200
+ "BUILDCHAIN_LOG_PATH",
201
+ "buildchain-log-events",
202
+ "buildchain-log-summary",
203
+ "bin/buildchain.mjs mark",
204
+ "bin/buildchain.mjs span",
205
+ "verify observability-log",
206
+ "bin/buildchain.mjs log summary",
207
+ "collect github-release",
208
+ "verify release-passport",
209
+ "gh release upload",
210
+ ]) {
211
+ if (!binaryDistributionWorkflow.includes(requiredSnippet)) {
212
+ throw new Error(`binary distribution workflow missing required snippet: ${requiredSnippet}`);
213
+ }
214
+ }
215
+ if (/runs-on:\s*self-hosted/.test(binaryDistributionWorkflow)) {
216
+ throw new Error("binary distribution production workflow must not require self-hosted runners");
217
+ }
218
+ if (!selfHostedRunnerSmokeWorkflow.includes("BUILDCHAIN_RUNNER_KIND: self-hosted")) {
219
+ throw new Error("self-hosted smoke must remain a release-passport compatibility fixture");
220
+ }
150
221
 
151
222
  const inventory = JSON.parse(
152
223
  fs.readFileSync(path.join(root, "tests/buildchain-inventory.json"), "utf8")
@@ -167,6 +238,20 @@ if (inventory.stableRefs?.actions !== "kungfu-systems/buildchain/actions/<name>@
167
238
  if (inventory.stableRefs?.workflows !== "kungfu-systems/buildchain/.github/workflows/<workflow>.yml@v2") {
168
239
  throw new Error("inventory stable workflow ref must point at @v2");
169
240
  }
241
+ if (inventory.safety?.releasePassport?.line !== "v2.2") {
242
+ throw new Error("release passport inventory must be registered as a v2.2 surface");
243
+ }
244
+ if (inventory.safety?.releasePassport?.binaryDistribution?.productionRunnerDefault !== "github-hosted") {
245
+ throw new Error("release passport binary distribution must default to GitHub-hosted production runners");
246
+ }
247
+ if (inventory.safety?.releasePassport?.binaryDistribution?.selfHostedRole !== "compatibility-fixture") {
248
+ throw new Error("self-hosted runners must remain release passport compatibility fixtures");
249
+ }
250
+ for (const artifact of ["buildchain.release.json", "artifact-evidence.json", "impact.json", "agent-index.json", "llms.txt"]) {
251
+ if (!inventory.safety?.releasePassport?.protocolArtifacts?.includes(artifact)) {
252
+ throw new Error(`release passport inventory missing protocol artifact ${artifact}`);
253
+ }
254
+ }
170
255
 
171
256
  if (!Array.isArray(inventory.workflowSources) || inventory.workflowSources.length < 1) {
172
257
  throw new Error("workflowSources must include the migrated workflows repository");