@kungfu-tech/buildchain 4.0.1-alpha.5 → 4.0.1-alpha.6
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/actions/release-tail/README.md +8 -0
- package/architecture/internal-capabilities.json +65 -5
- package/architecture/maintainability-policy.json +22 -16
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/bin/buildchain.mjs +7 -1
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +1 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/archive-template.json +19 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/bootstrap-positive.json +93 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-positive.json +29 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/gate-unknown-selector.json +26 -0
- package/contracts/fixtures/v4-adopter-delivery-v1/offline-vectors.json +50 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/artifacts/product.bin +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/config/buildchain.toml +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/release-activation.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/documents/signed-channel.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/evidence/qualification.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/candidate.json +5 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/candidate/manifests/release-passport.json +1 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/capsule.json +707 -0
- package/contracts/fixtures/v4-publication-rehearsal-v1/offline-vectors.json +27 -0
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/agent-index.json +3 -0
- package/dist/site/artifact-schemas.json +6 -0
- package/dist/site/buildchain-contract.json +14 -14
- package/dist/site/buildchain-site.json +219 -22
- package/dist/site/capability-registry.json +10 -9
- package/dist/site/cli-registry.json +53 -1
- package/dist/site/kfd-claims.json +253 -14
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +20 -5
- package/dist/site/node-api-registry.json +11501 -9312
- package/dist/site/page-registry.json +202 -13
- package/dist/site/public-surface-audit.json +195 -11
- package/dist/site/publication-authority-registry.json +17 -4
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +14 -0
- package/dist/site/schemas/release-tail-capabilities-v1.schema.json +199 -0
- package/dist/site/schemas/v4-adopter-delivery-v1.schema.json +81 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +17 -9
- package/dist/site/workflow-registry.json +89 -8
- package/docs/MAP.md +3 -1
- package/docs/cli-reference.md +68 -0
- package/docs/cli.md +8 -0
- package/docs/node-api-reference.md +198 -51
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/package.json +17 -3
- package/packages/core/adopter-delivery-passport.js +259 -0
- package/packages/core/adopter-delivery-vectors.js +158 -0
- package/packages/core/adopter-delivery-vectors.json +77 -0
- package/packages/core/buildchain-agent-manuals.js +1 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-delivery-bootstrap.js +240 -0
- package/packages/core/buildchain-delivery-infrastructure.js +164 -0
- package/packages/core/buildchain-delivery-self-dogfood.js +395 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/published-delivery-authority.js +266 -0
- package/packages/core/release-passport.js +26 -8
- package/packages/core/v4-adopter-delivery-parity.js +3 -3
- package/packages/core/v4-adopter-delivery.js +359 -0
- package/packages/core/v4-publication-rehearsal-capsule.js +468 -0
- package/packages/core/v4-publication-rehearsal-provider-bindings.js +212 -0
- package/packages/core/v4-publication-rehearsal.js +481 -0
- package/scripts/buildchain-cli-help.mjs +10 -0
- package/scripts/check-inventory.mjs +3 -3
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +30 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +545 -15
- package/scripts/site-capability-metadata.mjs +16 -0
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
executeReleaseTailTransaction,
|
|
7
|
+
releaseTailRoot,
|
|
8
|
+
validateReleaseTailTransaction,
|
|
9
|
+
} from "./release-tail-provider-plane.js";
|
|
10
|
+
import {
|
|
11
|
+
V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
12
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
13
|
+
V4_PUBLICATION_REHEARSAL_CORE_VERSION,
|
|
14
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
15
|
+
V4_PUBLICATION_REHEARSAL_PROVIDER_POLICY_CONTRACT,
|
|
16
|
+
V4PublicationRehearsalFault,
|
|
17
|
+
assertPublicationRehearsalExactKeys as exactKeys,
|
|
18
|
+
createV4PublicationRehearsalCapsule,
|
|
19
|
+
createV4PublicationRehearsalProviderPolicy,
|
|
20
|
+
publicationRehearsalFault as fault,
|
|
21
|
+
publicationRehearsalRoot,
|
|
22
|
+
validateV4PublicationRehearsalCapsule,
|
|
23
|
+
validateV4PublicationRehearsalProviderBindings,
|
|
24
|
+
} from "./v4-publication-rehearsal-capsule.js";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
28
|
+
V4_PUBLICATION_REHEARSAL_CAPSULE_CONTRACT,
|
|
29
|
+
V4_PUBLICATION_REHEARSAL_CORE_VERSION,
|
|
30
|
+
V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
31
|
+
V4_PUBLICATION_REHEARSAL_PROVIDER_POLICY_CONTRACT,
|
|
32
|
+
V4PublicationRehearsalFault,
|
|
33
|
+
createV4PublicationRehearsalCapsule,
|
|
34
|
+
createV4PublicationRehearsalProviderPolicy,
|
|
35
|
+
validateV4PublicationRehearsalCapsule,
|
|
36
|
+
validateV4PublicationRehearsalProviderBindings,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const ROOT = /^sha256:[0-9a-f]{64}$/u;
|
|
40
|
+
const MODES = new Set(["simulate", "replay", "provider"]);
|
|
41
|
+
|
|
42
|
+
function fileRoot(filePath) {
|
|
43
|
+
const hash = crypto.createHash("sha256");
|
|
44
|
+
const descriptor = fs.openSync(filePath, "r");
|
|
45
|
+
const buffer = Buffer.allocUnsafe(8 * 1024 * 1024);
|
|
46
|
+
try {
|
|
47
|
+
for (;;) {
|
|
48
|
+
const count = fs.readSync(descriptor, buffer, 0, buffer.length, null);
|
|
49
|
+
if (count === 0) break;
|
|
50
|
+
hash.update(buffer.subarray(0, count));
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
fs.closeSync(descriptor);
|
|
54
|
+
}
|
|
55
|
+
return `sha256:${hash.digest("hex")}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveCandidateFile(candidateRootPath, relative, location) {
|
|
59
|
+
const base = fs.realpathSync(candidateRootPath);
|
|
60
|
+
const resolved = path.resolve(base, relative);
|
|
61
|
+
if (!resolved.startsWith(`${base}${path.sep}`))
|
|
62
|
+
fault(
|
|
63
|
+
"publication-rehearsal-path-escape",
|
|
64
|
+
location,
|
|
65
|
+
"candidate path escapes the explicit root",
|
|
66
|
+
);
|
|
67
|
+
if (
|
|
68
|
+
!fs.existsSync(resolved) ||
|
|
69
|
+
!fs.statSync(resolved).isFile() ||
|
|
70
|
+
fs.lstatSync(resolved).isSymbolicLink() ||
|
|
71
|
+
!fs.realpathSync(resolved).startsWith(`${base}${path.sep}`)
|
|
72
|
+
)
|
|
73
|
+
fault(
|
|
74
|
+
"publication-rehearsal-file-missing",
|
|
75
|
+
location,
|
|
76
|
+
"regular in-root file required",
|
|
77
|
+
);
|
|
78
|
+
return resolved;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveCandidateOutput(candidateRootPath, relative, location) {
|
|
82
|
+
const base = fs.realpathSync(candidateRootPath);
|
|
83
|
+
const resolved = path.resolve(base, relative);
|
|
84
|
+
if (!resolved.startsWith(`${base}${path.sep}`))
|
|
85
|
+
fault(
|
|
86
|
+
"publication-rehearsal-path-escape",
|
|
87
|
+
location,
|
|
88
|
+
"candidate output escapes the explicit root",
|
|
89
|
+
);
|
|
90
|
+
for (
|
|
91
|
+
let current = path.dirname(resolved);
|
|
92
|
+
current !== base;
|
|
93
|
+
current = path.dirname(current)
|
|
94
|
+
) {
|
|
95
|
+
if (!fs.existsSync(current)) continue;
|
|
96
|
+
if (
|
|
97
|
+
fs.lstatSync(current).isSymbolicLink() ||
|
|
98
|
+
!fs.statSync(current).isDirectory() ||
|
|
99
|
+
!fs.realpathSync(current).startsWith(`${base}${path.sep}`)
|
|
100
|
+
)
|
|
101
|
+
fault(
|
|
102
|
+
"publication-rehearsal-path-escape",
|
|
103
|
+
location,
|
|
104
|
+
"candidate output ancestor is not a regular in-root directory",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (
|
|
108
|
+
fs.existsSync(resolved) &&
|
|
109
|
+
(fs.lstatSync(resolved).isSymbolicLink() ||
|
|
110
|
+
!fs.statSync(resolved).isFile() ||
|
|
111
|
+
!fs.realpathSync(resolved).startsWith(`${base}${path.sep}`))
|
|
112
|
+
)
|
|
113
|
+
fault(
|
|
114
|
+
"publication-rehearsal-path-escape",
|
|
115
|
+
location,
|
|
116
|
+
"candidate output is not a regular in-root file",
|
|
117
|
+
);
|
|
118
|
+
return resolved;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function verifyV4PublicationRehearsalCapsule({
|
|
122
|
+
capsule,
|
|
123
|
+
candidateRoot: candidateRootPath,
|
|
124
|
+
}) {
|
|
125
|
+
const normalized = validateV4PublicationRehearsalCapsule(capsule);
|
|
126
|
+
if (!path.isAbsolute(candidateRootPath || ""))
|
|
127
|
+
fault(
|
|
128
|
+
"publication-rehearsal-implicit-root-forbidden",
|
|
129
|
+
"$/candidateRoot",
|
|
130
|
+
"explicit absolute candidate root required",
|
|
131
|
+
);
|
|
132
|
+
if (
|
|
133
|
+
!fs.existsSync(candidateRootPath) ||
|
|
134
|
+
!fs.statSync(candidateRootPath).isDirectory()
|
|
135
|
+
)
|
|
136
|
+
fault(
|
|
137
|
+
"publication-rehearsal-candidate-missing",
|
|
138
|
+
"$/candidateRoot",
|
|
139
|
+
"candidate directory required",
|
|
140
|
+
);
|
|
141
|
+
for (const entry of normalized.files) {
|
|
142
|
+
const resolved = resolveCandidateFile(
|
|
143
|
+
candidateRootPath,
|
|
144
|
+
entry.path,
|
|
145
|
+
`$/files/${entry.path}`,
|
|
146
|
+
);
|
|
147
|
+
if (
|
|
148
|
+
fs.statSync(resolved).size !== entry.size ||
|
|
149
|
+
fileRoot(resolved) !== entry.root
|
|
150
|
+
)
|
|
151
|
+
fault(
|
|
152
|
+
"publication-rehearsal-file-tampered",
|
|
153
|
+
`$/files/${entry.path}`,
|
|
154
|
+
"candidate bytes do not match the capsule",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return normalized;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveV4PublicationRehearsalProviderBindings({
|
|
161
|
+
capsule,
|
|
162
|
+
candidateRoot: candidateRootPath,
|
|
163
|
+
providerBindings = capsule?.providerBindings,
|
|
164
|
+
}) {
|
|
165
|
+
const normalized = verifyV4PublicationRehearsalCapsule({
|
|
166
|
+
capsule,
|
|
167
|
+
candidateRoot: candidateRootPath,
|
|
168
|
+
});
|
|
169
|
+
const supplied =
|
|
170
|
+
validateV4PublicationRehearsalProviderBindings(providerBindings);
|
|
171
|
+
if (
|
|
172
|
+
releaseTailRoot(supplied) !== normalized.providerBindingsRoot ||
|
|
173
|
+
JSON.stringify(supplied) !== JSON.stringify(normalized.providerBindings)
|
|
174
|
+
)
|
|
175
|
+
fault(
|
|
176
|
+
"publication-rehearsal-provider-bindings-root-mismatch",
|
|
177
|
+
"$/providerBindings",
|
|
178
|
+
"external provider bindings do not match the capsule",
|
|
179
|
+
);
|
|
180
|
+
return {
|
|
181
|
+
schema: supplied.schema,
|
|
182
|
+
artifacts: Object.fromEntries(
|
|
183
|
+
Object.entries(supplied.artifacts).map(([role, binding]) => [
|
|
184
|
+
role,
|
|
185
|
+
{
|
|
186
|
+
...binding,
|
|
187
|
+
path: resolveCandidateFile(
|
|
188
|
+
candidateRootPath,
|
|
189
|
+
binding.path,
|
|
190
|
+
`$/providerBindings/artifacts/${role}/path`,
|
|
191
|
+
),
|
|
192
|
+
},
|
|
193
|
+
]),
|
|
194
|
+
),
|
|
195
|
+
documents: Object.fromEntries(
|
|
196
|
+
Object.entries(supplied.documents).map(([capabilityId, binding]) => [
|
|
197
|
+
capabilityId,
|
|
198
|
+
{
|
|
199
|
+
...binding,
|
|
200
|
+
path: resolveCandidateFile(
|
|
201
|
+
candidateRootPath,
|
|
202
|
+
binding.path,
|
|
203
|
+
`$/providerBindings/documents/${capabilityId}/path`,
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
]),
|
|
207
|
+
),
|
|
208
|
+
evidence: {
|
|
209
|
+
inputs: supplied.evidence.inputs.map((inputPath, index) =>
|
|
210
|
+
resolveCandidateFile(
|
|
211
|
+
candidateRootPath,
|
|
212
|
+
inputPath,
|
|
213
|
+
`$/providerBindings/evidence/inputs/${index}`,
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
output: resolveCandidateOutput(
|
|
217
|
+
candidateRootPath,
|
|
218
|
+
supplied.evidence.output,
|
|
219
|
+
"$/providerBindings/evidence/output",
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function simulatedAdapters(declaration, transcript) {
|
|
226
|
+
const applied = new Set();
|
|
227
|
+
return Object.fromEntries(
|
|
228
|
+
declaration.capabilities.map((capability) => [
|
|
229
|
+
capability.adapter,
|
|
230
|
+
{
|
|
231
|
+
async readback(effect) {
|
|
232
|
+
const response = applied.has(effect.operationId)
|
|
233
|
+
? {
|
|
234
|
+
outcome: "observed",
|
|
235
|
+
subjectRoot: effect.subjectRoot,
|
|
236
|
+
targetRoot: effect.targetRoot,
|
|
237
|
+
evidenceRoots: [effect.targetRoot],
|
|
238
|
+
providerCode: "simulation-observed",
|
|
239
|
+
}
|
|
240
|
+
: {
|
|
241
|
+
outcome: "absent",
|
|
242
|
+
subjectRoot: "",
|
|
243
|
+
targetRoot: "",
|
|
244
|
+
evidenceRoots: [],
|
|
245
|
+
providerCode: "simulation-absent",
|
|
246
|
+
};
|
|
247
|
+
transcript.push({
|
|
248
|
+
operationId: effect.operationId,
|
|
249
|
+
method: "readback",
|
|
250
|
+
requestRoot: effect.effectRoot,
|
|
251
|
+
response,
|
|
252
|
+
});
|
|
253
|
+
return response;
|
|
254
|
+
},
|
|
255
|
+
async apply(effect) {
|
|
256
|
+
applied.add(effect.operationId);
|
|
257
|
+
transcript.push({
|
|
258
|
+
operationId: effect.operationId,
|
|
259
|
+
method: "apply",
|
|
260
|
+
requestRoot: effect.effectRoot,
|
|
261
|
+
response: { outcome: "simulated", code: "effect-disabled" },
|
|
262
|
+
});
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
]),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function observationAdapters(capsule, transcript, adapters, mode) {
|
|
270
|
+
const records = new Map(
|
|
271
|
+
capsule.expectedObservations.entries.map((entry) => [
|
|
272
|
+
entry.operationId,
|
|
273
|
+
{ ...entry, index: 0 },
|
|
274
|
+
]),
|
|
275
|
+
);
|
|
276
|
+
return Object.fromEntries(
|
|
277
|
+
capsule.declaration.capabilities.map((capability) => {
|
|
278
|
+
const provided = adapters?.[capability.adapter];
|
|
279
|
+
if (
|
|
280
|
+
mode === "provider" &&
|
|
281
|
+
(!provided ||
|
|
282
|
+
typeof provided.readback !== "function" ||
|
|
283
|
+
typeof provided.apply !== "function")
|
|
284
|
+
)
|
|
285
|
+
fault(
|
|
286
|
+
"publication-rehearsal-provider-adapter-missing",
|
|
287
|
+
"$/adapters",
|
|
288
|
+
`adapter missing: ${capability.adapter}`,
|
|
289
|
+
);
|
|
290
|
+
return [
|
|
291
|
+
capability.adapter,
|
|
292
|
+
{
|
|
293
|
+
async readback(effect) {
|
|
294
|
+
const record = records.get(effect.operationId);
|
|
295
|
+
const expected = record?.readbacks[record.index++];
|
|
296
|
+
if (!expected)
|
|
297
|
+
fault(
|
|
298
|
+
"publication-rehearsal-observation-exhausted",
|
|
299
|
+
"$/expectedObservations",
|
|
300
|
+
`readback missing for ${effect.operationId}`,
|
|
301
|
+
);
|
|
302
|
+
const response =
|
|
303
|
+
mode === "replay"
|
|
304
|
+
? structuredClone(expected)
|
|
305
|
+
: await provided.readback(effect);
|
|
306
|
+
if (JSON.stringify(response) !== JSON.stringify(expected))
|
|
307
|
+
fault(
|
|
308
|
+
"publication-rehearsal-provider-readback-mismatch",
|
|
309
|
+
"$/expectedObservations",
|
|
310
|
+
`provider readback drifted for ${effect.operationId}`,
|
|
311
|
+
);
|
|
312
|
+
transcript.push({
|
|
313
|
+
operationId: effect.operationId,
|
|
314
|
+
method: "readback",
|
|
315
|
+
requestRoot: effect.effectRoot,
|
|
316
|
+
response: structuredClone(response),
|
|
317
|
+
});
|
|
318
|
+
return response;
|
|
319
|
+
},
|
|
320
|
+
async apply(effect) {
|
|
321
|
+
const record = records.get(effect.operationId);
|
|
322
|
+
if (!record)
|
|
323
|
+
fault(
|
|
324
|
+
"publication-rehearsal-observation-exhausted",
|
|
325
|
+
"$/expectedObservations",
|
|
326
|
+
`apply missing for ${effect.operationId}`,
|
|
327
|
+
);
|
|
328
|
+
if (mode === "provider") await provided.apply(effect);
|
|
329
|
+
transcript.push({
|
|
330
|
+
operationId: effect.operationId,
|
|
331
|
+
method: "apply",
|
|
332
|
+
requestRoot: effect.effectRoot,
|
|
333
|
+
response: structuredClone(record.apply),
|
|
334
|
+
});
|
|
335
|
+
if (record.apply.outcome !== "applied") {
|
|
336
|
+
const error = new Error(
|
|
337
|
+
"expected provider apply did not succeed",
|
|
338
|
+
);
|
|
339
|
+
error.releaseTailCode = record.apply.code;
|
|
340
|
+
error.releaseTailClass = record.apply.classification;
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
];
|
|
346
|
+
}),
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function createV4PublicationRehearsalAuthority(
|
|
351
|
+
capsule,
|
|
352
|
+
{ authorizationRoot } = {},
|
|
353
|
+
) {
|
|
354
|
+
const normalized = validateV4PublicationRehearsalCapsule(capsule);
|
|
355
|
+
const body = {
|
|
356
|
+
schema: V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT,
|
|
357
|
+
mode: "provider-rehearsal",
|
|
358
|
+
capsuleRoot: normalized.capsuleRoot,
|
|
359
|
+
providerPolicyRoot: normalized.providerPolicy.root,
|
|
360
|
+
providerBindingsRoot: normalized.providerBindingsRoot,
|
|
361
|
+
authorizationRoot: publicationRehearsalRoot(
|
|
362
|
+
authorizationRoot,
|
|
363
|
+
"$/authorizationRoot",
|
|
364
|
+
),
|
|
365
|
+
productionAuthority: false,
|
|
366
|
+
};
|
|
367
|
+
return { ...body, authorityRoot: releaseTailRoot(body) };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function validateAuthority(value, capsule) {
|
|
371
|
+
exactKeys(
|
|
372
|
+
value,
|
|
373
|
+
[
|
|
374
|
+
"schema",
|
|
375
|
+
"mode",
|
|
376
|
+
"capsuleRoot",
|
|
377
|
+
"providerPolicyRoot",
|
|
378
|
+
"providerBindingsRoot",
|
|
379
|
+
"authorizationRoot",
|
|
380
|
+
"productionAuthority",
|
|
381
|
+
"authorityRoot",
|
|
382
|
+
],
|
|
383
|
+
"$/authority",
|
|
384
|
+
);
|
|
385
|
+
const body = { ...value };
|
|
386
|
+
delete body.authorityRoot;
|
|
387
|
+
if (
|
|
388
|
+
value.schema !== V4_PUBLICATION_REHEARSAL_AUTHORITY_CONTRACT ||
|
|
389
|
+
value.mode !== "provider-rehearsal" ||
|
|
390
|
+
value.productionAuthority !== false ||
|
|
391
|
+
value.capsuleRoot !== capsule.capsuleRoot ||
|
|
392
|
+
value.providerPolicyRoot !== capsule.providerPolicy.root ||
|
|
393
|
+
value.providerBindingsRoot !== capsule.providerBindingsRoot ||
|
|
394
|
+
!ROOT.test(value.authorizationRoot) ||
|
|
395
|
+
value.authorityRoot !== releaseTailRoot(body)
|
|
396
|
+
)
|
|
397
|
+
fault(
|
|
398
|
+
"publication-rehearsal-authority-mismatch",
|
|
399
|
+
"$/authority",
|
|
400
|
+
"exact rehearsal-only authority required",
|
|
401
|
+
);
|
|
402
|
+
return structuredClone(value);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function executeV4PublicationRehearsal({
|
|
406
|
+
capsule,
|
|
407
|
+
candidateRoot: candidateRootPath,
|
|
408
|
+
mode = "simulate",
|
|
409
|
+
adapters = {},
|
|
410
|
+
authority = null,
|
|
411
|
+
transaction = null,
|
|
412
|
+
checkpoint,
|
|
413
|
+
} = {}) {
|
|
414
|
+
if (!MODES.has(mode))
|
|
415
|
+
fault(
|
|
416
|
+
"invalid-publication-rehearsal-mode",
|
|
417
|
+
"$/mode",
|
|
418
|
+
"simulate, replay, or provider required",
|
|
419
|
+
);
|
|
420
|
+
const normalized = verifyV4PublicationRehearsalCapsule({
|
|
421
|
+
capsule,
|
|
422
|
+
candidateRoot: candidateRootPath,
|
|
423
|
+
});
|
|
424
|
+
const providerAuthority =
|
|
425
|
+
mode === "provider" ? validateAuthority(authority, normalized) : null;
|
|
426
|
+
const current = transaction
|
|
427
|
+
? structuredClone(transaction)
|
|
428
|
+
: structuredClone(normalized.transaction);
|
|
429
|
+
const validation = validateReleaseTailTransaction(current);
|
|
430
|
+
if (
|
|
431
|
+
!validation.valid ||
|
|
432
|
+
current.transactionRoot !== normalized.transaction.transactionRoot ||
|
|
433
|
+
current.declarationRoot !== normalized.transaction.declarationRoot ||
|
|
434
|
+
current.planRoot !== normalized.transaction.planRoot
|
|
435
|
+
)
|
|
436
|
+
fault(
|
|
437
|
+
"publication-rehearsal-transaction-mismatch",
|
|
438
|
+
"$/transaction",
|
|
439
|
+
"transaction is not bound to this capsule",
|
|
440
|
+
);
|
|
441
|
+
const transcript = [];
|
|
442
|
+
const selectedAdapters =
|
|
443
|
+
mode === "simulate"
|
|
444
|
+
? simulatedAdapters(normalized.declaration, transcript)
|
|
445
|
+
: observationAdapters(normalized, transcript, adapters, mode);
|
|
446
|
+
const result = await executeReleaseTailTransaction(current, {
|
|
447
|
+
adapters: selectedAdapters,
|
|
448
|
+
checkpoint,
|
|
449
|
+
});
|
|
450
|
+
const body = {
|
|
451
|
+
schema: V4_PUBLICATION_REHEARSAL_EVIDENCE_CONTRACT,
|
|
452
|
+
mode,
|
|
453
|
+
truth:
|
|
454
|
+
mode === "provider"
|
|
455
|
+
? "provider-observed"
|
|
456
|
+
: mode === "replay"
|
|
457
|
+
? "recorded-replay"
|
|
458
|
+
: "simulation-only",
|
|
459
|
+
capsuleRoot: normalized.capsuleRoot,
|
|
460
|
+
sourceRoot: normalized.source.root,
|
|
461
|
+
candidateRoot: normalized.candidate.root,
|
|
462
|
+
manifestRoot: normalized.manifest.root,
|
|
463
|
+
configRoot: normalized.config.root,
|
|
464
|
+
providerBindingsRoot: normalized.providerBindingsRoot,
|
|
465
|
+
providerPolicyRoot: normalized.providerPolicy.root,
|
|
466
|
+
expectedObservationRoot: normalized.expectedObservations.root,
|
|
467
|
+
coreVersion: normalized.coreVersion,
|
|
468
|
+
transactionRoot: result.transactionRoot,
|
|
469
|
+
stateRoot: result.stateRoot,
|
|
470
|
+
receiptRoots: result.receipts.map(({ receiptRoot }) => receiptRoot),
|
|
471
|
+
transcript,
|
|
472
|
+
transcriptRoot: releaseTailRoot(transcript),
|
|
473
|
+
authorityRoot: providerAuthority?.authorityRoot || null,
|
|
474
|
+
productionAuthority: false,
|
|
475
|
+
releasePassport: null,
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
transaction: result,
|
|
479
|
+
evidence: { ...body, evidenceRoot: releaseTailRoot(body) },
|
|
480
|
+
};
|
|
481
|
+
}
|
|
@@ -8,6 +8,11 @@ export const BUILDCHAIN_USAGE = `Usage:
|
|
|
8
8
|
buildchain architecture qualify --authority-revision <git-revision>
|
|
9
9
|
[--candidate-revision <git-revision>]
|
|
10
10
|
[--cwd <dir>] [--json]
|
|
11
|
+
buildchain adopter-delivery run --input <json-or-path> [--output <path>]
|
|
12
|
+
buildchain adopter-delivery verify --input <json-or-path>
|
|
13
|
+
--readback <json-or-path> [--output <path>]
|
|
14
|
+
buildchain adopter-delivery bootstrap --input <json-or-path> [--output <path>]
|
|
15
|
+
buildchain adopter-delivery archive --input <json-or-path> [--output <path>]
|
|
11
16
|
buildchain portable-cache plan --manifest <file-or-json> [--output <file>]
|
|
12
17
|
[--github-output <file>] [--json]
|
|
13
18
|
buildchain portable-cache receipt --plan <file-or-json> [--matched-key <key>]
|
|
@@ -44,6 +49,11 @@ export const BUILDCHAIN_USAGE = `Usage:
|
|
|
44
49
|
buildchain release-tail init --declaration <json-or-path> [--state <path>]
|
|
45
50
|
buildchain release-tail <status|verify> [--state <path>] [--output <path>]
|
|
46
51
|
buildchain release-tail compat --hooks-json <json-or-path> [--output <path>]
|
|
52
|
+
buildchain release-tail rehearse --capsule <absolute-path>
|
|
53
|
+
--candidate-root <absolute-path>
|
|
54
|
+
--mode <simulate|replay>
|
|
55
|
+
--state <absolute-path>
|
|
56
|
+
--evidence <absolute-path>
|
|
47
57
|
buildchain tail-reseal plan --request <request.json> [--output <plan.json>]
|
|
48
58
|
buildchain tail-reseal admit --request <request.json> [--output <admission.json>]
|
|
49
59
|
buildchain tail-reseal verify-platform --request <request.json> --platform <id>
|
|
@@ -1021,16 +1021,16 @@ for (const forbiddenSnippet of [
|
|
|
1021
1021
|
for (const requiredSnippet of [
|
|
1022
1022
|
"id-token: write",
|
|
1023
1023
|
"actions: write",
|
|
1024
|
-
"uses:
|
|
1024
|
+
"uses: kungfu-systems/buildchain/.github/workflows/.release-candidate-promote.yml@alpha/v4/v4.0",
|
|
1025
1025
|
"github.event.workflow_run.event == 'push'",
|
|
1026
1026
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
|
|
1027
1027
|
"!startsWith(github.event.workflow_run.display_title, 'chore(release): release v')",
|
|
1028
1028
|
"resume-candidate-run-id:",
|
|
1029
1029
|
"resume-expected-source-tree:",
|
|
1030
1030
|
"resume-buildchain-runtime-sha:",
|
|
1031
|
-
"github-
|
|
1031
|
+
"uses: kungfu-systems/buildchain/.github/workflows/release-candidate-promote.yml@v4",
|
|
1032
1032
|
"release-passport-buildchain-self-kfd: true",
|
|
1033
|
-
"
|
|
1033
|
+
"node .buildchain/router/scripts/promotion-identity-resolver.mjs",
|
|
1034
1034
|
"release-passport-impact-json: .buildchain/release-impact.json",
|
|
1035
1035
|
]) {
|
|
1036
1036
|
if (!buildchainRefPromotionWorkflow.includes(requiredSnippet)) {
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
const SHA = /^[0-9a-f]{40}$/u;
|
|
6
|
+
|
|
7
|
+
function exactSha(value, label) {
|
|
8
|
+
const normalized = String(value || "")
|
|
9
|
+
.trim()
|
|
10
|
+
.toLowerCase();
|
|
11
|
+
if (!SHA.test(normalized))
|
|
12
|
+
throw new Error(`${label} must be an exact 40-hex Git SHA`);
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function git(args, { cwd, encoding = "utf8" }) {
|
|
17
|
+
const result = spawnSync("git", args, {
|
|
18
|
+
cwd,
|
|
19
|
+
encoding,
|
|
20
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
21
|
+
});
|
|
22
|
+
if (result.status !== 0)
|
|
23
|
+
throw new Error(
|
|
24
|
+
`git ${args.join(" ")} failed: ${String(result.stderr || "").trim()}`,
|
|
25
|
+
);
|
|
26
|
+
return encoding === null ? result.stdout : String(result.stdout || "").trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function patchRoot(base, head, cwd) {
|
|
30
|
+
const patch = git(
|
|
31
|
+
[
|
|
32
|
+
"diff",
|
|
33
|
+
"--binary",
|
|
34
|
+
"--full-index",
|
|
35
|
+
"--no-ext-diff",
|
|
36
|
+
`${base}..${head}`,
|
|
37
|
+
],
|
|
38
|
+
{ cwd, encoding: null },
|
|
39
|
+
);
|
|
40
|
+
return `sha256:${crypto.createHash("sha256").update(patch).digest("hex")}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function linearRange(base, head, cwd) {
|
|
44
|
+
try {
|
|
45
|
+
git(["merge-base", "--is-ancestor", base, head], { cwd });
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const commits = git(
|
|
50
|
+
["rev-list", "--reverse", "--topo-order", `${base}..${head}`],
|
|
51
|
+
{ cwd },
|
|
52
|
+
)
|
|
53
|
+
.split(/\s+/u)
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
if (commits.length === 0) return null;
|
|
56
|
+
let expectedParent = base;
|
|
57
|
+
const trees = [];
|
|
58
|
+
const patchRoots = [];
|
|
59
|
+
for (const rangeCommit of commits) {
|
|
60
|
+
const [observedCommit, ...parents] = git(
|
|
61
|
+
["rev-list", "--parents", "-n", "1", rangeCommit],
|
|
62
|
+
{ cwd },
|
|
63
|
+
).split(/\s+/u);
|
|
64
|
+
if (
|
|
65
|
+
observedCommit !== rangeCommit ||
|
|
66
|
+
parents.length !== 1 ||
|
|
67
|
+
parents[0] !== expectedParent
|
|
68
|
+
) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
trees.push(
|
|
72
|
+
exactSha(
|
|
73
|
+
git(["rev-parse", `${rangeCommit}^{tree}`], { cwd }),
|
|
74
|
+
"replayedCommitTree",
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
patchRoots.push(patchRoot(expectedParent, rangeCommit, cwd));
|
|
78
|
+
expectedParent = rangeCommit;
|
|
79
|
+
}
|
|
80
|
+
return { commits, trees, patchRoots };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function exactMergeGroupBinding(input = {}) {
|
|
84
|
+
const cwd = path.resolve(input.cwd || process.cwd());
|
|
85
|
+
const mergeGroupHead = exactSha(input.mergeGroupHead, "mergeGroupHead");
|
|
86
|
+
const expectedTree = exactSha(input.mergeGroupTree, "mergeGroupTree");
|
|
87
|
+
const qualifiedBase = exactSha(input.qualifiedBase, "qualifiedBase");
|
|
88
|
+
const currentBase = exactSha(input.currentBase, "currentBase");
|
|
89
|
+
const sourceHead = exactSha(input.sourceHead, "sourceHead");
|
|
90
|
+
const sourcePatchRoot = String(input.sourcePatchRoot || "").trim();
|
|
91
|
+
const actualTree = exactSha(
|
|
92
|
+
git(["rev-parse", `${mergeGroupHead}^{tree}`], { cwd }),
|
|
93
|
+
"mergeGroupTree",
|
|
94
|
+
);
|
|
95
|
+
if (actualTree !== expectedTree)
|
|
96
|
+
return { ok: false, reason: "merge-group-tree-mismatch" };
|
|
97
|
+
try {
|
|
98
|
+
git(["merge-base", "--is-ancestor", qualifiedBase, currentBase], { cwd });
|
|
99
|
+
} catch {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
reason: "current-base-not-descendant-of-qualified-base",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
if (!/^sha256:[0-9a-f]{64}$/u.test(sourcePatchRoot))
|
|
106
|
+
throw new Error("sourcePatchRoot must be an exact SHA-256 root");
|
|
107
|
+
if (patchRoot(currentBase, mergeGroupHead, cwd) !== sourcePatchRoot)
|
|
108
|
+
return { ok: false, reason: "merge-group-source-patch-mismatch" };
|
|
109
|
+
|
|
110
|
+
const [commit, ...parents] = git(
|
|
111
|
+
["rev-list", "--parents", "-n", "1", mergeGroupHead],
|
|
112
|
+
{ cwd },
|
|
113
|
+
).split(/\s+/u);
|
|
114
|
+
if (commit !== mergeGroupHead)
|
|
115
|
+
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
116
|
+
if (
|
|
117
|
+
parents.length === 2 &&
|
|
118
|
+
parents[0] === currentBase &&
|
|
119
|
+
parents[1] === sourceHead
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
mergeGroupHead,
|
|
124
|
+
mergeGroupTree: actualTree,
|
|
125
|
+
parents,
|
|
126
|
+
compositionMode: "two-parent-merge",
|
|
127
|
+
replayedCommitTrees: [],
|
|
128
|
+
replayedCommitPatchRoots: [],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const sourceRange = linearRange(qualifiedBase, sourceHead, cwd);
|
|
133
|
+
const mergeGroupRange = linearRange(currentBase, mergeGroupHead, cwd);
|
|
134
|
+
if (
|
|
135
|
+
!sourceRange ||
|
|
136
|
+
!mergeGroupRange ||
|
|
137
|
+
sourceRange.commits.length !== mergeGroupRange.commits.length ||
|
|
138
|
+
sourceRange.patchRoots.some(
|
|
139
|
+
(root, index) => root !== mergeGroupRange.patchRoots[index],
|
|
140
|
+
)
|
|
141
|
+
) {
|
|
142
|
+
return { ok: false, reason: "merge-group-parent-mismatch" };
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
ok: true,
|
|
146
|
+
mergeGroupHead,
|
|
147
|
+
mergeGroupTree: actualTree,
|
|
148
|
+
parents,
|
|
149
|
+
compositionMode: "linear-replay",
|
|
150
|
+
replayedCommitTrees: mergeGroupRange.trees,
|
|
151
|
+
replayedCommitPatchRoots: mergeGroupRange.patchRoots,
|
|
152
|
+
};
|
|
153
|
+
}
|