@kungfu-tech/buildchain 2.14.6 → 2.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/site/buildchain-contract.json +4 -4
- package/dist/site/buildchain-site.json +44 -8
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/kfd-claims.json +46 -7
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +2 -1
- package/dist/site/node-api-registry.json +1 -1
- package/dist/site/page-registry.json +39 -3
- package/dist/site/public-surface-audit.json +34 -4
- package/dist/site/publication-authority-registry.json +25 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +29 -0
- package/docs/observed-evidence-patrol.md +84 -0
- package/docs/release-governance.md +6 -0
- package/package.json +1 -1
- package/packages/core/buildchain-publication-authority.js +1 -0
- package/scripts/check-inventory.mjs +3 -0
- package/scripts/generate-site-bundle.mjs +3 -0
- package/scripts/observed-evidence.mjs +193 -0
- package/scripts/web-surface-core.mjs +33 -5
- package/scripts/web-surface.mjs +1 -0
|
@@ -66,10 +66,12 @@ const requiredPaths = [
|
|
|
66
66
|
"scripts/publication-package.mjs",
|
|
67
67
|
"scripts/release-candidate-resolver.mjs",
|
|
68
68
|
"scripts/buildchain-patrol.mjs",
|
|
69
|
+
"scripts/observed-evidence.mjs",
|
|
69
70
|
"scripts/workflow-friction-report.mjs",
|
|
70
71
|
"scripts/web-surface-production-release-pr.mjs",
|
|
71
72
|
"docs/migration-inventory.md",
|
|
72
73
|
"docs/lifecycle-protocol.md",
|
|
74
|
+
"docs/observed-evidence-patrol.md",
|
|
73
75
|
"docs/ownership.md",
|
|
74
76
|
"tests/buildchain-inventory.json",
|
|
75
77
|
".buildchain/buildchain.toml",
|
|
@@ -86,6 +88,7 @@ const requiredPaths = [
|
|
|
86
88
|
".github/workflows/patrol-daily.yml",
|
|
87
89
|
".github/workflows/patrol-weekly.yml",
|
|
88
90
|
".github/workflows/patrol-monthly.yml",
|
|
91
|
+
".github/workflows/patrol-observed-evidence.yml",
|
|
89
92
|
".github/workflows/buildchain-patrol-daily.yml",
|
|
90
93
|
".github/workflows/buildchain-patrol-weekly.yml",
|
|
91
94
|
".github/workflows/buildchain-patrol-monthly.yml",
|
|
@@ -328,6 +328,7 @@ const manualMetaById = new Map(Object.entries({
|
|
|
328
328
|
"publish-transaction": { capabilityGroup: "release-passport-trust", audience: ["release-operator"], maturity: "stable", order: 120 },
|
|
329
329
|
"release-candidate": { capabilityGroup: "reusable-build", audience: ["release-operator", "consumer"], maturity: "stable", order: 130 },
|
|
330
330
|
"stable-candidate-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer"], maturity: "preview", order: 135 },
|
|
331
|
+
"observed-evidence-patrol": { capabilityGroup: "governance-versioning", audience: ["release-operator", "consumer", "agent"], maturity: "preview", order: 140 },
|
|
331
332
|
"reusable-build-surface": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator"], maturity: "stable", order: 200 },
|
|
332
333
|
"lifecycle-protocol": { capabilityGroup: "reusable-build", audience: ["consumer", "developer"], maturity: "stable", order: 210 },
|
|
333
334
|
"runtime-train-validation": { capabilityGroup: "governance-versioning", audience: ["maintainer", "consumer"], maturity: "stable", order: 220 },
|
|
@@ -782,6 +783,7 @@ function buildSiteBundle() {
|
|
|
782
783
|
"docs/reusable-build-surface.md",
|
|
783
784
|
"docs/release-candidate.md",
|
|
784
785
|
"docs/stable-candidate-patrol.md",
|
|
786
|
+
"docs/observed-evidence-patrol.md",
|
|
785
787
|
"docs/release-governance.md",
|
|
786
788
|
"docs/release-passport.md",
|
|
787
789
|
"docs/controller-evidence.md",
|
|
@@ -870,6 +872,7 @@ function buildSiteBundle() {
|
|
|
870
872
|
["patrol-daily", "repository-patrol"],
|
|
871
873
|
["patrol-weekly", "repository-patrol"],
|
|
872
874
|
["patrol-monthly", "repository-patrol"],
|
|
875
|
+
["patrol-observed-evidence", "repository-patrol"],
|
|
873
876
|
]);
|
|
874
877
|
const statusById = new Map([
|
|
875
878
|
["release-propagation", "preview"],
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export const OBSERVED_EVIDENCE_CONTRACT = "kungfu-buildchain-observed-evidence-bundle";
|
|
8
|
+
|
|
9
|
+
function sha256(file) {
|
|
10
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeRelative(value, label) {
|
|
14
|
+
const normalized = String(value || "").replaceAll("\\", "/").replace(/^\/+/, "");
|
|
15
|
+
if (!normalized || normalized.split("/").includes("..")) throw new Error(`invalid ${label}: ${value || "<empty>"}`);
|
|
16
|
+
return normalized;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readJson(file) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function assertHex(value, label) {
|
|
24
|
+
if (!/^[0-9a-f]{64}$/.test(String(value || ""))) throw new Error(`${label} must be a lowercase sha256 hex digest`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function validateObservedEvidenceBundle({ manifestPath, artifactRoot = path.dirname(manifestPath) }) {
|
|
28
|
+
const resolvedManifest = path.resolve(manifestPath);
|
|
29
|
+
const root = path.resolve(artifactRoot);
|
|
30
|
+
const manifest = readJson(resolvedManifest);
|
|
31
|
+
if (manifest.schemaVersion !== 1 || manifest.contract !== OBSERVED_EVIDENCE_CONTRACT) {
|
|
32
|
+
throw new Error(`observed evidence manifest must be ${OBSERVED_EVIDENCE_CONTRACT} schemaVersion 1`);
|
|
33
|
+
}
|
|
34
|
+
const snapshotId = String(manifest.snapshot?.id || "");
|
|
35
|
+
if (!snapshotId || !/^[A-Za-z0-9._:+-]+$/.test(snapshotId)) throw new Error("snapshot.id is invalid");
|
|
36
|
+
if (!Number.isFinite(Date.parse(manifest.snapshot?.observedAt || ""))) throw new Error("snapshot.observedAt must be ISO-8601");
|
|
37
|
+
const immutable = manifest.publication?.immutable || {};
|
|
38
|
+
const latest = manifest.publication?.latest || {};
|
|
39
|
+
const entries = [["immutable", immutable], ["latest", latest]].map(([kind, entry]) => {
|
|
40
|
+
const source = safeRelative(entry.source, `${kind}.source`);
|
|
41
|
+
const key = safeRelative(entry.key, `${kind}.key`);
|
|
42
|
+
const file = path.resolve(root, source);
|
|
43
|
+
if (file !== root && !file.startsWith(`${root}${path.sep}`)) throw new Error(`${kind}.source escapes artifact root`);
|
|
44
|
+
if (!fs.statSync(file).isFile()) throw new Error(`${kind}.source is not a file: ${source}`);
|
|
45
|
+
assertHex(entry.sha256, `${kind}.sha256`);
|
|
46
|
+
const actual = sha256(file);
|
|
47
|
+
if (actual !== entry.sha256) throw new Error(`${kind}.sha256 does not match ${source}`);
|
|
48
|
+
const document = readJson(file);
|
|
49
|
+
if (document.snapshotId !== snapshotId) throw new Error(`${kind} document snapshotId does not match manifest`);
|
|
50
|
+
return { kind, source, key, file, sha256: actual, contentType: entry.contentType || "application/json" };
|
|
51
|
+
});
|
|
52
|
+
if (!entries[0].key.includes(`/${snapshotId}.json`)) throw new Error("immutable key must bind snapshot.id below a versioned path");
|
|
53
|
+
if (entries[0].key === entries[1].key) throw new Error("immutable and latest keys must differ");
|
|
54
|
+
const invalidationPaths = [...new Set(manifest.publication?.invalidationPaths || [])];
|
|
55
|
+
if (invalidationPaths.some((entry) => typeof entry !== "string" || !entry.startsWith("/") || entry.includes(".."))) {
|
|
56
|
+
throw new Error("invalidation paths must be absolute viewer paths without traversal");
|
|
57
|
+
}
|
|
58
|
+
return { manifest, manifestPath: resolvedManifest, artifactRoot: root, snapshotId, entries, invalidationPaths };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defaultRunner(args) {
|
|
62
|
+
const result = spawnSync("aws", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
63
|
+
return { status: result.status ?? 1, stdout: String(result.stdout || ""), stderr: String(result.stderr || result.error?.message || "") };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function jsonOutput(result) {
|
|
67
|
+
try { return JSON.parse(result.stdout || "{}"); } catch { return {}; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function headObject(runner, bucket, key) {
|
|
71
|
+
return runner(["s3api", "head-object", "--bucket", bucket, "--key", key]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function headEvidence(result) {
|
|
75
|
+
if (result.status !== 0) return null;
|
|
76
|
+
const value = jsonOutput(result);
|
|
77
|
+
return {
|
|
78
|
+
sha256: String(value.Metadata?.sha256 || ""),
|
|
79
|
+
snapshotId: String(value.Metadata?.["snapshot-id"] || ""),
|
|
80
|
+
etag: String(value.ETag || "").replaceAll('"', ""),
|
|
81
|
+
versionId: String(value.VersionId || ""),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function base64Digest(hex) {
|
|
86
|
+
return Buffer.from(hex, "hex").toString("base64");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function putArgs({ bucket, entry, snapshotId, immutable }) {
|
|
90
|
+
const args = [
|
|
91
|
+
"s3api", "put-object", "--bucket", bucket, "--key", entry.key, "--body", entry.file,
|
|
92
|
+
"--content-type", entry.contentType,
|
|
93
|
+
"--cache-control", immutable ? "public,max-age=31536000,immutable" : "public,max-age=0,must-revalidate",
|
|
94
|
+
"--checksum-sha256", base64Digest(entry.sha256),
|
|
95
|
+
"--metadata", `snapshot-id=${snapshotId},sha256=${entry.sha256}`,
|
|
96
|
+
];
|
|
97
|
+
if (immutable) args.push("--if-none-match", "*");
|
|
98
|
+
return args;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function assertHeadMatches(head, entry, snapshotId, label) {
|
|
102
|
+
if (!head || head.sha256 !== entry.sha256 || head.snapshotId !== snapshotId) {
|
|
103
|
+
throw new Error(`${label} object does not match the admitted snapshot and sha256`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function publishObservedEvidence(options, { commandRunner = defaultRunner } = {}) {
|
|
108
|
+
const bundle = validateObservedEvidenceBundle(options);
|
|
109
|
+
const bucket = String(options.bucket || "").trim();
|
|
110
|
+
if (!bucket) throw new Error("bucket is required");
|
|
111
|
+
const distributionId = String(options.distributionId || "").trim();
|
|
112
|
+
const dryRun = options.dryRun !== false;
|
|
113
|
+
const [immutable, latest] = bundle.entries;
|
|
114
|
+
const receipt = {
|
|
115
|
+
schemaVersion: 1,
|
|
116
|
+
contract: "kungfu-buildchain-observed-evidence-publication-receipt",
|
|
117
|
+
status: dryRun ? "planned" : "applying",
|
|
118
|
+
dryRun,
|
|
119
|
+
snapshotId: bundle.snapshotId,
|
|
120
|
+
observedAt: bundle.manifest.snapshot.observedAt,
|
|
121
|
+
bucket,
|
|
122
|
+
distributionId,
|
|
123
|
+
immutable: { key: immutable.key, sha256: immutable.sha256, status: "planned" },
|
|
124
|
+
latest: { key: latest.key, sha256: latest.sha256, status: "planned" },
|
|
125
|
+
previousLatest: null,
|
|
126
|
+
invalidationPaths: bundle.invalidationPaths,
|
|
127
|
+
operations: [],
|
|
128
|
+
};
|
|
129
|
+
if (dryRun) return receipt;
|
|
130
|
+
|
|
131
|
+
const beforeImmutable = headEvidence(headObject(commandRunner, bucket, immutable.key));
|
|
132
|
+
if (beforeImmutable) {
|
|
133
|
+
assertHeadMatches(beforeImmutable, immutable, bundle.snapshotId, "existing immutable");
|
|
134
|
+
receipt.immutable.status = "reused";
|
|
135
|
+
} else {
|
|
136
|
+
const put = commandRunner(putArgs({ bucket, entry: immutable, snapshotId: bundle.snapshotId, immutable: true }));
|
|
137
|
+
receipt.operations.push({ action: "put-immutable", status: put.status });
|
|
138
|
+
if (put.status !== 0) {
|
|
139
|
+
const concurrent = headEvidence(headObject(commandRunner, bucket, immutable.key));
|
|
140
|
+
assertHeadMatches(concurrent, immutable, bundle.snapshotId, "concurrent immutable");
|
|
141
|
+
receipt.immutable.status = "reused-concurrent";
|
|
142
|
+
} else {
|
|
143
|
+
receipt.immutable.status = "written";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const verifiedImmutable = headEvidence(headObject(commandRunner, bucket, immutable.key));
|
|
147
|
+
assertHeadMatches(verifiedImmutable, immutable, bundle.snapshotId, "verified immutable");
|
|
148
|
+
receipt.immutable.verification = verifiedImmutable;
|
|
149
|
+
|
|
150
|
+
receipt.previousLatest = headEvidence(headObject(commandRunner, bucket, latest.key));
|
|
151
|
+
const latestPut = commandRunner(putArgs({ bucket, entry: latest, snapshotId: bundle.snapshotId, immutable: false }));
|
|
152
|
+
receipt.operations.push({ action: "advance-latest", status: latestPut.status });
|
|
153
|
+
if (latestPut.status !== 0) throw new Error(`latest alias update failed: ${latestPut.stderr || latestPut.stdout}`);
|
|
154
|
+
const verifiedLatest = headEvidence(headObject(commandRunner, bucket, latest.key));
|
|
155
|
+
assertHeadMatches(verifiedLatest, latest, bundle.snapshotId, "verified latest");
|
|
156
|
+
receipt.latest = { ...receipt.latest, status: "written", verification: verifiedLatest };
|
|
157
|
+
|
|
158
|
+
if (distributionId && bundle.invalidationPaths.length > 0) {
|
|
159
|
+
const invalidation = commandRunner([
|
|
160
|
+
"cloudfront", "create-invalidation", "--distribution-id", distributionId,
|
|
161
|
+
"--paths", ...bundle.invalidationPaths,
|
|
162
|
+
]);
|
|
163
|
+
receipt.operations.push({ action: "invalidate-cdn", status: invalidation.status, result: jsonOutput(invalidation) });
|
|
164
|
+
if (invalidation.status !== 0) throw new Error(`CloudFront invalidation failed: ${invalidation.stderr || invalidation.stdout}`);
|
|
165
|
+
}
|
|
166
|
+
receipt.status = "published";
|
|
167
|
+
receipt.publishedAt = new Date().toISOString();
|
|
168
|
+
return receipt;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function arg(name, fallback = "") {
|
|
172
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
173
|
+
return index === -1 ? fallback : process.argv[index + 1] || fallback;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
177
|
+
try {
|
|
178
|
+
const output = arg("output");
|
|
179
|
+
const result = publishObservedEvidence({
|
|
180
|
+
manifestPath: arg("manifest"),
|
|
181
|
+
artifactRoot: arg("artifact-root", path.dirname(arg("manifest"))),
|
|
182
|
+
bucket: arg("bucket"),
|
|
183
|
+
distributionId: arg("distribution-id"),
|
|
184
|
+
dryRun: arg("execute", "false") !== "true",
|
|
185
|
+
});
|
|
186
|
+
const json = `${JSON.stringify(result, null, 2)}\n`;
|
|
187
|
+
if (output) { fs.mkdirSync(path.dirname(path.resolve(output)), { recursive: true }); fs.writeFileSync(output, json); }
|
|
188
|
+
else process.stdout.write(json);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(error.stack || error.message);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -285,6 +285,7 @@ function syncStaticArtifactArgs({ artifactRoot, bucket, objectPrefix, deleteExcl
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
const PUBLICATION_ARCHIVE_POLICY_CONTRACT = "kungfu-buildchain-publication-archive-policy";
|
|
288
|
+
const OBSERVED_EVIDENCE_OWNERSHIP_CONTRACT = "kungfu-buildchain-observed-evidence-ownership";
|
|
288
289
|
|
|
289
290
|
function immutablePrefix(value, label) {
|
|
290
291
|
const raw = String(value || "").trim().replaceAll("\\", "/");
|
|
@@ -343,12 +344,37 @@ function publicationImmutablePolicy({ artifactRoot, binding }) {
|
|
|
343
344
|
};
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
function observedEvidenceOwnershipPolicy({ artifactRoot, binding }) {
|
|
348
|
+
const surfaceRoot = surfaceArtifactRootFor({ artifactRoot, binding });
|
|
349
|
+
const policyFile = path.join(surfaceRoot, ".buildchain", "observed-evidence-ownership.json");
|
|
350
|
+
if (!fs.existsSync(policyFile)) return null;
|
|
351
|
+
let policy;
|
|
352
|
+
try {
|
|
353
|
+
policy = JSON.parse(fs.readFileSync(policyFile, "utf8"));
|
|
354
|
+
} catch (error) {
|
|
355
|
+
throw new Error(`invalid observed evidence ownership policy: ${error.message}`);
|
|
356
|
+
}
|
|
357
|
+
if (policy?.schemaVersion !== 1 || policy?.contract !== OBSERVED_EVIDENCE_OWNERSHIP_CONTRACT) {
|
|
358
|
+
throw new Error(`observed evidence ownership policy must be ${OBSERVED_EVIDENCE_OWNERSHIP_CONTRACT} schemaVersion 1`);
|
|
359
|
+
}
|
|
360
|
+
const paths = [...new Set(policy.paths || [])].map((entry) => String(entry).replaceAll("\\", "/").replace(/^\/+/, ""));
|
|
361
|
+
if (paths.length === 0 || paths.some((entry) => !entry || entry.split("/").includes("..") || entry === "*")) {
|
|
362
|
+
throw new Error("observed evidence ownership policy requires bounded relative paths");
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
contract: OBSERVED_EVIDENCE_OWNERSHIP_CONTRACT,
|
|
366
|
+
policyPath: toPosix(path.relative(artifactRoot, policyFile)),
|
|
367
|
+
paths: paths.sort(),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
346
371
|
function withImmutablePublicationPolicies(bindings, { cwd, artifactPath }) {
|
|
347
372
|
const artifactRoot = path.resolve(cwd, artifactPath);
|
|
348
373
|
const withOwnPolicies = bindings.map((binding) => {
|
|
349
374
|
const immutablePublication = publicationImmutablePolicy({ artifactRoot, binding });
|
|
350
|
-
|
|
351
|
-
|
|
375
|
+
const observedEvidenceOwnership = observedEvidenceOwnershipPolicy({ artifactRoot, binding });
|
|
376
|
+
if (!immutablePublication && !observedEvidenceOwnership) return binding;
|
|
377
|
+
return { ...binding, ...(immutablePublication ? { immutablePublication } : {}), ...(observedEvidenceOwnership ? { observedEvidenceOwnership } : {}) };
|
|
352
378
|
});
|
|
353
379
|
const protectedRoots = withOwnPolicies.flatMap((binding) =>
|
|
354
380
|
(binding.immutablePublication?.preservedRoots || []).map((root) => ({
|
|
@@ -358,15 +384,17 @@ function withImmutablePublicationPolicies(bindings, { cwd, artifactPath }) {
|
|
|
358
384
|
);
|
|
359
385
|
const withDeleteExcludes = withOwnPolicies.map((binding) => {
|
|
360
386
|
const bindingPrefix = normalizeS3Key(binding.artifactPathPrefix);
|
|
361
|
-
const mutableDeleteExcludes =
|
|
387
|
+
const mutableDeleteExcludes = [...new Set([
|
|
388
|
+
...protectedRoots
|
|
362
389
|
.map((protectedRoot) => {
|
|
363
390
|
if (!bindingPrefix) return `${protectedRoot.path}/*`;
|
|
364
391
|
if (protectedRoot.path === bindingPrefix) return "*";
|
|
365
392
|
if (!protectedRoot.path.startsWith(`${bindingPrefix}/`)) return "";
|
|
366
393
|
return `${protectedRoot.path.slice(bindingPrefix.length + 1)}/*`;
|
|
367
394
|
})
|
|
368
|
-
.filter(Boolean)
|
|
369
|
-
.
|
|
395
|
+
.filter(Boolean),
|
|
396
|
+
...(binding.observedEvidenceOwnership?.paths || []),
|
|
397
|
+
])].sort();
|
|
370
398
|
return mutableDeleteExcludes.length > 0
|
|
371
399
|
? { ...binding, mutableDeleteExcludes }
|
|
372
400
|
: binding;
|
package/scripts/web-surface.mjs
CHANGED
|
@@ -220,6 +220,7 @@ export function compactWebSurfaceApplyResult(result = {}) {
|
|
|
220
220
|
accessControl: binding.accessControl || "",
|
|
221
221
|
healthStrategy: binding.healthStrategy || "",
|
|
222
222
|
mutableDeleteExcludes: binding.mutableDeleteExcludes || [],
|
|
223
|
+
observedEvidenceOwnership: binding.observedEvidenceOwnership || null,
|
|
223
224
|
}))
|
|
224
225
|
: [],
|
|
225
226
|
};
|