@kungfu-tech/buildchain 3.0.2-alpha.4 → 3.0.2-alpha.5

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 (46) hide show
  1. package/README.md +1 -0
  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/dist/site/artifact-schemas.json +5 -1
  7. package/dist/site/buildchain-contract.json +79 -28
  8. package/dist/site/buildchain-site.json +108 -25
  9. package/dist/site/capability-registry.json +8 -7
  10. package/dist/site/cli-registry.json +12 -0
  11. package/dist/site/controller-registry.json +40 -4
  12. package/dist/site/kfd-claims.json +203 -18
  13. package/dist/site/kfd-upstream-aggregate.json +1 -1
  14. package/dist/site/manual-registry.json +19 -5
  15. package/dist/site/node-api-registry.json +22 -9
  16. package/dist/site/page-registry.json +89 -15
  17. package/dist/site/public-surface-audit.json +125 -15
  18. package/dist/site/publication-authority-registry.json +27 -2
  19. package/dist/site/publication-registry.json +4 -4
  20. package/dist/site/release-model.json +2 -1
  21. package/dist/site/release-passport-check-manifest.json +1 -0
  22. package/dist/site/release-provenance.json +1 -0
  23. package/dist/site/schemas/release-passport-v1.schema.json +6 -0
  24. package/dist/site/site-manifest.json +17 -9
  25. package/dist/site/workflow-registry.json +84 -7
  26. package/docs/MAP.md +3 -1
  27. package/docs/binary-distribution.md +7 -0
  28. package/docs/cli.md +9 -0
  29. package/docs/github-artifact-attestation.md +219 -0
  30. package/docs/release-passport.md +13 -0
  31. package/docs/reusable-build-surface.md +6 -0
  32. package/package.json +2 -1
  33. package/packages/core/buildchain-contract.js +6 -0
  34. package/packages/core/buildchain-kfd-claims.js +5 -0
  35. package/packages/core/buildchain-publication-authority.js +1 -0
  36. package/packages/core/github-artifact-attestation.js +642 -0
  37. package/packages/core/index.js +19 -0
  38. package/packages/core/publication-authority.js +1 -1
  39. package/packages/core/release-passport-contract.js +2 -0
  40. package/packages/core/release-passport.js +51 -0
  41. package/scripts/check-inventory.mjs +4 -0
  42. package/scripts/create-github-artifact-attestation-policy.mjs +62 -0
  43. package/scripts/generate-site-bundle.mjs +12 -0
  44. package/scripts/publish-github-artifact-attestation-evidence.mjs +201 -0
  45. package/scripts/release-candidate-resolver.mjs +12 -0
  46. package/scripts/stage-github-artifact-attestation-inputs.mjs +65 -0
