@kungfu-tech/buildchain 4.0.1-alpha.4 → 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/decisions/0002-next-development-transition.md +101 -0
- package/architecture/internal-capabilities.json +117 -8
- package/architecture/maintainability-policy.json +38 -20
- package/architecture/release-tail-contract-inventory.json +5 -2
- package/architecture/v3-core-mechanism-inventory.json +89 -0
- package/architecture/v4-adopter-delivery-parity.json +32 -5
- package/architecture/v4-capability-state-machine-manifest.json +73 -0
- package/architecture/v4-compatibility-facts-parity.json +77 -0
- package/architecture/v4-next-development-parity.json +192 -0
- package/architecture/v4-publication-rehearsal-parity.json +84 -0
- package/architecture/v4-release-train-parity.json +1 -0
- package/architecture/v4-tail-reseal-parity.json +150 -0
- package/bin/buildchain.mjs +23 -2
- package/bin/internal/adopter-delivery-cli.mjs +70 -0
- package/bin/internal/command-registry.mjs +3 -0
- package/bin/internal/compatibility-facts-cli.mjs +101 -0
- package/contracts/fixtures/next-development-transition-v1/anchored-manual-waiting.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/semver-auto-planned.json +47 -0
- package/contracts/fixtures/next-development-transition-v1/version-model-cases.json +40 -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-compatibility-facts-v1/shared.json +1129 -0
- package/contracts/fixtures/v4-floating-consumer-policy-v1/cases.json +7 -1
- 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/fixtures/v4-tail-reseal-v1/valid.json +229 -0
- package/contracts/next-development-request-v1.schema.json +66 -0
- package/contracts/next-development-transition-v1.schema.json +292 -0
- package/contracts/v4-adopter-delivery-v1.schema.json +81 -0
- package/contracts/v4-compatibility-facts-v1.schema.json +230 -0
- package/contracts/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/contracts/v4-tail-reseal-v1.schema.json +276 -0
- package/dist/site/agent-index.json +4 -0
- package/dist/site/artifact-schemas.json +8 -0
- package/dist/site/buildchain-contract.json +1393 -28
- package/dist/site/buildchain-site.json +462 -27
- package/dist/site/capability-registry.json +17 -14
- package/dist/site/cli-registry.json +159 -1
- package/dist/site/kfd-claims.json +429 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +52 -6
- package/dist/site/node-api-registry.json +13972 -9697
- package/dist/site/page-registry.json +428 -17
- package/dist/site/public-surface-audit.json +429 -11
- package/dist/site/publication-authority-registry.json +22 -4
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +21 -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-compatibility-facts-v1.schema.json +230 -0
- package/dist/site/schemas/v4-publication-rehearsal-capsule-v1.schema.json +233 -0
- package/dist/site/site-manifest.json +34 -10
- package/dist/site/workflow-registry.json +121 -8
- package/docs/MAP.md +7 -2
- package/docs/cli-reference.md +193 -0
- package/docs/cli.md +8 -0
- package/docs/dev-alpha-candidate-patrol.md +8 -0
- package/docs/next-development-transition.md +118 -0
- package/docs/node-api-reference.md +448 -168
- package/docs/release-tail-provider-plane.md +30 -0
- package/docs/v4-adopter-delivery.md +133 -0
- package/docs/v4-compatibility-facts.md +91 -0
- package/docs/v4-publication-rehearsal.md +130 -0
- package/docs/v4-tail-reseal.md +73 -0
- package/docs/versioning.md +4 -3
- package/package.json +29 -6
- 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 +3 -0
- package/packages/core/buildchain-compatibility-authority.js +336 -0
- package/packages/core/buildchain-compatibility-fact.js +394 -0
- package/packages/core/buildchain-compatibility-facts.json +217 -0
- package/packages/core/buildchain-compatibility-proof.js +566 -0
- package/packages/core/buildchain-config.js +84 -0
- package/packages/core/buildchain-contract.js +93 -141
- 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/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +55 -0
- package/packages/core/dev-alpha-candidate-selection.js +10 -2
- package/packages/core/dev-delivery-execution-transfer.js +21 -1
- package/packages/core/dev-delivery-process-boundary.js +19 -6
- package/packages/core/dev-delivery-provider-heartbeat.js +18 -6
- package/packages/core/kungfu-temporal-fact.js +557 -0
- package/packages/core/legacy-kfd-adopter-driver.js +167 -0
- package/packages/core/next-development-candidate-reservation.js +186 -0
- package/packages/core/next-development-controller.js +728 -0
- package/packages/core/next-development-projection.js +287 -0
- package/packages/core/next-development-transition.js +738 -0
- package/packages/core/paper-agent-entry.js +11 -5
- package/packages/core/paper.js +16 -5
- 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-canonical-contracts.js +5 -0
- package/packages/core/v4-floating-consumer-policy.js +29 -9
- 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/packages/core/v4-tail-reseal-contract.js +26 -0
- package/packages/core/v4-tail-reseal-github.js +178 -0
- package/packages/core/v4-tail-reseal-receipt.js +225 -0
- package/packages/core/v4-tail-reseal.js +594 -0
- package/scripts/buildchain-cli-help.mjs +30 -0
- package/scripts/check-inventory.mjs +14 -3
- package/scripts/dev-alpha-candidate-patrol.mjs +22 -1
- package/scripts/dev-delivery-source-proof-replay.mjs +153 -0
- package/scripts/dev-delivery-source-proof-reuse.mjs +13 -97
- package/scripts/generate-next-development-guidance.mjs +49 -0
- package/scripts/generate-release-candidate-passport.mjs +152 -0
- package/scripts/generate-site-bundle.mjs +46 -3
- package/scripts/init-repo.mjs +22 -4
- package/scripts/next-development-self-dogfood-harness.mjs +460 -0
- package/scripts/next-development-self-dogfood.mjs +597 -0
- package/scripts/next-development-transition.mjs +47 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +545 -15
- package/scripts/site-capability-metadata.mjs +28 -0
- package/scripts/v4-consumer-policy.mjs +1 -5
- package/scripts/v4-publication-rehearsal-fixture.mjs +144 -0
- package/scripts/v4-tail-reseal-macos-rehearsal.mjs +134 -0
- package/scripts/v4-tail-reseal.mjs +416 -0
|
@@ -3,6 +3,7 @@ import crypto from "node:crypto";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
6
7
|
import { pathToFileURL } from "node:url";
|
|
7
8
|
|
|
8
9
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
@@ -27,6 +28,15 @@ import {
|
|
|
27
28
|
} from "../packages/core/publication-artifact-candidate.js";
|
|
28
29
|
import { createPublicationSealedBundle } from "../packages/core/publication-sealed-bundle.js";
|
|
29
30
|
import { releaseTransactionStateRef } from "../packages/core/publish-transaction.js";
|
|
31
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
32
|
+
import { validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
|
|
33
|
+
import {
|
|
34
|
+
authorizeV4RuntimeSelection,
|
|
35
|
+
createV4RuntimeResumeLineage,
|
|
36
|
+
scanV4RuntimeSelectorPersistence,
|
|
37
|
+
v4RuntimeResumeDocumentRoot,
|
|
38
|
+
verifyV4RuntimeAuthorizationReceipt,
|
|
39
|
+
} from "../packages/core/v4-runtime-ref-resume-authority.js";
|
|
30
40
|
|
|
31
41
|
function env(name, fallback = "") {
|
|
32
42
|
return process.env[name] || fallback;
|
|
@@ -99,8 +109,373 @@ function readOnlyJson(files, label) {
|
|
|
99
109
|
return JSON.parse(fs.readFileSync(files[0].absolutePath, "utf8"));
|
|
100
110
|
}
|
|
101
111
|
|
|
102
|
-
|
|
103
|
-
const
|
|
112
|
+
function trackedRuntimePersistenceScan() {
|
|
113
|
+
const paths = execFileSync(
|
|
114
|
+
"git",
|
|
115
|
+
["ls-files", ".github/workflows", ".github/actions", ".buildchain"],
|
|
116
|
+
{ encoding: "utf8" },
|
|
117
|
+
)
|
|
118
|
+
.split(/\r?\n/)
|
|
119
|
+
.filter((entry) => /\.(?:json|toml|ya?ml)$/u.test(entry));
|
|
120
|
+
return scanV4RuntimeSelectorPersistence({ root: process.cwd(), paths });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads }) {
|
|
124
|
+
if (
|
|
125
|
+
!sidecar ||
|
|
126
|
+
sidecar.contract !==
|
|
127
|
+
"kungfu-buildchain-v4-release-candidate-stage-capsules/v1" ||
|
|
128
|
+
sidecar.status !== "sealed"
|
|
129
|
+
) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"cross-runtime recovery requires original sealed release-candidate Stage Capsules",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const payload = { ...sidecar };
|
|
135
|
+
delete payload.root;
|
|
136
|
+
const expectedAttempt =
|
|
137
|
+
`github-run:${passport.workflow.runId}:attempt:${passport.workflow.runAttempt}`;
|
|
138
|
+
if (
|
|
139
|
+
sidecar.root !== v4RuntimeResumeDocumentRoot(payload) ||
|
|
140
|
+
sidecar.repository !== passport.repository ||
|
|
141
|
+
sidecar.source?.sha !== passport.source.headSha ||
|
|
142
|
+
sidecar.source?.treeSha !== passport.source.treeHash ||
|
|
143
|
+
sidecar.buildAttempt?.id !== expectedAttempt ||
|
|
144
|
+
sidecar.buildAttempt?.runtimeSha !== passport.buildchain.sha ||
|
|
145
|
+
sidecar.consumerPolicyReceiptRoot !== passport.consumerPolicy?.receiptRoot
|
|
146
|
+
) {
|
|
147
|
+
throw new Error("release-candidate Stage Capsule sidecar identity mismatch");
|
|
148
|
+
}
|
|
149
|
+
const artifactByName = new Map(downloads.map((entry) => [entry.artifact.name, entry]));
|
|
150
|
+
const required = [...passport.platformMatrix.map((entry) => entry.platformId)].sort();
|
|
151
|
+
const entries = [...(sidecar.capsules || [])].sort((left, right) =>
|
|
152
|
+
left.platform.localeCompare(right.platform),
|
|
153
|
+
);
|
|
154
|
+
if (
|
|
155
|
+
entries.length !== required.length ||
|
|
156
|
+
entries.some((entry, index) => entry.platform !== required[index])
|
|
157
|
+
) {
|
|
158
|
+
throw new Error("release-candidate Stage Capsule platform set is incomplete");
|
|
159
|
+
}
|
|
160
|
+
return entries.map((entry) => {
|
|
161
|
+
validateV4StageCapsule(entry.capsule);
|
|
162
|
+
const download = artifactByName.get(entry.artifactName);
|
|
163
|
+
const artifact = entry.artifact;
|
|
164
|
+
if (
|
|
165
|
+
!download ||
|
|
166
|
+
artifact?.platformId !== entry.platform ||
|
|
167
|
+
artifact?.id !== String(download.artifact.id) ||
|
|
168
|
+
artifact?.name !== download.artifact.name ||
|
|
169
|
+
artifact?.digest !== entry.artifactDigest ||
|
|
170
|
+
artifact?.digest !== download.artifact.digest ||
|
|
171
|
+
artifact?.digest !== download.record.digest ||
|
|
172
|
+
artifact?.digest !== download.record.downloadedDigest ||
|
|
173
|
+
artifact?.expiresAt !==
|
|
174
|
+
new Date(download.artifact.expires_at).toISOString() ||
|
|
175
|
+
entry.capsule.identity.policyRoot !== sidecar.consumerPolicyReceiptRoot ||
|
|
176
|
+
entry.capsule.identity.sourceRoot !==
|
|
177
|
+
v4ContentRoot("candidate-identity", passport.source) ||
|
|
178
|
+
entry.capsule.identity.runtimeRoot !==
|
|
179
|
+
v4ContentRoot("candidate-identity", { sha: passport.buildchain.sha })
|
|
180
|
+
) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`Stage Capsule ${entry.platform} does not bind the verified provider artifact`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
platform: entry.platform,
|
|
187
|
+
capsuleRoot: entry.capsule.capsuleRoot,
|
|
188
|
+
identityRoot: entry.capsule.identityRoot,
|
|
189
|
+
artifactDigest: entry.artifactDigest,
|
|
190
|
+
sourceSha: sidecar.source.sha,
|
|
191
|
+
sourceTreeSha: sidecar.source.treeSha,
|
|
192
|
+
policyRoot: sidecar.consumerPolicyReceiptRoot,
|
|
193
|
+
buildRuntimeSha: sidecar.buildAttempt.runtimeSha,
|
|
194
|
+
sealed: true,
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function validateV4RuntimeResumePublicReadback({
|
|
200
|
+
targetRef,
|
|
201
|
+
targetSha,
|
|
202
|
+
alphaSha,
|
|
203
|
+
exactTagSha,
|
|
204
|
+
tagLineage,
|
|
205
|
+
runtimeSha,
|
|
206
|
+
version,
|
|
207
|
+
transaction,
|
|
208
|
+
main,
|
|
209
|
+
npm,
|
|
210
|
+
}) {
|
|
211
|
+
if (
|
|
212
|
+
transaction?.target_ref !== targetRef ||
|
|
213
|
+
transaction?.version !== version ||
|
|
214
|
+
exactTagSha !== transaction?.source_sha ||
|
|
215
|
+
!["ahead", "identical"].includes(tagLineage?.status) ||
|
|
216
|
+
alphaSha !== runtimeSha ||
|
|
217
|
+
!targetSha ||
|
|
218
|
+
npm["dist-tags"]?.alpha !== version ||
|
|
219
|
+
npm.versions?.[version]?.dist?.integrity !== main.digest
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
"cross-runtime final public readback does not match durable publication bytes and runtime",
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function readPublicResumeState({
|
|
228
|
+
repoInfo,
|
|
229
|
+
targetRef,
|
|
230
|
+
runtimeSha,
|
|
231
|
+
version,
|
|
232
|
+
transaction,
|
|
233
|
+
token,
|
|
234
|
+
apiUrl,
|
|
235
|
+
fetchImpl,
|
|
236
|
+
}) {
|
|
237
|
+
const readRef = async (ref) =>
|
|
238
|
+
(await githubJson({
|
|
239
|
+
apiUrl,
|
|
240
|
+
token,
|
|
241
|
+
fetchImpl,
|
|
242
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/ref/${ref}`,
|
|
243
|
+
})).object?.sha || "";
|
|
244
|
+
const main = transaction?.artifacts?.find((entry) => entry.kind === "npm" && entry.required !== false);
|
|
245
|
+
if (!main || main.ref !== version || !main.digest) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
"cross-runtime recovery requires exact durable npm publication evidence",
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
const packageUrl =
|
|
251
|
+
`https://registry.npmjs.org/${encodeURIComponent(main.name)}`;
|
|
252
|
+
const exactTag = transaction.exact_tag || `v${version}`;
|
|
253
|
+
const [targetSha, alphaSha, exactTagSha, npmResponse] = await Promise.all([
|
|
254
|
+
readRef(`heads/${targetRef}`),
|
|
255
|
+
readRef("tags/v4-alpha"),
|
|
256
|
+
readRef(`tags/${exactTag}`),
|
|
257
|
+
fetchImpl(packageUrl),
|
|
258
|
+
]);
|
|
259
|
+
if (!npmResponse.ok) {
|
|
260
|
+
throw new Error(`npm public readback failed with HTTP ${npmResponse.status}`);
|
|
261
|
+
}
|
|
262
|
+
const npm = await npmResponse.json();
|
|
263
|
+
const tagLineage = await githubJson({
|
|
264
|
+
apiUrl,
|
|
265
|
+
token,
|
|
266
|
+
fetchImpl,
|
|
267
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${exactTagSha}...${targetSha}`,
|
|
268
|
+
});
|
|
269
|
+
validateV4RuntimeResumePublicReadback({
|
|
270
|
+
targetRef,
|
|
271
|
+
targetSha,
|
|
272
|
+
alphaSha,
|
|
273
|
+
exactTagSha,
|
|
274
|
+
tagLineage,
|
|
275
|
+
runtimeSha,
|
|
276
|
+
version,
|
|
277
|
+
transaction,
|
|
278
|
+
main,
|
|
279
|
+
npm,
|
|
280
|
+
});
|
|
281
|
+
const body = {
|
|
282
|
+
schemaVersion: 1,
|
|
283
|
+
contract: "kungfu-buildchain-v4-runtime-resume-public-readback/v1",
|
|
284
|
+
observedAt: new Date().toISOString(),
|
|
285
|
+
repository: repoInfo.fullName,
|
|
286
|
+
version,
|
|
287
|
+
refs: {
|
|
288
|
+
target: { ref: targetRef, sha: targetSha },
|
|
289
|
+
floating: { ref: "v4-alpha", sha: alphaSha },
|
|
290
|
+
exactTag: { ref: exactTag, sha: exactTagSha },
|
|
291
|
+
},
|
|
292
|
+
npm: {
|
|
293
|
+
package: main.name,
|
|
294
|
+
version,
|
|
295
|
+
distTag: "alpha",
|
|
296
|
+
integrity: npm.versions[version].dist.integrity,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function prepareRuntimeResumeEvidence({
|
|
303
|
+
repoInfo,
|
|
304
|
+
targetRef,
|
|
305
|
+
runtimeSha,
|
|
306
|
+
version,
|
|
307
|
+
passport,
|
|
308
|
+
sidecar,
|
|
309
|
+
stageCapsules,
|
|
310
|
+
recovery,
|
|
311
|
+
outputDir,
|
|
312
|
+
}) {
|
|
313
|
+
const authorizationPath = path.resolve(
|
|
314
|
+
".buildchain/release-candidate/v4-runtime-authorization.json",
|
|
315
|
+
);
|
|
316
|
+
if (!fs.existsSync(authorizationPath)) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"cross-runtime recovery requires a fresh runtime authorization receipt",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
const delegated = readOnlyJson([{ absolutePath: authorizationPath }], "runtime authorization");
|
|
322
|
+
const delegatedVerification = verifyV4RuntimeAuthorizationReceipt({
|
|
323
|
+
receipt: delegated.receipt,
|
|
324
|
+
receiptRoot: delegated.receiptRoot,
|
|
325
|
+
repository: repoInfo.fullName,
|
|
326
|
+
runtimeSha,
|
|
327
|
+
});
|
|
328
|
+
if (!delegatedVerification.ok) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
`fresh recovery runtime authorization rejected: ${delegatedVerification.failures.join(", ")}`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const policy = passport.consumerPolicy.receipt;
|
|
334
|
+
const authorization = authorizeV4RuntimeSelection({
|
|
335
|
+
repository: repoInfo.fullName,
|
|
336
|
+
eventName: "workflow_dispatch",
|
|
337
|
+
mode: "resume",
|
|
338
|
+
actor: delegated.receipt.actor.login,
|
|
339
|
+
actorPermission: delegated.receipt.actor.permission,
|
|
340
|
+
reason:
|
|
341
|
+
`resume sealed candidate run ${passport.workflow.runId} from a tree-equivalent protected source`,
|
|
342
|
+
authorizedAt: new Date().toISOString(),
|
|
343
|
+
sourceSha: sidecar.source.sha,
|
|
344
|
+
sourceTreeSha: sidecar.source.treeSha,
|
|
345
|
+
requestedRef: delegated.receipt.request.ref,
|
|
346
|
+
resolvedRuntimeSha: runtimeSha,
|
|
347
|
+
approvedRefReadbacks: delegated.receipt.runtime.reachableFrom,
|
|
348
|
+
stableContractLockRoot: policy.contractLocks.stable.root,
|
|
349
|
+
alphaContractLockRoot: policy.contractLocks.alpha.root,
|
|
350
|
+
consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
|
|
351
|
+
persistenceScan: trackedRuntimePersistenceScan(),
|
|
352
|
+
});
|
|
353
|
+
const planBody = {
|
|
354
|
+
schemaVersion: 1,
|
|
355
|
+
contract: "kungfu-buildchain-v4-runtime-resume-plan/v1",
|
|
356
|
+
originalRunId: String(passport.workflow.runId),
|
|
357
|
+
resumeRunId: String(env("GITHUB_RUN_ID")),
|
|
358
|
+
recoveryReceiptRoot: recovery.receipt.root,
|
|
359
|
+
requiredPlatforms: stageCapsules.map((entry) => entry.platform),
|
|
360
|
+
reusedCapsuleRoots: stageCapsules.map((entry) => entry.capsuleRoot),
|
|
361
|
+
rebuildPlatforms: [],
|
|
362
|
+
skippedBuildStages: recovery.receipt.skippedBuildStages,
|
|
363
|
+
};
|
|
364
|
+
const plan = { ...planBody, root: v4RuntimeResumeDocumentRoot(planBody) };
|
|
365
|
+
const materialBody = {
|
|
366
|
+
schemaVersion: 1,
|
|
367
|
+
contract: "kungfu-buildchain-v4-runtime-resume-material/v1",
|
|
368
|
+
repository: repoInfo.fullName,
|
|
369
|
+
targetRef,
|
|
370
|
+
runtimeSha,
|
|
371
|
+
version,
|
|
372
|
+
authorization: authorization.receipt,
|
|
373
|
+
authorizationRoot: authorization.receiptRoot,
|
|
374
|
+
buildAttempt: sidecar.buildAttempt,
|
|
375
|
+
resumeAttempt: {
|
|
376
|
+
id: `github-run:${env("GITHUB_RUN_ID")}:attempt:${env("GITHUB_RUN_ATTEMPT", "1")}`,
|
|
377
|
+
runtimeSha,
|
|
378
|
+
},
|
|
379
|
+
source: sidecar.source,
|
|
380
|
+
consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
|
|
381
|
+
requiredPlatforms: stageCapsules.map((entry) => entry.platform),
|
|
382
|
+
stageCapsules,
|
|
383
|
+
resumePlanRoot: plan.root,
|
|
384
|
+
floatingRefBefore: {
|
|
385
|
+
ref: "v4-alpha",
|
|
386
|
+
sha: sidecar.buildAttempt.runtimeSha,
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
const material = {
|
|
390
|
+
...materialBody,
|
|
391
|
+
root: v4RuntimeResumeDocumentRoot(materialBody),
|
|
392
|
+
};
|
|
393
|
+
for (const [name, value] of [
|
|
394
|
+
["v4-runtime-resume-plan.json", plan],
|
|
395
|
+
["v4-runtime-resume-material.json", material],
|
|
396
|
+
]) {
|
|
397
|
+
fs.writeFileSync(
|
|
398
|
+
path.join(outputDir, name),
|
|
399
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
return path.join(outputDir, "v4-runtime-resume-evidence.json");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export async function finalizeRuntimeResumeEvidence({
|
|
406
|
+
materialPath,
|
|
407
|
+
transaction,
|
|
408
|
+
token = env("GITHUB_TOKEN", env("INPUT_TOKEN")),
|
|
409
|
+
apiUrl = env("GITHUB_API_URL", "https://api.github.com"),
|
|
410
|
+
fetchImpl = globalThis.fetch,
|
|
411
|
+
outputDir = path.dirname(materialPath),
|
|
412
|
+
}) {
|
|
413
|
+
const material = JSON.parse(fs.readFileSync(materialPath, "utf8"));
|
|
414
|
+
const body = { ...material };
|
|
415
|
+
delete body.root;
|
|
416
|
+
if (
|
|
417
|
+
material.contract !== "kungfu-buildchain-v4-runtime-resume-material/v1" ||
|
|
418
|
+
material.root !== v4RuntimeResumeDocumentRoot(body)
|
|
419
|
+
) {
|
|
420
|
+
throw new Error("cross-runtime resume material root mismatch");
|
|
421
|
+
}
|
|
422
|
+
const repoInfo = splitRepository(material.repository);
|
|
423
|
+
const version = env("BUILDCHAIN_RELEASE_VERSION", material.version);
|
|
424
|
+
const targetRef = env("BUILDCHAIN_RELEASE_TARGET_REF", material.targetRef);
|
|
425
|
+
if (
|
|
426
|
+
version !== material.version ||
|
|
427
|
+
targetRef !== material.targetRef ||
|
|
428
|
+
transaction?.version !== version ||
|
|
429
|
+
transaction?.state !== "complete"
|
|
430
|
+
) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
"cross-runtime resume finalization does not match the completed publication transaction",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const readback = await readPublicResumeState({
|
|
436
|
+
repoInfo,
|
|
437
|
+
targetRef,
|
|
438
|
+
runtimeSha: material.runtimeSha,
|
|
439
|
+
version,
|
|
440
|
+
transaction,
|
|
441
|
+
token,
|
|
442
|
+
apiUrl,
|
|
443
|
+
fetchImpl,
|
|
444
|
+
});
|
|
445
|
+
const resumed = createV4RuntimeResumeLineage({
|
|
446
|
+
authorization: material.authorization,
|
|
447
|
+
authorizationRoot: material.authorizationRoot,
|
|
448
|
+
buildAttempt: material.buildAttempt,
|
|
449
|
+
resumeAttempt: material.resumeAttempt,
|
|
450
|
+
source: material.source,
|
|
451
|
+
consumerPolicyReceiptRoot: material.consumerPolicyReceiptRoot,
|
|
452
|
+
requiredPlatforms: material.requiredPlatforms,
|
|
453
|
+
stageCapsules: material.stageCapsules,
|
|
454
|
+
resumePlanRoot: material.resumePlanRoot,
|
|
455
|
+
finalPublicReadbackRoot: readback.root,
|
|
456
|
+
floatingRefBefore: material.floatingRefBefore,
|
|
457
|
+
floatingRefAfter: { ref: "v4-alpha", sha: material.runtimeSha },
|
|
458
|
+
});
|
|
459
|
+
const evidence = {
|
|
460
|
+
authorization: material.authorization,
|
|
461
|
+
authorizationRoot: material.authorizationRoot,
|
|
462
|
+
lineage: resumed.lineage,
|
|
463
|
+
lineageRoot: resumed.lineageRoot,
|
|
464
|
+
};
|
|
465
|
+
const evidencePath = path.join(outputDir, "v4-runtime-resume-evidence.json");
|
|
466
|
+
for (const [name, value] of [
|
|
467
|
+
["v4-runtime-resume-public-readback.json", readback],
|
|
468
|
+
[path.basename(evidencePath), evidence],
|
|
469
|
+
]) {
|
|
470
|
+
fs.writeFileSync(
|
|
471
|
+
path.join(outputDir, name),
|
|
472
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
return { evidence, path: evidencePath, readback };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function readTransactionAtRef({ repoInfo, apiUrl, token, fetchImpl, stateRef }) {
|
|
104
479
|
const response = await githubJson({
|
|
105
480
|
apiUrl,
|
|
106
481
|
token,
|
|
@@ -109,10 +484,65 @@ async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, ver
|
|
|
109
484
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/state.json?ref=${encodeURIComponent(stateRef)}`,
|
|
110
485
|
});
|
|
111
486
|
if (!response) return undefined;
|
|
112
|
-
if (
|
|
113
|
-
|
|
487
|
+
if (
|
|
488
|
+
response.type !== "file" ||
|
|
489
|
+
response.encoding !== "base64" ||
|
|
490
|
+
!response.content
|
|
491
|
+
) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`durable transaction ${stateRef} did not expose a base64 state.json file`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
return JSON.parse(
|
|
497
|
+
Buffer.from(
|
|
498
|
+
String(response.content).replace(/\s/g, ""),
|
|
499
|
+
"base64",
|
|
500
|
+
).toString("utf8"),
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function readExistingTransaction({
|
|
505
|
+
repoInfo,
|
|
506
|
+
apiUrl,
|
|
507
|
+
token,
|
|
508
|
+
fetchImpl,
|
|
509
|
+
version,
|
|
510
|
+
}) {
|
|
511
|
+
return readTransactionAtRef({
|
|
512
|
+
repoInfo,
|
|
513
|
+
apiUrl,
|
|
514
|
+
token,
|
|
515
|
+
fetchImpl,
|
|
516
|
+
stateRef: releaseTransactionStateRef(version),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function readExistingTransactionById({
|
|
521
|
+
repoInfo,
|
|
522
|
+
apiUrl,
|
|
523
|
+
token,
|
|
524
|
+
fetchImpl,
|
|
525
|
+
transactionId,
|
|
526
|
+
}) {
|
|
527
|
+
if (!transactionId) return undefined;
|
|
528
|
+
const statePrefix = "buildchain/release-state/";
|
|
529
|
+
const refs = await githubJson({
|
|
530
|
+
apiUrl,
|
|
531
|
+
token,
|
|
532
|
+
fetchImpl,
|
|
533
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/matching-refs/heads/${statePrefix}`,
|
|
534
|
+
});
|
|
535
|
+
for (const ref of refs) {
|
|
536
|
+
const transaction = await readTransactionAtRef({
|
|
537
|
+
repoInfo,
|
|
538
|
+
apiUrl,
|
|
539
|
+
token,
|
|
540
|
+
fetchImpl,
|
|
541
|
+
stateRef: String(ref.ref).replace(/^refs\/heads\//, ""),
|
|
542
|
+
});
|
|
543
|
+
if (transaction?.id === transactionId) return transaction;
|
|
114
544
|
}
|
|
115
|
-
|
|
545
|
+
throw new Error(`durable publication transaction ${transactionId} was not found`);
|
|
116
546
|
}
|
|
117
547
|
|
|
118
548
|
function outputPath(filePath) {
|
|
@@ -349,6 +779,14 @@ async function recoverCandidateEvidence({
|
|
|
349
779
|
for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
350
780
|
const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
|
|
351
781
|
const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
|
|
782
|
+
const sidecarFiles = initialDownloads[0].files.filter(
|
|
783
|
+
(file) =>
|
|
784
|
+
path.basename(file.path) === "release-candidate-stage-capsules.json",
|
|
785
|
+
);
|
|
786
|
+
const stageCapsuleSidecar =
|
|
787
|
+
sidecarFiles.length === 1
|
|
788
|
+
? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
|
|
789
|
+
: undefined;
|
|
352
790
|
const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
|
|
353
791
|
const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
|
|
354
792
|
if (chosen.length !== requiredNames.size) {
|
|
@@ -360,7 +798,7 @@ async function recoverCandidateEvidence({
|
|
|
360
798
|
for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
361
799
|
return {
|
|
362
800
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
363
|
-
passport, buildSummary, chosen, downloads, publicationNames,
|
|
801
|
+
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
364
802
|
};
|
|
365
803
|
}
|
|
366
804
|
|
|
@@ -370,6 +808,52 @@ async function resolveTargetAdvance({ observedTargetSha, targetSha, transactionI
|
|
|
370
808
|
return { status: comparison.status, mergeIsAncestor: ["ahead", "identical"].includes(comparison.status) };
|
|
371
809
|
}
|
|
372
810
|
|
|
811
|
+
async function resolveRecoveryTransaction({
|
|
812
|
+
repoInfo,
|
|
813
|
+
apiUrl,
|
|
814
|
+
token,
|
|
815
|
+
fetchImpl,
|
|
816
|
+
transactionId,
|
|
817
|
+
publicationVersion,
|
|
818
|
+
}) {
|
|
819
|
+
const byId = await readExistingTransactionById({
|
|
820
|
+
repoInfo,
|
|
821
|
+
apiUrl,
|
|
822
|
+
token,
|
|
823
|
+
fetchImpl,
|
|
824
|
+
transactionId,
|
|
825
|
+
});
|
|
826
|
+
const version = byId?.version || publicationVersion;
|
|
827
|
+
const transaction = byId || await readExistingTransaction({
|
|
828
|
+
repoInfo,
|
|
829
|
+
apiUrl,
|
|
830
|
+
token,
|
|
831
|
+
fetchImpl,
|
|
832
|
+
version,
|
|
833
|
+
});
|
|
834
|
+
return { version, transaction };
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function resolveRecoveredStageCapsules({
|
|
838
|
+
candidateRuntimeSha,
|
|
839
|
+
runtimeSha,
|
|
840
|
+
sidecar,
|
|
841
|
+
passport,
|
|
842
|
+
downloads,
|
|
843
|
+
}) {
|
|
844
|
+
if (candidateRuntimeSha === runtimeSha) return [];
|
|
845
|
+
return verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads });
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot) {
|
|
849
|
+
const runId = String(candidateRunId || "").trim();
|
|
850
|
+
if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
|
|
851
|
+
if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
|
|
852
|
+
throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
|
|
853
|
+
}
|
|
854
|
+
return runId;
|
|
855
|
+
}
|
|
856
|
+
|
|
373
857
|
export async function resumeFromCandidateRun({
|
|
374
858
|
repository, targetRepository = repository, candidateRunId,
|
|
375
859
|
expectedWorkflowFile, expectedWorkflowName,
|
|
@@ -384,16 +868,12 @@ export async function resumeFromCandidateRun({
|
|
|
384
868
|
recoveryRunId = env("GITHUB_RUN_ID"), fetchImpl = globalThis.fetch,
|
|
385
869
|
} = {}) {
|
|
386
870
|
const repoInfo = splitRepository(repository);
|
|
387
|
-
const runId =
|
|
388
|
-
if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
|
|
389
|
-
if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
|
|
390
|
-
throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
|
|
391
|
-
}
|
|
871
|
+
const runId = validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot);
|
|
392
872
|
const archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-resume-"));
|
|
393
873
|
try {
|
|
394
874
|
const {
|
|
395
875
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
396
|
-
passport, buildSummary, chosen, downloads, publicationNames,
|
|
876
|
+
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
397
877
|
} = await recoverCandidateEvidence({
|
|
398
878
|
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
399
879
|
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
@@ -419,8 +899,24 @@ export async function resumeFromCandidateRun({
|
|
|
419
899
|
releasePatterns,
|
|
420
900
|
platformManifests: platformManifestEvidence.manifests,
|
|
421
901
|
});
|
|
422
|
-
const
|
|
423
|
-
|
|
902
|
+
const stageCapsules = resolveRecoveredStageCapsules({
|
|
903
|
+
candidateRuntimeSha,
|
|
904
|
+
runtimeSha,
|
|
905
|
+
sidecar: stageCapsuleSidecar,
|
|
906
|
+
passport,
|
|
907
|
+
downloads,
|
|
908
|
+
});
|
|
909
|
+
const {
|
|
910
|
+
version: candidateVersion,
|
|
911
|
+
transaction: existingTransaction,
|
|
912
|
+
} = await resolveRecoveryTransaction({
|
|
913
|
+
repoInfo,
|
|
914
|
+
apiUrl,
|
|
915
|
+
token,
|
|
916
|
+
fetchImpl,
|
|
917
|
+
transactionId,
|
|
918
|
+
publicationVersion: publication.version,
|
|
919
|
+
});
|
|
424
920
|
const targetAdvance = await resolveTargetAdvance({
|
|
425
921
|
observedTargetSha, targetSha, transactionId, existingTransaction,
|
|
426
922
|
repoInfo, apiUrl, token, fetchImpl,
|
|
@@ -491,6 +987,19 @@ export async function resumeFromCandidateRun({
|
|
|
491
987
|
if (publication.manifest) fs.writeFileSync(sealedManifestPath, `${JSON.stringify(publication.manifest, null, 2)}\n`);
|
|
492
988
|
const publishRequiredArtifacts = publication.publishRequiredArtifacts;
|
|
493
989
|
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(publishRequiredArtifacts, null, 2)}\n`);
|
|
990
|
+
const runtimeResumeEvidencePath = stageCapsules.length
|
|
991
|
+
? prepareRuntimeResumeEvidence({
|
|
992
|
+
repoInfo,
|
|
993
|
+
targetRef,
|
|
994
|
+
runtimeSha,
|
|
995
|
+
version: candidateVersion,
|
|
996
|
+
passport,
|
|
997
|
+
sidecar: stageCapsuleSidecar,
|
|
998
|
+
stageCapsules,
|
|
999
|
+
recovery,
|
|
1000
|
+
outputDir: resolvedOutput,
|
|
1001
|
+
})
|
|
1002
|
+
: "";
|
|
494
1003
|
const tarballs = publication.npmArtifacts.map((entry) => outputPath(entry.file.absolutePath));
|
|
495
1004
|
return {
|
|
496
1005
|
enabled: true,
|
|
@@ -514,6 +1023,9 @@ export async function resumeFromCandidateRun({
|
|
|
514
1023
|
sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
|
|
515
1024
|
sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
|
|
516
1025
|
recoveryReceipt: outputPath(recoveryReceiptPath),
|
|
1026
|
+
runtimeResumeEvidence: runtimeResumeEvidencePath
|
|
1027
|
+
? outputPath(runtimeResumeEvidencePath)
|
|
1028
|
+
: "",
|
|
517
1029
|
},
|
|
518
1030
|
};
|
|
519
1031
|
} finally {
|
|
@@ -565,6 +1077,10 @@ export async function resumeFromCandidateRunCli() {
|
|
|
565
1077
|
"release-candidate-run-url": result.run.url,
|
|
566
1078
|
"release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
|
|
567
1079
|
"release-candidate-recovery-root": result.receipt.root,
|
|
1080
|
+
"v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
|
|
1081
|
+
"v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
|
|
1082
|
+
? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
|
|
1083
|
+
: "",
|
|
568
1084
|
"release-candidate-root": result.candidateRoot,
|
|
569
1085
|
"release-candidate-artifact-root": result.artifactRoot,
|
|
570
1086
|
"publish-sealed-bundle-root": result.paths.sealedBundleRoot,
|
|
@@ -586,7 +1102,21 @@ export async function resumeFromCandidateRunCli() {
|
|
|
586
1102
|
}
|
|
587
1103
|
|
|
588
1104
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
589
|
-
|
|
1105
|
+
const operation = process.argv[2] === "finalize"
|
|
1106
|
+
? finalizeRuntimeResumeEvidence({
|
|
1107
|
+
materialPath: requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL"),
|
|
1108
|
+
transaction: JSON.parse(
|
|
1109
|
+
requiredEnv("BUILDCHAIN_RELEASE_TRANSACTION_JSON"),
|
|
1110
|
+
),
|
|
1111
|
+
outputDir: env(
|
|
1112
|
+
"BUILDCHAIN_RELEASE_PASSPORT_OUTPUT_DIR",
|
|
1113
|
+
path.dirname(requiredEnv("BUILDCHAIN_V4_RUNTIME_RESUME_MATERIAL")),
|
|
1114
|
+
),
|
|
1115
|
+
}).then((result) =>
|
|
1116
|
+
console.log(JSON.stringify({ files: [outputPath(result.path)] })),
|
|
1117
|
+
)
|
|
1118
|
+
: resumeFromCandidateRunCli();
|
|
1119
|
+
operation.catch((error) => {
|
|
590
1120
|
const failure = error.recoveryFailure || recoveryFailure(error);
|
|
591
1121
|
console.error(`candidate recovery rejected [${failure.code}]: ${failure.reason}`);
|
|
592
1122
|
console.error(`next action: ${failure.nextAction}`);
|