@kungfu-tech/buildchain 4.0.1-alpha.4 → 4.0.1-alpha.41
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/ci-lane-change-budget.json +187 -0
- package/architecture/decisions/0002-next-development-transition.md +101 -0
- package/architecture/internal-capabilities.json +117 -8
- package/architecture/maintainability-policy.json +43 -25
- 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-floating-consumer-policy.json +6 -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-floating-consumer-policy-receipt-v1.schema.json +4 -2
- 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 +477 -32
- package/dist/site/capability-registry.json +17 -14
- package/dist/site/cli-registry.json +159 -1
- package/dist/site/kfd-claims.json +455 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +53 -7
- package/dist/site/node-api-registry.json +14008 -9602
- package/dist/site/page-registry.json +442 -21
- package/dist/site/public-surface-audit.json +440 -11
- package/dist/site/publication-authority-registry.json +44 -8
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +22 -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 +35 -11
- package/dist/site/workflow-registry.json +142 -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/dev-delivery-warrant.md +22 -1
- package/docs/next-development-transition.md +118 -0
- package/docs/node-api-reference.md +583 -288
- package/docs/release-candidate.md +7 -0
- 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 +30 -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/buildchain-publication-authority.js +1 -0
- package/packages/core/channel-candidate.js +8 -0
- package/packages/core/channel-promotion-baseline.js +55 -0
- package/packages/core/ci-lane-change-budget.js +247 -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-native-proof.js +43 -27
- package/packages/core/dev-delivery-process-boundary.js +19 -6
- package/packages/core/dev-delivery-provider-heartbeat.js +18 -6
- package/packages/core/dev-delivery-warrant-settlement.js +234 -2
- package/packages/core/dev-delivery-warrant-state.js +46 -10
- package/packages/core/dev-delivery-warrant.js +16 -3
- package/packages/core/github-governance-authority.js +1 -0
- package/packages/core/index.js +5 -0
- 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-candidate-recovery.js +70 -15
- package/packages/core/release-candidate.js +2 -2
- package/packages/core/release-passport.js +36 -9
- 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-delivery-warrant-read-candidate.js +10 -3
- package/packages/core/v4-floating-consumer-evidence.js +18 -4
- package/packages/core/v4-floating-consumer-policy.js +143 -20
- package/packages/core/v4-floating-consumer-release-passport.js +35 -8
- 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/packages/core/workflow-yaml-contract.js +24 -2
- package/scripts/artifact-signing-controller-core.mjs +1 -1
- package/scripts/buildchain-cli-help.mjs +30 -0
- package/scripts/check-ci-lane-change-budget.mjs +100 -0
- package/scripts/check-inventory.mjs +14 -3
- package/scripts/check-project-cut-merge-queue-admission.mjs +87 -0
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +23 -4
- 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/dev-delivery-warrant-options.mjs +27 -11
- package/scripts/dev-delivery-warrant-store.mjs +10 -6
- package/scripts/dev-delivery-warrant.mjs +50 -6
- package/scripts/dev-pr-auto-merge.mjs +25 -11
- package/scripts/dev-pr-delivery-warrant.mjs +45 -0
- package/scripts/dev-pr-prequeue-guard.mjs +0 -18
- package/scripts/dev-qualification-patrol.mjs +9 -9
- package/scripts/dispatch-artifact-signing-authority.mjs +1 -1
- package/scripts/generate-channel-promotion-workflow.mjs +2 -2
- 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/project-cut-merge-queue-admission.mjs +394 -0
- package/scripts/promotion-channel-router.mjs +4 -4
- package/scripts/release-candidate-anchor-provenance.mjs +99 -0
- package/scripts/release-tail.mjs +51 -3
- package/scripts/resume-from-candidate-run.mjs +595 -65
- package/scripts/site-capability-metadata.mjs +29 -0
- package/scripts/stable-candidate-qualification.mjs +9 -10
- package/scripts/v4-consumer-policy.mjs +5 -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,9 +3,13 @@ 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 {
|
|
6
|
+
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
8
|
|
|
8
9
|
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
10
|
+
import { compareSemver } from "./publication-registry-hydrate.mjs";
|
|
11
|
+
import { normalizeAnchorProvenance, normalizeCandidateRun, recoverCandidateProvenance } from "./release-candidate-anchor-provenance.mjs";
|
|
12
|
+
export { resolveAnchorRecoveryRequest } from "./release-candidate-anchor-provenance.mjs";
|
|
9
13
|
import {
|
|
10
14
|
generatePublishRequiredArtifacts,
|
|
11
15
|
githubDownload,
|
|
@@ -27,11 +31,17 @@ import {
|
|
|
27
31
|
} from "../packages/core/publication-artifact-candidate.js";
|
|
28
32
|
import { createPublicationSealedBundle } from "../packages/core/publication-sealed-bundle.js";
|
|
29
33
|
import { releaseTransactionStateRef } from "../packages/core/publish-transaction.js";
|
|
34
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
35
|
+
import { validateV4StageCapsule } from "../packages/core/v4-stage-capsule.js";
|
|
36
|
+
import {
|
|
37
|
+
authorizeV4RuntimeSelection,
|
|
38
|
+
createV4RuntimeResumeLineage,
|
|
39
|
+
scanV4RuntimeSelectorPersistence,
|
|
40
|
+
v4RuntimeResumeDocumentRoot,
|
|
41
|
+
verifyV4RuntimeAuthorizationReceipt,
|
|
42
|
+
} from "../packages/core/v4-runtime-ref-resume-authority.js";
|
|
30
43
|
|
|
31
|
-
function env(name, fallback = "") {
|
|
32
|
-
return process.env[name] || fallback;
|
|
33
|
-
}
|
|
34
|
-
|
|
44
|
+
function env(name, fallback = "") { return process.env[name] || fallback; }
|
|
35
45
|
function requiredEnv(name) {
|
|
36
46
|
const value = env(name).trim();
|
|
37
47
|
if (!value) throw new Error(`${name} is required for candidate recovery`);
|
|
@@ -44,9 +54,7 @@ function splitRepository(repository) {
|
|
|
44
54
|
return { owner: match[1], repo: match[2], fullName: `${match[1]}/${match[2]}` };
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
function splitPatterns(value = "") {
|
|
48
|
-
return String(value || "").split(/\r?\n|,/).map((entry) => entry.trim()).filter(Boolean);
|
|
49
|
-
}
|
|
57
|
+
function splitPatterns(value = "") { return String(value || "").split(/\r?\n|,/).map((entry) => entry.trim()).filter(Boolean); }
|
|
50
58
|
|
|
51
59
|
function patternMatcher(pattern) {
|
|
52
60
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
@@ -99,8 +107,376 @@ function readOnlyJson(files, label) {
|
|
|
99
107
|
return JSON.parse(fs.readFileSync(files[0].absolutePath, "utf8"));
|
|
100
108
|
}
|
|
101
109
|
|
|
102
|
-
|
|
103
|
-
|
|
110
|
+
export function trackedRuntimePersistenceScan({
|
|
111
|
+
runtimeRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."),
|
|
112
|
+
} = {}) {
|
|
113
|
+
const paths = execFileSync(
|
|
114
|
+
"git",
|
|
115
|
+
["-C", runtimeRoot, "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: runtimeRoot, 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
|
+
targetVersion,
|
|
203
|
+
alphaSha,
|
|
204
|
+
exactTagSha,
|
|
205
|
+
tagLineage,
|
|
206
|
+
runtimeLineage,
|
|
207
|
+
floatingTargetLineage,
|
|
208
|
+
runtimeSha,
|
|
209
|
+
version,
|
|
210
|
+
transaction,
|
|
211
|
+
main,
|
|
212
|
+
npm,
|
|
213
|
+
}) {
|
|
214
|
+
const channelVersion = npm["dist-tags"]?.alpha;
|
|
215
|
+
let channelDidNotRegress = false;
|
|
216
|
+
try {
|
|
217
|
+
channelDidNotRegress = compareSemver(channelVersion, version) >= 0;
|
|
218
|
+
} catch { channelDidNotRegress = false; }
|
|
219
|
+
if (
|
|
220
|
+
transaction?.target_ref !== targetRef ||
|
|
221
|
+
transaction?.version !== version ||
|
|
222
|
+
exactTagSha !== transaction?.source_sha ||
|
|
223
|
+
!["ahead", "identical"].includes(tagLineage?.status) ||
|
|
224
|
+
!["ahead", "identical"].includes(runtimeLineage?.status) ||
|
|
225
|
+
!["ahead", "identical"].includes(floatingTargetLineage?.status) ||
|
|
226
|
+
!alphaSha ||
|
|
227
|
+
!targetSha ||
|
|
228
|
+
!runtimeSha ||
|
|
229
|
+
channelVersion !== targetVersion ||
|
|
230
|
+
!channelDidNotRegress ||
|
|
231
|
+
!npm.versions?.[channelVersion]?.dist?.integrity ||
|
|
232
|
+
npm.versions?.[version]?.dist?.integrity !== main.digest
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"cross-runtime final public readback does not match durable publication bytes and runtime",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function readPublicResumeState({
|
|
241
|
+
repoInfo,
|
|
242
|
+
targetRef,
|
|
243
|
+
runtimeSha,
|
|
244
|
+
version,
|
|
245
|
+
transaction,
|
|
246
|
+
token,
|
|
247
|
+
apiUrl,
|
|
248
|
+
fetchImpl,
|
|
249
|
+
}) {
|
|
250
|
+
const readRef = async (ref) => (await githubJson({ apiUrl, token, fetchImpl,
|
|
251
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/ref/${ref}` })).object?.sha || "";
|
|
252
|
+
const main = transaction?.artifacts?.find((entry) => entry.kind === "npm" && entry.required !== false);
|
|
253
|
+
if (!main || main.ref !== version || !main.digest)
|
|
254
|
+
throw new Error("cross-runtime recovery requires exact durable npm publication evidence");
|
|
255
|
+
const packageUrl = `https://registry.npmjs.org/${encodeURIComponent(main.name)}`;
|
|
256
|
+
const exactTag = transaction.exact_tag || `v${version}`;
|
|
257
|
+
const [targetSha, alphaSha, exactTagSha, npmResponse] = await Promise.all([
|
|
258
|
+
readRef(`heads/${targetRef}`),
|
|
259
|
+
readRef("tags/v4-alpha"),
|
|
260
|
+
readRef(`tags/${exactTag}`),
|
|
261
|
+
fetchImpl(packageUrl),
|
|
262
|
+
]);
|
|
263
|
+
if (!npmResponse.ok) {
|
|
264
|
+
throw new Error(`npm public readback failed with HTTP ${npmResponse.status}`);
|
|
265
|
+
}
|
|
266
|
+
const npm = await npmResponse.json();
|
|
267
|
+
const [tagLineage, runtimeLineage, floatingTargetLineage, targetPackageFile] = await Promise.all([
|
|
268
|
+
githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${exactTagSha}...${targetSha}` }),
|
|
269
|
+
githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${runtimeSha}...${alphaSha}` }),
|
|
270
|
+
githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${alphaSha}...${targetSha}` }),
|
|
271
|
+
githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/package.json?ref=${encodeURIComponent(targetSha)}` }),
|
|
272
|
+
]);
|
|
273
|
+
if (targetPackageFile?.type !== "file" || targetPackageFile.encoding !== "base64" || !targetPackageFile.content)
|
|
274
|
+
throw new Error("cross-runtime recovery requires protected target version state");
|
|
275
|
+
const targetVersion = JSON.parse(Buffer.from(String(targetPackageFile.content).replace(/\s/g, ""), "base64").toString("utf8")).version;
|
|
276
|
+
validateV4RuntimeResumePublicReadback({ targetRef, targetSha, targetVersion, alphaSha,
|
|
277
|
+
exactTagSha, tagLineage, runtimeLineage, floatingTargetLineage, runtimeSha,
|
|
278
|
+
version, transaction, main, npm });
|
|
279
|
+
const body = {
|
|
280
|
+
schemaVersion: 1,
|
|
281
|
+
contract: "kungfu-buildchain-v4-runtime-resume-public-readback/v1",
|
|
282
|
+
observedAt: new Date().toISOString(),
|
|
283
|
+
repository: repoInfo.fullName,
|
|
284
|
+
version,
|
|
285
|
+
refs: {
|
|
286
|
+
target: { ref: targetRef, sha: targetSha },
|
|
287
|
+
floating: { ref: "v4-alpha", sha: alphaSha },
|
|
288
|
+
exactTag: { ref: exactTag, sha: exactTagSha },
|
|
289
|
+
},
|
|
290
|
+
npm: {
|
|
291
|
+
package: main.name,
|
|
292
|
+
version,
|
|
293
|
+
distTag: "alpha",
|
|
294
|
+
integrity: npm.versions[version].dist.integrity,
|
|
295
|
+
targetVersion, distTagVersion: npm["dist-tags"].alpha,
|
|
296
|
+
distTagIntegrity: npm.versions[npm["dist-tags"].alpha].dist.integrity,
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
return { ...body, root: v4RuntimeResumeDocumentRoot(body) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export const resolveV4RuntimeResumePublicRuntimeSha = (material) =>
|
|
303
|
+
material?.buildAttempt?.runtimeSha || "";
|
|
304
|
+
|
|
305
|
+
function prepareRuntimeResumeEvidence({
|
|
306
|
+
repoInfo,
|
|
307
|
+
targetRef,
|
|
308
|
+
runtimeSha,
|
|
309
|
+
version,
|
|
310
|
+
passport,
|
|
311
|
+
sidecar,
|
|
312
|
+
stageCapsules,
|
|
313
|
+
recovery,
|
|
314
|
+
outputDir,
|
|
315
|
+
}) {
|
|
316
|
+
const authorizationPath = path.resolve(".buildchain/release-candidate/v4-runtime-authorization.json");
|
|
317
|
+
const delegatedRaw = env("BUILDCHAIN_RUNTIME_AUTHORIZATION_JSON").trim();
|
|
318
|
+
const delegatedRoot = env("BUILDCHAIN_RUNTIME_AUTHORIZATION_ROOT").trim();
|
|
319
|
+
const authorizationFileExists = fs.existsSync(authorizationPath);
|
|
320
|
+
if (!authorizationFileExists && (!delegatedRaw || !delegatedRoot)) throw new Error("cross-runtime recovery requires a fresh runtime authorization receipt");
|
|
321
|
+
const delegated = authorizationFileExists ? readOnlyJson([{ absolutePath: authorizationPath }], "runtime authorization") : JSON.parse(delegatedRaw);
|
|
322
|
+
if (!authorizationFileExists && delegated.receiptRoot !== delegatedRoot) throw new Error("fresh recovery runtime authorization handoff root mismatch");
|
|
323
|
+
const delegatedVerification = verifyV4RuntimeAuthorizationReceipt({
|
|
324
|
+
receipt: delegated.receipt,
|
|
325
|
+
receiptRoot: delegated.receiptRoot,
|
|
326
|
+
repository: repoInfo.fullName,
|
|
327
|
+
runtimeSha,
|
|
328
|
+
});
|
|
329
|
+
if (!delegatedVerification.ok) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`fresh recovery runtime authorization rejected: ${delegatedVerification.failures.join(", ")}`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const policy = passport.consumerPolicy.receipt;
|
|
335
|
+
const authorization = authorizeV4RuntimeSelection({
|
|
336
|
+
repository: repoInfo.fullName,
|
|
337
|
+
eventName: "workflow_dispatch",
|
|
338
|
+
mode: "resume",
|
|
339
|
+
actor: delegated.receipt.actor.login,
|
|
340
|
+
actorPermission: delegated.receipt.actor.permission,
|
|
341
|
+
reason:
|
|
342
|
+
`resume sealed candidate run ${passport.workflow.runId} from a tree-equivalent protected source`,
|
|
343
|
+
authorizedAt: new Date().toISOString(),
|
|
344
|
+
sourceSha: sidecar.source.sha,
|
|
345
|
+
sourceTreeSha: sidecar.source.treeSha,
|
|
346
|
+
requestedRef: delegated.receipt.request.ref,
|
|
347
|
+
resolvedRuntimeSha: runtimeSha,
|
|
348
|
+
approvedRefReadbacks: delegated.receipt.runtime.reachableFrom,
|
|
349
|
+
stableContractLockRoot: policy.contractLocks.stable.root,
|
|
350
|
+
alphaContractLockRoot: policy.contractLocks.alpha.root,
|
|
351
|
+
consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
|
|
352
|
+
persistenceScan: trackedRuntimePersistenceScan(),
|
|
353
|
+
});
|
|
354
|
+
const planBody = {
|
|
355
|
+
schemaVersion: 1,
|
|
356
|
+
contract: "kungfu-buildchain-v4-runtime-resume-plan/v1",
|
|
357
|
+
originalRunId: String(passport.workflow.runId),
|
|
358
|
+
resumeRunId: String(env("GITHUB_RUN_ID")),
|
|
359
|
+
recoveryReceiptRoot: recovery.receipt.root,
|
|
360
|
+
requiredPlatforms: stageCapsules.map((entry) => entry.platform),
|
|
361
|
+
reusedCapsuleRoots: stageCapsules.map((entry) => entry.capsuleRoot),
|
|
362
|
+
rebuildPlatforms: [],
|
|
363
|
+
skippedBuildStages: recovery.receipt.skippedBuildStages,
|
|
364
|
+
};
|
|
365
|
+
const plan = { ...planBody, root: v4RuntimeResumeDocumentRoot(planBody) };
|
|
366
|
+
const materialBody = {
|
|
367
|
+
schemaVersion: 1,
|
|
368
|
+
contract: "kungfu-buildchain-v4-runtime-resume-material/v1",
|
|
369
|
+
repository: repoInfo.fullName,
|
|
370
|
+
targetRef,
|
|
371
|
+
runtimeSha,
|
|
372
|
+
version,
|
|
373
|
+
authorization: authorization.receipt,
|
|
374
|
+
authorizationRoot: authorization.receiptRoot,
|
|
375
|
+
buildAttempt: sidecar.buildAttempt,
|
|
376
|
+
resumeAttempt: {
|
|
377
|
+
id: `github-run:${env("GITHUB_RUN_ID")}:attempt:${env("GITHUB_RUN_ATTEMPT", "1")}`,
|
|
378
|
+
runtimeSha,
|
|
379
|
+
},
|
|
380
|
+
source: sidecar.source,
|
|
381
|
+
consumerPolicyReceiptRoot: sidecar.consumerPolicyReceiptRoot,
|
|
382
|
+
requiredPlatforms: stageCapsules.map((entry) => entry.platform),
|
|
383
|
+
stageCapsules,
|
|
384
|
+
resumePlanRoot: plan.root,
|
|
385
|
+
floatingRefBefore: {
|
|
386
|
+
ref: "v4-alpha",
|
|
387
|
+
sha: sidecar.buildAttempt.runtimeSha,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
const material = {
|
|
391
|
+
...materialBody,
|
|
392
|
+
root: v4RuntimeResumeDocumentRoot(materialBody),
|
|
393
|
+
};
|
|
394
|
+
for (const [name, value] of [
|
|
395
|
+
["v4-runtime-resume-plan.json", plan],
|
|
396
|
+
["v4-runtime-resume-material.json", material],
|
|
397
|
+
]) {
|
|
398
|
+
fs.writeFileSync(
|
|
399
|
+
path.join(outputDir, name),
|
|
400
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return path.join(outputDir, "v4-runtime-resume-evidence.json");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function finalizeRuntimeResumeEvidence({
|
|
407
|
+
materialPath,
|
|
408
|
+
transaction,
|
|
409
|
+
token = env("GITHUB_TOKEN", env("INPUT_TOKEN")),
|
|
410
|
+
apiUrl = env("GITHUB_API_URL", "https://api.github.com"),
|
|
411
|
+
fetchImpl = globalThis.fetch,
|
|
412
|
+
outputDir = path.dirname(materialPath),
|
|
413
|
+
}) {
|
|
414
|
+
const material = JSON.parse(fs.readFileSync(materialPath, "utf8"));
|
|
415
|
+
const body = { ...material };
|
|
416
|
+
delete body.root;
|
|
417
|
+
if (
|
|
418
|
+
material.contract !== "kungfu-buildchain-v4-runtime-resume-material/v1" ||
|
|
419
|
+
material.root !== v4RuntimeResumeDocumentRoot(body)
|
|
420
|
+
) {
|
|
421
|
+
throw new Error("cross-runtime resume material root mismatch");
|
|
422
|
+
}
|
|
423
|
+
const repoInfo = splitRepository(material.repository);
|
|
424
|
+
const version = env("BUILDCHAIN_RELEASE_VERSION", material.version);
|
|
425
|
+
const targetRef = env("BUILDCHAIN_RELEASE_TARGET_REF", material.targetRef);
|
|
426
|
+
if (
|
|
427
|
+
version !== material.version ||
|
|
428
|
+
targetRef !== material.targetRef ||
|
|
429
|
+
transaction?.version !== version ||
|
|
430
|
+
transaction?.state !== "complete"
|
|
431
|
+
) {
|
|
432
|
+
throw new Error(
|
|
433
|
+
"cross-runtime resume finalization does not match the completed publication transaction",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
const readback = await readPublicResumeState({
|
|
437
|
+
repoInfo,
|
|
438
|
+
targetRef,
|
|
439
|
+
runtimeSha: resolveV4RuntimeResumePublicRuntimeSha(material),
|
|
440
|
+
version,
|
|
441
|
+
transaction,
|
|
442
|
+
token,
|
|
443
|
+
apiUrl,
|
|
444
|
+
fetchImpl,
|
|
445
|
+
});
|
|
446
|
+
const resumed = createV4RuntimeResumeLineage({
|
|
447
|
+
authorization: material.authorization,
|
|
448
|
+
authorizationRoot: material.authorizationRoot,
|
|
449
|
+
buildAttempt: material.buildAttempt,
|
|
450
|
+
resumeAttempt: material.resumeAttempt,
|
|
451
|
+
source: material.source,
|
|
452
|
+
consumerPolicyReceiptRoot: material.consumerPolicyReceiptRoot,
|
|
453
|
+
requiredPlatforms: material.requiredPlatforms,
|
|
454
|
+
stageCapsules: material.stageCapsules,
|
|
455
|
+
resumePlanRoot: material.resumePlanRoot,
|
|
456
|
+
finalPublicReadbackRoot: readback.root,
|
|
457
|
+
floatingRefBefore: material.floatingRefBefore,
|
|
458
|
+
floatingRefAfter: { ref: "v4-alpha", sha: readback.refs.floating.sha },
|
|
459
|
+
});
|
|
460
|
+
const evidence = {
|
|
461
|
+
authorization: material.authorization,
|
|
462
|
+
authorizationRoot: material.authorizationRoot,
|
|
463
|
+
lineage: resumed.lineage,
|
|
464
|
+
lineageRoot: resumed.lineageRoot,
|
|
465
|
+
};
|
|
466
|
+
const evidencePath = path.join(outputDir, "v4-runtime-resume-evidence.json");
|
|
467
|
+
for (const [name, value] of [
|
|
468
|
+
["v4-runtime-resume-public-readback.json", readback],
|
|
469
|
+
[path.basename(evidencePath), evidence],
|
|
470
|
+
]) {
|
|
471
|
+
fs.writeFileSync(
|
|
472
|
+
path.join(outputDir, name),
|
|
473
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return { evidence, path: evidencePath, readback };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function readTransactionAtRef({ repoInfo, apiUrl, token, fetchImpl, stateRef }) {
|
|
104
480
|
const response = await githubJson({
|
|
105
481
|
apiUrl,
|
|
106
482
|
token,
|
|
@@ -109,10 +485,65 @@ async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, ver
|
|
|
109
485
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/state.json?ref=${encodeURIComponent(stateRef)}`,
|
|
110
486
|
});
|
|
111
487
|
if (!response) return undefined;
|
|
112
|
-
if (
|
|
113
|
-
|
|
488
|
+
if (
|
|
489
|
+
response.type !== "file" ||
|
|
490
|
+
response.encoding !== "base64" ||
|
|
491
|
+
!response.content
|
|
492
|
+
) {
|
|
493
|
+
throw new Error(
|
|
494
|
+
`durable transaction ${stateRef} did not expose a base64 state.json file`,
|
|
495
|
+
);
|
|
114
496
|
}
|
|
115
|
-
return JSON.parse(
|
|
497
|
+
return JSON.parse(
|
|
498
|
+
Buffer.from(
|
|
499
|
+
String(response.content).replace(/\s/g, ""),
|
|
500
|
+
"base64",
|
|
501
|
+
).toString("utf8"),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function readExistingTransaction({
|
|
506
|
+
repoInfo,
|
|
507
|
+
apiUrl,
|
|
508
|
+
token,
|
|
509
|
+
fetchImpl,
|
|
510
|
+
version,
|
|
511
|
+
}) {
|
|
512
|
+
return readTransactionAtRef({
|
|
513
|
+
repoInfo,
|
|
514
|
+
apiUrl,
|
|
515
|
+
token,
|
|
516
|
+
fetchImpl,
|
|
517
|
+
stateRef: releaseTransactionStateRef(version),
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function readExistingTransactionById({
|
|
522
|
+
repoInfo,
|
|
523
|
+
apiUrl,
|
|
524
|
+
token,
|
|
525
|
+
fetchImpl,
|
|
526
|
+
transactionId,
|
|
527
|
+
}) {
|
|
528
|
+
if (!transactionId) return undefined;
|
|
529
|
+
const statePrefix = "buildchain/release-state/";
|
|
530
|
+
const refs = await githubJson({
|
|
531
|
+
apiUrl,
|
|
532
|
+
token,
|
|
533
|
+
fetchImpl,
|
|
534
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/matching-refs/heads/${statePrefix}`,
|
|
535
|
+
});
|
|
536
|
+
for (const ref of refs) {
|
|
537
|
+
const transaction = await readTransactionAtRef({
|
|
538
|
+
repoInfo,
|
|
539
|
+
apiUrl,
|
|
540
|
+
token,
|
|
541
|
+
fetchImpl,
|
|
542
|
+
stateRef: String(ref.ref).replace(/^refs\/heads\//, ""),
|
|
543
|
+
});
|
|
544
|
+
if (transaction?.id === transactionId) return transaction;
|
|
545
|
+
}
|
|
546
|
+
throw new Error(`durable publication transaction ${transactionId} was not found`);
|
|
116
547
|
}
|
|
117
548
|
|
|
118
549
|
function outputPath(filePath) {
|
|
@@ -264,14 +695,26 @@ export function createRecoveredPublicationCandidate({
|
|
|
264
695
|
};
|
|
265
696
|
return { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
|
|
266
697
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
698
|
+
export function resolveRecoveredPublicationVersion({ artifactVersion, channel, rematerializeOnResume = false } = {}) {
|
|
699
|
+
const version = String(artifactVersion || "").trim(), match = version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u);
|
|
700
|
+
if (!match) throw new Error(`recovered npm artifact has invalid publication version: ${version || "<empty>"}`);
|
|
701
|
+
return channel === "release" && rematerializeOnResume ? match[1] : version;
|
|
702
|
+
}
|
|
703
|
+
export function resolveRecoveredCandidateVersion({ artifactVersion, publicationVersion, channel, rematerializeOnResume = false, targetRef = "", candidateRef = "" } = {}) {
|
|
704
|
+
const version = String(artifactVersion || "").trim(), target = String(targetRef || "").replace(/^refs\/heads\//u, ""), candidate = String(candidateRef || "").replace(/^refs\/heads\//u, "");
|
|
705
|
+
if (channel !== "release" || !rematerializeOnResume) return version;
|
|
706
|
+
const prefix = `publish-gate/${target}/`;
|
|
707
|
+
if (!target || !candidate.startsWith(prefix)) throw new Error(`stable recovery candidate ref must descend from ${prefix || "publish-gate/<target>/"}`);
|
|
708
|
+
const candidateVersion = candidate.slice(prefix.length);
|
|
709
|
+
if (!/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(candidateVersion)) throw new Error(`stable recovery candidate ref must bind an exact alpha version, got ${candidateVersion || "<empty>"}`);
|
|
710
|
+
if (candidateVersion.replace(/-alpha\.\d+$/u, "") !== publicationVersion) throw new Error(`stable recovery candidate ${candidateVersion} does not match publication ${publicationVersion || "<empty>"}`);
|
|
711
|
+
if ((version.match(/^(\d+\.\d+\.\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u)?.[1] || "") !== publicationVersion) throw new Error(`recovered npm artifact version ${version || "<empty>"} does not match publication ${publicationVersion}`);
|
|
712
|
+
return candidateVersion;
|
|
713
|
+
}
|
|
714
|
+
export function createRecoveredPublication({ downloads, bundleRoot, repository, passport, candidateRuntimeSha, publishArtifactKind, publishPackageMain, releasePatterns, platformManifests, channel, targetRef = "", candidateRef = "", rematerializeOnResume = false }) {
|
|
715
|
+
const allFiles = downloads.flatMap((download) => download.files.map((file) => ({ path: path.relative(bundleRoot, file.absolutePath).split(path.sep).join("/"),
|
|
716
|
+
size: file.size, sha256: file.sha256.replace(/^sha256:/, ""), absolutePath: file.absolutePath })))
|
|
717
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
275
718
|
const kind = String(publishArtifactKind || "npm");
|
|
276
719
|
const releaseMatchers = splitPatterns(releasePatterns).map(patternMatcher);
|
|
277
720
|
const releaseAssets = allFiles.filter((file) => releaseMatchers.some((matcher) => matcher.test(path.basename(file.path))));
|
|
@@ -279,12 +722,8 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
|
|
|
279
722
|
createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
|
|
280
723
|
const version = String(passport.target?.version || "").trim();
|
|
281
724
|
if (!version) throw new Error("candidate recovery requires a passport publication version");
|
|
282
|
-
return {
|
|
283
|
-
|
|
284
|
-
npmArtifacts: [],
|
|
285
|
-
allFiles,
|
|
286
|
-
releaseAssets,
|
|
287
|
-
version,
|
|
725
|
+
return { manifest: undefined, npmArtifacts: [], allFiles, releaseAssets,
|
|
726
|
+
version, candidateVersion: version,
|
|
288
727
|
publishRequiredArtifacts: generatePublishRequiredArtifacts({ manifests: platformManifests, version, kind }),
|
|
289
728
|
};
|
|
290
729
|
}
|
|
@@ -296,12 +735,9 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
|
|
|
296
735
|
const npmReleaseAssets = allFiles.filter((file) => releaseMatchers.length
|
|
297
736
|
? releaseMatchers.some((matcher) => matcher.test(path.basename(file.path)))
|
|
298
737
|
: file.path.toLowerCase().endsWith(".tgz"));
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
passport,
|
|
303
|
-
candidateRuntimeSha,
|
|
304
|
-
});
|
|
738
|
+
const publicationVersion = resolveRecoveredPublicationVersion({ artifactVersion: main.metadata.ref, channel, rematerializeOnResume });
|
|
739
|
+
const candidateVersion = resolveRecoveredCandidateVersion({ artifactVersion: main.metadata.ref, publicationVersion, channel, rematerializeOnResume, targetRef, candidateRef });
|
|
740
|
+
const candidate = createRecoveredPublicationCandidate({ allFiles, repository, passport, candidateRuntimeSha });
|
|
305
741
|
const manifest = createPublicationSealedBundle({
|
|
306
742
|
candidate,
|
|
307
743
|
packageName: main.metadata.name,
|
|
@@ -315,12 +751,12 @@ export function createRecoveredPublication({ downloads, bundleRoot, repository,
|
|
|
315
751
|
npmArtifacts,
|
|
316
752
|
allFiles,
|
|
317
753
|
releaseAssets: npmReleaseAssets,
|
|
318
|
-
version:
|
|
754
|
+
version: publicationVersion, candidateVersion,
|
|
319
755
|
publishRequiredArtifacts: generatePublishRequiredArtifacts({
|
|
320
756
|
kind: "npm",
|
|
321
757
|
tarballPaths: npmArtifacts.map((entry) => entry.file.absolutePath),
|
|
322
758
|
mainPackage: publishPackageMain,
|
|
323
|
-
}),
|
|
759
|
+
}).map((artifact) => rematerializeOnResume ? { ...artifact, ref: publicationVersion } : artifact),
|
|
324
760
|
};
|
|
325
761
|
}
|
|
326
762
|
|
|
@@ -349,6 +785,14 @@ async function recoverCandidateEvidence({
|
|
|
349
785
|
for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
350
786
|
const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
|
|
351
787
|
const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
|
|
788
|
+
const sidecarFiles = initialDownloads[0].files.filter(
|
|
789
|
+
(file) =>
|
|
790
|
+
path.basename(file.path) === "release-candidate-stage-capsules.json",
|
|
791
|
+
);
|
|
792
|
+
const stageCapsuleSidecar =
|
|
793
|
+
sidecarFiles.length === 1
|
|
794
|
+
? readOnlyJson(sidecarFiles, "release-candidate-stage-capsules.json")
|
|
795
|
+
: undefined;
|
|
352
796
|
const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
|
|
353
797
|
const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
|
|
354
798
|
if (chosen.length !== requiredNames.size) {
|
|
@@ -360,7 +804,7 @@ async function recoverCandidateEvidence({
|
|
|
360
804
|
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
805
|
return {
|
|
362
806
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
363
|
-
passport, buildSummary, chosen, downloads, publicationNames,
|
|
807
|
+
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
364
808
|
};
|
|
365
809
|
}
|
|
366
810
|
|
|
@@ -370,13 +814,58 @@ async function resolveTargetAdvance({ observedTargetSha, targetSha, transactionI
|
|
|
370
814
|
return { status: comparison.status, mergeIsAncestor: ["ahead", "identical"].includes(comparison.status) };
|
|
371
815
|
}
|
|
372
816
|
|
|
817
|
+
export async function resolveRecoveryTransaction({
|
|
818
|
+
repoInfo,
|
|
819
|
+
apiUrl,
|
|
820
|
+
token,
|
|
821
|
+
fetchImpl,
|
|
822
|
+
transactionId,
|
|
823
|
+
publicationVersion,
|
|
824
|
+
}) {
|
|
825
|
+
let transaction = publicationVersion
|
|
826
|
+
? await readExistingTransaction({
|
|
827
|
+
repoInfo,
|
|
828
|
+
apiUrl,
|
|
829
|
+
token,
|
|
830
|
+
fetchImpl,
|
|
831
|
+
version: publicationVersion,
|
|
832
|
+
})
|
|
833
|
+
: undefined;
|
|
834
|
+
if (!transaction) {
|
|
835
|
+
transaction = transactionId
|
|
836
|
+
? await readExistingTransactionById({ repoInfo, apiUrl, token, fetchImpl, transactionId })
|
|
837
|
+
: undefined;
|
|
838
|
+
}
|
|
839
|
+
return { version: transaction?.version || publicationVersion, transaction };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function resolveRecoveredStageCapsules({
|
|
843
|
+
candidateRuntimeSha,
|
|
844
|
+
runtimeSha,
|
|
845
|
+
sidecar,
|
|
846
|
+
passport,
|
|
847
|
+
downloads,
|
|
848
|
+
}) {
|
|
849
|
+
if (candidateRuntimeSha === runtimeSha) return [];
|
|
850
|
+
return verifyReleaseCandidateStageCapsules({ sidecar, passport, downloads });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot) {
|
|
854
|
+
const runId = String(candidateRunId || "").trim();
|
|
855
|
+
if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
|
|
856
|
+
if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
|
|
857
|
+
throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
|
|
858
|
+
}
|
|
859
|
+
return runId;
|
|
860
|
+
}
|
|
861
|
+
|
|
373
862
|
export async function resumeFromCandidateRun({
|
|
374
863
|
repository, targetRepository = repository, candidateRunId,
|
|
375
864
|
expectedWorkflowFile, expectedWorkflowName,
|
|
376
865
|
channel, targetRef, targetSha,
|
|
377
866
|
expectedSourceTree = "", expectedCandidateRoot = "",
|
|
378
867
|
candidateRuntimeSha, runtimeSha,
|
|
379
|
-
transactionId = "", artifactName = "", artifactPatterns = "", releasePatterns = "",
|
|
868
|
+
transactionId = "", artifactName = "", artifactPatterns = "", releasePatterns = "", rematerializeOnResume = false,
|
|
380
869
|
requiredArtifactCount = 0,
|
|
381
870
|
publishArtifactKind = "npm", publishPackageMain = "",
|
|
382
871
|
outputDir = ".buildchain/release-candidate-recovery",
|
|
@@ -384,21 +873,23 @@ export async function resumeFromCandidateRun({
|
|
|
384
873
|
recoveryRunId = env("GITHUB_RUN_ID"), fetchImpl = globalThis.fetch,
|
|
385
874
|
} = {}) {
|
|
386
875
|
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
|
-
}
|
|
876
|
+
const runId = validateRecoveryCoordinates(candidateRunId, expectedSourceTree, expectedCandidateRoot);
|
|
392
877
|
const archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-resume-"));
|
|
393
878
|
try {
|
|
394
879
|
const {
|
|
395
880
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
396
|
-
passport, buildSummary, chosen, downloads, publicationNames,
|
|
881
|
+
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
397
882
|
} = await recoverCandidateEvidence({
|
|
398
883
|
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
399
884
|
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
400
885
|
});
|
|
401
|
-
const
|
|
886
|
+
const { anchorProvenance, provenancePassport } = await recoverCandidateProvenance({
|
|
887
|
+
passport, buildSummary, transactionId, repoInfo, artifactName,
|
|
888
|
+
expectedWorkflowFile, expectedWorkflowName, channel, apiUrl, token,
|
|
889
|
+
fetchImpl, archiveDir, bundleRoot, githubJson, selectReleaseCandidateArtifacts,
|
|
890
|
+
downloadArtifact, readOnlyJson,
|
|
891
|
+
});
|
|
892
|
+
const prNumber = Number(provenancePassport.pullRequest?.number || 0);
|
|
402
893
|
if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
|
|
403
894
|
const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
|
|
404
895
|
const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
|
|
@@ -417,10 +908,26 @@ export async function resumeFromCandidateRun({
|
|
|
417
908
|
publishArtifactKind,
|
|
418
909
|
publishPackageMain,
|
|
419
910
|
releasePatterns,
|
|
420
|
-
platformManifests: platformManifestEvidence.manifests,
|
|
911
|
+
platformManifests: platformManifestEvidence.manifests, channel, targetRef, candidateRef: pullRequest.head?.ref || "", rematerializeOnResume,
|
|
912
|
+
});
|
|
913
|
+
const stageCapsules = resolveRecoveredStageCapsules({
|
|
914
|
+
candidateRuntimeSha,
|
|
915
|
+
runtimeSha,
|
|
916
|
+
sidecar: stageCapsuleSidecar,
|
|
917
|
+
passport,
|
|
918
|
+
downloads,
|
|
919
|
+
});
|
|
920
|
+
const {
|
|
921
|
+
version: publicationVersion,
|
|
922
|
+
transaction: existingTransaction,
|
|
923
|
+
} = await resolveRecoveryTransaction({
|
|
924
|
+
repoInfo,
|
|
925
|
+
apiUrl,
|
|
926
|
+
token,
|
|
927
|
+
fetchImpl,
|
|
928
|
+
transactionId,
|
|
929
|
+
publicationVersion: publication.version,
|
|
421
930
|
});
|
|
422
|
-
const candidateVersion = publication.version;
|
|
423
|
-
const existingTransaction = await readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version: candidateVersion });
|
|
424
931
|
const targetAdvance = await resolveTargetAdvance({
|
|
425
932
|
observedTargetSha, targetSha, transactionId, existingTransaction,
|
|
426
933
|
repoInfo, apiUrl, token, fetchImpl,
|
|
@@ -448,20 +955,9 @@ export async function resumeFromCandidateRun({
|
|
|
448
955
|
expectedRuntimeSha: candidateRuntimeSha,
|
|
449
956
|
expectedTransactionId: transactionId,
|
|
450
957
|
existingTransaction,
|
|
451
|
-
run:
|
|
452
|
-
id: String(run.id),
|
|
453
|
-
repository: repoInfo.fullName,
|
|
454
|
-
headRepository: run.head_repository?.full_name || "",
|
|
455
|
-
status: run.status,
|
|
456
|
-
conclusion: run.conclusion,
|
|
457
|
-
event: run.event,
|
|
458
|
-
path: run.path,
|
|
459
|
-
name: run.name,
|
|
460
|
-
headSha: run.head_sha || "",
|
|
461
|
-
headBranch: run.head_branch || "",
|
|
462
|
-
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
463
|
-
},
|
|
958
|
+
run: normalizeCandidateRun(run, repoInfo.fullName),
|
|
464
959
|
workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
|
|
960
|
+
anchorProvenance: normalizeAnchorProvenance(anchorProvenance, repoInfo.fullName),
|
|
465
961
|
pullRequest: {
|
|
466
962
|
number: pullRequest.number,
|
|
467
963
|
merged: pullRequest.merged === true,
|
|
@@ -480,7 +976,7 @@ export async function resumeFromCandidateRun({
|
|
|
480
976
|
platformManifestEvidence: platformManifestEvidence.evidence,
|
|
481
977
|
productPayloadManifests,
|
|
482
978
|
artifacts: downloads.map((download) => download.record),
|
|
483
|
-
publicationVersion
|
|
979
|
+
publicationVersion,
|
|
484
980
|
currentToolingSha: runtimeSha,
|
|
485
981
|
recoveryRunId,
|
|
486
982
|
});
|
|
@@ -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: publicationVersion,
|
|
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,
|
|
@@ -498,7 +1007,7 @@ export async function resumeFromCandidateRun({
|
|
|
498
1007
|
repository: repoInfo.fullName,
|
|
499
1008
|
run: { id: runId, url: run.html_url || "", name: run.name },
|
|
500
1009
|
artifacts: { passport: selected.passport.name, summary: selected.summary.name, payloads: chosen.map((artifact) => artifact.name), sourceSha: selected.sourceSha },
|
|
501
|
-
version: candidateVersion,
|
|
1010
|
+
version: publicationVersion, candidateVersion: publication.candidateVersion,
|
|
502
1011
|
candidateRoot: recovery.receipt.recovered.candidateRoot,
|
|
503
1012
|
artifactRoot: recovery.receipt.recovered.artifactRoot,
|
|
504
1013
|
receipt: recovery.receipt,
|
|
@@ -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 {
|
|
@@ -536,7 +1048,7 @@ export async function resumeFromCandidateRunCli() {
|
|
|
536
1048
|
expectedCandidateRoot: env("BUILDCHAIN_RESUME_EXPECTED_CANDIDATE_ROOT"),
|
|
537
1049
|
candidateRuntimeSha: requiredEnv("BUILDCHAIN_RESUME_EXPECTED_CANDIDATE_RUNTIME_SHA"),
|
|
538
1050
|
runtimeSha: requiredEnv("BUILDCHAIN_RESUME_RUNTIME_SHA"),
|
|
539
|
-
transactionId: env("BUILDCHAIN_RESUME_TRANSACTION_ID"),
|
|
1051
|
+
transactionId: env("BUILDCHAIN_RESUME_TRANSACTION_ID"), rematerializeOnResume: env("BUILDCHAIN_PUBLISH_REMATERIALIZE_ON_RESUME") === "true",
|
|
540
1052
|
artifactName: env("BUILDCHAIN_ARTIFACT_NAME"),
|
|
541
1053
|
artifactPatterns: env("BUILDCHAIN_ARTIFACT_PATTERNS"),
|
|
542
1054
|
releasePatterns: env("BUILDCHAIN_GITHUB_RELEASE_PAYLOAD_PATTERNS"),
|
|
@@ -550,7 +1062,7 @@ export async function resumeFromCandidateRunCli() {
|
|
|
550
1062
|
"release-candidate-action": result.action,
|
|
551
1063
|
"release-candidate-passport-path": result.paths.passport,
|
|
552
1064
|
"release-candidate-build-summary-path": result.paths.buildSummary,
|
|
553
|
-
"release-candidate-version": result.version,
|
|
1065
|
+
"release-candidate-version": result.candidateVersion, "release-candidate-publication-version": result.version,
|
|
554
1066
|
"release-candidate-source-sha": result.artifacts.sourceSha,
|
|
555
1067
|
"release-candidate-artifact": result.artifacts.passport,
|
|
556
1068
|
"release-candidate-build-summary-artifact": result.artifacts.summary,
|
|
@@ -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}`);
|