@kungfu-tech/buildchain 3.0.2-alpha.0 → 3.0.2-alpha.10

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.
Files changed (83) hide show
  1. package/README.md +4 -2
  2. package/actions/github-artifact-attestation/README.md +10 -0
  3. package/actions/promote-buildchain-ref/README.md +7 -0
  4. package/bin/buildchain.mjs +5 -0
  5. package/bin/internal/trust-release-cli.mjs +74 -3
  6. package/contracts/auditable-demo-media-profiles-v1.json +168 -0
  7. package/contracts/evidence/auditable-demo-web-delivery-v1.json +103 -0
  8. package/contracts/fixtures/auditable-demo-web-delivery-v1/complete-transcript.txt +2 -0
  9. package/contracts/fixtures/auditable-demo-web-delivery-v1/public-projection.json +16 -0
  10. package/contracts/fixtures/auditable-demo-web-delivery-v1/scene.json +12 -0
  11. package/dist/site/artifact-schemas.json +5 -1
  12. package/dist/site/buildchain-contract.json +133 -34
  13. package/dist/site/buildchain-site.json +159 -40
  14. package/dist/site/capability-registry.json +14 -13
  15. package/dist/site/cli-registry.json +12 -0
  16. package/dist/site/controller-registry.json +72 -4
  17. package/dist/site/kfd-claims.json +338 -20
  18. package/dist/site/kfd-upstream-aggregate.json +1 -1
  19. package/dist/site/manual-registry.json +24 -9
  20. package/dist/site/node-api-registry.json +89 -11
  21. package/dist/site/page-registry.json +135 -25
  22. package/dist/site/public-surface-audit.json +203 -21
  23. package/dist/site/publication-authority-registry.json +72 -2
  24. package/dist/site/publication-registry.json +4 -4
  25. package/dist/site/release-model.json +2 -1
  26. package/dist/site/release-passport-check-manifest.json +1 -0
  27. package/dist/site/release-provenance.json +6 -0
  28. package/dist/site/schemas/release-passport-v1.schema.json +6 -0
  29. package/dist/site/site-manifest.json +21 -13
  30. package/dist/site/workflow-registry.json +167 -13
  31. package/docs/MAP.md +5 -1
  32. package/docs/auditable-demo.md +55 -3
  33. package/docs/binary-distribution.md +7 -0
  34. package/docs/cli.md +9 -0
  35. package/docs/dev-alpha-candidate-patrol.md +111 -0
  36. package/docs/github-artifact-attestation.md +219 -0
  37. package/docs/release-governance.md +32 -0
  38. package/docs/release-passport.md +13 -0
  39. package/docs/reusable-build-surface.md +83 -61
  40. package/docs/runtime-train-validation.md +21 -0
  41. package/docs/versioning.md +1 -0
  42. package/package.json +8 -1
  43. package/packages/core/artifact-signing-result.js +228 -0
  44. package/packages/core/artifact-signing.js +412 -0
  45. package/packages/core/buildchain-config.js +58 -0
  46. package/packages/core/buildchain-contract.js +14 -0
  47. package/packages/core/buildchain-kfd-claims.js +5 -0
  48. package/packages/core/buildchain-publication-authority.js +3 -0
  49. package/packages/core/cache-evidence.js +288 -0
  50. package/packages/core/channel-candidate.js +186 -0
  51. package/packages/core/detached-artifact-signature.js +121 -0
  52. package/packages/core/diagnostics.js +276 -10
  53. package/packages/core/github-artifact-attestation.js +642 -0
  54. package/packages/core/github-governance-authority.js +77 -16
  55. package/packages/core/index.js +62 -0
  56. package/packages/core/publication-authority.js +1 -1
  57. package/packages/core/release-passport-contract.js +2 -0
  58. package/packages/core/release-passport.js +60 -3
  59. package/scripts/auditable-demo.mjs +520 -28
  60. package/scripts/build-contract-core.mjs +31 -0
  61. package/scripts/buildchain-channel-router.mjs +8 -2
  62. package/scripts/check-inventory.mjs +9 -0
  63. package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
  64. package/scripts/dev-alpha-candidate-patrol.mjs +864 -0
  65. package/scripts/dispatch-artifact-signing-authority.mjs +152 -0
  66. package/scripts/finalize-native-artifact-signing-result.mjs +96 -0
  67. package/scripts/generate-channel-promotion-workflow.mjs +6 -0
  68. package/scripts/generate-site-bundle.mjs +20 -0
  69. package/scripts/import-artifact-signing-results.mjs +76 -0
  70. package/scripts/inspect-artifact-signing-requests.mjs +101 -0
  71. package/scripts/locked-source-checkout.mjs +48 -0
  72. package/scripts/materialize-artifact-signing-request.mjs +66 -0
  73. package/scripts/merge-artifact-signing-results.mjs +76 -0
  74. package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
  75. package/scripts/reconcile-github-governance.mjs +158 -25
  76. package/scripts/release-candidate-resolver.mjs +12 -0
  77. package/scripts/release-line-policy.mjs +27 -0
  78. package/scripts/runtime-ref-core.mjs +10 -6
  79. package/scripts/seal-artifact-signing-requests.mjs +368 -0
  80. package/scripts/shifu-gate-profile.mjs +26 -20
  81. package/scripts/sign-detached-artifact-requests.mjs +237 -0
  82. package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
  83. package/scripts/verify-artifact-signing-results.mjs +99 -0
