@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.
- package/dist/site/buildchain-contract.json +25 -17
- package/dist/site/buildchain-site.json +18 -18
- package/dist/site/kfd-claims.json +5 -4
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +4 -4
- package/dist/site/node-api-registry.json +3 -3
- package/dist/site/page-registry.json +10 -10
- package/dist/site/public-surface-audit.json +4 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +8 -8
- package/dist/site/workflow-registry.json +2 -1
- package/docs/lifecycle-protocol.md +4 -1
- package/docs/publication-artifacts.md +9 -0
- package/docs/release-governance.md +9 -3
- package/docs/reusable-build-surface.md +6 -2
- package/docs/web-surface-deployments.md +15 -1
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +9 -1
- package/packages/core/publication-artifact.js +56 -8
- package/packages/core/publication-package.js +5 -0
- package/scripts/locked-source-checkout.mjs +43 -9
- package/scripts/publication-artifact.mjs +20 -2
- package/scripts/publication-registry-hydrate.mjs +216 -0
- package/scripts/run-lifecycle-core.mjs +7 -2
- package/scripts/web-surface-core.mjs +28 -0
|
@@ -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:
|
|
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:
|
|
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];
|