@kungfu-tech/buildchain 2.8.15 → 2.8.16-alpha.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/README.md +19 -11
- package/bin/buildchain.mjs +65 -0
- package/dist/site/buildchain-contract.json +45 -10
- package/dist/site/buildchain-site.json +66 -17
- package/dist/site/kfd-claims.json +45 -3
- package/dist/site/manual-registry.json +9 -2
- package/dist/site/node-api-registry.json +17 -6
- package/dist/site/page-registry.json +50 -9
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +14 -6
- package/docs/MAP.md +4 -0
- package/docs/cli.md +21 -3
- package/docs/readme-badges.md +124 -0
- package/package.json +2 -1
- package/packages/core/README.md +12 -0
- package/packages/core/buildchain-contract.js +26 -0
- package/packages/core/buildchain-kfd-claims.js +19 -0
- package/packages/core/index.js +11 -0
- package/packages/core/readme-badges.js +450 -0
- package/packages/core/release-passport.js +10 -1
- package/scripts/check-inventory.mjs +37 -0
- package/scripts/generate-site-bundle.mjs +1 -0
|
@@ -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
|
+
}
|
|
@@ -1789,13 +1789,22 @@ export function createReleaseCheckReport({
|
|
|
1789
1789
|
};
|
|
1790
1790
|
}
|
|
1791
1791
|
|
|
1792
|
-
export async function readJsonFromLocation(location) {
|
|
1792
|
+
export async function readJsonFromLocation(location, redirectCount = 0) {
|
|
1793
1793
|
const input = nonEmptyString(location, "location");
|
|
1794
|
+
if (redirectCount > 5) {
|
|
1795
|
+
throw new Error(`too many redirects while reading ${input}`);
|
|
1796
|
+
}
|
|
1794
1797
|
if (/^https?:\/\//.test(input)) {
|
|
1795
1798
|
const client = input.startsWith("https:") ? https : http;
|
|
1796
1799
|
return new Promise((resolve, reject) => {
|
|
1797
1800
|
client
|
|
1798
1801
|
.get(input, (response) => {
|
|
1802
|
+
if ([301, 302, 303, 307, 308].includes(response.statusCode) && response.headers.location) {
|
|
1803
|
+
const nextLocation = new URL(response.headers.location, input).toString();
|
|
1804
|
+
response.resume();
|
|
1805
|
+
readJsonFromLocation(nextLocation, redirectCount + 1).then(resolve, reject);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1799
1808
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
1800
1809
|
reject(new Error(`HTTP ${response.statusCode} while reading ${input}`));
|
|
1801
1810
|
response.resume();
|
|
@@ -20,6 +20,7 @@ const requiredPaths = [
|
|
|
20
20
|
"docs/consumer-issue-reporting.md",
|
|
21
21
|
"docs/install.md",
|
|
22
22
|
"docs/product-mechanism.md",
|
|
23
|
+
"docs/readme-badges.md",
|
|
23
24
|
"docs/release-passport.md",
|
|
24
25
|
"docs/release-propagation.md",
|
|
25
26
|
"docs/site-bundle-contract.md",
|
|
@@ -111,6 +112,9 @@ if (rootPackage.exports?.["./buildchain-contract"] !== "./packages/core/buildcha
|
|
|
111
112
|
if (rootPackage.exports?.["./issue-reporting"] !== "./packages/core/issue-reporting.js") {
|
|
112
113
|
throw new Error("root package must export @kungfu-tech/buildchain/issue-reporting");
|
|
113
114
|
}
|
|
115
|
+
if (rootPackage.exports?.["./readme-badges"] !== "./packages/core/readme-badges.js") {
|
|
116
|
+
throw new Error("root package must export @kungfu-tech/buildchain/readme-badges");
|
|
117
|
+
}
|
|
114
118
|
if (rootPackage.exports?.["./logging"] !== "./packages/core/logging.js") {
|
|
115
119
|
throw new Error("root package must export @kungfu-tech/buildchain/logging");
|
|
116
120
|
}
|
|
@@ -199,6 +203,16 @@ if (!coreIndexSource.includes("KFD2_TRUST_PROOF_CONTRACT")) {
|
|
|
199
203
|
if (!coreIndexSource.includes("createSurfaceTimestampPolicy")) {
|
|
200
204
|
throw new Error("packages/core/index.js must export surface manifest timestamp policy APIs");
|
|
201
205
|
}
|
|
206
|
+
for (const requiredSnippet of [
|
|
207
|
+
"collectReadmeBadgeFacts",
|
|
208
|
+
"renderReadmeBadgeBlock",
|
|
209
|
+
"checkReadmeBadgeBlock",
|
|
210
|
+
"updateReadmeBadgeBlock",
|
|
211
|
+
]) {
|
|
212
|
+
if (!coreIndexSource.includes(requiredSnippet)) {
|
|
213
|
+
throw new Error(`packages/core/index.js must export README badge API: ${requiredSnippet}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
202
216
|
if (siteBundle.contract !== "kungfu-buildchain-site-bundle") {
|
|
203
217
|
throw new Error("buildchain-site.json must expose the Buildchain site bundle contract");
|
|
204
218
|
}
|
|
@@ -376,11 +390,25 @@ for (const requiredSnippet of [
|
|
|
376
390
|
"release propagation",
|
|
377
391
|
"manual-registry.json",
|
|
378
392
|
"node-api-registry.json",
|
|
393
|
+
"README badge",
|
|
379
394
|
]) {
|
|
380
395
|
if (!docsMap.includes(requiredSnippet)) {
|
|
381
396
|
throw new Error(`documentation map missing capability coverage snippet: ${requiredSnippet}`);
|
|
382
397
|
}
|
|
383
398
|
}
|
|
399
|
+
const readmeBadgesDoc = fs.readFileSync(path.join(root, "docs/readme-badges.md"), "utf8");
|
|
400
|
+
for (const requiredSnippet of [
|
|
401
|
+
"buildchain badges readme --check",
|
|
402
|
+
"collectReadmeBadgeFacts",
|
|
403
|
+
"kungfu-buildchain-readme-badge-facts",
|
|
404
|
+
"KFD passed",
|
|
405
|
+
"release passport",
|
|
406
|
+
"<!-- buildchain:badges:start -->",
|
|
407
|
+
]) {
|
|
408
|
+
if (!readmeBadgesDoc.includes(requiredSnippet)) {
|
|
409
|
+
throw new Error(`README badges doc missing required snippet: ${requiredSnippet}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
384
412
|
const reusableBuildSurfaceDoc = fs.readFileSync(path.join(root, "docs/reusable-build-surface.md"), "utf8");
|
|
385
413
|
for (const requiredSnippet of [
|
|
386
414
|
"Floating Ref Contract Lock",
|
|
@@ -403,6 +431,15 @@ for (const [docName, docSource] of Object.entries({ "docs/cli.md": cliDoc, "docs
|
|
|
403
431
|
}
|
|
404
432
|
}
|
|
405
433
|
}
|
|
434
|
+
for (const requiredSnippet of [
|
|
435
|
+
"buildchain badges readme --check",
|
|
436
|
+
"buildchain badges readme --write",
|
|
437
|
+
"@kungfu-tech/buildchain/readme-badges",
|
|
438
|
+
]) {
|
|
439
|
+
if (!cliDoc.includes(requiredSnippet)) {
|
|
440
|
+
throw new Error(`CLI doc missing README badge command snippet: ${requiredSnippet}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
406
443
|
const releaseLineDryRunScript = fs.readFileSync(path.join(root, "scripts/release-line-dry-run.mjs"), "utf8");
|
|
407
444
|
const standaloneBinaryScript = fs.readFileSync(path.join(root, "scripts/build-standalone-binary.mjs"), "utf8");
|
|
408
445
|
for (const requiredSnippet of [
|
|
@@ -356,6 +356,7 @@ function buildSiteBundle() {
|
|
|
356
356
|
})),
|
|
357
357
|
docs: [
|
|
358
358
|
{ id: "cli-and-node-package", path: "docs/cli.md", digest: sha256File("docs/cli.md") },
|
|
359
|
+
{ id: "readme-badges", path: "docs/readme-badges.md", digest: sha256File("docs/readme-badges.md") },
|
|
359
360
|
{ id: "site-bundle-contract", path: "docs/site-bundle-contract.md", digest: sha256File("docs/site-bundle-contract.md") },
|
|
360
361
|
],
|
|
361
362
|
guidance: "These are the public Node import surfaces shipped by the npm package. Agents should prefer these exports over internal file paths.",
|