@kungfu-tech/buildchain 2.14.0 → 2.14.1-alpha.0
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 +54 -0
- package/dist/site/buildchain-contract.json +5 -5
- package/dist/site/buildchain-site.json +10 -10
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/cli-registry.json +12 -0
- package/dist/site/kfd-claims.json +31 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -2
- package/dist/site/node-api-registry.json +16 -3
- package/dist/site/page-registry.json +4 -4
- package/dist/site/public-surface-audit.json +24 -4
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +1 -0
- package/dist/site/site-manifest.json +6 -6
- package/docs/MAP.md +1 -0
- package/docs/cli.md +37 -0
- package/package.json +2 -1
- package/packages/core/index.js +6 -0
- package/packages/core/portable-dev-cache.js +288 -0
- package/scripts/generate-site-bundle.mjs +4 -0
package/packages/core/index.js
CHANGED
|
@@ -51,6 +51,12 @@ export {
|
|
|
51
51
|
writeReleaseTransaction,
|
|
52
52
|
} from "./publish-transaction.js";
|
|
53
53
|
|
|
54
|
+
export {
|
|
55
|
+
createPortableDevCachePlan,
|
|
56
|
+
createPortableDevCacheReceipt,
|
|
57
|
+
verifyPortableDevCachePlan,
|
|
58
|
+
} from "./portable-dev-cache.js";
|
|
59
|
+
|
|
54
60
|
export {
|
|
55
61
|
explainReleaseLineDryRun,
|
|
56
62
|
formatReleaseLineDryRun,
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DIGEST_RE = /^sha256:[0-9a-f]{64}$/;
|
|
4
|
+
const SOURCE_RE = /^[0-9a-f]{40,64}$/;
|
|
5
|
+
const SAFE_PATH_RE = /^(?:~\/|[A-Za-z0-9._-]+\/)[A-Za-z0-9._/+-]+$/;
|
|
6
|
+
const LAYERS = new Set(["dependency", "compiler"]);
|
|
7
|
+
|
|
8
|
+
function assert(condition, message) {
|
|
9
|
+
if (!condition) throw new Error(message);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ordered(value) {
|
|
13
|
+
if (Array.isArray(value)) return value.map(ordered);
|
|
14
|
+
if (value && typeof value === "object") {
|
|
15
|
+
return Object.fromEntries(
|
|
16
|
+
Object.keys(value)
|
|
17
|
+
.sort()
|
|
18
|
+
.map((key) => [key, ordered(value[key])]),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function stableJson(value) {
|
|
25
|
+
return JSON.stringify(ordered(value));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function digest(value) {
|
|
29
|
+
return `sha256:${crypto
|
|
30
|
+
.createHash("sha256")
|
|
31
|
+
.update(typeof value === "string" ? value : stableJson(value))
|
|
32
|
+
.digest("hex")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function shortDigest(value) {
|
|
36
|
+
return value.replace(/^sha256:/, "").slice(0, 24);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function exactKeys(value, allowed, label) {
|
|
40
|
+
assert(
|
|
41
|
+
value && typeof value === "object" && !Array.isArray(value),
|
|
42
|
+
`${label} must be an object`,
|
|
43
|
+
);
|
|
44
|
+
for (const key of Object.keys(value))
|
|
45
|
+
assert(allowed.has(key), `${label}.${key} is not allowed`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function checkedText(value, label) {
|
|
49
|
+
assert(
|
|
50
|
+
typeof value === "string" && value.trim() === value && value.length > 0,
|
|
51
|
+
`${label} is required`,
|
|
52
|
+
);
|
|
53
|
+
assert(!/[\r\n\0]/.test(value), `${label} contains control characters`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function checkedDigest(value, label) {
|
|
58
|
+
assert(DIGEST_RE.test(value), `${label} must be a sha256 digest`);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeRoots(roots) {
|
|
63
|
+
assert(
|
|
64
|
+
Array.isArray(roots) && roots.length > 0 && roots.length <= 8,
|
|
65
|
+
"roots must contain 1-8 entries",
|
|
66
|
+
);
|
|
67
|
+
const ids = new Set();
|
|
68
|
+
const paths = new Set();
|
|
69
|
+
return roots
|
|
70
|
+
.map((root, index) => {
|
|
71
|
+
exactKeys(root, new Set(["id", "path"]), `roots[${index}]`);
|
|
72
|
+
const id = checkedText(root.id, `roots[${index}].id`);
|
|
73
|
+
assert(
|
|
74
|
+
/^[a-z0-9][a-z0-9-]{0,31}$/.test(id),
|
|
75
|
+
`roots[${index}].id is invalid`,
|
|
76
|
+
);
|
|
77
|
+
const normalizedPath = checkedText(
|
|
78
|
+
root.path,
|
|
79
|
+
`roots[${index}].path`,
|
|
80
|
+
).replaceAll("\\", "/");
|
|
81
|
+
assert(
|
|
82
|
+
SAFE_PATH_RE.test(normalizedPath),
|
|
83
|
+
`roots[${index}].path must be workspace-relative or start with ~/`,
|
|
84
|
+
);
|
|
85
|
+
assert(
|
|
86
|
+
!normalizedPath.split("/").includes(".."),
|
|
87
|
+
`roots[${index}].path cannot escape its root`,
|
|
88
|
+
);
|
|
89
|
+
assert(!ids.has(id), `duplicate root id: ${id}`);
|
|
90
|
+
assert(
|
|
91
|
+
!paths.has(normalizedPath),
|
|
92
|
+
`duplicate cache root: ${normalizedPath}`,
|
|
93
|
+
);
|
|
94
|
+
ids.add(id);
|
|
95
|
+
paths.add(normalizedPath);
|
|
96
|
+
return { id, path: normalizedPath };
|
|
97
|
+
})
|
|
98
|
+
.sort((left, right) => left.id.localeCompare(right.id));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeManifest(manifest) {
|
|
102
|
+
exactKeys(
|
|
103
|
+
manifest,
|
|
104
|
+
new Set(["schema", "layer", "roots", "identity"]),
|
|
105
|
+
"manifest",
|
|
106
|
+
);
|
|
107
|
+
assert(
|
|
108
|
+
manifest.schema === "buildchain.portable-dev-cache-manifest/v1",
|
|
109
|
+
"unsupported portable dev cache manifest schema",
|
|
110
|
+
);
|
|
111
|
+
assert(LAYERS.has(manifest.layer), "layer must be dependency or compiler");
|
|
112
|
+
exactKeys(
|
|
113
|
+
manifest.identity,
|
|
114
|
+
new Set([
|
|
115
|
+
"platform",
|
|
116
|
+
"arch",
|
|
117
|
+
"runnerImage",
|
|
118
|
+
"toolchainDigest",
|
|
119
|
+
"dependencyLockDigest",
|
|
120
|
+
"profileDigest",
|
|
121
|
+
"sourceSha",
|
|
122
|
+
"planDigest",
|
|
123
|
+
]),
|
|
124
|
+
"manifest.identity",
|
|
125
|
+
);
|
|
126
|
+
const identity = {
|
|
127
|
+
platform: checkedText(
|
|
128
|
+
manifest.identity.platform,
|
|
129
|
+
"manifest.identity.platform",
|
|
130
|
+
).toLowerCase(),
|
|
131
|
+
arch: checkedText(
|
|
132
|
+
manifest.identity.arch,
|
|
133
|
+
"manifest.identity.arch",
|
|
134
|
+
).toLowerCase(),
|
|
135
|
+
runnerImage: checkedText(
|
|
136
|
+
manifest.identity.runnerImage,
|
|
137
|
+
"manifest.identity.runnerImage",
|
|
138
|
+
),
|
|
139
|
+
toolchainDigest: checkedDigest(
|
|
140
|
+
manifest.identity.toolchainDigest,
|
|
141
|
+
"manifest.identity.toolchainDigest",
|
|
142
|
+
),
|
|
143
|
+
dependencyLockDigest: checkedDigest(
|
|
144
|
+
manifest.identity.dependencyLockDigest,
|
|
145
|
+
"manifest.identity.dependencyLockDigest",
|
|
146
|
+
),
|
|
147
|
+
profileDigest: checkedDigest(
|
|
148
|
+
manifest.identity.profileDigest,
|
|
149
|
+
"manifest.identity.profileDigest",
|
|
150
|
+
),
|
|
151
|
+
sourceSha: checkedText(
|
|
152
|
+
manifest.identity.sourceSha,
|
|
153
|
+
"manifest.identity.sourceSha",
|
|
154
|
+
).toLowerCase(),
|
|
155
|
+
planDigest: checkedDigest(
|
|
156
|
+
manifest.identity.planDigest,
|
|
157
|
+
"manifest.identity.planDigest",
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
assert(
|
|
161
|
+
SOURCE_RE.test(identity.sourceSha),
|
|
162
|
+
"manifest.identity.sourceSha must be a 40-64 character Git SHA",
|
|
163
|
+
);
|
|
164
|
+
return {
|
|
165
|
+
schema: manifest.schema,
|
|
166
|
+
layer: manifest.layer,
|
|
167
|
+
roots: normalizeRoots(manifest.roots),
|
|
168
|
+
identity,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function createPortableDevCachePlan(manifest) {
|
|
173
|
+
const normalized = normalizeManifest(manifest);
|
|
174
|
+
const compatibility = {
|
|
175
|
+
schema: normalized.schema,
|
|
176
|
+
layer: normalized.layer,
|
|
177
|
+
roots: normalized.roots,
|
|
178
|
+
platform: normalized.identity.platform,
|
|
179
|
+
arch: normalized.identity.arch,
|
|
180
|
+
runnerImage: normalized.identity.runnerImage,
|
|
181
|
+
toolchainDigest: normalized.identity.toolchainDigest,
|
|
182
|
+
dependencyLockDigest: normalized.identity.dependencyLockDigest,
|
|
183
|
+
profileDigest: normalized.identity.profileDigest,
|
|
184
|
+
};
|
|
185
|
+
const compatibilityDigest = digest(compatibility);
|
|
186
|
+
const exactRootDigest = digest({
|
|
187
|
+
compatibilityDigest,
|
|
188
|
+
sourceSha: normalized.identity.sourceSha,
|
|
189
|
+
planDigest: normalized.identity.planDigest,
|
|
190
|
+
});
|
|
191
|
+
const prefix = [
|
|
192
|
+
"buildchain-pdc-v1",
|
|
193
|
+
normalized.layer,
|
|
194
|
+
normalized.identity.platform.replace(/[^a-z0-9_-]+/g, "-"),
|
|
195
|
+
normalized.identity.arch.replace(/[^a-z0-9_-]+/g, "-"),
|
|
196
|
+
shortDigest(compatibilityDigest),
|
|
197
|
+
].join("-");
|
|
198
|
+
const plan = {
|
|
199
|
+
schema: "buildchain.portable-dev-cache-plan/v1",
|
|
200
|
+
provider: "github-actions-cache",
|
|
201
|
+
manifest: normalized,
|
|
202
|
+
compatibilityDigest,
|
|
203
|
+
exactRootDigest,
|
|
204
|
+
key: `${prefix}-${shortDigest(exactRootDigest)}`,
|
|
205
|
+
restoreKeys: [`${prefix}-`],
|
|
206
|
+
paths: normalized.roots.map(({ path }) => path),
|
|
207
|
+
};
|
|
208
|
+
return { ...plan, planDigest: digest(plan) };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function verifyPortableDevCachePlan(plan) {
|
|
212
|
+
assert(
|
|
213
|
+
plan?.schema === "buildchain.portable-dev-cache-plan/v1",
|
|
214
|
+
"unsupported portable dev cache plan schema",
|
|
215
|
+
);
|
|
216
|
+
const { planDigest, ...body } = plan;
|
|
217
|
+
assert(planDigest === digest(body), "portable dev cache plan digest drift");
|
|
218
|
+
const rebuilt = createPortableDevCachePlan(plan.manifest);
|
|
219
|
+
assert(
|
|
220
|
+
stableJson(rebuilt) === stableJson(plan),
|
|
221
|
+
"portable dev cache plan does not match its manifest",
|
|
222
|
+
);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function createPortableDevCacheReceipt({
|
|
227
|
+
plan,
|
|
228
|
+
matchedKey = "",
|
|
229
|
+
cacheHit = "",
|
|
230
|
+
validationStatus = "pass",
|
|
231
|
+
validationReason = "",
|
|
232
|
+
coldFallbackStatus = "not-run",
|
|
233
|
+
}) {
|
|
234
|
+
verifyPortableDevCachePlan(plan);
|
|
235
|
+
assert(
|
|
236
|
+
["", "true", "false"].includes(String(cacheHit)),
|
|
237
|
+
"cacheHit must be empty, true, or false",
|
|
238
|
+
);
|
|
239
|
+
assert(
|
|
240
|
+
["pass", "fail"].includes(validationStatus),
|
|
241
|
+
"validationStatus must be pass or fail",
|
|
242
|
+
);
|
|
243
|
+
assert(
|
|
244
|
+
["not-run", "passed", "failed"].includes(coldFallbackStatus),
|
|
245
|
+
"coldFallbackStatus must be not-run, passed, or failed",
|
|
246
|
+
);
|
|
247
|
+
let outcome = "miss";
|
|
248
|
+
if (matchedKey === plan.key && String(cacheHit) === "true") outcome = "exact";
|
|
249
|
+
else if (
|
|
250
|
+
matchedKey &&
|
|
251
|
+
plan.restoreKeys.some((prefix) => matchedKey.startsWith(prefix)) &&
|
|
252
|
+
String(cacheHit) !== "true"
|
|
253
|
+
)
|
|
254
|
+
outcome = "compatible";
|
|
255
|
+
else if (matchedKey)
|
|
256
|
+
throw new Error("matched cache key is outside the portable plan authority");
|
|
257
|
+
else if (String(cacheHit) === "true")
|
|
258
|
+
throw new Error("cacheHit=true requires the exact planned key");
|
|
259
|
+
if (validationStatus === "fail") outcome = "corrupt";
|
|
260
|
+
const cacheUsable =
|
|
261
|
+
validationStatus === "pass" && ["exact", "compatible"].includes(outcome);
|
|
262
|
+
const coldFallbackRequired = outcome === "miss" || outcome === "corrupt";
|
|
263
|
+
if (!coldFallbackRequired && coldFallbackStatus !== "not-run") {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"cold fallback evidence is only valid for miss or corrupt outcomes",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
const receipt = {
|
|
269
|
+
schema: "buildchain.portable-dev-cache-receipt/v1",
|
|
270
|
+
provider: plan.provider,
|
|
271
|
+
planDigest: plan.planDigest,
|
|
272
|
+
exactRootDigest: plan.exactRootDigest,
|
|
273
|
+
compatibilityDigest: plan.compatibilityDigest,
|
|
274
|
+
sourceSha: plan.manifest.identity.sourceSha,
|
|
275
|
+
planRootDigest: plan.manifest.identity.planDigest,
|
|
276
|
+
layer: plan.manifest.layer,
|
|
277
|
+
outcome,
|
|
278
|
+
usable: cacheUsable,
|
|
279
|
+
coldFallbackRequired,
|
|
280
|
+
coldFallbackStatus,
|
|
281
|
+
qualified:
|
|
282
|
+
cacheUsable ||
|
|
283
|
+
(coldFallbackRequired && coldFallbackStatus === "passed"),
|
|
284
|
+
matchedKey: matchedKey || null,
|
|
285
|
+
validation: { status: validationStatus, reason: validationReason || null },
|
|
286
|
+
};
|
|
287
|
+
return { ...receipt, receiptDigest: digest(receipt) };
|
|
288
|
+
}
|
|
@@ -441,6 +441,9 @@ function cliCommandMeta(id) {
|
|
|
441
441
|
logging: { group: "observability-diagnostics", purpose: "Emit timestamped build events, summarize logs, and enforce required phases." },
|
|
442
442
|
mark: { group: "observability-diagnostics", purpose: "Emit a single Buildchain log event." },
|
|
443
443
|
npm: { group: "release-passport-trust", purpose: "Inspect npm publishing command families." },
|
|
444
|
+
"portable-cache": { group: "observability-diagnostics", purpose: "Plan exact portable dependency/compiler cache inputs and seal provider outcomes." },
|
|
445
|
+
"portable-cache-plan": { group: "observability-diagnostics", purpose: "Validate a consumer-neutral cache manifest and emit exact GitHub Actions cache inputs." },
|
|
446
|
+
"portable-cache-receipt": { group: "observability-diagnostics", purpose: "Seal exact, compatible, miss, or corrupt provider evidence against one cache plan." },
|
|
444
447
|
"npm-dry-run": { group: "release-passport-trust", purpose: "Verify npm publish shape before a release transaction." },
|
|
445
448
|
"publish-source": { group: "release-passport-trust", purpose: "Create, inspect, or verify publish-gate source-lock refs." },
|
|
446
449
|
"publication-artifact": { group: "reusable-build", purpose: "Generate publication artifact manifests, passports, and source bundles for paper/report repositories." },
|
|
@@ -484,6 +487,7 @@ function nodeApiMeta(exportName) {
|
|
|
484
487
|
"./build-facts": { group: "observability-diagnostics", summary: "Git source, version, module output, product artifact, and legacy Kungfu build fact APIs." },
|
|
485
488
|
"./diagnostics": { group: "observability-diagnostics", summary: "Native diagnostics collection, summarization, cache, compiler, and process-sampler APIs." },
|
|
486
489
|
"./logging": { group: "observability-diagnostics", summary: "Buildchain JSONL logging, span, summary, and verification APIs." },
|
|
490
|
+
"./portable-dev-cache": { group: "observability-diagnostics", summary: "Portable dependency/compiler cache plan, exact-root verification, and provider receipt APIs." },
|
|
487
491
|
"./publication-artifact": { group: "reusable-build", summary: "Publication artifact manifest, source bundle, and publication passport APIs." },
|
|
488
492
|
"./publication-package": { group: "reusable-build", summary: "Publication npm package synthesis APIs for Buildchain-managed paper release presets." },
|
|
489
493
|
"./publication-authority": { group: "release-passport-trust", summary: "Sealed publication authority registry, runner provenance, control-plane audit, admission, and independent verification APIs." },
|