@@ -177,6 +177,21 @@ function gitObjectDirectory(referencePath) {
177
177
  return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) || "";
178
178
  }
179
179
 
180
+ function directoryBytes(directory) {
181
+ if (!fs.existsSync(directory)) return 0;
182
+ let bytes = 0;
183
+ const pending = [directory];
184
+ while (pending.length) {
185
+ const current = pending.pop();
186
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
187
+ const child = path.join(current, entry.name);
188
+ if (entry.isDirectory()) pending.push(child);
189
+ else if (entry.isFile()) bytes += fs.statSync(child).size;
190
+ }
191
+ }
192
+ return bytes;
193
+ }
194
+
180
195
  function writeAlternates(targetPath, referencePath) {
181
196
  const objectDirectory = gitObjectDirectory(referencePath);
182
197
  if (!objectDirectory) {
@@ -413,6 +428,13 @@ export function lockedSourceCheckout({
413
428
  fallbackUsed: normalizedMode === "off",
414
429
  fallbackReason: normalizedMode === "off" ? "cache disabled" : "",
415
430
  githubFetchAttempts: 0,
431
+ lookupDurationMs: 0,
432
+ restoreDurationMs: 0,
433
+ githubFetchDurationMs: 0,
434
+ restoredBytes: null,
435
+ restoredBytesStatus:
436
+ normalizedMode === "off" ? "not-applicable" : "unavailable",
437
+ restoredBytesMethod: "",
416
438
  },
417
439
  verification: {
418
440
  head: "",
@@ -425,8 +447,13 @@ export function lockedSourceCheckout({
425
447
  let checkoutError;
426
448
  let checkoutSha = sha;
427
449
  if (normalizedMode !== "off") {
450
+ const cacheAttemptStartedAt = Date.now();
451
+ const objectBytesBefore = directoryBytes(
452
+ path.join(targetPath, ".git", "objects"),
453
+ );
428
454
  try {
429
455
  if (renderedReferenceRepository) {
456
+ const lookupStartedAt = Date.now();
430
457
  const alternates = writeAlternates(targetPath, renderedReferenceRepository);
431
458
  evidence.cache.transport = "reference-repository";
432
459
  evidence.cache.referenceAvailable = alternates;
@@ -436,9 +463,13 @@ export function lockedSourceCheckout({
436
463
  if (!hasCommit(targetPath, sha, timeoutMs)) {
437
464
  throw new Error("reference repository does not contain source commit");
438
465
  }
466
+ evidence.cache.lookupDurationMs = Date.now() - lookupStartedAt;
467
+ const restoreStartedAt = Date.now();
439
468
  checkoutFetchedCommit(targetPath, sha, timeoutMs);
469
+ evidence.cache.restoreDurationMs = Date.now() - restoreStartedAt;
440
470
  } else if (renderedMirrorUrl) {
441
471
  evidence.cache.transport = "mirror-url";
472
+ const restoreStartedAt = Date.now();
442
473
  const fetchResult = fetchSourceCommit({
443
474
  targetPath,
444
475
  remoteName: "buildchain-cache",
@@ -452,15 +483,29 @@ export function lockedSourceCheckout({
452
483
  evidence.cache.fetchMode = fetchResult.fetchMode;
453
484
  checkoutSha = fetchResult.checkoutSha || sha;
454
485
  checkoutFetchedCommit(targetPath, checkoutSha, timeoutMs);
486
+ evidence.cache.restoreDurationMs = Date.now() - restoreStartedAt;
455
487
  } else {
456
488
  throw new Error("checkout cache is enabled but no mirror URL or reference repository template was provided");
457
489
  }
458
490
  evidence.cache.hit = true;
459
491
  evidence.cache.fallbackUsed = false;
492
+ evidence.cache.restoredBytes = Math.max(
493
+ 0,
494
+ directoryBytes(path.join(targetPath, ".git", "objects")) -
495
+ objectBytesBefore,
496
+ );
497
+ evidence.cache.restoredBytesStatus = "observed";
498
+ evidence.cache.restoredBytesMethod = "git-object-store-delta";
460
499
  } catch (error) {
461
500
  checkoutError = error;
462
501
  evidence.cache.hit = false;
463
502
  evidence.cache.fallbackReason = error.message;
503
+ if (
504
+ evidence.cache.lookupDurationMs === 0 &&
505
+ evidence.cache.restoreDurationMs === 0
506
+ ) {
507
+ evidence.cache.lookupDurationMs = Date.now() - cacheAttemptStartedAt;
508
+ }
464
509
  if (normalizedMode === "require" || normalizedFallback === "fail") {
465
510
  evidence.durationMs = Date.now() - startedAt;
466
511
  writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
@@ -477,6 +522,7 @@ export function lockedSourceCheckout({
477
522
  evidence.cache.fallbackReason = checkoutError.message;
478
523
  }
479
524
  try {
525
+ const githubFetchStartedAt = Date.now();
480
526
  const fetchResult = runBoundedFetch({
481
527
  attempts: normalizedFetchAttempts,
482
528
  fetch: () => fetchSourceCommit({
@@ -496,6 +542,8 @@ export function lockedSourceCheckout({
496
542
  evidence.cache.githubFetchAttempts = fetchResult.attempts;
497
543
  evidence.cache.fetchMode = fetchResult.value.fetchMode;
498
544
  checkoutSha = fetchResult.value.checkoutSha || sha;
545
+ evidence.cache.githubFetchDurationMs =
546
+ Date.now() - githubFetchStartedAt;
499
547
  } catch (error) {
500
548
  evidence.durationMs = Date.now() - startedAt;
501
549
  writeEvidence(path.resolve(workspace, diagnosticsPath), evidence);
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { validateArtifactSigningRequest } from "../packages/core/artifact-signing.js";
8
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
9
+
10
+ function required(value, label) {
11
+ const normalized = String(value || "").trim();
12
+ if (!normalized) throw new Error(`${label} is required`);
13
+ return normalized;
14
+ }
15
+
16
+ function resolveBelow(root, relative, label) {
17
+ const target = path.resolve(root, required(relative, label));
18
+ const rel = path.relative(path.resolve(root), target);
19
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) throw new Error(`${label} must resolve below its root`);
20
+ return target;
21
+ }
22
+
23
+ function sha256File(filePath) {
24
+ return `sha256:${crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex")}`;
25
+ }
26
+
27
+ export function materializeArtifactSigningRequest({
28
+ requestRoot = process.env.BUILDCHAIN_SIGNING_REQUEST_ROOT,
29
+ requestPath = process.env.BUILDCHAIN_SIGNING_REQUEST_PATH,
30
+ expectedProfile = process.env.BUILDCHAIN_SIGNING_EXPECTED_PROFILE,
31
+ outputPath = process.env.BUILDCHAIN_UNSIGNED_OUTPUT,
32
+ } = {}) {
33
+ const root = path.resolve(required(requestRoot, "signing request root"));
34
+ const requestFile = resolveBelow(root, requestPath, "request path");
35
+ const request = JSON.parse(fs.readFileSync(requestFile, "utf8"));
36
+ const check = validateArtifactSigningRequest(request);
37
+ if (!check.ok) throw new Error(`invalid artifact signing request: ${check.issues.join(", ")}`);
38
+ if (expectedProfile && request.signature.profile !== expectedProfile) throw new Error("artifact signing profile mismatch");
39
+ if (request.artifact.transport?.format !== "exact-file") throw new Error("native executable signing requires exact-file transport");
40
+ const requestIndexRoot = path.dirname(path.dirname(requestFile));
41
+ const payload = resolveBelow(requestIndexRoot, request.artifact.transport.file, "transport payload");
42
+ const stat = fs.statSync(payload);
43
+ const payloadDigest = sha256File(payload);
44
+ if (!stat.isFile() || stat.size !== request.artifact.transport.bytes || stat.size !== request.artifact.bytes || payloadDigest !== request.artifact.transport.digest || payloadDigest !== request.artifact.digest) {
45
+ throw new Error("signing payload does not match its sealed request");
46
+ }
47
+ const output = path.resolve(required(outputPath, "unsigned output path"));
48
+ fs.mkdirSync(path.dirname(output), { recursive: true });
49
+ fs.copyFileSync(payload, output, fs.constants.COPYFILE_EXCL);
50
+ writeGitHubOutputs({
51
+ "artifact-id": request.artifact.id,
52
+ "payload-path": output,
53
+ "request-digest": request.digest,
54
+ "signature-provider": request.signature.provider,
55
+ });
56
+ return { request, output };
57
+ }
58
+
59
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
60
+ try {
61
+ materializeArtifactSigningRequest();
62
+ } catch (error) {
63
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
64
+ process.exitCode = 1;
65
+ }
66
+ }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+
6
+ import { writeGitHubOutputs } from "./build-contract-core.mjs";
7
+
8
+ function required(value, label) {
9
+ const normalized = String(value || "").trim();
10
+ if (!normalized) throw new Error(`${label} is required`);
11
+ return normalized;
12
+ }
13
+
14
+ function safeId(value) {
15
+ const normalized = String(value || "").replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "");
16
+ if (!normalized || normalized === "." || normalized === "..") throw new Error("unsafe signing result id");
17
+ return normalized;
18
+ }
19
+
20
+ function walk(root, output = []) {
21
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
22
+ const child = path.join(root, entry.name);
23
+ if (entry.isDirectory()) walk(child, output);
24
+ else if (entry.isFile() && entry.name === "index.json") output.push(child);
25
+ }
26
+ return output;
27
+ }
28
+
29
+ export function mergeArtifactSigningResults({
30
+ inputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_INPUT_ROOT,
31
+ outputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
32
+ } = {}) {
33
+ const input = path.resolve(required(inputRoot, "signing result input root"));
34
+ const output = path.resolve(required(outputRoot, "signing result output root"));
35
+ fs.mkdirSync(output, { recursive: true });
36
+ const merged = [];
37
+ const seen = new Set();
38
+ for (const indexPath of walk(input)) {
39
+ const index = JSON.parse(fs.readFileSync(indexPath, "utf8"));
40
+ if (index.contract !== "kungfu-buildchain-artifact-signing-result-index/v1") continue;
41
+ for (const entry of index.results || []) {
42
+ if (seen.has(entry.id)) throw new Error(`duplicate signing result: ${entry.id}`);
43
+ seen.add(entry.id);
44
+ const sourceResult = path.resolve(path.dirname(indexPath), entry.result);
45
+ const sourceDirectory = path.dirname(sourceResult);
46
+ const relative = path.relative(path.dirname(indexPath), sourceDirectory);
47
+ if (relative.startsWith("..") || path.isAbsolute(relative)) throw new Error("signing result escapes provider root");
48
+ const destinationName = safeId(entry.id);
49
+ const destination = path.join(output, destinationName);
50
+ fs.cpSync(sourceDirectory, destination, { recursive: true, errorOnExist: true, force: false });
51
+ merged.push({
52
+ ...entry,
53
+ result: `${destinationName}/${path.basename(sourceResult)}`,
54
+ ...(entry.payload ? { payload: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.payload)).split(path.sep).join("/")}` } : {}),
55
+ ...(entry.envelope ? { envelope: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.envelope)).split(path.sep).join("/")}` } : {}),
56
+ ...(entry.receipt ? { receipt: `${destinationName}/${path.relative(sourceDirectory, path.resolve(path.dirname(indexPath), entry.receipt)).split(path.sep).join("/")}` } : {}),
57
+ });
58
+ }
59
+ }
60
+ if (merged.length === 0) throw new Error("no artifact signing results found");
61
+ merged.sort((a, b) => a.id.localeCompare(b.id));
62
+ const index = { schemaVersion: 1, contract: "kungfu-buildchain-artifact-signing-result-index/v1", results: merged };
63
+ const indexPath = path.join(output, "index.json");
64
+ fs.writeFileSync(indexPath, `${JSON.stringify(index, null, 2)}\n`);
65
+ writeGitHubOutputs({ "result-count": String(merged.length), "result-index": indexPath, "result-root": output });
66
+ return index;
67
+ }
68
+
69
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
70
+ try {
71
+ mergeArtifactSigningResults();
72
+ } catch (error) {
73
+ console.error(`::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`);
74
+ process.exitCode = 1;
75
+ }
76
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { spawnSync } from "node:child_process";
8
+
9
+ import {
10
+ createGitHubArtifactAttestationVerificationPlan,
11
+ verifyGitHubArtifactAttestationEvidence,
12
+ } from "../packages/core/github-artifact-attestation.js";
13
+
14
+ function parseArgs(argv) {
15
+ const result = {};
16
+ for (let index = 0; index < argv.length; index += 2) {
17
+ const key = argv[index];
18
+ const value = argv[index + 1];
19
+ if (!key?.startsWith("--") || value === undefined) throw new Error(`invalid argument near ${key || "<end>"}`);
20
+ result[key.slice(2)] = value;
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function required(value, label) {
26
+ const normalized = String(value || "").trim();
27
+ if (!normalized) throw new Error(`${label} is required`);
28
+ return normalized;
29
+ }
30
+
31
+ function sha256(value) {
32
+ return crypto.createHash("sha256").update(value).digest("hex");
33
+ }
34
+
35
+ function sha256File(filePath) {
36
+ return `sha256:${sha256(fs.readFileSync(filePath))}`;
37
+ }
38
+
39
+ function readJson(filePath, label) {
40
+ try {
41
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
42
+ } catch (error) {
43
+ throw new Error(`${label} is not readable JSON: ${error.message}`);
44
+ }
45
+ }
46
+
47
+ function safeAssetStem(value) {
48
+ return path.basename(required(value, "subject name"))
49
+ .replace(/[^0-9A-Za-z._-]+/g, "-")
50
+ .replace(/^-+|-+$/g, "");
51
+ }
52
+
53
+ async function api({ token, apiUrl, route, method = "GET", headers = {}, body }) {
54
+ const response = await fetch(`${apiUrl}${route}`, {
55
+ method,
56
+ headers: {
57
+ accept: "application/vnd.github+json",
58
+ authorization: `Bearer ${token}`,
59
+ "x-github-api-version": "2022-11-28",
60
+ ...headers,
61
+ },
62
+ body,
63
+ });
64
+ if (!response.ok) {
65
+ const detail = (await response.text()).slice(0, 500);
66
+ throw new Error(`GitHub API ${method} ${route} failed with ${response.status}: ${detail}`);
67
+ }
68
+ return response;
69
+ }
70
+
71
+ async function remoteAssetDigest({ token, apiUrl, repository, asset }) {
72
+ const declared = String(asset.digest || "").match(/^sha256:([0-9a-f]{64})$/i);
73
+ if (declared) return `sha256:${declared[1].toLowerCase()}`;
74
+ const response = await api({
75
+ token,
76
+ apiUrl,
77
+ route: `/repos/${repository}/releases/assets/${asset.id}`,
78
+ headers: { accept: "application/octet-stream" },
79
+ });
80
+ return `sha256:${sha256(Buffer.from(await response.arrayBuffer()))}`;
81
+ }
82
+
83
+ async function uploadImmutable({ token, apiUrl, repository, release, filePath, assetName }) {
84
+ const localDigest = sha256File(filePath);
85
+ const existing = (release.assets || []).filter((asset) => asset.name === assetName);
86
+ if (existing.length > 1) throw new Error(`release asset ${assetName} exists more than once`);
87
+ if (existing.length === 1) {
88
+ const remoteDigest = await remoteAssetDigest({ token, apiUrl, repository, asset: existing[0] });
89
+ if (remoteDigest !== localDigest) {
90
+ throw new Error(`immutable release asset collision for ${assetName}: ${remoteDigest} != ${localDigest}`);
91
+ }
92
+ return { action: "preserved", name: assetName, digest: localDigest, url: existing[0].browser_download_url };
93
+ }
94
+ const uploadBase = required(release.upload_url, "release.upload_url").replace(/\{.*$/, "");
95
+ const response = await fetch(`${uploadBase}?name=${encodeURIComponent(assetName)}`, {
96
+ method: "POST",
97
+ headers: {
98
+ accept: "application/vnd.github+json",
99
+ authorization: `Bearer ${token}`,
100
+ "content-type": "application/octet-stream",
101
+ "x-github-api-version": "2022-11-28",
102
+ },
103
+ body: fs.readFileSync(filePath),
104
+ });
105
+ if (!response.ok) throw new Error(`GitHub release asset upload failed with ${response.status}: ${(await response.text()).slice(0, 500)}`);
106
+ const asset = await response.json();
107
+ release.assets = [...(release.assets || []), asset];
108
+ const remoteDigest = await remoteAssetDigest({ token, apiUrl, repository, asset });
109
+ if (remoteDigest !== localDigest) throw new Error(`release asset read-back mismatch for ${assetName}`);
110
+ return { action: "uploaded", name: assetName, digest: localDigest, url: asset.browser_download_url };
111
+ }
112
+
113
+ function appendOutput(name, value) {
114
+ if (process.env.GITHUB_OUTPUT) fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${String(value)}\n`);
115
+ }
116
+
117
+ async function main() {
118
+ const options = parseArgs(process.argv.slice(2));
119
+ const token = required(process.env.GITHUB_TOKEN || process.env.GH_TOKEN, "GITHUB_TOKEN");
120
+ const apiUrl = (process.env.GITHUB_API_URL || "https://api.github.com").replace(/\/$/, "");
121
+ const repository = required(options.repository || process.env.GITHUB_REPOSITORY, "repository");
122
+ const tag = required(options.tag, "tag");
123
+ const subjectPath = path.resolve(required(options.subject, "subject"));
124
+ const manifestPath = path.resolve(required(options.manifest, "manifest"));
125
+ const passportPath = path.resolve(required(options.passport, "passport"));
126
+ const bundlePath = path.resolve(required(options.bundle, "bundle"));
127
+ const evidencePath = path.resolve(required(options.evidence, "evidence"));
128
+ const predicatePath = path.resolve(required(options.predicate, "predicate"));
129
+ const providerVerificationPath = path.resolve(required(options["provider-verification"], "provider-verification"));
130
+ const receiptPath = path.resolve(required(options.receipt, "receipt"));
131
+ const evidence = readJson(evidencePath, "Buildchain attestation evidence");
132
+ const plan = createGitHubArtifactAttestationVerificationPlan({ artifactPath: subjectPath, bundlePath, evidence });
133
+ const provider = spawnSync(plan.command, plan.args, { encoding: "utf8", env: process.env });
134
+ if (provider.status !== 0) throw new Error(`gh attestation verify failed: ${String(provider.stderr || provider.stdout).slice(0, 1000)}`);
135
+ const verificationResults = JSON.parse(provider.stdout);
136
+ const local = verifyGitHubArtifactAttestationEvidence({
137
+ artifactPath: subjectPath,
138
+ platformManifestPath: manifestPath,
139
+ releasePassportPath: passportPath,
140
+ bundlePath,
141
+ evidence,
142
+ verificationResults,
143
+ });
144
+ if (!local.ok) throw new Error(`Buildchain evidence verification failed: ${local.issues.map((issue) => issue.message).join("; ")}`);
145
+ const retainedProvider = readJson(providerVerificationPath, "retained provider verification");
146
+ const retained = verifyGitHubArtifactAttestationEvidence({
147
+ artifactPath: subjectPath,
148
+ platformManifestPath: manifestPath,
149
+ releasePassportPath: passportPath,
150
+ bundlePath,
151
+ evidence,
152
+ verificationResults: retainedProvider,
153
+ });
154
+ if (!retained.ok) {
155
+ throw new Error(`retained provider verification failed: ${retained.issues.map((issue) => issue.message).join("; ")}`);
156
+ }
157
+ const releaseResponse = await api({ token, apiUrl, route: `/repos/${repository}/releases/tags/${encodeURIComponent(tag)}` });
158
+ const release = await releaseResponse.json();
159
+ const stem = safeAssetStem(evidence.subject?.name);
160
+ const declarations = [
161
+ [bundlePath, `${stem}.sigstore-bundle.json`],
162
+ [evidencePath, `${stem}.buildchain-attestation.json`],
163
+ [predicatePath, `${stem}.buildchain-predicate.json`],
164
+ [providerVerificationPath, `${stem}.github-verification.json`],
165
+ ];
166
+ const assets = [];
167
+ for (const [filePath, assetName] of declarations) {
168
+ assets.push(await uploadImmutable({ token, apiUrl, repository, release, filePath, assetName }));
169
+ }
170
+ const receipt = {
171
+ contract: "buildchain.github-artifact-attestation-publication/v1",
172
+ repository,
173
+ tag,
174
+ releaseUrl: release.html_url,
175
+ subject: evidence.subject,
176
+ evidenceRoot: evidence.evidenceRoot,
177
+ attestation: evidence.attestation,
178
+ assets,
179
+ verified: true,
180
+ };
181
+ fs.mkdirSync(path.dirname(receiptPath), { recursive: true });
182
+ fs.writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`);
183
+ const receiptAsset = await uploadImmutable({
184
+ token,
185
+ apiUrl,
186
+ repository,
187
+ release,
188
+ filePath: receiptPath,
189
+ assetName: `${stem}.buildchain-attestation-publication.json`,
190
+ });
191
+ appendOutput("release-url", release.html_url);
192
+ appendOutput("evidence-root", evidence.evidenceRoot);
193
+ appendOutput("publication-receipt", receiptPath);
194
+ appendOutput("publication-receipt-digest", receiptAsset.digest);
195
+ process.stdout.write(`${JSON.stringify({ ...receipt, receiptAsset }, null, 2)}\n`);
196
+ }
197
+
198
+ main().catch((error) => {
199
+ console.error(error.message);
200
+ process.exitCode = 1;
201
+ });