@@ -21,6 +21,9 @@ import {
21
21
  import { createSurfaceTimestampPolicy } from "./surface-manifest.js";
22
22
  import { validatePublishEvidence as validateTransactionPublishEvidence } from "./publish-transaction.js";
23
23
  import { normalizeControllerReceiptReferences } from "./controller-evidence.js";
24
+ import {
25
+ normalizeGitHubArtifactAttestationPolicy,
26
+ } from "./github-artifact-attestation.js";
24
27
 
25
28
  export const RELEASE_PASSPORT_CONTRACT = "kungfu-buildchain-release-passport";
26
29
  export const ARTIFACT_EVIDENCE_CONTRACT = "kungfu-buildchain-artifact-evidence";
@@ -1166,6 +1169,7 @@ export function createReleasePassport({
1166
1169
  kfdAgentHubEvidencePath = "",
1167
1170
  controllerReceipts = [],
1168
1171
  controllerReceiptReferences = [],
1172
+ githubArtifactAttestations = [],
1169
1173
  } = {}) {
1170
1174
  const normalizedTag = nonEmptyString(tag, "tag");
1171
1175
  const artifactEvidence = createArtifactEvidence({ assets, repository, tag: normalizedTag, sourceSha, workflow });
@@ -1231,6 +1235,8 @@ export function createReleasePassport({
1231
1235
  : [],
1232
1236
  requirePassed: true,
1233
1237
  });
1238
+ const normalizedGitHubArtifactAttestations = (githubArtifactAttestations || [])
1239
+ .map(normalizeGitHubArtifactAttestationPolicy);
1234
1240
  const publishArtifacts = normalizedPublishEvidence?.artifacts || [];
1235
1241
  const normalizedPublishSummary = normalizePublishSummary({
1236
1242
  packageSet: normalizedPackageSet,
@@ -1360,6 +1366,9 @@ export function createReleasePassport({
1360
1366
  ...(normalizedKfdAgentHub ? { kfdAgentHub: normalizedKfdAgentHub } : {}),
1361
1367
  ...(invariantPassports ? { invariantPassports } : {}),
1362
1368
  ...(normalizedControllerReceipts.length > 0 ? { controllerReceipts: normalizedControllerReceipts } : {}),
1369
+ ...(normalizedGitHubArtifactAttestations.length > 0
1370
+ ? { githubArtifactAttestations: normalizedGitHubArtifactAttestations }
1371
+ : {}),
1363
1372
  versionImpact: normalizedImpact.versionImpact,
1364
1373
  surfaceImpacts: normalizedImpact.surfaceImpacts,
1365
1374
  artifacts: [
@@ -1450,6 +1459,7 @@ export function collectGitHubReleasePassport({
1450
1459
  invariantPassportCommand = "",
1451
1460
  kfdAgentHubEvidenceJson = "",
1452
1461
  controllerReceiptReferences = [],
1462
+ githubArtifactAttestationPolicyJsons = [],
1453
1463
  basePassportJson = "",
1454
1464
  requireBaseKfd = false,
1455
1465
  releaseJsonExtra = "",
@@ -1526,6 +1536,13 @@ export function collectGitHubReleasePassport({
1526
1536
  undefined,
1527
1537
  { cwd, label: "kfdAgentHubEvidenceJson" },
1528
1538
  );
1539
+ const githubArtifactAttestationPolicies = (githubArtifactAttestationPolicyJsons || [])
1540
+ .filter(Boolean)
1541
+ .map((policyJson) => parseJsonInput(policyJson, undefined, {
1542
+ cwd,
1543
+ label: "githubArtifactAttestationPolicyJsons entry",
1544
+ }))
1545
+ .map(normalizeGitHubArtifactAttestationPolicy);
1529
1546
  const kfd3ArtifactWitnesses = [
1530
1547
  ...kfd3ArtifactWitnessMetas.map((meta) => meta.value),
1531
1548
  ...(kfd3ArtifactCommandMeta.value ? [kfd3ArtifactCommandMeta.value] : []),
@@ -1623,6 +1640,7 @@ export function collectGitHubReleasePassport({
1623
1640
  : undefined,
1624
1641
  kfdAgentHubEvidencePath: kfdAgentHubEvidenceMeta.value ? "kfd-agent-hub-evidence.json" : "",
1625
1642
  controllerReceiptReferences,
1643
+ githubArtifactAttestations: githubArtifactAttestationPolicies,
1626
1644
  publishEvidencePath: publishEvidenceMeta.path ? path.relative(resolvedOutputDir, publishEvidenceMeta.path).split(path.sep).join("/") : "",
1627
1645
  transactionStatePath: transactionMeta.path ? path.relative(resolvedOutputDir, transactionMeta.path).split(path.sep).join("/") : "",
1628
1646
  workflow,
@@ -1972,6 +1990,39 @@ export function createReleaseCheckReport({
1972
1990
  issues.push(issue("error", "kfdSupport.section", "KFD support evidence is present without a release-passport projection"));
1973
1991
  }
1974
1992
 
1993
+ for (const [index, value] of (passport?.githubArtifactAttestations || []).entries()) {
1994
+ try {
1995
+ const policy = normalizeGitHubArtifactAttestationPolicy(value);
1996
+ if (policy.caller.sourceSha !== String(passport?.release?.sourceSha || "").toLowerCase()) {
1997
+ issues.push(issue(
1998
+ "error",
1999
+ `githubArtifactAttestations[${index}].caller.sourceSha`,
2000
+ "attestation policy source SHA must match passport.release.sourceSha",
2001
+ ));
2002
+ }
2003
+ const artifact = (passport?.artifacts || []).find((entry) => entry.name === policy.subject.name);
2004
+ if (!artifact) {
2005
+ issues.push(issue(
2006
+ "error",
2007
+ `githubArtifactAttestations[${index}].subject.name`,
2008
+ `attestation subject ${policy.subject.name} is absent from the Release Passport artifacts`,
2009
+ ));
2010
+ } else if (artifact.sha256 !== policy.subject.digest.sha256) {
2011
+ issues.push(issue(
2012
+ "error",
2013
+ `githubArtifactAttestations[${index}].subject.digest`,
2014
+ `attestation subject ${policy.subject.name} digest differs from the Release Passport artifact`,
2015
+ ));
2016
+ }
2017
+ } catch (error) {
2018
+ issues.push(issue(
2019
+ "error",
2020
+ `githubArtifactAttestations[${index}]`,
2021
+ error.message,
2022
+ ));
2023
+ }
2024
+ }
2025
+
1975
2026
  const tag = passport?.release?.tag || "";
1976
2027
  if (!tag) {
1977
2028
  issues.push(issue("error", "release.tag", "release.tag is required"));
@@ -68,6 +68,7 @@ const requiredPaths = [
68
68
  "docs/product-mechanism.md",
69
69
  "docs/readme-badges.md",
70
70
  "docs/release-passport.md",
71
+ "docs/github-artifact-attestation.md",
71
72
  "docs/shifu-gate-profiles.md",
72
73
  "docs/auditable-demo.md",
73
74
  "docs/release-propagation.md",
@@ -358,6 +359,9 @@ if (rootPackage.exports?.["./kfd-gate"] !== "./packages/core/kfd-gate.js") {
358
359
  if (rootPackage.exports?.["./release-passport"] !== "./packages/core/release-passport.js") {
359
360
  throw new Error("root package must export @kungfu-tech/buildchain/release-passport");
360
361
  }
362
+ if (rootPackage.exports?.["./github-artifact-attestation"] !== "./packages/core/github-artifact-attestation.js") {
363
+ throw new Error("root package must export @kungfu-tech/buildchain/github-artifact-attestation");
364
+ }
361
365
  if (rootPackage.exports?.["./release-passport-contract"] !== "./packages/core/release-passport-contract.js") {
362
366
  throw new Error("root package must export @kungfu-tech/buildchain/release-passport-contract");
363
367
  }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ import {
8
+ GITHUB_ARTIFACT_ATTESTATION_WORKFLOW,
9
+ createGitHubArtifactAttestationPolicy,
10
+ githubArtifactAttestationSha256File,
11
+ } from "../packages/core/github-artifact-attestation.js";
12
+
13
+ function env(name, fallback = "") {
14
+ return String(process.env[name] || fallback).trim();
15
+ }
16
+
17
+ function required(name) {
18
+ const value = env(name);
19
+ if (!value) throw new Error(`${name} is required`);
20
+ return value;
21
+ }
22
+
23
+ const cwd = path.resolve(env("BUILDCHAIN_SOURCE_CWD", "."));
24
+ const subjectRelativePath = required("BUILDCHAIN_GITHUB_ATTESTATION_SUBJECT_PATH").replace(/\\/g, "/");
25
+ const subjectPath = path.resolve(cwd, subjectRelativePath);
26
+ const manifestPath = path.resolve(required("BUILDCHAIN_GITHUB_ATTESTATION_PLATFORM_MANIFEST"));
27
+ const outputPath = path.resolve(required("BUILDCHAIN_GITHUB_ATTESTATION_POLICY_OUTPUT"));
28
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
29
+ const manifestEntry = (manifest.files || []).find((entry) => (
30
+ String(entry.path || "").replace(/\\/g, "/") === subjectRelativePath
31
+ ));
32
+ if (!manifestEntry) throw new Error(`platform manifest does not contain ${subjectRelativePath}`);
33
+ const digest = githubArtifactAttestationSha256File(subjectPath);
34
+ const size = fs.statSync(subjectPath).size;
35
+ if (`sha256:${String(manifestEntry.sha256 || "").toLowerCase()}` !== digest || Number(manifestEntry.size) !== size) {
36
+ throw new Error("subject bytes do not match the final platform manifest");
37
+ }
38
+ const manifestDigest = githubArtifactAttestationSha256File(manifestPath);
39
+ const runtimeSha = required("BUILDCHAIN_RUNTIME_SHA").toLowerCase();
40
+ const signerSha = required("BUILDCHAIN_GITHUB_ATTESTATION_SIGNER_SHA").toLowerCase();
41
+ const policy = createGitHubArtifactAttestationPolicy({
42
+ subject: { name: path.basename(subjectPath), path: subjectRelativePath, size, digest },
43
+ caller: {
44
+ repository: required("BUILDCHAIN_SOURCE_REPOSITORY"),
45
+ sourceSha: required("BUILDCHAIN_SOURCE_SHA").toLowerCase(),
46
+ sourceTreeSha: required("BUILDCHAIN_SOURCE_TREE_SHA").toLowerCase(),
47
+ },
48
+ signer: {
49
+ repository: "kungfu-systems/buildchain",
50
+ workflowPath: GITHUB_ARTIFACT_ATTESTATION_WORKFLOW,
51
+ workflowDigest: signerSha,
52
+ },
53
+ build: {
54
+ platform: required("BUILDCHAIN_PLATFORM_ID"),
55
+ platformManifestDigest: manifestDigest,
56
+ runnerReceiptRoot: manifestDigest,
57
+ buildchainRuntimeSha: runtimeSha,
58
+ },
59
+ });
60
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
61
+ fs.writeFileSync(outputPath, `${JSON.stringify(policy, null, 2)}\n`);
62
+ process.stdout.write(`github-artifact-attestation-policy=${outputPath}\n`);
@@ -328,6 +328,7 @@ const manualMetaById = new Map(Object.entries({
328
328
  "product-mechanism": { capabilityGroup: "getting-started", audience: ["agent", "maintainer"], maturity: "stable", order: 30 },
329
329
  cli: { capabilityGroup: "api-cli-reference", audience: ["agent", "developer"], maturity: "stable", order: 40 },
330
330
  "release-passport": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "stable", order: 100 },
331
+ "github-artifact-attestation": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 108 },
331
332
  "publication-authority": { capabilityGroup: "release-passport-trust", audience: ["release-operator", "agent"], maturity: "preview", order: 105 },
332
333
  "github-governance-authority": { capabilityGroup: "governance-versioning", audience: ["maintainer", "release-operator", "agent"], maturity: "preview", order: 106 },
333
334
  "controller-evidence": { capabilityGroup: "reusable-build", audience: ["consumer", "release-operator", "agent"], maturity: "draft", order: 205 },
@@ -405,6 +406,7 @@ function cliCommandMeta(id) {
405
406
  "collect-github-release": { group: "release-passport-trust", purpose: "Collect GitHub Release assets into a release passport." },
406
407
  create: { group: "release-passport-trust", purpose: "Create canonical sealed publication evidence documents." },
407
408
  "create-publication-admission": { group: "release-passport-trust", purpose: "Create a canonical short-lived publication admission envelope from exact consumer bindings." },
409
+ "create-github-artifact-attestation-policy": { group: "release-passport-trust", purpose: "Create an exact source, signer, Linux build, and Release Passport attestation policy." },
408
410
  "create-runner-provenance": { group: "release-passport-trust", purpose: "Create runner provenance evidence with an explicit qualification floor." },
409
411
  diagnostics: { group: "observability-diagnostics", purpose: "Inspect diagnostics command families." },
410
412
  "diagnostics-summary": { group: "observability-diagnostics", purpose: "Summarize diagnostics artifacts into JSON and cross-platform lifecycle timing tables." },
@@ -494,6 +496,7 @@ function cliCommandMeta(id) {
494
496
  verify: { group: "release-passport-trust", purpose: "Inspect release and artifact verification command families." },
495
497
  "verify-artifact": { group: "release-passport-trust", purpose: "Verify artifact subjects against release passport evidence." },
496
498
  "verify-artifact-envelope": { group: "release-passport-trust", purpose: "Verify exact roots, identity, lifecycle, revocation, and an existing KFD assessment in a sealed artifact envelope." },
499
+ "verify-github-artifact-attestation": { group: "release-passport-trust", purpose: "Verify GitHub keyless attestation identity plus local artifact, manifest, Passport, predicate, bundle, and evidence bindings." },
497
500
  "verify-infra-contract-evidence-bundle": { group: "governance-versioning", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
498
501
  "verify-observability-log": { group: "observability-diagnostics", purpose: "Verify Buildchain observability log events." },
499
502
  "verify-publication-admission": { group: "release-passport-trust", purpose: "Independently verify sealed publication admission, runner provenance, control-plane audit, nonce freshness, and exact artifact bindings." },
@@ -532,6 +535,7 @@ function nodeApiMeta(exportName) {
532
535
  "./artifact-verification-envelope": { group: "release-passport-trust", summary: "Sealed exact-root, lifecycle, identity, and existing KFD assessment inputs for KFX admission." },
533
536
  "./anchored-version-material": { group: "reusable-build", summary: "Anchored/manual derived version material preflight, exact-tree binding, and digest evidence APIs." },
534
537
  "./release-passport": { group: "release-passport-trust", summary: "Release passport collection, verification, explanation, and evidence APIs." },
538
+ "./github-artifact-attestation": { group: "release-passport-trust", summary: "GitHub keyless artifact attestation policy, predicate, provider evidence, and fail-closed verification APIs." },
535
539
  "./kfd-agent-hub": { group: "kfd-trust", summary: "Declarative Agent Hub adapter inspection, fixed-suite execution, exact KFD cut locking, and agent explanation APIs." },
536
540
  "./release-passport-contract": { group: "release-passport-trust", summary: "Standalone release passport JSON Schema, ownership/check manifest, and structural validation APIs." },
537
541
  "./release-candidate": { group: "reusable-build", summary: "PR-stage release-candidate artifact, passport, and promote-only resolver APIs." },
@@ -606,6 +610,7 @@ function buildCapabilityRegistry({ docs, pages, cliRegistry, manualRegistry, nod
606
610
  }
607
611
 
608
612
  function workflowCapabilityGroup(entry) {
613
+ if (entry.id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
609
614
  if (["web-surface", "release-propagation"].includes(entry.id)) return capabilityGroup("site-and-propagation");
610
615
  if (["build", "release-candidate-promote", "publication-artifact", "paper-release"].includes(entry.id)) return capabilityGroup("reusable-build");
611
616
  if (["buildchain-ref-promotion", "release-line-bootstrap"].includes(entry.id)) return capabilityGroup("release-passport-trust");
@@ -615,6 +620,7 @@ function workflowCapabilityGroup(entry) {
615
620
  }
616
621
 
617
622
  function actionCapabilityGroup(id) {
623
+ if (id === "github-artifact-attestation") return capabilityGroup("release-passport-trust");
618
624
  if (id === "promote-buildchain-ref") return capabilityGroup("release-passport-trust");
619
625
  if (id === "run-lifecycle" || id === "validate-config") return capabilityGroup("reusable-build");
620
626
  if (id === "report-buildchain-issue") return capabilityGroup("observability-diagnostics");
@@ -898,6 +904,7 @@ function buildSiteBundle() {
898
904
  ["dev-pr-auto-merge", "dev-governance"],
899
905
  ["github-governance-audit", "dev-governance"],
900
906
  ["binary-distribution", "release-passport"],
907
+ ["github-artifact-attestation", "release-passport"],
901
908
  ["buildchain-patrol", "repository-patrol"],
902
909
  ["buildchain-patrol-daily", "repository-patrol"],
903
910
  ["buildchain-patrol-weekly", "repository-patrol"],
@@ -991,6 +998,7 @@ function buildSiteBundle() {
991
998
  "publish evidence JSON",
992
999
  "buildchain.release.json",
993
1000
  "release passport assets",
1001
+ "GitHub artifact attestation Sigstore bundle and Buildchain evidence JSON",
994
1002
  ],
995
1003
  owner: "promote-buildchain-ref",
996
1004
  },
@@ -1023,6 +1031,10 @@ function buildSiteBundle() {
1023
1031
  "llms.txt",
1024
1032
  "buildchain-release-bundle.json",
1025
1033
  "buildchain-release-bundle.tar.gz",
1034
+ "github-artifact-attestation.policy.json",
1035
+ "github-artifact-attestation.predicate.json",
1036
+ "github-artifact-attestation.evidence.json",
1037
+ "attestation.sigstore.json",
1026
1038
  ],
1027
1039
  site: [
1028
1040
  "buildchain-site.json",
@@ -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
+ });
@@ -608,6 +608,10 @@ export async function resolveReleaseCandidateArtifacts({
608
608
  }
609
609
  const passport = JSON.parse(fs.readFileSync(passportPath, "utf8"));
610
610
  const platformManifestPaths = findDownloadedFiles(payloadDir, "manifest.json");
611
+ const githubArtifactAttestationPolicyPaths = findDownloadedFiles(
612
+ payloadDir,
613
+ "github-artifact-attestation-policy.json",
614
+ );
611
615
  const npmTarballPaths = publishArtifactKind === "npm"
612
616
  ? findDownloadedFilesByExtension(payloadDir, [".tgz"])
613
617
  : [];
@@ -639,6 +643,7 @@ export async function resolveReleaseCandidateArtifacts({
639
643
  buildSummary: outputPath(buildSummaryPath),
640
644
  payloads: outputPath(payloadDir),
641
645
  platformManifests: platformManifestPaths.map(outputPath),
646
+ githubArtifactAttestationPolicies: githubArtifactAttestationPolicyPaths.map(outputPath),
642
647
  npmTarballs: npmTarballPaths.map(outputPath),
643
648
  releaseAssets: releaseAssetPaths.map(outputPath),
644
649
  publishRequiredArtifacts: outputPath(requiredArtifactsPath),
@@ -647,6 +652,7 @@ export async function resolveReleaseCandidateArtifacts({
647
652
  candidateHash: passport.candidateHash || "",
648
653
  payloadCount: payloadArtifacts.length,
649
654
  platformManifestCount: platformManifestPaths.length,
655
+ githubArtifactAttestationPolicyCount: githubArtifactAttestationPolicyPaths.length,
650
656
  npmTarballCount: npmTarballPaths.length,
651
657
  publishRequiredArtifacts: generatedRequiredArtifacts,
652
658
  };
@@ -684,6 +690,12 @@ export async function resolveReleaseCandidateArtifactsCli() {
684
690
  "release-candidate-payload-dir": result.paths?.payloads || "",
685
691
  "release-candidate-platform-manifest-paths": (result.paths?.platformManifests || []).join(","),
686
692
  "release-candidate-platform-manifest-count": String(result.platformManifestCount || 0),
693
+ "release-candidate-github-artifact-attestation-policy-paths": (
694
+ result.paths?.githubArtifactAttestationPolicies || []
695
+ ).join(","),
696
+ "release-candidate-github-artifact-attestation-policy-count": String(
697
+ result.githubArtifactAttestationPolicyCount || 0,
698
+ ),
687
699
  "release-candidate-npm-tarball-paths": (result.paths?.npmTarballs || []).join(","),
688
700
  "release-candidate-npm-tarball-count": String(result.npmTarballCount || 0),
689
701
  "release-candidate-github-release-artifact-paths": (
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ import { stageGitHubArtifactAttestationInputs } from "../packages/core/github-artifact-attestation.js";
8
+
9
+ function args(argv) {
10
+ const values = {};
11
+ for (let index = 0; index < argv.length; index += 2) {
12
+ const key = argv[index];
13
+ const value = argv[index + 1];
14
+ if (!key?.startsWith("--") || value === undefined) {
15
+ throw new Error(`invalid argument near ${key || "<end>"}`);
16
+ }
17
+ values[key.slice(2)] = value;
18
+ }
19
+ return values;
20
+ }
21
+
22
+ function splitPaths(value) {
23
+ return String(value || "")
24
+ .split(/[\n,]/)
25
+ .map((entry) => entry.trim())
26
+ .filter(Boolean);
27
+ }
28
+
29
+ function readPolicy(value) {
30
+ const candidate = path.resolve(String(value || ""));
31
+ return fs.existsSync(candidate)
32
+ ? JSON.parse(fs.readFileSync(candidate, "utf8"))
33
+ : JSON.parse(String(value || ""));
34
+ }
35
+
36
+ function appendOutput(name, value) {
37
+ const output = process.env.GITHUB_OUTPUT;
38
+ if (!output) return;
39
+ fs.appendFileSync(output, `${name}=${String(value)}\n`);
40
+ }
41
+
42
+ const options = args(process.argv.slice(2));
43
+ const result = stageGitHubArtifactAttestationInputs({
44
+ policy: readPolicy(options.policy),
45
+ subjectRoots: splitPaths(options["subject-roots"]),
46
+ platformManifestPaths: splitPaths(options["platform-manifests"]),
47
+ releasePassportPath: options["release-passport"],
48
+ outputDir: options["output-dir"],
49
+ });
50
+ appendOutput("input-dir", result.outputDir);
51
+ appendOutput("policy-json", result.policyJson);
52
+ appendOutput("subject-relative-path", result.relativePaths.subject);
53
+ appendOutput("platform-manifest-relative-path", result.relativePaths.platformManifest);
54
+ appendOutput("release-passport-relative-path", result.relativePaths.releasePassport);
55
+ appendOutput("source-sha", result.policy.caller.sourceSha);
56
+ appendOutput("signer-sha", result.policy.signer.workflowDigest);
57
+ appendOutput("buildchain-runtime-sha", result.policy.build.buildchainRuntimeSha);
58
+ appendOutput("subject-name", result.policy.subject.name);
59
+ process.stdout.write(`${JSON.stringify({
60
+ contract: result.contract,
61
+ subject: result.policy.subject,
62
+ caller: result.policy.caller,
63
+ signer: result.policy.signer,
64
+ paths: result.relativePaths,
65
+ }, null, 2)}\n`);