@kungfu-tech/buildchain 2.8.15 → 2.8.16-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/README.md +19 -11
- package/bin/buildchain.mjs +150 -0
- package/dist/site/buildchain-contract.json +83 -11
- package/dist/site/buildchain-site.json +121 -20
- package/dist/site/cli-registry.json +5 -0
- package/dist/site/kfd-claims.json +85 -3
- package/dist/site/manual-registry.json +18 -3
- package/dist/site/node-api-registry.json +28 -6
- package/dist/site/page-registry.json +88 -11
- package/dist/site/release-model.json +8 -0
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +23 -7
- package/docs/MAP.md +8 -0
- package/docs/cli.md +40 -3
- package/docs/homebrew.md +116 -0
- package/docs/readme-badges.md +124 -0
- package/docs/web-surface-deployments.md +46 -8
- package/package.json +3 -1
- package/packages/core/README.md +12 -0
- package/packages/core/buildchain-config.js +2 -2
- package/packages/core/buildchain-contract.js +56 -0
- package/packages/core/buildchain-kfd-claims.js +37 -0
- package/packages/core/homebrew.js +414 -0
- package/packages/core/index.js +21 -0
- package/packages/core/readme-badges.js +450 -0
- package/packages/core/release-passport.js +24 -9
- package/scripts/check-inventory.mjs +69 -0
- package/scripts/generate-site-bundle.mjs +12 -0
- package/scripts/web-surface-core.mjs +177 -5
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import { loadBuildchainConfig } from "./buildchain-config.js";
|
|
6
|
+
import {
|
|
7
|
+
readJsonFromLocation,
|
|
8
|
+
verifyReleasePassport,
|
|
9
|
+
} from "./release-passport.js";
|
|
10
|
+
|
|
11
|
+
export const README_BADGE_FACTS_CONTRACT = "kungfu-buildchain-readme-badge-facts";
|
|
12
|
+
export const README_BADGE_BLOCK_START = "<!-- buildchain:badges:start -->";
|
|
13
|
+
export const README_BADGE_BLOCK_END = "<!-- buildchain:badges:end -->";
|
|
14
|
+
|
|
15
|
+
const KFD_KEYS = [
|
|
16
|
+
{ key: "kfd-1", id: "kfd1", label: "KFD-1", text: "contract world" },
|
|
17
|
+
{ key: "kfd-2", id: "kfd2", label: "KFD-2", text: "trust passport" },
|
|
18
|
+
{ key: "kfd-3", id: "kfd3", label: "KFD-3", text: "collaboration interface" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const STATE_COLORS = {
|
|
22
|
+
passed: "2ea44f",
|
|
23
|
+
aligned: "0969da",
|
|
24
|
+
declared: "6e7781",
|
|
25
|
+
planned: "bf8700",
|
|
26
|
+
draft: "8250df",
|
|
27
|
+
downgraded: "bf8700",
|
|
28
|
+
failed: "cf222e",
|
|
29
|
+
missing: "6e7781",
|
|
30
|
+
unknown: "6e7781",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function readJsonIfExists(filePath) {
|
|
34
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readTextIfExists(filePath) {
|
|
41
|
+
return fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sha256File(filePath) {
|
|
45
|
+
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function posixPath(value) {
|
|
49
|
+
return String(value || "").split(path.sep).join("/");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function encodeBadge(value) {
|
|
53
|
+
return encodeURIComponent(String(value || ""))
|
|
54
|
+
.replace(/-/g, "--")
|
|
55
|
+
.replace(/_/g, "__");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function badgeUrl({ label, message, color }) {
|
|
59
|
+
return `https://img.shields.io/badge/${encodeBadge(label)}-${encodeBadge(message)}-${color || "6e7781"}.svg`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeState(value, fallback = "planned") {
|
|
63
|
+
const normalized = String(value || fallback).trim().toLowerCase();
|
|
64
|
+
if (["passed", "aligned", "declared", "planned", "draft", "downgraded", "failed", "missing"].includes(normalized)) {
|
|
65
|
+
return normalized;
|
|
66
|
+
}
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function normalizeDeclarationState(value, fallback = "planned") {
|
|
71
|
+
const normalized = normalizeState(value, fallback);
|
|
72
|
+
return normalized === "passed" ? "declared" : normalized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeStringArray(value) {
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return value
|
|
80
|
+
.map((entry) => String(entry || "").trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readRepositoryFromGit(cwd) {
|
|
85
|
+
const remote = spawnSync("git", ["config", "--get", "remote.origin.url"], {
|
|
86
|
+
cwd,
|
|
87
|
+
encoding: "utf8",
|
|
88
|
+
});
|
|
89
|
+
const value = remote.status === 0 ? remote.stdout.trim() : "";
|
|
90
|
+
const match = value.match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?$/);
|
|
91
|
+
if (!match) {
|
|
92
|
+
return { fullName: "", owner: "", name: "", url: "" };
|
|
93
|
+
}
|
|
94
|
+
const fullName = `${match[1]}/${match[2]}`;
|
|
95
|
+
return {
|
|
96
|
+
fullName,
|
|
97
|
+
owner: match[1],
|
|
98
|
+
name: match[2],
|
|
99
|
+
url: `https://github.com/${fullName}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizePackageRepository(repository) {
|
|
104
|
+
const value = typeof repository === "string" ? repository : repository?.url || "";
|
|
105
|
+
const match = String(value).match(/github\.com[:/]([^/\s]+)\/([^/\s.]+)(?:\.git)?/);
|
|
106
|
+
if (!match) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const fullName = `${match[1]}/${match[2]}`;
|
|
110
|
+
return {
|
|
111
|
+
fullName,
|
|
112
|
+
owner: match[1],
|
|
113
|
+
name: match[2],
|
|
114
|
+
url: `https://github.com/${fullName}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function discoverWorkflowFacts({ cwd, repository, badgeConfig = {} }) {
|
|
119
|
+
const workflowDir = path.join(cwd, ".github", "workflows");
|
|
120
|
+
const configured = normalizeStringArray(badgeConfig.workflows);
|
|
121
|
+
const defaults = ["verify.yml", "build.yml", "buildchain-ref-promotion.yml"];
|
|
122
|
+
const names = configured.length > 0 ? configured : defaults;
|
|
123
|
+
if (!fs.existsSync(workflowDir)) {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
return names
|
|
127
|
+
.map((file) => {
|
|
128
|
+
const workflowPath = path.join(workflowDir, file);
|
|
129
|
+
if (!fs.existsSync(workflowPath)) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
const source = fs.readFileSync(workflowPath, "utf8");
|
|
133
|
+
const nameMatch = source.match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
134
|
+
const name = String(nameMatch?.[1] || path.basename(file, path.extname(file))).trim();
|
|
135
|
+
return {
|
|
136
|
+
file,
|
|
137
|
+
name,
|
|
138
|
+
badgeUrl: repository.fullName
|
|
139
|
+
? `https://github.com/${repository.fullName}/actions/workflows/${file}/badge.svg`
|
|
140
|
+
: "",
|
|
141
|
+
url: repository.fullName
|
|
142
|
+
? `https://github.com/${repository.fullName}/actions/workflows/${file}`
|
|
143
|
+
: "",
|
|
144
|
+
};
|
|
145
|
+
})
|
|
146
|
+
.filter(Boolean);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function defaultReleasePassportLocation({ cwd, repository, badgeConfig = {} }) {
|
|
150
|
+
const configured = badgeConfig.release_passport_url || badgeConfig.releasePassportUrl || badgeConfig.release_passport || "";
|
|
151
|
+
if (configured) {
|
|
152
|
+
return String(configured);
|
|
153
|
+
}
|
|
154
|
+
const localCandidates = [
|
|
155
|
+
"buildchain.release.json",
|
|
156
|
+
".buildchain/release-passport/buildchain.release.json",
|
|
157
|
+
];
|
|
158
|
+
for (const candidate of localCandidates) {
|
|
159
|
+
const filePath = path.join(cwd, candidate);
|
|
160
|
+
if (fs.existsSync(filePath)) {
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return repository.fullName
|
|
165
|
+
? `https://github.com/${repository.fullName}/releases/latest/download/buildchain.release.json`
|
|
166
|
+
: "";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function siblingUrl(location, filename) {
|
|
170
|
+
if (!/^https?:\/\//.test(location)) {
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
return `${location.replace(/\/[^/]*$/, "")}/${filename}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function readPassportAndReport({ cwd, location }) {
|
|
177
|
+
if (!location) {
|
|
178
|
+
return { passport: undefined, report: undefined, error: "" };
|
|
179
|
+
}
|
|
180
|
+
const resolvedLocation = /^https?:\/\//.test(location) ? location : path.resolve(cwd, location);
|
|
181
|
+
try {
|
|
182
|
+
const passport = await readJsonFromLocation(resolvedLocation);
|
|
183
|
+
const report = await verifyReleasePassport({
|
|
184
|
+
passportLocation: resolvedLocation,
|
|
185
|
+
artifactEvidenceLocation: siblingUrl(resolvedLocation, "artifact-evidence.json"),
|
|
186
|
+
impactLocation: siblingUrl(resolvedLocation, "impact.json"),
|
|
187
|
+
agentIndexLocation: siblingUrl(resolvedLocation, "agent-index.json"),
|
|
188
|
+
productMechanismLocation: siblingUrl(resolvedLocation, "product-mechanism.json"),
|
|
189
|
+
});
|
|
190
|
+
return { passport, report, error: "" };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return { passport: undefined, report: undefined, error: error.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function kfdSectionPassed({ passport, report, key }) {
|
|
197
|
+
if (!passport || !report?.ok) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
const section = passport[key] || (key === "kfd-1" ? passport.kfd1 : undefined) || (key === "kfd-3" ? passport.kfd3 : undefined);
|
|
201
|
+
return section?.status === "passed";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function declaredKfdState({ badgeConfig = {}, key, id }) {
|
|
205
|
+
const kfdConfig = badgeConfig.kfd && typeof badgeConfig.kfd === "object" ? badgeConfig.kfd : {};
|
|
206
|
+
return normalizeDeclarationState(
|
|
207
|
+
badgeConfig[key] ||
|
|
208
|
+
badgeConfig[id] ||
|
|
209
|
+
badgeConfig[key.replace("-", "_")] ||
|
|
210
|
+
kfdConfig[key] ||
|
|
211
|
+
kfdConfig[id] ||
|
|
212
|
+
kfdConfig[key.replace("kfd-", "")],
|
|
213
|
+
"planned",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function releasePassportState({ report, error, badgeConfig = {} }) {
|
|
218
|
+
if (report?.ok) {
|
|
219
|
+
return "passed";
|
|
220
|
+
}
|
|
221
|
+
if (error) {
|
|
222
|
+
return normalizeDeclarationState(badgeConfig.release_passport_state || badgeConfig.releasePassportState, "declared");
|
|
223
|
+
}
|
|
224
|
+
return normalizeDeclarationState(badgeConfig.release_passport_state || badgeConfig.releasePassportState, "planned");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function licenseFromFiles(cwd, packageJson) {
|
|
228
|
+
if (packageJson.license) {
|
|
229
|
+
return String(packageJson.license);
|
|
230
|
+
}
|
|
231
|
+
const licenseFile = fs.readdirSync(cwd).find((entry) => /^licen[sc]e($|\.)/i.test(entry));
|
|
232
|
+
return licenseFile ? licenseFile.replace(/^LICENSE[.-]?/i, "") || "present" : "";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function collectPlatformFacts({ badgeConfig = {}, passport = undefined }) {
|
|
236
|
+
const configured = normalizeStringArray(badgeConfig.platforms);
|
|
237
|
+
if (configured.length > 0) {
|
|
238
|
+
return configured;
|
|
239
|
+
}
|
|
240
|
+
const assets = Array.isArray(passport?.artifacts) ? passport.artifacts : [];
|
|
241
|
+
const platforms = new Set();
|
|
242
|
+
for (const asset of assets) {
|
|
243
|
+
const platform = String(asset.platform || "").trim();
|
|
244
|
+
if (platform) {
|
|
245
|
+
platforms.add(platform);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return [...platforms].sort();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function localFactFileSummary(cwd, candidates = []) {
|
|
252
|
+
for (const relPath of candidates) {
|
|
253
|
+
const filePath = path.join(cwd, relPath);
|
|
254
|
+
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const value = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
259
|
+
return {
|
|
260
|
+
location: relPath,
|
|
261
|
+
sha256: sha256File(filePath),
|
|
262
|
+
contract: value.contract || "",
|
|
263
|
+
claimCount: Array.isArray(value.publicClaims) ? value.publicClaims.length : undefined,
|
|
264
|
+
category: value.category || "",
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
location: relPath,
|
|
269
|
+
error: error.message,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function buildBadgeEntries(facts) {
|
|
277
|
+
const entries = [];
|
|
278
|
+
for (const kfd of facts.kfd) {
|
|
279
|
+
entries.push({
|
|
280
|
+
id: kfd.key,
|
|
281
|
+
alt: `${kfd.label}: ${kfd.state}`,
|
|
282
|
+
image: badgeUrl({
|
|
283
|
+
label: kfd.label,
|
|
284
|
+
message: `${kfd.text} ${kfd.state}`,
|
|
285
|
+
color: STATE_COLORS[kfd.state] || STATE_COLORS.unknown,
|
|
286
|
+
}),
|
|
287
|
+
link: kfd.url,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
entries.push({
|
|
291
|
+
id: "release-passport",
|
|
292
|
+
alt: `Release Passport: ${facts.releasePassport.state}`,
|
|
293
|
+
image: badgeUrl({
|
|
294
|
+
label: "release passport",
|
|
295
|
+
message: facts.releasePassport.state,
|
|
296
|
+
color: STATE_COLORS[facts.releasePassport.state] || STATE_COLORS.unknown,
|
|
297
|
+
}),
|
|
298
|
+
link: facts.releasePassport.url,
|
|
299
|
+
});
|
|
300
|
+
if (facts.package.license) {
|
|
301
|
+
entries.push({
|
|
302
|
+
id: "license",
|
|
303
|
+
alt: `License: ${facts.package.license}`,
|
|
304
|
+
image: badgeUrl({ label: "license", message: facts.package.license, color: "0969da" }),
|
|
305
|
+
link: facts.repository.url ? `${facts.repository.url}/blob/HEAD/LICENSE` : "",
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (facts.platforms.length > 0) {
|
|
309
|
+
entries.push({
|
|
310
|
+
id: "platform",
|
|
311
|
+
alt: `Platform: ${facts.platforms.join(" | ")}`,
|
|
312
|
+
image: badgeUrl({ label: "platform", message: facts.platforms.join(" | "), color: "6e7781" }),
|
|
313
|
+
link: facts.releasePassport.url,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
for (const workflow of facts.workflows) {
|
|
317
|
+
entries.push({
|
|
318
|
+
id: `workflow:${workflow.file}`,
|
|
319
|
+
alt: workflow.name,
|
|
320
|
+
image: workflow.badgeUrl,
|
|
321
|
+
link: workflow.url,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return entries;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function collectReadmeBadgeFacts({ cwd = process.cwd() } = {}) {
|
|
328
|
+
const resolvedCwd = path.resolve(cwd);
|
|
329
|
+
const loadedConfig = loadBuildchainConfig(resolvedCwd);
|
|
330
|
+
const badgeConfig = loadedConfig?.config?.badges && typeof loadedConfig.config.badges === "object"
|
|
331
|
+
? loadedConfig.config.badges
|
|
332
|
+
: {};
|
|
333
|
+
const packageJson = readJsonIfExists(path.join(resolvedCwd, "package.json")) || {};
|
|
334
|
+
const packageRepository = normalizePackageRepository(packageJson.repository);
|
|
335
|
+
const repository = packageRepository || readRepositoryFromGit(resolvedCwd);
|
|
336
|
+
const releasePassportLocation = defaultReleasePassportLocation({ cwd: resolvedCwd, repository, badgeConfig });
|
|
337
|
+
const { passport, report, error } = await readPassportAndReport({
|
|
338
|
+
cwd: resolvedCwd,
|
|
339
|
+
location: releasePassportLocation,
|
|
340
|
+
});
|
|
341
|
+
const releasePassportUrl = /^https?:\/\//.test(releasePassportLocation)
|
|
342
|
+
? releasePassportLocation
|
|
343
|
+
: repository.fullName && releasePassportLocation
|
|
344
|
+
? `${repository.url}/blob/HEAD/${posixPath(releasePassportLocation)}`
|
|
345
|
+
: releasePassportLocation;
|
|
346
|
+
const releaseState = releasePassportState({ report, error, badgeConfig });
|
|
347
|
+
const kfd = KFD_KEYS.map((entry) => {
|
|
348
|
+
const passed = kfdSectionPassed({ passport, report, key: entry.key });
|
|
349
|
+
const state = passed ? "passed" : declaredKfdState({ badgeConfig, key: entry.key, id: entry.id });
|
|
350
|
+
return {
|
|
351
|
+
key: entry.key,
|
|
352
|
+
label: entry.label,
|
|
353
|
+
text: entry.text,
|
|
354
|
+
state,
|
|
355
|
+
source: passed ? "release-passport" : "declaration",
|
|
356
|
+
url: releasePassportUrl,
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
const facts = {
|
|
360
|
+
schemaVersion: 1,
|
|
361
|
+
contract: README_BADGE_FACTS_CONTRACT,
|
|
362
|
+
cwd: resolvedCwd,
|
|
363
|
+
repository,
|
|
364
|
+
package: {
|
|
365
|
+
name: packageJson.name || "",
|
|
366
|
+
version: packageJson.version || "",
|
|
367
|
+
license: licenseFromFiles(resolvedCwd, packageJson),
|
|
368
|
+
},
|
|
369
|
+
releasePassport: {
|
|
370
|
+
location: releasePassportLocation,
|
|
371
|
+
url: releasePassportUrl,
|
|
372
|
+
state: releaseState,
|
|
373
|
+
verified: Boolean(report?.ok),
|
|
374
|
+
error,
|
|
375
|
+
reportSummary: report ? {
|
|
376
|
+
ok: report.ok,
|
|
377
|
+
trust: report.trust,
|
|
378
|
+
issueCount: Array.isArray(report.issues) ? report.issues.length : 0,
|
|
379
|
+
} : undefined,
|
|
380
|
+
},
|
|
381
|
+
kfdClaimRegistry: localFactFileSummary(resolvedCwd, [
|
|
382
|
+
"dist/site/kfd-claims.json",
|
|
383
|
+
".buildchain/kfd-claims.json",
|
|
384
|
+
]),
|
|
385
|
+
productMechanism: localFactFileSummary(resolvedCwd, [
|
|
386
|
+
"dist/site/product-mechanism.json",
|
|
387
|
+
"product-mechanism.json",
|
|
388
|
+
".buildchain/product-mechanism.json",
|
|
389
|
+
]),
|
|
390
|
+
kfd,
|
|
391
|
+
platforms: collectPlatformFacts({ badgeConfig, passport }),
|
|
392
|
+
workflows: discoverWorkflowFacts({ cwd: resolvedCwd, repository, badgeConfig }),
|
|
393
|
+
badges: [],
|
|
394
|
+
};
|
|
395
|
+
facts.badges = buildBadgeEntries(facts);
|
|
396
|
+
return facts;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function renderReadmeBadgeBlock(facts) {
|
|
400
|
+
const lines = [
|
|
401
|
+
README_BADGE_BLOCK_START,
|
|
402
|
+
...facts.badges.map((badge) => (
|
|
403
|
+
badge.link
|
|
404
|
+
? `[](${badge.link})`
|
|
405
|
+
: ``
|
|
406
|
+
)),
|
|
407
|
+
README_BADGE_BLOCK_END,
|
|
408
|
+
];
|
|
409
|
+
return `${lines.join("\n")}\n`;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function badgeBlockRegex() {
|
|
413
|
+
return new RegExp(`${README_BADGE_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${README_BADGE_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function checkReadmeBadgeBlock({ readmeText, facts } = {}) {
|
|
417
|
+
const expected = renderReadmeBadgeBlock(facts);
|
|
418
|
+
const match = String(readmeText || "").match(badgeBlockRegex());
|
|
419
|
+
const actual = match ? match[0] : "";
|
|
420
|
+
const normalizedActual = actual.endsWith("\n") ? actual : `${actual}\n`;
|
|
421
|
+
const ok = normalizedActual === expected;
|
|
422
|
+
return {
|
|
423
|
+
schemaVersion: 1,
|
|
424
|
+
contract: "kungfu-buildchain-readme-badge-check",
|
|
425
|
+
ok,
|
|
426
|
+
missing: !match,
|
|
427
|
+
stale: Boolean(match) && !ok,
|
|
428
|
+
expected,
|
|
429
|
+
actual,
|
|
430
|
+
facts,
|
|
431
|
+
message: ok ? "README badge block is current" : (match ? "README badge block is stale" : "README badge block is missing"),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function updateReadmeBadgeBlock({ readmeText, facts } = {}) {
|
|
436
|
+
const source = String(readmeText || "");
|
|
437
|
+
const block = renderReadmeBadgeBlock(facts);
|
|
438
|
+
if (badgeBlockRegex().test(source)) {
|
|
439
|
+
return source.replace(badgeBlockRegex(), block);
|
|
440
|
+
}
|
|
441
|
+
const h1 = source.match(/^# .+\n+/);
|
|
442
|
+
if (h1) {
|
|
443
|
+
return `${h1[0]}${block}\n${source.slice(h1[0].length)}`;
|
|
444
|
+
}
|
|
445
|
+
return `${block}\n${source}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function readReadme({ cwd = process.cwd(), readmePath = "README.md" } = {}) {
|
|
449
|
+
return readTextIfExists(path.resolve(cwd, readmePath));
|
|
450
|
+
}
|
|
@@ -1465,10 +1465,16 @@ function validateKfd2ReleaseTrustPassportAudit(section, issues) {
|
|
|
1465
1465
|
}
|
|
1466
1466
|
}
|
|
1467
1467
|
|
|
1468
|
-
function resolveSiblingJson(basePath, relativePath) {
|
|
1469
|
-
if (!basePath || !relativePath
|
|
1468
|
+
async function resolveSiblingJson(basePath, relativePath) {
|
|
1469
|
+
if (!basePath || !relativePath) {
|
|
1470
1470
|
return undefined;
|
|
1471
1471
|
}
|
|
1472
|
+
if (/^https?:\/\//.test(relativePath)) {
|
|
1473
|
+
return readJsonFromLocation(relativePath);
|
|
1474
|
+
}
|
|
1475
|
+
if (/^https?:\/\//.test(basePath)) {
|
|
1476
|
+
return readJsonFromLocation(new URL(relativePath, basePath).toString());
|
|
1477
|
+
}
|
|
1472
1478
|
const candidate = path.resolve(path.dirname(basePath), relativePath);
|
|
1473
1479
|
if (!fs.existsSync(candidate)) {
|
|
1474
1480
|
return undefined;
|
|
@@ -1789,13 +1795,22 @@ export function createReleaseCheckReport({
|
|
|
1789
1795
|
};
|
|
1790
1796
|
}
|
|
1791
1797
|
|
|
1792
|
-
export async function readJsonFromLocation(location) {
|
|
1798
|
+
export async function readJsonFromLocation(location, redirectCount = 0) {
|
|
1793
1799
|
const input = nonEmptyString(location, "location");
|
|
1800
|
+
if (redirectCount > 5) {
|
|
1801
|
+
throw new Error(`too many redirects while reading ${input}`);
|
|
1802
|
+
}
|
|
1794
1803
|
if (/^https?:\/\//.test(input)) {
|
|
1795
1804
|
const client = input.startsWith("https:") ? https : http;
|
|
1796
1805
|
return new Promise((resolve, reject) => {
|
|
1797
1806
|
client
|
|
1798
1807
|
.get(input, (response) => {
|
|
1808
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
1809
|
+
const nextLocation = new URL(response.headers.location, input).toString();
|
|
1810
|
+
response.resume();
|
|
1811
|
+
readJsonFromLocation(nextLocation, redirectCount + 1).then(resolve, reject);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1799
1814
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
1800
1815
|
reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
|
|
1801
1816
|
response.resume();
|
|
@@ -1829,27 +1844,27 @@ export async function verifyReleasePassport({
|
|
|
1829
1844
|
productMechanismLocation = "",
|
|
1830
1845
|
} = {}) {
|
|
1831
1846
|
const passport = await readJsonFromLocation(passportLocation);
|
|
1832
|
-
const basePath = /^https?:\/\//.test(passportLocation) ?
|
|
1847
|
+
const basePath = /^https?:\/\//.test(passportLocation) ? passportLocation : path.resolve(passportLocation);
|
|
1833
1848
|
const artifactEvidence =
|
|
1834
1849
|
artifactEvidenceLocation
|
|
1835
1850
|
? await readJsonFromLocation(artifactEvidenceLocation)
|
|
1836
|
-
: resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
|
|
1851
|
+
: await resolveSiblingJson(basePath, passport.evidence?.artifactEvidence) || {};
|
|
1837
1852
|
const publishEvidence =
|
|
1838
1853
|
publishEvidenceLocation
|
|
1839
1854
|
? await readJsonFromLocation(publishEvidenceLocation)
|
|
1840
|
-
: resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
|
|
1855
|
+
: await resolveSiblingJson(basePath, passport.evidence?.publishEvidence) || {};
|
|
1841
1856
|
const impact =
|
|
1842
1857
|
impactLocation
|
|
1843
1858
|
? await readJsonFromLocation(impactLocation)
|
|
1844
|
-
: resolveSiblingJson(basePath, passport.evidence?.impact) || {};
|
|
1859
|
+
: await resolveSiblingJson(basePath, passport.evidence?.impact) || {};
|
|
1845
1860
|
const agentIndex =
|
|
1846
1861
|
agentIndexLocation
|
|
1847
1862
|
? await readJsonFromLocation(agentIndexLocation)
|
|
1848
|
-
: resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
|
|
1863
|
+
: await resolveSiblingJson(basePath, passport.evidence?.agentIndex) || {};
|
|
1849
1864
|
const productMechanism =
|
|
1850
1865
|
productMechanismLocation
|
|
1851
1866
|
? await readJsonFromLocation(productMechanismLocation)
|
|
1852
|
-
: resolveSiblingJson(basePath, passport.product?.mechanism) || {};
|
|
1867
|
+
: await resolveSiblingJson(basePath, passport.product?.mechanism) || {};
|
|
1853
1868
|
return createReleaseCheckReport({
|
|
1854
1869
|
passport,
|
|
1855
1870
|
artifactEvidence,
|
|
@@ -14,12 +14,15 @@ const requiredPaths = [
|
|
|
14
14
|
".github/ISSUE_TEMPLATE/config.yml",
|
|
15
15
|
".github/pull_request_template.md",
|
|
16
16
|
"bin/buildchain.mjs",
|
|
17
|
+
"packages/core/homebrew.js",
|
|
17
18
|
"docs/MAP.md",
|
|
18
19
|
"docs/binary-distribution.md",
|
|
19
20
|
"docs/cli.md",
|
|
20
21
|
"docs/consumer-issue-reporting.md",
|
|
22
|
+
"docs/homebrew.md",
|
|
21
23
|
"docs/install.md",
|
|
22
24
|
"docs/product-mechanism.md",
|
|
25
|
+
"docs/readme-badges.md",
|
|
23
26
|
"docs/release-passport.md",
|
|
24
27
|
"docs/release-propagation.md",
|
|
25
28
|
"docs/site-bundle-contract.md",
|
|
@@ -105,12 +108,18 @@ if (rootPackage.exports?.["."] !== "./packages/core/index.js") {
|
|
|
105
108
|
if (rootPackage.exports?.["./diagnostics"] !== "./packages/core/diagnostics.js") {
|
|
106
109
|
throw new Error("root package must export @kungfu-tech/buildchain/diagnostics");
|
|
107
110
|
}
|
|
111
|
+
if (rootPackage.exports?.["./homebrew"] !== "./packages/core/homebrew.js") {
|
|
112
|
+
throw new Error("root package must export @kungfu-tech/buildchain/homebrew");
|
|
113
|
+
}
|
|
108
114
|
if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildchain-contract.js") {
|
|
109
115
|
throw new Error("root package must export @kungfu-tech/buildchain/buildchain-contract");
|
|
110
116
|
}
|
|
111
117
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
112
118
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
113
119
|
}
|
|
120
|
+
if (rootPackage.exports?.["./readme-badges"] !== "./packages/core/readme-badges.js") {
|
|
121
|
+
throw new Error("root package must export @kungfu-tech/buildchain/readme-badges");
|
|
122
|
+
}
|
|
114
123
|
if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
|
|
115
124
|
throw new Error("root package must export @kungfu-tech/buildchain/logging");
|
|
116
125
|
}
|
|
@@ -199,6 +208,26 @@ if (!coreIndexSource.includes("KFD2_TRUST_PROOF_CONTRACT")) {
|
|
|
199
208
|
if (!coreIndexSource.includes("createSurfaceTimestampPolicy")) {
|
|
200
209
|
throw new Error("packages/core/index.js must export surface manifest timestamp policy APIs");
|
|
201
210
|
}
|
|
211
|
+
for (const requiredSnippet of [
|
|
212
|
+
"collectReadmeBadgeFacts",
|
|
213
|
+
"renderReadmeBadgeBlock",
|
|
214
|
+
"checkReadmeBadgeBlock",
|
|
215
|
+
"updateReadmeBadgeBlock",
|
|
216
|
+
]) {
|
|
217
|
+
if (!coreIndexSource.includes(requiredSnippet)) {
|
|
218
|
+
throw new Error(`packages/core/index.js must export README badge API: ${requiredSnippet}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const requiredSnippet of [
|
|
222
|
+
"collectHomebrewTapFacts",
|
|
223
|
+
"renderHomebrewFormula",
|
|
224
|
+
"checkHomebrewTap",
|
|
225
|
+
"updateHomebrewTap",
|
|
226
|
+
]) {
|
|
227
|
+
if (!coreIndexSource.includes(requiredSnippet)) {
|
|
228
|
+
throw new Error(`packages/core/index.js must export Homebrew API: ${requiredSnippet}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
202
231
|
if (siteBundle.contract !== "kungfu-buildchain-site-bundle") {
|
|
203
232
|
throw new Error("buildchain-site.json must expose the Buildchain site bundle contract");
|
|
204
233
|
}
|
|
@@ -374,13 +403,41 @@ for (const requiredSnippet of [
|
|
|
374
403
|
"npm publish transactions",
|
|
375
404
|
"GitHub Release",
|
|
376
405
|
"release propagation",
|
|
406
|
+
"Homebrew tap distribution indexes",
|
|
377
407
|
"manual-registry.json",
|
|
378
408
|
"node-api-registry.json",
|
|
409
|
+
"README badge",
|
|
379
410
|
]) {
|
|
380
411
|
if (!docsMap.includes(requiredSnippet)) {
|
|
381
412
|
throw new Error(`documentation map missing capability coverage snippet: ${requiredSnippet}`);
|
|
382
413
|
}
|
|
383
414
|
}
|
|
415
|
+
const readmeBadgesDoc = fs.readFileSync(path.join(root, "docs/readme-badges.md"), "utf8");
|
|
416
|
+
for (const requiredSnippet of [
|
|
417
|
+
"buildchain badges readme --check",
|
|
418
|
+
"collectReadmeBadgeFacts",
|
|
419
|
+
"kungfu-buildchain-readme-badge-facts",
|
|
420
|
+
"KFD passed",
|
|
421
|
+
"release passport",
|
|
422
|
+
"<!-- buildchain:badges:start -->",
|
|
423
|
+
]) {
|
|
424
|
+
if (!readmeBadgesDoc.includes(requiredSnippet)) {
|
|
425
|
+
throw new Error(`README badges doc missing required snippet: ${requiredSnippet}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const homebrewDoc = fs.readFileSync(path.join(root, "docs/homebrew.md"), "utf8");
|
|
429
|
+
for (const requiredSnippet of [
|
|
430
|
+
"project.type = \"distribution-index\"",
|
|
431
|
+
"buildchain homebrew update-formula",
|
|
432
|
+
"buildchain homebrew check",
|
|
433
|
+
"collectHomebrewTapFacts",
|
|
434
|
+
"kungfu-buildchain-homebrew-tap-manifest",
|
|
435
|
+
"KFD passed",
|
|
436
|
+
]) {
|
|
437
|
+
if (!homebrewDoc.includes(requiredSnippet)) {
|
|
438
|
+
throw new Error(`Homebrew doc missing required snippet: ${requiredSnippet}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
384
441
|
const reusableBuildSurfaceDoc = fs.readFileSync(path.join(root, "docs/reusable-build-surface.md"), "utf8");
|
|
385
442
|
for (const requiredSnippet of [
|
|
386
443
|
"Floating Ref Contract Lock",
|
|
@@ -403,6 +460,18 @@ for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs
|
|
|
403
460
|
}
|
|
404
461
|
}
|
|
405
462
|
}
|
|
463
|
+
for (const requiredSnippet of [
|
|
464
|
+
"buildchain badges readme --check",
|
|
465
|
+
"buildchain badges readme --write",
|
|
466
|
+
"@kungfu-tech/buildchain/readme-badges",
|
|
467
|
+
"buildchain homebrew update-formula",
|
|
468
|
+
"buildchain homebrew check",
|
|
469
|
+
"@kungfu-tech/buildchain/homebrew",
|
|
470
|
+
]) {
|
|
471
|
+
if (!cliDoc.includes(requiredSnippet)) {
|
|
472
|
+
throw new Error(`CLI doc missing README badge command snippet: ${requiredSnippet}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
406
475
|
const releaseLineDryRunScript = fs.readFileSync(path.join(root, "scripts/release-line-dry-run.mjs"), "utf8");
|
|
407
476
|
const standaloneBinaryScript = fs.readFileSync(path.join(root, "scripts/build-standalone-binary.mjs"), "utf8");
|
|
408
477
|
for (const requiredSnippet of [
|
|
@@ -293,6 +293,7 @@ function buildSiteBundle() {
|
|
|
293
293
|
{ id: "diagnostics-summary", usage: "buildchain diagnostics summary <diagnostics.json>...", purpose: "Summarize small diagnostics artifacts into JSON and a cross-platform lifecycle timing table." },
|
|
294
294
|
{ id: "npm-dry-run", usage: "buildchain npm dry-run --json", purpose: "Verify npm publish shape before a release transaction." },
|
|
295
295
|
{ id: "infra-contract", usage: "buildchain infra-contract --mode validate|ci|plan|contract|propagation-plan|propagation-apply|apply|evidence-bundle", purpose: "Validate and publish provider-neutral infrastructure contract evidence with a mutation-free CI evidence chain, provider command plans, configured provider command execution, saved-plan apply gates, dry-run-first propagation, and lifecycle evidence bundles." },
|
|
296
|
+
{ id: "homebrew", usage: "buildchain homebrew update-formula|check", purpose: "Generate and verify Homebrew tap Formula metadata as a distribution-index projection of upstream release passport evidence." },
|
|
296
297
|
],
|
|
297
298
|
};
|
|
298
299
|
|
|
@@ -317,6 +318,7 @@ function buildSiteBundle() {
|
|
|
317
318
|
"docs/release-governance.md",
|
|
318
319
|
"docs/release-passport.md",
|
|
319
320
|
"docs/publish-transaction.md",
|
|
321
|
+
"docs/homebrew.md",
|
|
320
322
|
"docs/site-bundle-contract.md",
|
|
321
323
|
"docs/runtime-train-validation.md",
|
|
322
324
|
"docs/consumer-issue-reporting.md",
|
|
@@ -356,6 +358,8 @@ function buildSiteBundle() {
|
|
|
356
358
|
})),
|
|
357
359
|
docs: [
|
|
358
360
|
{ id: "cli-and-node-package", path: "docs/cli.md", digest: sha256File("docs/cli.md") },
|
|
361
|
+
{ id: "readme-badges", path: "docs/readme-badges.md", digest: sha256File("docs/readme-badges.md") },
|
|
362
|
+
{ id: "homebrew", path: "docs/homebrew.md", digest: sha256File("docs/homebrew.md") },
|
|
359
363
|
{ id: "site-bundle-contract", path: "docs/site-bundle-contract.md", digest: sha256File("docs/site-bundle-contract.md") },
|
|
360
364
|
],
|
|
361
365
|
guidance: "These are the public Node import surfaces shipped by the npm package. Agents should prefer these exports over internal file paths.",
|
|
@@ -417,6 +421,14 @@ function buildSiteBundle() {
|
|
|
417
421
|
],
|
|
418
422
|
owner: "promote-buildchain-ref",
|
|
419
423
|
},
|
|
424
|
+
distributionIndexes: {
|
|
425
|
+
homebrewTap: {
|
|
426
|
+
projectType: "distribution-index",
|
|
427
|
+
manifest: "tap-manifest.json",
|
|
428
|
+
command: "buildchain homebrew check",
|
|
429
|
+
sourceOfTruth: "upstream release passport and sibling evidence",
|
|
430
|
+
},
|
|
431
|
+
},
|
|
420
432
|
};
|
|
421
433
|
|
|
422
434
|
const artifactSchemas = {
|