@kungfu-tech/buildchain 2.9.0 → 2.9.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/bin/buildchain.mjs +47 -0
- package/dist/site/agent-index.json +1 -0
- package/dist/site/artifact-schemas.json +1 -0
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +29 -22
- package/dist/site/cli-registry.json +239 -42
- package/dist/site/kfd-claims.json +2181 -1
- package/dist/site/manual-registry.json +6 -6
- package/dist/site/node-api-registry.json +17 -5
- package/dist/site/page-registry.json +17 -12
- package/dist/site/public-surface-audit.json +1969 -0
- package/dist/site/release-provenance.json +3 -0
- package/dist/site/site-manifest.json +11 -10
- package/dist/site/workflow-registry.json +741 -18
- package/docs/MAP.md +1 -0
- package/docs/cli.md +47 -0
- package/docs/release-flow.md +31 -0
- package/docs/release-passport.md +10 -0
- package/docs/site-bundle-contract.md +7 -0
- package/docs/web-surface-deployments.md +10 -0
- package/package.json +4 -1
- package/packages/core/buildchain-kfd-claims.js +118 -7
- package/packages/core/index.js +16 -0
- package/packages/core/public-surface-audit.js +298 -0
- package/packages/core/release-line-bootstrap.js +242 -0
- package/scripts/check-inventory.mjs +32 -1
- package/scripts/generate-site-bundle.mjs +95 -32
- package/scripts/web-surface-core.mjs +79 -6
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const BUILDCHAIN_PUBLIC_SURFACE_AUDIT_CONTRACT = "kungfu-buildchain-public-surface-reverse-audit";
|
|
6
|
+
|
|
7
|
+
function readText(root, relPath) {
|
|
8
|
+
return fs.readFileSync(path.join(root, relPath), "utf8");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readJson(root, relPath, fallback = undefined) {
|
|
12
|
+
const filePath = path.join(root, relPath);
|
|
13
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
14
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sha256(value) {
|
|
18
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function uniqueSorted(values) {
|
|
22
|
+
return [...new Set(values.filter(Boolean))].sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function listFiles(root, dir, predicate = () => true) {
|
|
26
|
+
const base = path.join(root, dir);
|
|
27
|
+
if (!fs.existsSync(base)) return [];
|
|
28
|
+
return fs.readdirSync(base, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isFile() && predicate(entry.name))
|
|
30
|
+
.map((entry) => `${dir}/${entry.name}`)
|
|
31
|
+
.sort();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function listDirectories(root, dir) {
|
|
35
|
+
const base = path.join(root, dir);
|
|
36
|
+
if (!fs.existsSync(base)) return [];
|
|
37
|
+
return fs.readdirSync(base, { withFileTypes: true })
|
|
38
|
+
.filter((entry) => entry.isDirectory())
|
|
39
|
+
.map((entry) => `${dir}/${entry.name}`)
|
|
40
|
+
.sort();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function commandId(first = "", second = "") {
|
|
44
|
+
const head = String(first || "").trim();
|
|
45
|
+
const sub = String(second || "").trim();
|
|
46
|
+
if (!head) return "";
|
|
47
|
+
if (["-h", "--help", "help"].includes(head)) return "help";
|
|
48
|
+
if (["-v", "--version", "version"].includes(head)) return "version";
|
|
49
|
+
if (head === "release" && ["--dry-run", "dry-run", "explain"].includes(sub)) return "release-dry-run";
|
|
50
|
+
if (head === "release" && sub === "line") return "release-line-open";
|
|
51
|
+
if (head === "release") return "release-transaction";
|
|
52
|
+
if (head === "transaction") return "transaction-inspect";
|
|
53
|
+
if (head === "collect" && sub) return `collect-${sub}`;
|
|
54
|
+
if (head === "verify" && sub) return `verify-${sub}`;
|
|
55
|
+
if (head === "explain" && sub) return `explain-${sub}`;
|
|
56
|
+
if (head === "inspect" && sub) return `inspect-${sub}`;
|
|
57
|
+
if (head === "npm" && sub) return `npm-${sub}`;
|
|
58
|
+
if (head === "lifecycle" && sub) return "lifecycle";
|
|
59
|
+
if (head === "log" && sub) return "logging";
|
|
60
|
+
if (head === "diagnostics" && sub) return `diagnostics-${sub}`;
|
|
61
|
+
if (head === "facts" && sub) return "build-facts";
|
|
62
|
+
if (head === "sample" && sub) return `sample-${sub}`;
|
|
63
|
+
if (head === "badges" && sub) return `badges-${sub}`;
|
|
64
|
+
if (head === "homebrew" && sub) return `homebrew-${sub}`;
|
|
65
|
+
if (head === "release-propagation") return "release-propagation";
|
|
66
|
+
if (head === "publish-source") return "publish-source";
|
|
67
|
+
if (head === "build-contract") return "build-contract";
|
|
68
|
+
if (head === "infra-contract") return "infra-contract";
|
|
69
|
+
if (head === "web-surface") return "web-surface";
|
|
70
|
+
return head;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function enumerateCliCommandsFromBin({ root = process.cwd(), binPath = "bin/buildchain.mjs" } = {}) {
|
|
74
|
+
const source = readText(root, binPath);
|
|
75
|
+
const usageMatch = source.match(/return `Usage:\n([\s\S]*?)`;\n}/);
|
|
76
|
+
const usage = usageMatch?.[1] || "";
|
|
77
|
+
const usageCommands = [];
|
|
78
|
+
for (const line of usage.split(/\r?\n/)) {
|
|
79
|
+
const match = line.trim().match(/^buildchain\s+([^\s]+)(?:\s+([^\s]+))?/);
|
|
80
|
+
if (!match) continue;
|
|
81
|
+
usageCommands.push({
|
|
82
|
+
id: commandId(match[1], match[2]),
|
|
83
|
+
usage: line.trim().replace(/\s+/g, " "),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const dispatchCommands = [...source.matchAll(/if\s*\(\s*command\s*===\s*"([^"]+)"/g)]
|
|
87
|
+
.map((match) => commandId(match[1]));
|
|
88
|
+
return uniqueSorted([
|
|
89
|
+
...usageCommands.map((entry) => entry.id),
|
|
90
|
+
...dispatchCommands,
|
|
91
|
+
]).map((id) => ({
|
|
92
|
+
id,
|
|
93
|
+
source: "bin/buildchain.mjs",
|
|
94
|
+
usage: usageCommands.find((entry) => entry.id === id)?.usage || `buildchain ${id}`,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseYamlTopLevelInputs(text) {
|
|
99
|
+
const lines = text.split(/\r?\n/);
|
|
100
|
+
const inputs = [];
|
|
101
|
+
let inInputs = false;
|
|
102
|
+
let indent = 0;
|
|
103
|
+
for (const line of lines) {
|
|
104
|
+
const match = line.match(/^(\s*)inputs:\s*$/);
|
|
105
|
+
if (match) {
|
|
106
|
+
inInputs = true;
|
|
107
|
+
indent = match[1].length;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!inInputs) continue;
|
|
111
|
+
const currentIndent = line.match(/^(\s*)/)?.[1].length || 0;
|
|
112
|
+
if (line.trim() && currentIndent <= indent) {
|
|
113
|
+
inInputs = false;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const inputMatch = line.match(new RegExp(`^\\s{${indent + 2}}([A-Za-z0-9_-]+):\\s*$`));
|
|
117
|
+
if (inputMatch) inputs.push(inputMatch[1]);
|
|
118
|
+
}
|
|
119
|
+
return uniqueSorted(inputs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function enumerateWorkflowInputs({ root = process.cwd() } = {}) {
|
|
123
|
+
return listFiles(root, ".github/workflows", (name) => /\.ya?ml$/.test(name)).map((relPath) => {
|
|
124
|
+
const inputs = parseYamlTopLevelInputs(readText(root, relPath));
|
|
125
|
+
return {
|
|
126
|
+
id: relPath.replace(/^\.github\/workflows\//, "").replace(/\.ya?ml$/, ""),
|
|
127
|
+
path: relPath,
|
|
128
|
+
inputs,
|
|
129
|
+
inputCount: inputs.length,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function enumerateActionInputs({ root = process.cwd() } = {}) {
|
|
135
|
+
return listDirectories(root, "actions").map((dir) => {
|
|
136
|
+
const relPath = `${dir}/action.yml`;
|
|
137
|
+
const inputs = fs.existsSync(path.join(root, relPath))
|
|
138
|
+
? parseYamlTopLevelInputs(readText(root, relPath))
|
|
139
|
+
: [];
|
|
140
|
+
return {
|
|
141
|
+
id: dir.replace(/^actions\//, ""),
|
|
142
|
+
path: relPath,
|
|
143
|
+
inputs,
|
|
144
|
+
inputCount: inputs.length,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function enumerateSitePages({ root = process.cwd() } = {}) {
|
|
150
|
+
const pageRegistry = readJson(root, "dist/site/page-registry.json", {});
|
|
151
|
+
return (Array.isArray(pageRegistry?.pages) ? pageRegistry.pages : []).map((page) => ({
|
|
152
|
+
id: page.id || page.path,
|
|
153
|
+
path: page.path,
|
|
154
|
+
category: page.category || "",
|
|
155
|
+
})).sort((a, b) => String(a.id).localeCompare(String(b.id)));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function enumerateDocCommandRefs({ root = process.cwd() } = {}) {
|
|
159
|
+
const knownCommandIds = new Set(enumerateCliCommandsFromBin({ root }).map((entry) => entry.id));
|
|
160
|
+
const docs = [
|
|
161
|
+
"README.md",
|
|
162
|
+
...listFiles(root, "docs", (name) => name.endsWith(".md")),
|
|
163
|
+
...listDirectories(root, "actions").map((dir) => `${dir}/README.md`).filter((relPath) => fs.existsSync(path.join(root, relPath))),
|
|
164
|
+
];
|
|
165
|
+
const refs = [];
|
|
166
|
+
for (const relPath of docs) {
|
|
167
|
+
const text = readText(root, relPath);
|
|
168
|
+
const codeSegments = [];
|
|
169
|
+
for (const match of text.matchAll(/```[\w-]*\n([\s\S]*?)```/g)) {
|
|
170
|
+
codeSegments.push({ kind: "block", text: match[1] });
|
|
171
|
+
}
|
|
172
|
+
for (const match of text.matchAll(/`([^`\n]*\bbuildchain\b[^`\n]*)`/g)) {
|
|
173
|
+
codeSegments.push({ kind: "inline", text: match[1] });
|
|
174
|
+
}
|
|
175
|
+
for (const segment of codeSegments) {
|
|
176
|
+
const commandText = segment.kind === "block"
|
|
177
|
+
? segment.text.split(/\r?\n/)
|
|
178
|
+
.map((line) => line.trim())
|
|
179
|
+
.filter((line) => /^(?:[$>]\s*)?(?:npx\s+(?:@kungfu-tech\/buildchain|buildchain)\s+|node\s+bin\/buildchain\.mjs\s+|buildchain\s+)/.test(line))
|
|
180
|
+
.join("\n")
|
|
181
|
+
: segment.text;
|
|
182
|
+
for (const match of commandText.matchAll(/(?:^|[\s$>])(?:npx\s+(?:@kungfu-tech\/buildchain|buildchain)\s+|node\s+bin\/buildchain\.mjs\s+|buildchain\s+)([a-z0-9][a-z0-9-]*|--help|--version|-h|-v)(?:\s+([a-z0-9][a-z0-9-]*|--[a-z0-9-]+))?/gm)) {
|
|
183
|
+
const id = commandId(match[1], match[2]);
|
|
184
|
+
if (segment.kind === "inline" && !knownCommandIds.has(id)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
refs.push({
|
|
188
|
+
id,
|
|
189
|
+
path: relPath,
|
|
190
|
+
command: `buildchain ${match[1]}${match[2] ? ` ${match[2]}` : ""}`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return refs
|
|
196
|
+
.filter((entry) => entry.id)
|
|
197
|
+
.sort((a, b) => `${a.id}:${a.path}`.localeCompare(`${b.id}:${b.path}`));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function registryIds(entries) {
|
|
201
|
+
return new Set((entries || []).map((entry) => entry.id).filter(Boolean));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function collectPublicSurfaceReverseAudit({
|
|
205
|
+
root = process.cwd(),
|
|
206
|
+
cliRegistry: suppliedCliRegistry = undefined,
|
|
207
|
+
workflowRegistry: suppliedWorkflowRegistry = undefined,
|
|
208
|
+
pageRegistry: suppliedPageRegistry = undefined,
|
|
209
|
+
} = {}) {
|
|
210
|
+
const cliCommands = enumerateCliCommandsFromBin({ root });
|
|
211
|
+
const workflowInputs = enumerateWorkflowInputs({ root });
|
|
212
|
+
const actionInputs = enumerateActionInputs({ root });
|
|
213
|
+
const sitePages = suppliedPageRegistry
|
|
214
|
+
? (Array.isArray(suppliedPageRegistry?.pages) ? suppliedPageRegistry.pages : []).map((page) => ({
|
|
215
|
+
id: page.id || page.path,
|
|
216
|
+
path: page.path,
|
|
217
|
+
category: page.category || "",
|
|
218
|
+
})).sort((a, b) => String(a.id).localeCompare(String(b.id)))
|
|
219
|
+
: enumerateSitePages({ root });
|
|
220
|
+
const docCommandRefs = enumerateDocCommandRefs({ root });
|
|
221
|
+
const cliRegistry = suppliedCliRegistry || readJson(root, "dist/site/cli-registry.json", { commands: [] });
|
|
222
|
+
const workflowRegistry = suppliedWorkflowRegistry || readJson(root, "dist/site/workflow-registry.json", { workflows: [], actions: [] });
|
|
223
|
+
const pageRegistry = suppliedPageRegistry || readJson(root, "dist/site/page-registry.json", { pages: [] });
|
|
224
|
+
const declaredCli = registryIds(cliRegistry.commands);
|
|
225
|
+
const declaredWorkflows = registryIds(workflowRegistry.workflows);
|
|
226
|
+
const declaredActions = registryIds(workflowRegistry.actions);
|
|
227
|
+
const declaredPages = registryIds(pageRegistry.pages);
|
|
228
|
+
const missingCliRegistry = cliCommands.filter((entry) => !declaredCli.has(entry.id));
|
|
229
|
+
const missingWorkflowRegistry = workflowInputs.filter((entry) => !declaredWorkflows.has(entry.id) && entry.inputCount > 0);
|
|
230
|
+
const missingActionRegistry = actionInputs.filter((entry) => !declaredActions.has(entry.id) && entry.inputCount > 0);
|
|
231
|
+
const missingPageRegistry = sitePages.filter((entry) => entry.id && !declaredPages.has(entry.id));
|
|
232
|
+
const unknownDocCommandRefs = docCommandRefs.filter((entry) => !declaredCli.has(entry.id));
|
|
233
|
+
const failures = [
|
|
234
|
+
...missingCliRegistry.map((entry) => `cli:${entry.id}`),
|
|
235
|
+
...missingWorkflowRegistry.map((entry) => `workflow:${entry.id}`),
|
|
236
|
+
...missingActionRegistry.map((entry) => `action:${entry.id}`),
|
|
237
|
+
...missingPageRegistry.map((entry) => `site:${entry.id}`),
|
|
238
|
+
...unknownDocCommandRefs.map((entry) => `doc-command:${entry.id}:${entry.path}`),
|
|
239
|
+
];
|
|
240
|
+
const result = {
|
|
241
|
+
schemaVersion: 1,
|
|
242
|
+
contract: BUILDCHAIN_PUBLIC_SURFACE_AUDIT_CONTRACT,
|
|
243
|
+
status: failures.length === 0 ? "passed" : "failed",
|
|
244
|
+
summary: {
|
|
245
|
+
cliCommandCount: cliCommands.length,
|
|
246
|
+
workflowCount: workflowInputs.length,
|
|
247
|
+
actionCount: actionInputs.length,
|
|
248
|
+
sitePageCount: sitePages.length,
|
|
249
|
+
docCommandRefCount: docCommandRefs.length,
|
|
250
|
+
failureCount: failures.length,
|
|
251
|
+
},
|
|
252
|
+
enumerated: {
|
|
253
|
+
cliCommands,
|
|
254
|
+
workflowInputs,
|
|
255
|
+
actionInputs,
|
|
256
|
+
sitePages,
|
|
257
|
+
docCommandRefs,
|
|
258
|
+
},
|
|
259
|
+
declared: {
|
|
260
|
+
cliRegistryPath: "dist/site/cli-registry.json",
|
|
261
|
+
workflowRegistryPath: "dist/site/workflow-registry.json",
|
|
262
|
+
pageRegistryPath: "dist/site/page-registry.json",
|
|
263
|
+
cliRegistryDigest: fs.existsSync(path.join(root, "dist/site/cli-registry.json")) ? sha256(readText(root, "dist/site/cli-registry.json")) : "",
|
|
264
|
+
workflowRegistryDigest: fs.existsSync(path.join(root, "dist/site/workflow-registry.json")) ? sha256(readText(root, "dist/site/workflow-registry.json")) : "",
|
|
265
|
+
pageRegistryDigest: fs.existsSync(path.join(root, "dist/site/page-registry.json")) ? sha256(readText(root, "dist/site/page-registry.json")) : "",
|
|
266
|
+
},
|
|
267
|
+
comparison: {
|
|
268
|
+
missingCliRegistry,
|
|
269
|
+
missingWorkflowRegistry,
|
|
270
|
+
missingActionRegistry,
|
|
271
|
+
missingPageRegistry,
|
|
272
|
+
unknownDocCommandRefs,
|
|
273
|
+
},
|
|
274
|
+
auditBoundary: {
|
|
275
|
+
mode: "closed-world-enumerable",
|
|
276
|
+
scope: "Buildchain CLI usage/dispatch, reusable workflow inputs, action inputs, site pages, and documentation command references",
|
|
277
|
+
residualRisk: [
|
|
278
|
+
"Shell commands delegated through helper scripts are only counted when exposed through bin/buildchain.mjs usage or docs.",
|
|
279
|
+
"YAML parsing is limited to first-class action/workflow inputs, not arbitrary step environment variables.",
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function assertPublicSurfaceReverseAudit(report) {
|
|
287
|
+
if (report.status !== "passed") {
|
|
288
|
+
const failures = [
|
|
289
|
+
...report.comparison.missingCliRegistry.map((entry) => `missing CLI registry: ${entry.id}`),
|
|
290
|
+
...report.comparison.missingWorkflowRegistry.map((entry) => `missing workflow registry: ${entry.id}`),
|
|
291
|
+
...report.comparison.missingActionRegistry.map((entry) => `missing action registry: ${entry.id}`),
|
|
292
|
+
...report.comparison.missingPageRegistry.map((entry) => `missing site page registry: ${entry.id}`),
|
|
293
|
+
...report.comparison.unknownDocCommandRefs.map((entry) => `unknown docs command ref: ${entry.command} in ${entry.path}`),
|
|
294
|
+
];
|
|
295
|
+
throw new Error(`Buildchain public surface reverse audit failed:\n${failures.join("\n")}`);
|
|
296
|
+
}
|
|
297
|
+
return report;
|
|
298
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
discoverConfiguredVersionStateFiles,
|
|
6
|
+
getLifecycleStage,
|
|
7
|
+
loadBuildchainConfig,
|
|
8
|
+
runLifecycleStage,
|
|
9
|
+
updateConfiguredVersionStateContents,
|
|
10
|
+
} from "./buildchain-config.js";
|
|
11
|
+
|
|
12
|
+
function asPositiveInteger(value, name) {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isInteger(parsed) || parsed < 0) {
|
|
15
|
+
throw new Error(`${name} must be a non-negative integer`);
|
|
16
|
+
}
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertSemver(value, name) {
|
|
21
|
+
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(String(value || ""))) {
|
|
22
|
+
throw new Error(`${name} must be a semver version without a leading v`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeRef(ref) {
|
|
27
|
+
return String(ref || "").trim().replace(/^refs\/heads\//, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function defaultSourceRef({ major, minor }) {
|
|
31
|
+
if (minor > 0) {
|
|
32
|
+
return `release/v${major}/v${major}.${minor - 1}`;
|
|
33
|
+
}
|
|
34
|
+
return `v${major}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function defaultInitialVersion({ major, minor }) {
|
|
38
|
+
return `${major}.${minor}.0-alpha.0`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function defaultBootstrapBranch({ major, minor }) {
|
|
42
|
+
return `buildchain/release-line/v${major}.${minor}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function currentGitHead(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
return execFileSync("git", ["rev-parse", "HEAD"], {
|
|
48
|
+
cwd,
|
|
49
|
+
encoding: "utf8",
|
|
50
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
51
|
+
}).trim();
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readJsonIfExists(filePath) {
|
|
58
|
+
if (!fs.existsSync(filePath)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeJson(value) {
|
|
65
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function discoverVersionStateFiles(cwd, loadedConfig) {
|
|
69
|
+
if (loadedConfig?.config?.version?.files?.length) {
|
|
70
|
+
return discoverConfiguredVersionStateFiles(cwd, loadedConfig);
|
|
71
|
+
}
|
|
72
|
+
const files = [];
|
|
73
|
+
for (const relativePath of ["lerna.json", "package.json"]) {
|
|
74
|
+
const filePath = path.join(cwd, relativePath);
|
|
75
|
+
const content = readJsonIfExists(filePath);
|
|
76
|
+
if (content && typeof content.version === "string") {
|
|
77
|
+
files.push({
|
|
78
|
+
path: relativePath,
|
|
79
|
+
kind: relativePath === "lerna.json" ? "lerna" : "package",
|
|
80
|
+
content,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return files;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function updateDiscoveredVersionStateContents(files, version) {
|
|
88
|
+
if (files.some((file) => file.type)) {
|
|
89
|
+
return updateConfiguredVersionStateContents(files, version);
|
|
90
|
+
}
|
|
91
|
+
return files
|
|
92
|
+
.map((file) => {
|
|
93
|
+
const next = { ...file.content, version };
|
|
94
|
+
const content = writeJson(next);
|
|
95
|
+
return {
|
|
96
|
+
path: file.path,
|
|
97
|
+
kind: file.kind,
|
|
98
|
+
changed: content !== writeJson(file.content),
|
|
99
|
+
content,
|
|
100
|
+
};
|
|
101
|
+
})
|
|
102
|
+
.filter((file) => file.changed);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function changedPaths(cwd) {
|
|
106
|
+
const output = execFileSync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
107
|
+
cwd,
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
}).trimEnd();
|
|
110
|
+
return output
|
|
111
|
+
.split(/\r?\n/)
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.map((line) => line.slice(3).trim())
|
|
114
|
+
.sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function planReleaseLineBootstrap({
|
|
118
|
+
cwd = process.cwd(),
|
|
119
|
+
major,
|
|
120
|
+
minor,
|
|
121
|
+
sourceRef = "",
|
|
122
|
+
initialVersion = "",
|
|
123
|
+
requiredStatusCheck = "check",
|
|
124
|
+
setDefault = true,
|
|
125
|
+
createAlphaPr = true,
|
|
126
|
+
approvalCount = 1,
|
|
127
|
+
bootstrapBranch = "",
|
|
128
|
+
} = {}) {
|
|
129
|
+
const parsedMajor = asPositiveInteger(major, "major");
|
|
130
|
+
const parsedMinor = asPositiveInteger(minor, "minor");
|
|
131
|
+
const resolvedInitialVersion = initialVersion || defaultInitialVersion({ major: parsedMajor, minor: parsedMinor });
|
|
132
|
+
assertSemver(resolvedInitialVersion, "initialVersion");
|
|
133
|
+
const line = `v${parsedMajor}.${parsedMinor}`;
|
|
134
|
+
const devRef = `dev/v${parsedMajor}/${line}`;
|
|
135
|
+
const alphaRef = `alpha/v${parsedMajor}/${line}`;
|
|
136
|
+
const releaseRef = `release/v${parsedMajor}/${line}`;
|
|
137
|
+
const loadedConfig = loadBuildchainConfig(cwd);
|
|
138
|
+
const versionFiles = discoverVersionStateFiles(cwd, loadedConfig);
|
|
139
|
+
const lifecycleVersionState =
|
|
140
|
+
getLifecycleStage(loadedConfig, "version-state") ||
|
|
141
|
+
getLifecycleStage(loadedConfig, "version_state");
|
|
142
|
+
const source = normalizeRef(sourceRef) || defaultSourceRef({ major: parsedMajor, minor: parsedMinor });
|
|
143
|
+
const branch = normalizeRef(bootstrapBranch) || defaultBootstrapBranch({ major: parsedMajor, minor: parsedMinor });
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
contract: "kungfu-buildchain-release-line-bootstrap",
|
|
148
|
+
dryRun: true,
|
|
149
|
+
cwd,
|
|
150
|
+
line,
|
|
151
|
+
major: parsedMajor,
|
|
152
|
+
minor: parsedMinor,
|
|
153
|
+
initialVersion: resolvedInitialVersion,
|
|
154
|
+
source: {
|
|
155
|
+
ref: source,
|
|
156
|
+
sha: currentGitHead(cwd) || "",
|
|
157
|
+
},
|
|
158
|
+
refs: {
|
|
159
|
+
dev: devRef,
|
|
160
|
+
alpha: alphaRef,
|
|
161
|
+
release: releaseRef,
|
|
162
|
+
bootstrap: branch,
|
|
163
|
+
},
|
|
164
|
+
versionState: {
|
|
165
|
+
files: versionFiles.map((file) => file.path),
|
|
166
|
+
lifecycle: lifecycleVersionState ? "version-state" : "none",
|
|
167
|
+
},
|
|
168
|
+
protection: {
|
|
169
|
+
requiredStatusCheck,
|
|
170
|
+
strictStatusChecks: true,
|
|
171
|
+
requiredApprovingReviewCount: approvalCount,
|
|
172
|
+
requiredConversationResolution: true,
|
|
173
|
+
enforceAdmins: true,
|
|
174
|
+
protectedRefs: [devRef, alphaRef, releaseRef],
|
|
175
|
+
},
|
|
176
|
+
repositoryActions: [
|
|
177
|
+
{ action: "create-or-verify-source-ref", ref: source },
|
|
178
|
+
{ action: "commit-initial-version-state", ref: branch, version: resolvedInitialVersion },
|
|
179
|
+
{ action: "create-dev-branch", ref: devRef, from: branch },
|
|
180
|
+
{ action: "create-alpha-branch", ref: alphaRef, from: source },
|
|
181
|
+
{ action: "create-release-branch", ref: releaseRef, from: source },
|
|
182
|
+
...(setDefault ? [{ action: "set-default-branch", ref: devRef }] : []),
|
|
183
|
+
{ action: "protect-branch", ref: devRef },
|
|
184
|
+
{ action: "protect-branch", ref: alphaRef },
|
|
185
|
+
{ action: "protect-branch", ref: releaseRef },
|
|
186
|
+
...(createAlphaPr ? [{ action: "open-alpha-pr", head: devRef, base: alphaRef }] : []),
|
|
187
|
+
],
|
|
188
|
+
notes: [
|
|
189
|
+
"This bootstrap creates the new minor line before the first alpha promotion.",
|
|
190
|
+
"The source ref stays as the baseline for alpha/release until reviewed channel PRs move them.",
|
|
191
|
+
"The dev branch receives only the initial version-state commit and becomes the active line when setDefault is true.",
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function writeReleaseLineBootstrapVersionState({
|
|
197
|
+
cwd = process.cwd(),
|
|
198
|
+
major,
|
|
199
|
+
minor,
|
|
200
|
+
sourceRef = "",
|
|
201
|
+
initialVersion = "",
|
|
202
|
+
runVersionStateLifecycle = true,
|
|
203
|
+
generatedAt = "",
|
|
204
|
+
} = {}) {
|
|
205
|
+
const plan = planReleaseLineBootstrap({ cwd, major, minor, sourceRef, initialVersion });
|
|
206
|
+
const loadedConfig = loadBuildchainConfig(cwd);
|
|
207
|
+
const files = discoverVersionStateFiles(cwd, loadedConfig);
|
|
208
|
+
if (files.length === 0) {
|
|
209
|
+
throw new Error("release line bootstrap requires at least one version-state file");
|
|
210
|
+
}
|
|
211
|
+
const changed = updateDiscoveredVersionStateContents(files, plan.initialVersion);
|
|
212
|
+
for (const file of changed) {
|
|
213
|
+
fs.writeFileSync(path.join(cwd, file.path), file.content);
|
|
214
|
+
}
|
|
215
|
+
const lifecycleVersionState =
|
|
216
|
+
getLifecycleStage(loadedConfig, "version-state") ||
|
|
217
|
+
getLifecycleStage(loadedConfig, "version_state");
|
|
218
|
+
if (runVersionStateLifecycle && lifecycleVersionState) {
|
|
219
|
+
const timestamp = generatedAt || new Date().toISOString();
|
|
220
|
+
runLifecycleStage({
|
|
221
|
+
cwd,
|
|
222
|
+
loadedConfig,
|
|
223
|
+
name: "version-state",
|
|
224
|
+
stage: lifecycleVersionState,
|
|
225
|
+
env: {
|
|
226
|
+
BUILDCHAIN_VERSION: plan.initialVersion,
|
|
227
|
+
BUILDCHAIN_SITE_GENERATED_AT: timestamp,
|
|
228
|
+
BUILDCHAIN_SITE_PUBLISHED_AT: timestamp,
|
|
229
|
+
BUILDCHAIN_SITE_TIMESTAMP_POLICY: "ci-injected",
|
|
230
|
+
BUILDCHAIN_SURFACE_GENERATED_AT: timestamp,
|
|
231
|
+
BUILDCHAIN_SURFACE_PUBLISHED_AT: timestamp,
|
|
232
|
+
BUILDCHAIN_SURFACE_TIMESTAMP_POLICY: "ci-injected",
|
|
233
|
+
BUILDCHAIN_SOURCE_SHA: plan.source.sha,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...plan,
|
|
239
|
+
dryRun: false,
|
|
240
|
+
changedFiles: changedPaths(cwd),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
assertPublicSurfaceReverseAudit,
|
|
5
|
+
collectPublicSurfaceReverseAudit,
|
|
6
|
+
} from "../packages/core/public-surface-audit.js";
|
|
3
7
|
|
|
4
8
|
const root = process.cwd();
|
|
5
9
|
const sharedActionTsupConfig = fs.readFileSync(path.join(root, "scripts/tsup-action.config.mjs"), "utf8");
|
|
@@ -16,6 +20,8 @@ const requiredPaths = [
|
|
|
16
20
|
"bin/buildchain.mjs",
|
|
17
21
|
"packages/core/homebrew.js",
|
|
18
22
|
"packages/core/build-facts.js",
|
|
23
|
+
"packages/core/release-line-bootstrap.js",
|
|
24
|
+
"packages/core/public-surface-audit.js",
|
|
19
25
|
"docs/MAP.md",
|
|
20
26
|
"docs/build-facts.md",
|
|
21
27
|
"docs/binary-distribution.md",
|
|
@@ -52,6 +58,7 @@ const requiredPaths = [
|
|
|
52
58
|
".github/actionlint.yaml",
|
|
53
59
|
".github/workflows/self-hosted-runner-smoke.yml",
|
|
54
60
|
".github/workflows/buildchain-ref-promotion.yml",
|
|
61
|
+
".github/workflows/release-line-bootstrap.yml",
|
|
55
62
|
".github/workflows/dev-pr-auto-merge.yml",
|
|
56
63
|
".github/workflows/buildchain-patrol.yml",
|
|
57
64
|
".github/workflows/patrol-daily.yml",
|
|
@@ -119,6 +126,9 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
|
|
|
119
126
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
120
127
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
121
128
|
}
|
|
129
|
+
if (rootPackage.exports?.["./release-line-bootstrap"] !== "./packages/core/release-line-bootstrap.js") {
|
|
130
|
+
throw new Error("root package must export @kungfu-tech/buildchain/release-line-bootstrap");
|
|
131
|
+
}
|
|
122
132
|
if (rootPackage.exports?.["./readme-badges"] !== "./packages/core/readme-badges.js") {
|
|
123
133
|
throw new Error("root package must export @kungfu-tech/buildchain/readme-badges");
|
|
124
134
|
}
|
|
@@ -146,6 +156,9 @@ if (rootPackage.exports?.["./surface-manifest"] !== "./packages/core/surface-man
|
|
|
146
156
|
if (rootPackage.exports?.["./buildchain-kfd-claims"] !== "./packages/core/buildchain-kfd-claims.js") {
|
|
147
157
|
throw new Error("root package must export @kungfu-tech/buildchain/buildchain-kfd-claims");
|
|
148
158
|
}
|
|
159
|
+
if (rootPackage.exports?.["./public-surface-audit"] !== "./packages/core/public-surface-audit.js") {
|
|
160
|
+
throw new Error("root package must export @kungfu-tech/buildchain/public-surface-audit");
|
|
161
|
+
}
|
|
149
162
|
if (rootPackage.exports?.["./site/buildchain-site.json"] !== "./dist/site/buildchain-site.json") {
|
|
150
163
|
throw new Error("root package must export @kungfu-tech/buildchain/site/buildchain-site.json");
|
|
151
164
|
}
|
|
@@ -164,6 +177,9 @@ if (rootPackage.exports?.["./site/node-api-registry.json"] !== "./dist/site/node
|
|
|
164
177
|
if (rootPackage.exports?.["./site/kfd-claims.json"] !== "./dist/site/kfd-claims.json") {
|
|
165
178
|
throw new Error("root package must export @kungfu-tech/buildchain/site/kfd-claims.json");
|
|
166
179
|
}
|
|
180
|
+
if (rootPackage.exports?.["./site/public-surface-audit.json"] !== "./dist/site/public-surface-audit.json") {
|
|
181
|
+
throw new Error("root package must export @kungfu-tech/buildchain/site/public-surface-audit.json");
|
|
182
|
+
}
|
|
167
183
|
if (rootPackage.publishConfig?.access !== "public") {
|
|
168
184
|
throw new Error("root package publishConfig.access must be public");
|
|
169
185
|
}
|
|
@@ -190,6 +206,7 @@ const siteBundle = JSON.parse(fs.readFileSync(path.join(root, "dist/site/buildch
|
|
|
190
206
|
const siteManifest = JSON.parse(fs.readFileSync(path.join(root, "dist/site/site-manifest.json"), "utf8"));
|
|
191
207
|
const pageRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/page-registry.json"), "utf8"));
|
|
192
208
|
const badgeEndpointRegistry = JSON.parse(fs.readFileSync(path.join(root, "dist/site/badge-endpoint-registry.json"), "utf8"));
|
|
209
|
+
const publicSurfaceAudit = JSON.parse(fs.readFileSync(path.join(root, "dist/site/public-surface-audit.json"), "utf8"));
|
|
193
210
|
if (!cliSource.startsWith("#!/usr/bin/env node")) {
|
|
194
211
|
throw new Error("bin/buildchain.mjs must be executable with a node shebang");
|
|
195
212
|
}
|
|
@@ -306,6 +323,18 @@ if (siteBundle.pageRegistry?.path !== "page-registry.json" || siteBundle.pageReg
|
|
|
306
323
|
if (!siteManifest.facts?.includes("page-registry.json") || !siteBundle.entrypoints?.includes("page-registry.json")) {
|
|
307
324
|
throw new Error("site bundle entrypoints must include page-registry.json");
|
|
308
325
|
}
|
|
326
|
+
if (!siteManifest.facts?.includes("public-surface-audit.json") || !siteBundle.entrypoints?.includes("public-surface-audit.json")) {
|
|
327
|
+
throw new Error("site bundle entrypoints must include public-surface-audit.json");
|
|
328
|
+
}
|
|
329
|
+
if (publicSurfaceAudit.contract !== "kungfu-buildchain-public-surface-reverse-audit") {
|
|
330
|
+
throw new Error("public-surface-audit.json must expose the reverse audit contract");
|
|
331
|
+
}
|
|
332
|
+
assertPublicSurfaceReverseAudit(publicSurfaceAudit);
|
|
333
|
+
const livePublicSurfaceAudit = collectPublicSurfaceReverseAudit({ root });
|
|
334
|
+
assertPublicSurfaceReverseAudit(livePublicSurfaceAudit);
|
|
335
|
+
if (JSON.stringify(publicSurfaceAudit.summary) !== JSON.stringify(livePublicSurfaceAudit.summary)) {
|
|
336
|
+
throw new Error("public-surface-audit.json summary is stale; run pnpm run generate:site");
|
|
337
|
+
}
|
|
309
338
|
for (const [name, manifest] of [["buildchain-site.json", siteBundle], ["site-manifest.json", siteManifest]]) {
|
|
310
339
|
if (!manifest.generatedAt) {
|
|
311
340
|
throw new Error(`${name} must expose generatedAt`);
|
|
@@ -361,6 +390,8 @@ for (const requiredSnippet of [
|
|
|
361
390
|
"createBuildchainKfd2Claims",
|
|
362
391
|
"createBuildchainKfd3PrebuildWitness",
|
|
363
392
|
"BUILDCHAIN_AGENT_MANUALS",
|
|
393
|
+
"collectPublicSurfaceReverseAudit",
|
|
394
|
+
"assertPublicSurfaceReverseAudit",
|
|
364
395
|
]) {
|
|
365
396
|
if (!coreIndexSource.includes(requiredSnippet)) {
|
|
366
397
|
throw new Error(`packages/core/index.js must export Buildchain self KFD claim API: ${requiredSnippet}`);
|
|
@@ -820,7 +851,7 @@ if (!badgeEndpointRegistry.badges?.some((entry) => entry.id === "buildchain-rele
|
|
|
820
851
|
throw new Error("badge endpoint registry must include Buildchain Release Passport badge");
|
|
821
852
|
}
|
|
822
853
|
|
|
823
|
-
for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "release-model.json", "buildchain-contract.json"]) {
|
|
854
|
+
for (const siteFile of ["buildchain-site.json", "site-manifest.json", "badge-endpoint-registry.json", "cli-registry.json", "manual-registry.json", "node-api-registry.json", "workflow-registry.json", "public-surface-audit.json", "release-model.json", "buildchain-contract.json"]) {
|
|
824
855
|
if (!fs.existsSync(path.join(root, "dist", "site", siteFile))) {
|
|
825
856
|
throw new Error(`site bundle missing ${siteFile}`);
|
|
826
857
|
}
|