@kungfu-tech/buildchain 2.11.14-alpha.1 → 2.11.14-alpha.2

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,216 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { loadBuildchainConfig } from "../packages/core/buildchain-config.js";
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
9
+
10
+ function semverParts(value) {
11
+ const match = String(value || "")
12
+ .trim()
13
+ .match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
14
+ if (!match) return undefined;
15
+ return {
16
+ core: match.slice(1, 4).map(Number),
17
+ prerelease: match[4] ? match[4].split(".") : [],
18
+ };
19
+ }
20
+
21
+ export function compareSemver(left, right) {
22
+ const a = semverParts(left);
23
+ const b = semverParts(right);
24
+ if (!a || !b) throw new Error(`invalid semver comparison: ${left}, ${right}`);
25
+ for (let index = 0; index < 3; index += 1) {
26
+ if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index];
27
+ }
28
+ if (a.prerelease.length === 0 || b.prerelease.length === 0) {
29
+ return a.prerelease.length === b.prerelease.length
30
+ ? 0
31
+ : a.prerelease.length === 0
32
+ ? 1
33
+ : -1;
34
+ }
35
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
36
+ for (let index = 0; index < length; index += 1) {
37
+ const av = a.prerelease[index];
38
+ const bv = b.prerelease[index];
39
+ if (av === undefined || bv === undefined) return av === undefined ? -1 : 1;
40
+ if (av === bv) continue;
41
+ const an = /^\d+$/.test(av);
42
+ const bn = /^\d+$/.test(bv);
43
+ if (an && bn) return Number(av) - Number(bv);
44
+ if (an !== bn) return an ? -1 : 1;
45
+ return av.localeCompare(bv);
46
+ }
47
+ return 0;
48
+ }
49
+
50
+ function run(command, args, options = {}) {
51
+ return execFileSync(command, args, {
52
+ encoding: "utf8",
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ ...options,
55
+ }).trim();
56
+ }
57
+
58
+ function packageVersions(packageName, registry, commandRunner) {
59
+ try {
60
+ const output = commandRunner("npm", [
61
+ "view",
62
+ packageName,
63
+ "versions",
64
+ "--json",
65
+ `--registry=${registry}`,
66
+ ]);
67
+ const parsed = JSON.parse(output || "[]");
68
+ return Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
69
+ } catch (error) {
70
+ const detail = [error?.message, error?.stderr, error?.stdout]
71
+ .filter(Boolean)
72
+ .join("\n");
73
+ if (/E404|404 Not Found|is not in this registry/i.test(detail)) {
74
+ return [];
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ export function hydratePublishedPublicationRegistry({
81
+ cwd = process.cwd(),
82
+ outputDir = ".buildchain/publication/registry-inputs",
83
+ evidencePath = ".buildchain/publication/registry-hydration.json",
84
+ registry = DEFAULT_REGISTRY,
85
+ commandRunner = run,
86
+ } = {}) {
87
+ const resolvedCwd = path.resolve(cwd);
88
+ const config = loadBuildchainConfig(resolvedCwd).config;
89
+ const packageName =
90
+ config.publish?.package || config.publish?.mainPackage || "";
91
+ const currentVersion = config.publication?.version || "";
92
+ const registryPath = config.publication?.archive?.registryPath || "";
93
+ const resolvedOutputDir = path.resolve(resolvedCwd, outputDir);
94
+ fs.rmSync(resolvedOutputDir, { recursive: true, force: true });
95
+ fs.mkdirSync(resolvedOutputDir, { recursive: true });
96
+ if (!packageName || !currentVersion || !registryPath) {
97
+ return {
98
+ status: "not-configured",
99
+ packageName,
100
+ currentVersion,
101
+ inputDir: path
102
+ .relative(resolvedCwd, resolvedOutputDir)
103
+ .split(path.sep)
104
+ .join("/"),
105
+ sources: [],
106
+ };
107
+ }
108
+ const publishedVersions = packageVersions(
109
+ packageName,
110
+ registry,
111
+ commandRunner,
112
+ )
113
+ .filter(
114
+ (version) =>
115
+ semverParts(version) && compareSemver(version, currentVersion) < 0,
116
+ )
117
+ .sort(compareSemver);
118
+ const sources = [];
119
+ for (const version of publishedVersions) {
120
+ const packageDir = path.join(resolvedOutputDir, `.package-${version}`);
121
+ fs.mkdirSync(packageDir, { recursive: true });
122
+ const packed = JSON.parse(
123
+ commandRunner(
124
+ "npm",
125
+ [
126
+ "pack",
127
+ `${packageName}@${version}`,
128
+ "--json",
129
+ "--ignore-scripts",
130
+ "--pack-destination",
131
+ packageDir,
132
+ `--registry=${registry}`,
133
+ ],
134
+ { cwd: resolvedCwd },
135
+ ),
136
+ );
137
+ const pack = Array.isArray(packed) ? packed[0] : packed;
138
+ if (!pack?.filename || !pack?.integrity) {
139
+ throw new Error(
140
+ `npm pack did not report filename and integrity for ${packageName}@${version}`,
141
+ );
142
+ }
143
+ const tarball = path.resolve(packageDir, pack.filename);
144
+ const archiveRegistryPath = `package/${registryPath}`;
145
+ const archiveEntries = commandRunner("tar", ["-tzf", tarball]).split(/\r?\n/);
146
+ if (!archiveEntries.includes(archiveRegistryPath)) {
147
+ sources.push({
148
+ package: packageName,
149
+ version,
150
+ integrity: pack.integrity,
151
+ status: "registry-not-present",
152
+ });
153
+ continue;
154
+ }
155
+ const target = path.join(resolvedOutputDir, `${version}.json`);
156
+ const registryJson = commandRunner("tar", ["-xOzf", tarball, archiveRegistryPath]);
157
+ fs.writeFileSync(target, `${registryJson}\n`);
158
+ sources.push({
159
+ package: packageName,
160
+ version,
161
+ integrity: pack.integrity,
162
+ status: "accepted-input",
163
+ registryPath: path
164
+ .relative(resolvedCwd, target)
165
+ .split(path.sep)
166
+ .join("/"),
167
+ });
168
+ }
169
+ for (const entry of fs.readdirSync(resolvedOutputDir)) {
170
+ if (entry.startsWith(".package-")) {
171
+ fs.rmSync(path.join(resolvedOutputDir, entry), {
172
+ recursive: true,
173
+ force: true,
174
+ });
175
+ }
176
+ }
177
+ const evidence = {
178
+ schemaVersion: 1,
179
+ contract: "kungfu-buildchain-publication-registry-hydration",
180
+ status: sources.some((source) => source.status === "accepted-input")
181
+ ? "hydrated"
182
+ : "bootstrap",
183
+ authentication: "npm-registry-https-plus-package-integrity",
184
+ registry,
185
+ packageName,
186
+ currentVersion,
187
+ inputDir: path
188
+ .relative(resolvedCwd, resolvedOutputDir)
189
+ .split(path.sep)
190
+ .join("/"),
191
+ sources,
192
+ };
193
+ const resolvedEvidencePath = path.resolve(resolvedCwd, evidencePath);
194
+ fs.mkdirSync(path.dirname(resolvedEvidencePath), { recursive: true });
195
+ fs.writeFileSync(
196
+ resolvedEvidencePath,
197
+ `${JSON.stringify(evidence, null, 2)}\n`,
198
+ );
199
+ return evidence;
200
+ }
201
+
202
+ if (
203
+ process.argv[1] &&
204
+ import.meta.url === pathToFileURL(process.argv[1]).href
205
+ ) {
206
+ try {
207
+ const cwdIndex = process.argv.indexOf("--cwd");
208
+ const result = hydratePublishedPublicationRegistry({
209
+ cwd: cwdIndex === -1 ? process.cwd() : process.argv[cwdIndex + 1],
210
+ });
211
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
212
+ } catch (error) {
213
+ console.error(`publication-registry-hydrate: ${error.message}`);
214
+ process.exitCode = 1;
215
+ }
216
+ }
@@ -446,6 +446,9 @@ export function runLifecycle({
446
446
 
447
447
  if (command.trim()) {
448
448
  commandSource = "workflow-input";
449
+ const lifecycle = loadedConfig?.config?.lifecycle || {};
450
+ const configuredStage = stageName ? lifecycle[stageName] : undefined;
451
+ const commandShell = configuredStage?.shell || true;
449
452
  const startedAt = Date.now();
450
453
  userLog.info("lifecycle.command.start", {
451
454
  attributes: {
@@ -457,6 +460,8 @@ export function runLifecycle({
457
460
  try {
458
461
  const commandEnv = {
459
462
  ...process.env,
463
+ ...(lifecycle.env || {}),
464
+ ...(configuredStage?.env || {}),
460
465
  ...(resolvedLogPath
461
466
  ? {
462
467
  BUILDCHAIN_LOG_PATH: resolvedLogPath,
@@ -469,7 +474,7 @@ export function runLifecycle({
469
474
  command,
470
475
  cwd: resolvedCwd,
471
476
  env: commandEnv,
472
- shell: true,
477
+ shell: commandShell,
473
478
  label: `lifecycle-${stageName || "command"}`,
474
479
  processSummaryPath: resolvedProcessSummaryPath,
475
480
  processSamplesPath: resolvedProcessSamplesPath,
@@ -480,7 +485,7 @@ export function runLifecycle({
480
485
  execSync(command, {
481
486
  cwd: resolvedCwd,
482
487
  env: commandEnv,
483
- shell: true,
488
+ shell: commandShell,
484
489
  stdio: "inherit",
485
490
  });
486
491
  }
@@ -1099,6 +1099,33 @@ export function planWebSurfaceDeploy({
1099
1099
  };
1100
1100
  }
1101
1101
 
1102
+ export function verifyWebSurfaceArtifactChannelFacts({ cwd = process.cwd(), plan } = {}) {
1103
+ const resolvedPlan = assertDeployPlan(plan);
1104
+ const artifactRoot = path.resolve(cwd, resolvedPlan.artifact.path);
1105
+ const expectedHosts = new Set((resolvedPlan.manifest?.surfaceBindings || []).map((binding) => {
1106
+ try { return new URL(binding.url).host; } catch { return ""; }
1107
+ }).filter(Boolean));
1108
+ const observed = [];
1109
+ for (const filePath of listFiles(artifactRoot, artifactRoot).filter((entry) => entry.endsWith(".json"))) {
1110
+ let value;
1111
+ try { value = JSON.parse(fs.readFileSync(filePath, "utf8")); } catch { continue; }
1112
+ if (!value || typeof value !== "object" || Array.isArray(value) || !String(value.contract || "").includes("manifest")) continue;
1113
+ const facts = [];
1114
+ if (typeof value.canonicalHost === "string" && value.canonicalHost.trim()) facts.push({ field: "canonicalHost", host: value.canonicalHost.trim() });
1115
+ if (Array.isArray(value.pages)) {
1116
+ for (const [index, page] of value.pages.entries()) {
1117
+ if (typeof page?.host === "string" && page.host.trim()) facts.push({ field: `pages[${index}].host`, host: page.host.trim() });
1118
+ }
1119
+ }
1120
+ for (const fact of facts) observed.push({ path: toPosix(path.relative(cwd, filePath)), ...fact });
1121
+ }
1122
+ const mismatches = observed.filter((fact) => !expectedHosts.has(fact.host));
1123
+ if (mismatches.length > 0) {
1124
+ throw new Error(`web-surface artifact channel facts mismatch for ${resolvedPlan.channel}: ${mismatches.map((fact) => `${fact.path}#${fact.field}=${fact.host}`).join(", ")}`);
1125
+ }
1126
+ return { channel: resolvedPlan.channel, expectedHosts: [...expectedHosts].sort(), observed };
1127
+ }
1128
+
1102
1129
  export function applyWebSurfaceDeploy({
1103
1130
  cwd = process.cwd(),
1104
1131
  channel = "preview",
@@ -1125,6 +1152,7 @@ export function applyWebSurfaceDeploy({
1125
1152
  dryRun: true,
1126
1153
  deployedAt: appliedAt,
1127
1154
  });
1155
+ verifyWebSurfaceArtifactChannelFacts({ cwd, plan: resolvedPlan });
1128
1156
  const loadedConfig = loadBuildchainConfig(cwd);
1129
1157
  const config = assertWebSurfaceConfig(loadedConfig);
1130
1158
  const deployConfig = config.deploy?.[resolvedPlan.channel];