@kungfu-tech/buildchain 3.0.6-alpha.1 → 3.0.6-alpha.3
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/README.md +4 -4
- package/actions/promote-buildchain-ref/README.md +8 -0
- package/bin/buildchain.mjs +13 -1
- package/contracts/auditable-demo-scenario-v1.schema.json +52 -0
- package/contracts/release-candidate-recovery-v1.schema.json +102 -0
- package/dist/site/buildchain-contract.json +129 -34
- package/dist/site/buildchain-site.json +171 -45
- package/dist/site/capability-registry.json +5 -5
- package/dist/site/cli-registry.json +40 -4
- package/dist/site/controller-registry.json +56 -4
- package/dist/site/kfd-claims.json +184 -20
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -10
- package/dist/site/node-api-registry.json +1242 -121
- package/dist/site/page-registry.json +157 -31
- package/dist/site/public-surface-audit.json +422 -21
- package/dist/site/publication-authority-registry.json +66 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/release-provenance.json +2 -0
- package/dist/site/site-manifest.json +14 -14
- package/dist/site/workflow-registry.json +197 -15
- package/docs/MAP.md +1 -0
- package/docs/auditable-demo.md +58 -11
- package/docs/aws-us-elastic-runner-burst-plane.md +23 -19
- package/docs/cli-reference.md +154 -0
- package/docs/dev-alpha-candidate-patrol.md +13 -5
- package/docs/dev-delivery-warrant.md +158 -0
- package/docs/node-api-reference.md +96 -38
- package/docs/publication-authority.md +11 -0
- package/docs/publish-transaction.md +10 -1
- package/docs/release-candidate.md +106 -12
- package/docs/release-governance.md +52 -0
- package/docs/reusable-build-surface.md +11 -1
- package/docs/shifu-gate-profiles.md +12 -1
- package/docs/versioning.md +2 -0
- package/package.json +3 -1
- package/packages/core/buildchain-contract.js +30 -2
- package/packages/core/buildchain-publication-authority.js +3 -1
- package/packages/core/channel-candidate.js +2 -21
- package/packages/core/channel-promotion-baseline.js +199 -0
- package/packages/core/dev-delivery-candidate-identity.js +94 -0
- package/packages/core/dev-delivery-common.js +73 -0
- package/packages/core/dev-delivery-proof.js +252 -0
- package/packages/core/dev-delivery-warrant-cancellation.js +94 -0
- package/packages/core/dev-delivery-warrant-settlement.js +73 -0
- package/packages/core/dev-delivery-warrant.js +591 -0
- package/packages/core/index.js +7 -0
- package/packages/core/release-candidate-recovery.js +386 -0
- package/scripts/auditable-demo-bundle-verification.mjs +148 -0
- package/scripts/auditable-demo-platform.mjs +86 -50
- package/scripts/auditable-demo-presentation.mjs +83 -0
- package/scripts/auditable-demo-renditions.mjs +264 -0
- package/scripts/auditable-demo.mjs +24 -30
- package/scripts/build-contract-core.mjs +58 -3
- package/scripts/buildchain-cli-help.mjs +8 -0
- package/scripts/buildchain-patrol.mjs +9 -0
- package/scripts/check-inventory.mjs +13 -2
- package/scripts/dev-alpha-candidate-patrol.mjs +45 -48
- package/scripts/dev-delivery-proof.mjs +193 -0
- package/scripts/dev-delivery-warrant.mjs +426 -0
- package/scripts/dev-pr-auto-merge.mjs +488 -46
- package/scripts/dev-pr-delivery-warrant.mjs +209 -0
- package/scripts/gate-profile-core.mjs +24 -0
- package/scripts/generate-site-bundle.mjs +2 -2
- package/scripts/git-fetch-process-tree.mjs +142 -0
- package/scripts/lifecycle-substage-evidence.mjs +274 -0
- package/scripts/locked-source-checkout.mjs +6 -3
- package/scripts/release-candidate-resolver.mjs +35 -3
- package/scripts/resolve-artifact-transfer-mode.mjs +9 -0
- package/scripts/resolve-build-contract.mjs +7 -0
- package/scripts/resume-from-candidate-run.mjs +498 -0
- package/scripts/route-offline-runners.mjs +1 -0
- package/scripts/run-lifecycle-core.mjs +9 -9
- package/scripts/shifu-gate-profile.mjs +10 -16
- package/scripts/site-capability-metadata.mjs +15 -0
|
@@ -4,6 +4,7 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { runGitFetchSync } from "./git-fetch-process-tree.mjs";
|
|
7
8
|
|
|
8
9
|
export const LOCKED_SOURCE_CHECKOUT_CONTRACT = "kungfu-buildchain-locked-source-checkout-cache";
|
|
9
10
|
export const ISOLATED_GIT_GLOBAL_CONFIG = process.platform === "win32" ? "NUL" : "/dev/null";
|
|
@@ -111,9 +112,10 @@ function ensureCheckoutTarget(targetPath, workspace) {
|
|
|
111
112
|
|
|
112
113
|
function git(args, { cwd, env = {}, timeoutMs = 60000, stdio = ["ignore", "pipe", "pipe"] } = {}) {
|
|
113
114
|
try {
|
|
114
|
-
const
|
|
115
|
+
const commandEnv = { ...process.env, ...env };
|
|
116
|
+
const output = args[0] === "fetch" ? runGitFetchSync({ args, cwd, env: commandEnv, timeoutMs, stdio }) : execFileSync("git", args, {
|
|
115
117
|
cwd,
|
|
116
|
-
env:
|
|
118
|
+
env: commandEnv,
|
|
117
119
|
encoding: "utf8",
|
|
118
120
|
stdio,
|
|
119
121
|
timeout: timeoutMs,
|
|
@@ -233,7 +235,8 @@ function checkoutFetchedCommit(targetPath, sha, timeoutMs) {
|
|
|
233
235
|
function retryableGitFetchError(error) {
|
|
234
236
|
const code = String(error?.code || "").toUpperCase();
|
|
235
237
|
if (["ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EAI_AGAIN", "ENETUNREACH", "EPIPE"].includes(code)) return true;
|
|
236
|
-
|
|
238
|
+
const commandOutput = [error?.stderr, error?.stdout].filter(Boolean).map(String).join("\n").trim();
|
|
239
|
+
return /timed?\s*out|timeout|connection (?:reset|refused)|remote end hung up|early eof|rpc failed|http (?:429|5\d\d)|temporary failure|network is unreachable/i.test(commandOutput || String(error?.message || error || ""));
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
export function fetchSourceCommit({
|
|
@@ -49,7 +49,7 @@ function githubHeaders(token) {
|
|
|
49
49
|
return headers;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
async function githubJson({ apiUrl, token, method = "GET", path: requestPath, fetchImpl = globalThis.fetch }) {
|
|
52
|
+
export async function githubJson({ apiUrl, token, method = "GET", path: requestPath, fetchImpl = globalThis.fetch, allowNotFound = false }) {
|
|
53
53
|
if (typeof fetchImpl !== "function") {
|
|
54
54
|
throw new Error("fetch is required to resolve release candidate artifacts");
|
|
55
55
|
}
|
|
@@ -57,13 +57,14 @@ async function githubJson({ apiUrl, token, method = "GET", path: requestPath, fe
|
|
|
57
57
|
const response = await fetchImpl(url, { method, headers: githubHeaders(token) });
|
|
58
58
|
const text = await response.text();
|
|
59
59
|
const body = text ? JSON.parse(text) : {};
|
|
60
|
+
if (allowNotFound && response.status === 404) return undefined;
|
|
60
61
|
if (!response.ok) {
|
|
61
62
|
throw new Error(`GitHub API ${method} ${requestPath} failed with ${response.status}: ${body.message || text}`);
|
|
62
63
|
}
|
|
63
64
|
return body;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
async function githubDownload({ apiUrl, token, path: requestPath, outputPath, fetchImpl = globalThis.fetch }) {
|
|
67
|
+
export async function githubDownload({ apiUrl, token, path: requestPath, outputPath, fetchImpl = globalThis.fetch }) {
|
|
67
68
|
const url = `${String(apiUrl || "https://api.github.com").replace(/\/+$/, "")}${requestPath}`;
|
|
68
69
|
const response = await fetchImpl(url, { headers: githubHeaders(token) });
|
|
69
70
|
if (!response.ok) {
|
|
@@ -357,7 +358,35 @@ export function selectReleaseCandidateArtifacts({ artifacts = [], artifactName =
|
|
|
357
358
|
return { passport, summary: summaries[0], prefix, sourceSha: sha };
|
|
358
359
|
}
|
|
359
360
|
|
|
360
|
-
function
|
|
361
|
+
export function verifyArtifactArchive({ artifact, archivePath } = {}) {
|
|
362
|
+
const bytes = fs.readFileSync(archivePath);
|
|
363
|
+
const size = bytes.length;
|
|
364
|
+
const digest = `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
|
|
365
|
+
if (!artifact || artifact.expired === true) {
|
|
366
|
+
throw new Error(`candidate artifact is missing or expired: ${artifact?.name || "<unknown>"}`);
|
|
367
|
+
}
|
|
368
|
+
if (Number(artifact.size_in_bytes) !== size) {
|
|
369
|
+
throw new Error(`candidate artifact size mismatch for ${artifact.name}: expected ${artifact.size_in_bytes}, got ${size}`);
|
|
370
|
+
}
|
|
371
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(String(artifact.digest || ""))) {
|
|
372
|
+
throw new Error(`candidate artifact ${artifact.name} has no trusted sha256 digest metadata`);
|
|
373
|
+
}
|
|
374
|
+
if (String(artifact.digest).toLowerCase() !== digest) {
|
|
375
|
+
throw new Error(`candidate artifact digest mismatch for ${artifact.name}: expected ${artifact.digest}, got ${digest}`);
|
|
376
|
+
}
|
|
377
|
+
return { size, digest };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function unzip(zipPath, outputDir) {
|
|
381
|
+
const entries = execFileSync("unzip", ["-Z1", zipPath], { encoding: "utf8" })
|
|
382
|
+
.split(/\r?\n/)
|
|
383
|
+
.filter(Boolean);
|
|
384
|
+
for (const entry of entries) {
|
|
385
|
+
const normalized = entry.replaceAll("\\", "/");
|
|
386
|
+
if (normalized.startsWith("/") || normalized.split("/").some((part) => part === "..")) {
|
|
387
|
+
throw new Error(`candidate artifact contains an unsafe zip entry: ${entry}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
361
390
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
362
391
|
execFileSync("unzip", ["-q", "-o", zipPath, "-d", outputDir], { stdio: "inherit" });
|
|
363
392
|
}
|
|
@@ -579,6 +608,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
579
608
|
outputPath: passportZip,
|
|
580
609
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${selected.passport.id}/zip`,
|
|
581
610
|
});
|
|
611
|
+
verifyArtifactArchive({ artifact: selected.passport, archivePath: passportZip });
|
|
582
612
|
await githubDownload({
|
|
583
613
|
apiUrl,
|
|
584
614
|
token,
|
|
@@ -586,6 +616,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
586
616
|
outputPath: summaryZip,
|
|
587
617
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${selected.summary.id}/zip`,
|
|
588
618
|
});
|
|
619
|
+
verifyArtifactArchive({ artifact: selected.summary, archivePath: summaryZip });
|
|
589
620
|
for (const artifact of payloadArtifacts) {
|
|
590
621
|
const safeName = String(artifact.name || `artifact-${artifact.id}`).replace(/[^A-Za-z0-9._-]/g, "_");
|
|
591
622
|
const payloadZip = path.join(tempDir, `${safeName}.zip`);
|
|
@@ -596,6 +627,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
596
627
|
outputPath: payloadZip,
|
|
597
628
|
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${artifact.id}/zip`,
|
|
598
629
|
});
|
|
630
|
+
verifyArtifactArchive({ artifact, archivePath: payloadZip });
|
|
599
631
|
unzip(payloadZip, path.join(payloadDir, safeName));
|
|
600
632
|
}
|
|
601
633
|
unzip(passportZip, passportDir);
|
|
@@ -26,6 +26,15 @@ function resolveArtifactTransferMode(env = process.env) {
|
|
|
26
26
|
oidcAudience: "",
|
|
27
27
|
};
|
|
28
28
|
}
|
|
29
|
+
if (env.INPUT_RELAY_REQUIRED === "false") {
|
|
30
|
+
return {
|
|
31
|
+
mode: "github-artifacts",
|
|
32
|
+
s3Bucket: "",
|
|
33
|
+
s3Region: "",
|
|
34
|
+
s3Prefix: "",
|
|
35
|
+
oidcAudience: "",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
29
38
|
const s3Bucket = firstValue(env.INPUT_S3_BUCKET, env.VAR_S3_BUCKET);
|
|
30
39
|
const s3Region = firstValue(env.INPUT_S3_REGION, env.VAR_S3_REGION);
|
|
31
40
|
const s3Prefix = firstValue(
|
|
@@ -41,6 +41,13 @@ export function resolveBuildContractCli() {
|
|
|
41
41
|
"native-platform-count": String(resolved.nativePlatformCount),
|
|
42
42
|
"container-platforms-json": resolved.containerPlatformsJson,
|
|
43
43
|
"container-platform-count": String(resolved.containerPlatformCount),
|
|
44
|
+
"github-hosted-platforms-json": resolved.githubHostedPlatformsJson,
|
|
45
|
+
"github-hosted-platform-ids-json": resolved.githubHostedPlatformIdsJson,
|
|
46
|
+
"github-hosted-platform-count": String(
|
|
47
|
+
resolved.githubHostedPlatformCount,
|
|
48
|
+
),
|
|
49
|
+
"relay-platforms-json": resolved.relayPlatformsJson,
|
|
50
|
+
"relay-platform-count": String(resolved.relayPlatformCount),
|
|
44
51
|
"linux-container-enabled": String(resolved.linuxContainer.enabled),
|
|
45
52
|
"linux-container-preset": resolved.linuxContainer.preset,
|
|
46
53
|
"linux-container-image": resolved.linuxContainer.image,
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { writeGitHubOutputs } from "./build-contract-core.mjs";
|
|
9
|
+
import {
|
|
10
|
+
generatePublishRequiredArtifacts,
|
|
11
|
+
readNpmPackageArtifact,
|
|
12
|
+
selectReleaseCandidateArtifacts,
|
|
13
|
+
} from "./release-candidate-resolver.mjs";
|
|
14
|
+
import {
|
|
15
|
+
githubDownload,
|
|
16
|
+
githubJson,
|
|
17
|
+
unzip,
|
|
18
|
+
verifyArtifactArchive,
|
|
19
|
+
} from "./release-candidate-resolver.mjs";
|
|
20
|
+
import {
|
|
21
|
+
recoveryFailure,
|
|
22
|
+
verifyReleaseCandidateRecovery,
|
|
23
|
+
} from "../packages/core/release-candidate-recovery.js";
|
|
24
|
+
import {
|
|
25
|
+
PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
|
|
26
|
+
publicationArtifactCandidateDigest,
|
|
27
|
+
} from "../packages/core/publication-artifact-candidate.js";
|
|
28
|
+
import { createPublicationSealedBundle } from "../packages/core/publication-sealed-bundle.js";
|
|
29
|
+
import { releaseTransactionStateRef } from "../packages/core/publish-transaction.js";
|
|
30
|
+
|
|
31
|
+
function env(name, fallback = "") {
|
|
32
|
+
return process.env[name] || fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function requiredEnv(name) {
|
|
36
|
+
const value = env(name).trim();
|
|
37
|
+
if (!value) throw new Error(`${name} is required for candidate recovery`);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function splitRepository(repository) {
|
|
42
|
+
const match = String(repository || "").trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
43
|
+
if (!match) throw new Error(`candidate repository must be owner/repo, got ${repository || "<empty>"}`);
|
|
44
|
+
return { owner: match[1], repo: match[2], fullName: `${match[1]}/${match[2]}` };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function splitPatterns(value = "") {
|
|
48
|
+
return String(value || "").split(/\r?\n|,/).map((entry) => entry.trim()).filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function patternMatcher(pattern) {
|
|
52
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
53
|
+
return new RegExp(`^${escaped}$`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeName(value) {
|
|
57
|
+
return String(value || "artifact").replace(/[^A-Za-z0-9._-]/g, "_");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sha256File(filePath) {
|
|
61
|
+
return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function collectFiles(root) {
|
|
65
|
+
const resolvedRoot = path.resolve(root);
|
|
66
|
+
const files = [];
|
|
67
|
+
const pending = [resolvedRoot];
|
|
68
|
+
while (pending.length) {
|
|
69
|
+
const current = pending.pop();
|
|
70
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
71
|
+
const fullPath = path.join(current, entry.name);
|
|
72
|
+
if (entry.isDirectory()) pending.push(fullPath);
|
|
73
|
+
else if (entry.isFile()) files.push({
|
|
74
|
+
path: path.relative(resolvedRoot, fullPath).split(path.sep).join("/"),
|
|
75
|
+
size: fs.statSync(fullPath).size,
|
|
76
|
+
sha256: sha256File(fullPath),
|
|
77
|
+
absolutePath: fullPath,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function findFiles(root, predicate) {
|
|
85
|
+
return collectFiles(root).filter((file) => predicate(file.path, file.absolutePath));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readOnlyJson(files, label) {
|
|
89
|
+
if (files.length !== 1) throw new Error(`expected exactly one ${label}, found ${files.length}`);
|
|
90
|
+
return JSON.parse(fs.readFileSync(files[0].absolutePath, "utf8"));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version }) {
|
|
94
|
+
const stateRef = releaseTransactionStateRef(version);
|
|
95
|
+
const response = await githubJson({
|
|
96
|
+
apiUrl,
|
|
97
|
+
token,
|
|
98
|
+
fetchImpl,
|
|
99
|
+
allowNotFound: true,
|
|
100
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/contents/state.json?ref=${encodeURIComponent(stateRef)}`,
|
|
101
|
+
});
|
|
102
|
+
if (!response) return undefined;
|
|
103
|
+
if (response.type !== "file" || response.encoding !== "base64" || !response.content) {
|
|
104
|
+
throw new Error(`durable transaction ${stateRef} did not expose a base64 state.json file`);
|
|
105
|
+
}
|
|
106
|
+
return JSON.parse(Buffer.from(String(response.content).replace(/\s/g, ""), "base64").toString("utf8"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function outputPath(filePath) {
|
|
110
|
+
const relative = path.relative(process.cwd(), filePath).split(path.sep).join("/");
|
|
111
|
+
return relative.startsWith("../") ? filePath : relative;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }) {
|
|
115
|
+
const name = safeName(artifact.name);
|
|
116
|
+
const archivePath = path.join(archiveDir, `${name}.zip`);
|
|
117
|
+
const artifactRoot = path.join(bundleRoot, "artifacts", name);
|
|
118
|
+
await githubDownload({
|
|
119
|
+
apiUrl,
|
|
120
|
+
token,
|
|
121
|
+
fetchImpl,
|
|
122
|
+
outputPath: archivePath,
|
|
123
|
+
path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/artifacts/${artifact.id}/zip`,
|
|
124
|
+
});
|
|
125
|
+
const archive = verifyArtifactArchive({ artifact, archivePath });
|
|
126
|
+
unzip(archivePath, artifactRoot);
|
|
127
|
+
const files = collectFiles(artifactRoot);
|
|
128
|
+
return {
|
|
129
|
+
artifact,
|
|
130
|
+
artifactRoot,
|
|
131
|
+
record: {
|
|
132
|
+
name: artifact.name,
|
|
133
|
+
kind: "candidate",
|
|
134
|
+
size: Number(artifact.size_in_bytes),
|
|
135
|
+
downloadedSize: archive.size,
|
|
136
|
+
digest: artifact.digest,
|
|
137
|
+
downloadedDigest: archive.digest,
|
|
138
|
+
expired: artifact.expired === true,
|
|
139
|
+
files: files.map(({ path: filePath, size, sha256 }) => ({ path: filePath, size, sha256 })),
|
|
140
|
+
},
|
|
141
|
+
files,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function candidateArtifactNames({ passport, selected, artifacts, artifactPatterns }) {
|
|
146
|
+
const names = new Set([selected.passport.name, selected.summary.name]);
|
|
147
|
+
for (const reference of passport.controllerReceipts || []) {
|
|
148
|
+
if (!reference.artifact) throw new Error(`Passport controller receipt ${reference.controllerId} has no artifact identity`);
|
|
149
|
+
names.add(reference.artifact);
|
|
150
|
+
}
|
|
151
|
+
for (const platform of passport.platformMatrix || []) names.add(platform.artifactName);
|
|
152
|
+
const manifestPattern = new RegExp(`^${selected.prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-manifest-.+-${selected.sourceSha}$`);
|
|
153
|
+
for (const artifact of artifacts) {
|
|
154
|
+
if (manifestPattern.test(String(artifact.name || ""))) names.add(artifact.name);
|
|
155
|
+
}
|
|
156
|
+
const matchers = splitPatterns(artifactPatterns).map(patternMatcher);
|
|
157
|
+
for (const artifact of artifacts) {
|
|
158
|
+
if (matchers.some((matcher) => matcher.test(String(artifact.name || "")))) names.add(artifact.name);
|
|
159
|
+
}
|
|
160
|
+
return names;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function normalizePlatformManifests(downloads, passport) {
|
|
164
|
+
const manifests = [];
|
|
165
|
+
for (const download of downloads) {
|
|
166
|
+
if (!String(download.artifact.name).includes("-manifest-")) continue;
|
|
167
|
+
for (const file of download.files.filter((entry) => path.basename(entry.path) === "manifest.json")) {
|
|
168
|
+
const manifest = JSON.parse(fs.readFileSync(file.absolutePath, "utf8"));
|
|
169
|
+
if (!manifest.artifactName) {
|
|
170
|
+
const platformId = String(manifest.platform?.id || manifest.platformId || "");
|
|
171
|
+
manifest.artifactName = (passport.platformMatrix || []).find((entry) => entry.platformId === platformId)?.artifactName || "";
|
|
172
|
+
}
|
|
173
|
+
manifests.push(manifest);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return manifests;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeControllerReceipts(downloads, passport) {
|
|
180
|
+
const artifactNames = new Set((passport.controllerReceipts || []).map((reference) => reference.artifact));
|
|
181
|
+
const receipts = [];
|
|
182
|
+
for (const download of downloads.filter((entry) => artifactNames.has(entry.artifact.name))) {
|
|
183
|
+
const candidates = download.files.filter((file) => file.path.endsWith(".json")).map((file) => {
|
|
184
|
+
try { return JSON.parse(fs.readFileSync(file.absolutePath, "utf8")); } catch { return undefined; }
|
|
185
|
+
}).filter((value) => value?.contract === "buildchain.controller-evidence/v1" && value?.kind === "receipt");
|
|
186
|
+
if (candidates.length !== 1) throw new Error(`controller artifact ${download.artifact.name} must contain exactly one controller receipt`);
|
|
187
|
+
receipts.push(candidates[0]);
|
|
188
|
+
}
|
|
189
|
+
return receipts;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeProductPayloadManifests(downloads) {
|
|
193
|
+
return downloads.flatMap((download) => download.files
|
|
194
|
+
.filter((file) => path.basename(file.path) === "product-payload-manifest.json")
|
|
195
|
+
.map((file) => JSON.parse(fs.readFileSync(file.absolutePath, "utf8"))));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function createSealedBundle({ downloads, bundleRoot, repository, passport, runtimeSha, publishPackageMain, releasePatterns }) {
|
|
199
|
+
const allFiles = downloads.flatMap((download) => download.files.map((file) => ({
|
|
200
|
+
path: path.relative(bundleRoot, file.absolutePath).split(path.sep).join("/"),
|
|
201
|
+
size: file.size,
|
|
202
|
+
sha256: file.sha256.replace(/^sha256:/, ""),
|
|
203
|
+
absolutePath: file.absolutePath,
|
|
204
|
+
}))).sort((left, right) => left.path.localeCompare(right.path));
|
|
205
|
+
const tarballs = allFiles.filter((file) => file.path.toLowerCase().endsWith(".tgz"));
|
|
206
|
+
if (tarballs.length === 0) throw new Error("candidate recovery for npm publication requires at least one exact .tgz payload artifact");
|
|
207
|
+
const npmArtifacts = tarballs.map((file) => ({ file, metadata: readNpmPackageArtifact({ tarballPath: file.absolutePath, mainPackage: publishPackageMain }) }));
|
|
208
|
+
const main = npmArtifacts.find((entry) => entry.metadata.role === "main") || (npmArtifacts.length === 1 ? npmArtifacts[0] : undefined);
|
|
209
|
+
if (!main) throw new Error("candidate npm payload set has no unique main package tarball");
|
|
210
|
+
const releaseMatchers = splitPatterns(releasePatterns).map(patternMatcher);
|
|
211
|
+
const releaseAssets = allFiles.filter((file) => releaseMatchers.length
|
|
212
|
+
? releaseMatchers.some((matcher) => matcher.test(path.basename(file.path)))
|
|
213
|
+
: file.path.toLowerCase().endsWith(".tgz"));
|
|
214
|
+
const payload = {
|
|
215
|
+
schemaVersion: 1,
|
|
216
|
+
contract: PUBLICATION_ARTIFACT_CANDIDATE_CONTRACT,
|
|
217
|
+
repository,
|
|
218
|
+
sourceSha: passport.source.headSha,
|
|
219
|
+
sourceTreeSha: passport.source.treeHash,
|
|
220
|
+
runtimeSha,
|
|
221
|
+
releaseCandidateRoot: `sha256:${passport.candidateHash}`,
|
|
222
|
+
files: allFiles.map(({ path: filePath, size, sha256 }) => ({ path: filePath, size, sha256 })),
|
|
223
|
+
};
|
|
224
|
+
const candidate = { ...payload, candidateDigest: publicationArtifactCandidateDigest(payload) };
|
|
225
|
+
const manifest = createPublicationSealedBundle({
|
|
226
|
+
candidate,
|
|
227
|
+
packageName: main.metadata.name,
|
|
228
|
+
packageVersion: main.metadata.ref,
|
|
229
|
+
npmTarballPath: main.file.path,
|
|
230
|
+
npmIntegrity: main.metadata.integrity,
|
|
231
|
+
releaseAssetPaths: releaseAssets.map((file) => file.path),
|
|
232
|
+
});
|
|
233
|
+
return { manifest, npmArtifacts, allFiles };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function recoverCandidateEvidence({
|
|
237
|
+
repoInfo,
|
|
238
|
+
runId,
|
|
239
|
+
artifactName,
|
|
240
|
+
artifactPatterns,
|
|
241
|
+
requiredArtifactCount,
|
|
242
|
+
outputDir,
|
|
243
|
+
apiUrl,
|
|
244
|
+
token,
|
|
245
|
+
fetchImpl,
|
|
246
|
+
archiveDir,
|
|
247
|
+
}) {
|
|
248
|
+
const run = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}` });
|
|
249
|
+
const workflow = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/workflows/${run.workflow_id}` });
|
|
250
|
+
const artifactResponse = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/runs/${runId}/artifacts?per_page=100` });
|
|
251
|
+
const artifacts = Array.isArray(artifactResponse.artifacts) ? artifactResponse.artifacts : [];
|
|
252
|
+
if (Number(artifactResponse.total_count || artifacts.length) !== artifacts.length) throw new Error("candidate run has more than 100 artifacts; complete pagination is required before recovery");
|
|
253
|
+
const selected = selectReleaseCandidateArtifacts({ artifacts, artifactName });
|
|
254
|
+
const resolvedOutput = path.resolve(outputDir);
|
|
255
|
+
const bundleRoot = path.join(resolvedOutput, "sealed-candidate");
|
|
256
|
+
fs.mkdirSync(bundleRoot, { recursive: true });
|
|
257
|
+
const initialDownloads = [];
|
|
258
|
+
for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
259
|
+
const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
|
|
260
|
+
const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
|
|
261
|
+
const candidateVersion = String(passport.target?.version || "").trim();
|
|
262
|
+
if (!candidateVersion) throw new Error("Release Candidate Passport has no exact target version");
|
|
263
|
+
const existingTransaction = await readExistingTransaction({ repoInfo, apiUrl, token, fetchImpl, version: candidateVersion });
|
|
264
|
+
const requiredNames = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
|
|
265
|
+
const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
|
|
266
|
+
if (chosen.length !== requiredNames.size) {
|
|
267
|
+
const found = new Set(chosen.map((artifact) => artifact.name));
|
|
268
|
+
throw new Error(`candidate artifacts are missing: ${[...requiredNames].filter((name) => !found.has(name)).join(", ")}`);
|
|
269
|
+
}
|
|
270
|
+
if (Number(requiredArtifactCount || 0) > 0 && chosen.length < Number(requiredArtifactCount)) throw new Error(`candidate artifact count ${chosen.length} is below required ${requiredArtifactCount}`);
|
|
271
|
+
const downloads = [...initialDownloads];
|
|
272
|
+
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 }));
|
|
273
|
+
return {
|
|
274
|
+
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
275
|
+
passport, buildSummary, candidateVersion, existingTransaction, chosen, downloads,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export async function resumeFromCandidateRun({
|
|
280
|
+
repository,
|
|
281
|
+
targetRepository = repository,
|
|
282
|
+
candidateRunId,
|
|
283
|
+
expectedWorkflowFile,
|
|
284
|
+
expectedWorkflowName,
|
|
285
|
+
channel,
|
|
286
|
+
targetRef,
|
|
287
|
+
targetSha,
|
|
288
|
+
expectedSourceTree = "",
|
|
289
|
+
expectedCandidateRoot = "",
|
|
290
|
+
candidateRuntimeSha,
|
|
291
|
+
runtimeSha,
|
|
292
|
+
transactionId = "",
|
|
293
|
+
artifactName = "",
|
|
294
|
+
artifactPatterns = "",
|
|
295
|
+
releasePatterns = "",
|
|
296
|
+
requiredArtifactCount = 0,
|
|
297
|
+
publishPackageMain = "",
|
|
298
|
+
outputDir = ".buildchain/release-candidate-recovery",
|
|
299
|
+
token = env("GITHUB_TOKEN"),
|
|
300
|
+
apiUrl = env("GITHUB_API_URL", "https://api.github.com"),
|
|
301
|
+
recoveryRunId = env("GITHUB_RUN_ID"),
|
|
302
|
+
fetchImpl = globalThis.fetch,
|
|
303
|
+
} = {}) {
|
|
304
|
+
const repoInfo = splitRepository(repository);
|
|
305
|
+
const runId = String(candidateRunId || "").trim();
|
|
306
|
+
if (!/^\d+$/.test(runId)) throw new Error("candidate run ID must be numeric");
|
|
307
|
+
if (!String(expectedSourceTree || "").trim() && !String(expectedCandidateRoot || "").trim()) {
|
|
308
|
+
throw new Error("candidate recovery requires expectedSourceTree or expectedCandidateRoot");
|
|
309
|
+
}
|
|
310
|
+
const archiveDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-resume-"));
|
|
311
|
+
try {
|
|
312
|
+
const {
|
|
313
|
+
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
314
|
+
passport, buildSummary, candidateVersion, existingTransaction, chosen, downloads,
|
|
315
|
+
} = await recoverCandidateEvidence({
|
|
316
|
+
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
317
|
+
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
318
|
+
});
|
|
319
|
+
const prNumber = Number(passport.pullRequest?.number || 0);
|
|
320
|
+
if (!prNumber) throw new Error("Release Candidate Passport has no PR identity");
|
|
321
|
+
const pullRequest = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/pulls/${prNumber}` });
|
|
322
|
+
const targetCommit = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/commits/${targetSha}` });
|
|
323
|
+
const targetRefState = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/git/ref/heads/${targetRef.replace(/^refs\/heads\//, "")}` });
|
|
324
|
+
if (targetRefState.object?.sha !== targetSha) throw new Error(`target ref ${targetRef} no longer points at ${targetSha}`);
|
|
325
|
+
const compare = await githubJson({ apiUrl, token, fetchImpl, path: `/repos/${repoInfo.owner}/${repoInfo.repo}/compare/${pullRequest.merge_commit_sha}...${targetSha}` });
|
|
326
|
+
const platformManifests = normalizePlatformManifests(downloads, passport);
|
|
327
|
+
const controllerReceipts = normalizeControllerReceipts(downloads, passport);
|
|
328
|
+
const productPayloadManifests = normalizeProductPayloadManifests(downloads);
|
|
329
|
+
const recovery = verifyReleaseCandidateRecovery({
|
|
330
|
+
candidateRepository: repoInfo.fullName,
|
|
331
|
+
targetRepository,
|
|
332
|
+
expectedRunId: runId,
|
|
333
|
+
expectedWorkflowFile,
|
|
334
|
+
expectedWorkflowName,
|
|
335
|
+
channel,
|
|
336
|
+
targetRef,
|
|
337
|
+
targetSha,
|
|
338
|
+
targetTree: targetCommit.tree?.sha,
|
|
339
|
+
expectedSourceTree,
|
|
340
|
+
expectedCandidateRoot,
|
|
341
|
+
expectedRuntimeSha: candidateRuntimeSha,
|
|
342
|
+
expectedTransactionId: transactionId,
|
|
343
|
+
existingTransaction,
|
|
344
|
+
run: {
|
|
345
|
+
id: String(run.id),
|
|
346
|
+
repository: repoInfo.fullName,
|
|
347
|
+
headRepository: run.head_repository?.full_name || "",
|
|
348
|
+
status: run.status,
|
|
349
|
+
conclusion: run.conclusion,
|
|
350
|
+
event: run.event,
|
|
351
|
+
path: run.path,
|
|
352
|
+
name: run.name,
|
|
353
|
+
headSha: run.head_sha || "",
|
|
354
|
+
headBranch: run.head_branch || "",
|
|
355
|
+
pullRequestNumbers: (run.pull_requests || []).map((entry) => Number(entry.number)),
|
|
356
|
+
},
|
|
357
|
+
workflow: { path: workflow.path, name: workflow.name, state: workflow.state },
|
|
358
|
+
pullRequest: {
|
|
359
|
+
number: pullRequest.number,
|
|
360
|
+
merged: pullRequest.merged === true,
|
|
361
|
+
mergeSha: pullRequest.merge_commit_sha,
|
|
362
|
+
headRepository: pullRequest.head?.repo?.full_name || "",
|
|
363
|
+
baseRef: pullRequest.base?.ref || "",
|
|
364
|
+
authorAssociation: pullRequest.author_association || "",
|
|
365
|
+
headSha: pullRequest.head?.sha || "",
|
|
366
|
+
headRef: pullRequest.head?.ref || "",
|
|
367
|
+
},
|
|
368
|
+
ancestry: { status: compare.status, mergeIsAncestor: ["ahead", "identical"].includes(compare.status) },
|
|
369
|
+
passport,
|
|
370
|
+
buildSummary,
|
|
371
|
+
controllerReceipts,
|
|
372
|
+
platformManifests,
|
|
373
|
+
productPayloadManifests,
|
|
374
|
+
artifacts: downloads.map((download) => download.record),
|
|
375
|
+
currentToolingSha: runtimeSha,
|
|
376
|
+
recoveryRunId,
|
|
377
|
+
});
|
|
378
|
+
const sealed = createSealedBundle({
|
|
379
|
+
downloads,
|
|
380
|
+
bundleRoot,
|
|
381
|
+
repository: repoInfo.fullName,
|
|
382
|
+
passport,
|
|
383
|
+
runtimeSha,
|
|
384
|
+
publishPackageMain,
|
|
385
|
+
releasePatterns,
|
|
386
|
+
});
|
|
387
|
+
const recoveryReceiptPath = path.join(resolvedOutput, "recovery-receipt.json");
|
|
388
|
+
const sealedManifestPath = path.join(resolvedOutput, "sealed-bundle.json");
|
|
389
|
+
const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
|
|
390
|
+
fs.writeFileSync(recoveryReceiptPath, `${JSON.stringify(recovery.receipt, null, 2)}\n`);
|
|
391
|
+
fs.writeFileSync(sealedManifestPath, `${JSON.stringify(sealed.manifest, null, 2)}\n`);
|
|
392
|
+
const publishRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
393
|
+
kind: "npm",
|
|
394
|
+
tarballPaths: sealed.npmArtifacts.map((entry) => entry.file.absolutePath),
|
|
395
|
+
mainPackage: publishPackageMain,
|
|
396
|
+
});
|
|
397
|
+
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(publishRequiredArtifacts, null, 2)}\n`);
|
|
398
|
+
const tarballs = sealed.npmArtifacts.map((entry) => outputPath(entry.file.absolutePath));
|
|
399
|
+
return {
|
|
400
|
+
enabled: true,
|
|
401
|
+
action: "reused",
|
|
402
|
+
repository: repoInfo.fullName,
|
|
403
|
+
run: { id: runId, url: run.html_url || "", name: run.name },
|
|
404
|
+
artifacts: { passport: selected.passport.name, summary: selected.summary.name, payloads: chosen.map((artifact) => artifact.name), sourceSha: selected.sourceSha },
|
|
405
|
+
version: candidateVersion,
|
|
406
|
+
candidateRoot: recovery.receipt.recovered.candidateRoot,
|
|
407
|
+
artifactRoot: recovery.receipt.recovered.artifactRoot,
|
|
408
|
+
receipt: recovery.receipt,
|
|
409
|
+
publishRequiredArtifacts,
|
|
410
|
+
paths: {
|
|
411
|
+
passport: outputPath(initialDownloads[0].files.find((file) => path.basename(file.path) === "release-candidate-passport.json").absolutePath),
|
|
412
|
+
buildSummary: outputPath(initialDownloads[1].files.find((file) => path.basename(file.path) === "build-summary.json").absolutePath),
|
|
413
|
+
payloads: outputPath(path.join(bundleRoot, "artifacts")),
|
|
414
|
+
platformManifests: downloads.flatMap((download) => download.files.filter((file) => path.basename(file.path) === "manifest.json").map((file) => outputPath(file.absolutePath))),
|
|
415
|
+
npmTarballs: tarballs,
|
|
416
|
+
releaseAssets: sealed.manifest.releaseAssets.map((asset) => outputPath(path.join(bundleRoot, asset.path))),
|
|
417
|
+
publishRequiredArtifacts: outputPath(requiredArtifactsPath),
|
|
418
|
+
sealedBundleRoot: outputPath(bundleRoot),
|
|
419
|
+
sealedBundleManifest: outputPath(sealedManifestPath),
|
|
420
|
+
recoveryReceipt: outputPath(recoveryReceiptPath),
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
} finally {
|
|
424
|
+
fs.rmSync(archiveDir, { recursive: true, force: true });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export async function resumeFromCandidateRunCli() {
|
|
429
|
+
try {
|
|
430
|
+
const result = await resumeFromCandidateRun({
|
|
431
|
+
repository: requiredEnv("BUILDCHAIN_RESUME_CANDIDATE_REPOSITORY"),
|
|
432
|
+
targetRepository: env("GITHUB_REPOSITORY"),
|
|
433
|
+
candidateRunId: requiredEnv("BUILDCHAIN_RESUME_CANDIDATE_RUN_ID"),
|
|
434
|
+
expectedWorkflowFile: requiredEnv("BUILDCHAIN_RESUME_EXPECTED_WORKFLOW_FILE"),
|
|
435
|
+
expectedWorkflowName: requiredEnv("BUILDCHAIN_RESUME_EXPECTED_WORKFLOW_NAME"),
|
|
436
|
+
channel: requiredEnv("BUILDCHAIN_RESUME_CHANNEL"),
|
|
437
|
+
targetRef: requiredEnv("BUILDCHAIN_RESUME_TARGET_REF"),
|
|
438
|
+
targetSha: requiredEnv("BUILDCHAIN_RESUME_TARGET_SHA"),
|
|
439
|
+
expectedSourceTree: env("BUILDCHAIN_RESUME_EXPECTED_SOURCE_TREE"),
|
|
440
|
+
expectedCandidateRoot: env("BUILDCHAIN_RESUME_EXPECTED_CANDIDATE_ROOT"),
|
|
441
|
+
candidateRuntimeSha: requiredEnv("BUILDCHAIN_RESUME_EXPECTED_CANDIDATE_RUNTIME_SHA"),
|
|
442
|
+
runtimeSha: requiredEnv("BUILDCHAIN_RESUME_RUNTIME_SHA"),
|
|
443
|
+
transactionId: env("BUILDCHAIN_RESUME_TRANSACTION_ID"),
|
|
444
|
+
artifactName: env("BUILDCHAIN_ARTIFACT_NAME"),
|
|
445
|
+
artifactPatterns: env("BUILDCHAIN_ARTIFACT_PATTERNS"),
|
|
446
|
+
releasePatterns: env("BUILDCHAIN_GITHUB_RELEASE_PAYLOAD_PATTERNS"),
|
|
447
|
+
requiredArtifactCount: env("BUILDCHAIN_REQUIRED_ARTIFACT_COUNT", "0"),
|
|
448
|
+
publishPackageMain: env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN"),
|
|
449
|
+
outputDir: env("BUILDCHAIN_RC_OUTPUT_DIR", ".buildchain/release-candidate-recovery"),
|
|
450
|
+
});
|
|
451
|
+
writeGitHubOutputs({
|
|
452
|
+
"promote-only-release-candidate": "true",
|
|
453
|
+
"release-candidate-action": result.action,
|
|
454
|
+
"release-candidate-passport-path": result.paths.passport,
|
|
455
|
+
"release-candidate-build-summary-path": result.paths.buildSummary,
|
|
456
|
+
"release-candidate-version": result.version,
|
|
457
|
+
"release-candidate-source-sha": result.artifacts.sourceSha,
|
|
458
|
+
"release-candidate-artifact": result.artifacts.passport,
|
|
459
|
+
"release-candidate-build-summary-artifact": result.artifacts.summary,
|
|
460
|
+
"release-candidate-payload-artifacts": result.artifacts.payloads.join(","),
|
|
461
|
+
"release-candidate-payload-dir": result.paths.payloads,
|
|
462
|
+
"release-candidate-platform-manifest-paths": result.paths.platformManifests.join(","),
|
|
463
|
+
"release-candidate-npm-tarball-paths": result.paths.npmTarballs.join(","),
|
|
464
|
+
"release-candidate-github-release-artifact-paths": result.paths.releaseAssets.join("\n"),
|
|
465
|
+
"publish-required-artifacts-json": JSON.stringify(result.publishRequiredArtifacts),
|
|
466
|
+
"publish-required-artifacts-path": result.paths.publishRequiredArtifacts,
|
|
467
|
+
"release-candidate-run-id": result.run.id,
|
|
468
|
+
"release-candidate-run-url": result.run.url,
|
|
469
|
+
"release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
|
|
470
|
+
"release-candidate-recovery-root": result.receipt.root,
|
|
471
|
+
"release-candidate-root": result.candidateRoot,
|
|
472
|
+
"release-candidate-artifact-root": result.artifactRoot,
|
|
473
|
+
"publish-sealed-bundle-root": result.paths.sealedBundleRoot,
|
|
474
|
+
"publish-sealed-bundle-manifest": result.paths.sealedBundleManifest,
|
|
475
|
+
"release-candidate-diagnosis": `Reused sealed candidate run ${result.run.id}; product build stages skipped`,
|
|
476
|
+
});
|
|
477
|
+
console.log(JSON.stringify(result, null, 2));
|
|
478
|
+
return result;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const failure = recoveryFailure(error);
|
|
481
|
+
writeGitHubOutputs({
|
|
482
|
+
"release-candidate-action": "rejected",
|
|
483
|
+
"release-candidate-recovery-error-code": failure.code,
|
|
484
|
+
"release-candidate-recovery-next-action": failure.nextAction,
|
|
485
|
+
"release-candidate-diagnosis": `${failure.code}: ${failure.reason}; next: ${failure.nextAction}`,
|
|
486
|
+
});
|
|
487
|
+
throw Object.assign(error, { recoveryFailure: failure });
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
492
|
+
resumeFromCandidateRunCli().catch((error) => {
|
|
493
|
+
const failure = error.recoveryFailure || recoveryFailure(error);
|
|
494
|
+
console.error(`candidate recovery rejected [${failure.code}]: ${failure.reason}`);
|
|
495
|
+
console.error(`next action: ${failure.nextAction}`);
|
|
496
|
+
process.exitCode = 1;
|
|
497
|
+
});
|
|
498
|
+
}
|