@kungfu-tech/buildchain 4.0.1 → 4.0.2-alpha.2

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 (50) hide show
  1. package/architecture/agent-change-map.md +7 -5
  2. package/architecture/ci-lane-change-budget.json +76 -8
  3. package/architecture/internal-capabilities.json +4 -1
  4. package/architecture/maintainability-debt.json +45 -119
  5. package/architecture/maintainability-policy.json +13 -8
  6. package/architecture/release-tail-contract-inventory.json +4 -4
  7. package/architecture/v3-core-mechanism-inventory.json +2 -0
  8. package/architecture/v3-v4-live-capability-inventory.json +17 -17
  9. package/architecture/v4-release-invocation-fixtures.json +119 -0
  10. package/architecture/v4-release-topology.json +238 -0
  11. package/contracts/buildchain-v2-residuals-v1.json +0 -9
  12. package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
  13. package/contracts/v4-release-invocation-v1.schema.json +86 -0
  14. package/dist/site/buildchain-contract.json +9 -9
  15. package/dist/site/buildchain-site.json +9 -9
  16. package/dist/site/kfd-claims.json +9 -7
  17. package/dist/site/kfd-upstream-aggregate.json +1 -1
  18. package/dist/site/manual-registry.json +1 -1
  19. package/dist/site/node-api-registry.json +3 -3
  20. package/dist/site/page-registry.json +4 -4
  21. package/dist/site/public-surface-audit.json +23 -10
  22. package/dist/site/publication-authority-registry.json +2 -3
  23. package/dist/site/publication-registry.json +4 -4
  24. package/dist/site/site-manifest.json +5 -5
  25. package/dist/site/workflow-registry.json +19 -11
  26. package/docs/dev-delivery-warrant.md +21 -0
  27. package/docs/node-api-reference.md +3 -3
  28. package/package.json +2 -2
  29. package/packages/core/dev-delivery-candidate-identity.js +45 -17
  30. package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
  31. package/packages/core/dev-delivery-warrant-legacy-recovery.js +246 -0
  32. package/packages/core/dev-delivery-warrant-state.js +12 -28
  33. package/packages/core/v4-canonical-contracts.js +8 -0
  34. package/packages/core/v4-floating-consumer-policy.js +4 -1
  35. package/packages/core/v4-release-invocation.js +356 -0
  36. package/scripts/audit-publication-control-plane.mjs +0 -1
  37. package/scripts/check-inventory.mjs +27 -59
  38. package/scripts/check-maintainability.mjs +13 -5
  39. package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
  40. package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
  41. package/scripts/check-v4-release-topology.mjs +237 -0
  42. package/scripts/dev-delivery-warrant-options.mjs +15 -4
  43. package/scripts/dev-delivery-warrant.mjs +29 -7
  44. package/scripts/generate-channel-promotion-workflow.mjs +12 -54
  45. package/scripts/v3-v4-capability-catalog.mjs +107 -0
  46. package/scripts/v4-declarative-promotion-admission.mjs +4 -1
  47. package/scripts/capture-package-release-propagation.mjs +0 -263
  48. package/scripts/publication-commit-evidence.mjs +0 -444
  49. package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
  50. package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
@@ -1,444 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "node:fs";
4
- import { createHash } from "node:crypto";
5
- import path from "node:path";
6
- import { pathToFileURL } from "node:url";
7
-
8
- const SCHEMA = "kungfu-buildchain-publication-commit-evidence/v1";
9
- const INSTALLER_BUNDLE_SCHEMA = "kungfu.installer-publication-bundle/v1";
10
-
11
- function requiredString(value, label) {
12
- if (typeof value !== "string" || value.trim() === "") {
13
- throw new Error(`${label} is required`);
14
- }
15
- return value.trim();
16
- }
17
-
18
- function sha256Root(value, label) {
19
- const normalized = requiredString(value, label);
20
- if (!/^sha256:[0-9a-f]{64}$/.test(normalized)) {
21
- throw new Error(`${label} must be a lowercase sha256 root`);
22
- }
23
- return normalized;
24
- }
25
-
26
- function exactSha(value, label) {
27
- const normalized = requiredString(value, label);
28
- if (!/^[0-9a-f]{40}$/.test(normalized)) {
29
- throw new Error(`${label} must be an exact Git SHA`);
30
- }
31
- return normalized;
32
- }
33
-
34
- function publicHttps(value, label) {
35
- const normalized = requiredString(value, label);
36
- let url;
37
- try {
38
- url = new URL(normalized);
39
- } catch {
40
- throw new Error(`${label} must be a public HTTPS URL`);
41
- }
42
- if (
43
- url.protocol !== "https:" ||
44
- !url.hostname ||
45
- url.username ||
46
- url.password ||
47
- url.search ||
48
- url.hash
49
- ) {
50
- throw new Error(
51
- `${label} must be a public HTTPS URL without credentials, query, or fragment`,
52
- );
53
- }
54
- return normalized;
55
- }
56
-
57
- function canonical(value) {
58
- if (Array.isArray(value)) return value.map(canonical);
59
- if (value && typeof value === "object") {
60
- return Object.fromEntries(
61
- Object.entries(value)
62
- .filter(([, item]) => item !== undefined)
63
- .sort(([left], [right]) => left.localeCompare(right))
64
- .map(([key, item]) => [key, canonical(item)]),
65
- );
66
- }
67
- return value;
68
- }
69
-
70
- function digest(bytes) {
71
- return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
72
- }
73
-
74
- function semanticRoot(value) {
75
- return digest(Buffer.from(JSON.stringify(canonical(value))));
76
- }
77
-
78
- function expectedContentType(assetPath) {
79
- if (assetPath.endsWith(".json")) return "application/json; charset=utf-8";
80
- if (assetPath.endsWith(".sh")) return "text/x-shellscript; charset=utf-8";
81
- if (assetPath.endsWith(".ps1")) return "text/plain; charset=utf-8";
82
- throw new Error(`installer bundle asset type is unsupported: ${assetPath}`);
83
- }
84
-
85
- function validateInstallerBundle(evidence, expected) {
86
- const bundle = evidence.publication?.installerBundle;
87
- if (!bundle) return null;
88
- if (bundle.schema !== INSTALLER_BUNDLE_SCHEMA) {
89
- throw new Error(
90
- `installer bundle schema must be ${INSTALLER_BUNDLE_SCHEMA}`,
91
- );
92
- }
93
- const bundleRoot = sha256Root(
94
- bundle.bundleRoot,
95
- "installerBundle.bundleRoot",
96
- );
97
- if (
98
- bundleRoot !== evidence.publication.payloadRoot ||
99
- bundleRoot !== evidence.readback?.payloadRoot
100
- ) {
101
- throw new Error(
102
- "installer bundle root must be the publication payload root",
103
- );
104
- }
105
- if (
106
- exactSha(bundle.sourceCommit, "installerBundle.sourceCommit") !==
107
- expected.sourceSha ||
108
- !["alpha", "stable"].includes(bundle.channel)
109
- ) {
110
- throw new Error("installer bundle release identity mismatch");
111
- }
112
- sha256Root(bundle.channelPayloadRoot, "installerBundle.channelPayloadRoot");
113
- sha256Root(bundle.channelFileDigest, "installerBundle.channelFileDigest");
114
- sha256Root(
115
- bundle.releasePassport?.root,
116
- "installerBundle.releasePassport.root",
117
- );
118
- sha256Root(bundle.manifestDigest, "installerBundle.manifestDigest");
119
- if (
120
- evidence.readback?.manifestDigest !== bundle.manifestDigest ||
121
- !Array.isArray(bundle.assets) ||
122
- bundle.assets.length !== 7
123
- ) {
124
- throw new Error("installer bundle read-back or asset set is incomplete");
125
- }
126
- const paths = new Set();
127
- const topLevel = new Set([
128
- "installer-publication.json",
129
- "channel-index.json",
130
- "trusted-keys.json",
131
- "install.sh",
132
- "install.ps1",
133
- ]);
134
- const expectedRoles = new Map([
135
- ["installer-publication.json", "publication-manifest"],
136
- ["channel-index.json", "signed-channel-index"],
137
- ["trusted-keys.json", "public-trust-anchors"],
138
- ["install.sh", "friendly-installer"],
139
- ["install.ps1", "friendly-installer"],
140
- ]);
141
- const immutableDirectories = new Set();
142
- let immutableShell = 0;
143
- let immutablePowerShell = 0;
144
- const releaseBaseUrl =
145
- `https://github.com/kungfu-systems/kungfu/releases/download/` +
146
- expected.releaseTag;
147
- for (const asset of bundle.assets) {
148
- const assetPath = requiredString(
149
- asset.path,
150
- "installer bundle asset path",
151
- ).replaceAll("\\", "/");
152
- if (
153
- assetPath.startsWith("/") ||
154
- assetPath.endsWith("/") ||
155
- assetPath.split("/").some((part) => part === "" || part === "..") ||
156
- paths.has(assetPath)
157
- ) {
158
- throw new Error(
159
- `unsafe or duplicate installer bundle asset: ${assetPath}`,
160
- );
161
- }
162
- paths.add(assetPath);
163
- topLevel.delete(assetPath);
164
- if (assetPath.includes("/") && assetPath.endsWith("/install.sh")) {
165
- immutableShell += 1;
166
- immutableDirectories.add(path.posix.dirname(assetPath));
167
- }
168
- if (assetPath.includes("/") && assetPath.endsWith("/install.ps1")) {
169
- immutablePowerShell += 1;
170
- immutableDirectories.add(path.posix.dirname(assetPath));
171
- }
172
- if (!Number.isSafeInteger(asset.size) || asset.size < 1) {
173
- throw new Error(`installer bundle asset size is invalid: ${assetPath}`);
174
- }
175
- sha256Root(asset.digest, `installer bundle asset digest: ${assetPath}`);
176
- const releaseAsset = requiredString(
177
- asset.releaseAsset,
178
- `installer bundle release asset: ${assetPath}`,
179
- );
180
- if (
181
- asset.contentType !== expectedContentType(assetPath) ||
182
- publicHttps(
183
- asset.releaseUrl,
184
- `installer bundle asset URL: ${assetPath}`,
185
- ) !== `${releaseBaseUrl}/${releaseAsset}` ||
186
- !/^kungfu-[a-z0-9.-]+$/.test(releaseAsset)
187
- ) {
188
- throw new Error(
189
- `installer bundle asset transport metadata is invalid: ${assetPath}`,
190
- );
191
- }
192
- const expectedRole = assetPath.includes("/")
193
- ? "immutable-installer"
194
- : expectedRoles.get(assetPath);
195
- if (asset.role !== expectedRole) {
196
- throw new Error(`installer bundle asset role is invalid: ${assetPath}`);
197
- }
198
- }
199
- const immutableDirectory = [...immutableDirectories][0] || "";
200
- const immutableParts = immutableDirectory.split("/");
201
- if (
202
- topLevel.size !== 0 ||
203
- immutableShell !== 1 ||
204
- immutablePowerShell !== 1 ||
205
- immutableDirectories.size !== 1 ||
206
- immutableParts.length !== 5 ||
207
- immutableParts[0] !== "installers" ||
208
- immutableParts[1] !== "v1" ||
209
- immutableParts[2] !== bundle.channel ||
210
- immutableParts[3] !== expected.version ||
211
- !/^[a-f0-9]{64}$/.test(immutableParts[4])
212
- ) {
213
- throw new Error("installer bundle asset topology is incomplete");
214
- }
215
- if (
216
- bundle.cachePolicy?.friendly !== "public,max-age=300,must-revalidate" ||
217
- bundle.cachePolicy?.immutable !== "public,max-age=31536000,immutable"
218
- ) {
219
- throw new Error("installer bundle cache policy is invalid");
220
- }
221
- if (
222
- evidence.siteHandoff?.state !== "deferred-to-site-owned-consumer" ||
223
- evidence.siteHandoff?.productionAvailable !== false ||
224
- evidence.siteHandoff?.requiredBundleRoot !== bundleRoot
225
- ) {
226
- throw new Error("installer bundle site handoff must remain deferred");
227
- }
228
- return {
229
- schema: bundle.schema,
230
- bundleRoot,
231
- manifestDigest: bundle.manifestDigest,
232
- channel: bundle.channel,
233
- channelPayloadRoot: bundle.channelPayloadRoot,
234
- channelFileDigest: bundle.channelFileDigest,
235
- releasePassport: bundle.releasePassport,
236
- cachePolicy: bundle.cachePolicy,
237
- immutablePath: immutableDirectory,
238
- assets: bundle.assets,
239
- };
240
- }
241
-
242
- export function validatePublicationCommitEvidence(
243
- evidence,
244
- { version, sourceSha, releaseSha, releaseTag } = {},
245
- ) {
246
- if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
247
- throw new Error("publication commit evidence must be an object");
248
- }
249
- if (evidence.schema !== SCHEMA) {
250
- throw new Error(`publication commit evidence schema must be ${SCHEMA}`);
251
- }
252
- if (evidence.status !== "passed") {
253
- throw new Error("publication commit evidence status must be passed");
254
- }
255
- const identity = evidence.identity || {};
256
- const expected = {
257
- version: requiredString(version, "expected version"),
258
- sourceSha: exactSha(sourceSha, "expected sourceSha"),
259
- releaseSha: exactSha(releaseSha, "expected releaseSha"),
260
- releaseTag: requiredString(releaseTag, "expected releaseTag"),
261
- };
262
- for (const [field, value] of Object.entries(expected)) {
263
- if (identity[field] !== value) {
264
- throw new Error(`publication commit evidence ${field} mismatch`);
265
- }
266
- }
267
- const publicUrl = publicHttps(evidence.publication?.url, "publication.url");
268
- const payloadRoot = sha256Root(
269
- evidence.publication?.payloadRoot,
270
- "publication.payloadRoot",
271
- );
272
- if (
273
- evidence.readback?.status !== "passed" ||
274
- publicHttps(evidence.readback?.url, "readback.url") !== publicUrl ||
275
- sha256Root(evidence.readback?.payloadRoot, "readback.payloadRoot") !==
276
- payloadRoot
277
- ) {
278
- throw new Error(
279
- "publication read-back must pass at the canonical URL with the exact payload root",
280
- );
281
- }
282
- const previousAuthority = evidence.recovery?.previousAuthority;
283
- const rollbackReference = requiredString(
284
- evidence.recovery?.rollbackReference,
285
- "recovery.rollbackReference",
286
- );
287
- if (!["preserved", "none"].includes(previousAuthority)) {
288
- throw new Error(
289
- "publication recovery must preserve or explicitly declare no previous authority",
290
- );
291
- }
292
- const installerBundle = validateInstallerBundle(evidence, expected);
293
- return {
294
- schema: SCHEMA,
295
- status: "passed",
296
- publicUrl,
297
- payloadRoot,
298
- identity: expected,
299
- recovery: {
300
- previousAuthority,
301
- rollbackReference,
302
- },
303
- ...(installerBundle ? { installerBundle } : {}),
304
- };
305
- }
306
-
307
- export async function verifyInstallerBundleReadback(
308
- result,
309
- fetchImpl = globalThis.fetch,
310
- ) {
311
- const bundle = result.installerBundle;
312
- if (!bundle) return null;
313
- if (typeof fetchImpl !== "function") {
314
- throw new Error("installer bundle read-back requires fetch");
315
- }
316
- const manifestResponse = await fetchImpl(result.publicUrl, {
317
- redirect: "manual",
318
- cache: "no-store",
319
- });
320
- if (manifestResponse.status !== 200) {
321
- throw new Error(
322
- `installer bundle manifest read-back failed: HTTP ${manifestResponse.status}`,
323
- );
324
- }
325
- const manifestBytes = Buffer.from(await manifestResponse.arrayBuffer());
326
- if (digest(manifestBytes) !== bundle.manifestDigest) {
327
- throw new Error("installer bundle manifest digest mismatch");
328
- }
329
- const manifest = JSON.parse(manifestBytes);
330
- const unsigned = Object.fromEntries(
331
- Object.entries(manifest).filter(([key]) => key !== "bundleRoot"),
332
- );
333
- if (
334
- manifest.schema !== INSTALLER_BUNDLE_SCHEMA ||
335
- manifest.bundleRoot !== bundle.bundleRoot ||
336
- semanticRoot(unsigned) !== bundle.bundleRoot ||
337
- manifest.package?.name !== "@kungfu-tech/site" ||
338
- typeof manifest.package?.version !== "string" ||
339
- manifest.identity?.sourceCommit !== result.identity.sourceSha ||
340
- manifest.identity?.releaseSha !== result.identity.releaseSha ||
341
- manifest.identity?.releaseTag !== result.identity.releaseTag ||
342
- manifest.identity?.version !== result.identity.version ||
343
- manifest.identity?.channel !== bundle.channel ||
344
- manifest.identity?.channelPayloadRoot !== bundle.channelPayloadRoot ||
345
- manifest.identity?.channelFileDigest !== bundle.channelFileDigest ||
346
- manifest.identity?.releasePassport?.root !== bundle.releasePassport.root ||
347
- manifest.distribution?.repository !== "kungfu-systems/kungfu" ||
348
- manifest.routes?.immutablePath !== bundle.immutablePath ||
349
- manifest.routes?.friendly?.["install.sh"] !==
350
- "https://kungfu.tech/install.sh" ||
351
- manifest.routes?.friendly?.["install.ps1"] !==
352
- "https://kungfu.tech/install.ps1" ||
353
- JSON.stringify(canonical(manifest.cachePolicy)) !==
354
- JSON.stringify(canonical(bundle.cachePolicy)) ||
355
- JSON.stringify(canonical(manifest.assets)) !==
356
- JSON.stringify(canonical(bundle.assets)) ||
357
- `${manifest.distribution?.releaseBaseUrl}/` +
358
- manifest.distribution?.manifestAsset !==
359
- result.publicUrl
360
- ) {
361
- throw new Error("installer bundle manifest root mismatch");
362
- }
363
- const observations = [];
364
- const byUrl = new Map();
365
- for (const asset of bundle.assets) {
366
- let observation = byUrl.get(asset.releaseUrl);
367
- if (!observation) {
368
- const response = await fetchImpl(asset.releaseUrl, {
369
- redirect: "manual",
370
- cache: "no-store",
371
- });
372
- if (response.status !== 200) {
373
- throw new Error(
374
- `installer bundle asset read-back failed: HTTP ${response.status}`,
375
- );
376
- }
377
- const bytes = Buffer.from(await response.arrayBuffer());
378
- observation = {
379
- releaseUrl: asset.releaseUrl,
380
- size: bytes.length,
381
- digest: digest(bytes),
382
- };
383
- byUrl.set(asset.releaseUrl, observation);
384
- }
385
- if (
386
- observation.size !== asset.size ||
387
- observation.digest !== asset.digest
388
- ) {
389
- throw new Error(`installer bundle asset drifted: ${asset.path}`);
390
- }
391
- observations.push({ path: asset.path, ...observation });
392
- }
393
- const seal = {
394
- schema: "kungfu-buildchain-installer-publication-bundle-seal/v1",
395
- bundleRoot: bundle.bundleRoot,
396
- manifestDigest: bundle.manifestDigest,
397
- sourceCommit: result.identity.sourceSha,
398
- releaseTag: result.identity.releaseTag,
399
- releasePassport: bundle.releasePassport,
400
- observations,
401
- };
402
- return { ...seal, sealRoot: semanticRoot(seal) };
403
- }
404
-
405
- async function main(args) {
406
- const options = {};
407
- for (let index = 0; index < args.length; index += 1) {
408
- const value = args[index];
409
- if (value === "--evidence") options.evidence = args[++index];
410
- else if (value === "--version") options.version = args[++index];
411
- else if (value === "--source-sha") options.sourceSha = args[++index];
412
- else if (value === "--release-sha") options.releaseSha = args[++index];
413
- else if (value === "--release-tag") options.releaseTag = args[++index];
414
- else throw new Error(`unknown argument: ${value}`);
415
- }
416
- const evidencePath = path.resolve(
417
- requiredString(options.evidence, "--evidence"),
418
- );
419
- const result = validatePublicationCommitEvidence(
420
- JSON.parse(fs.readFileSync(evidencePath, "utf8")),
421
- options,
422
- );
423
- const installerBundleSeal = await verifyInstallerBundleReadback(result);
424
- process.stdout.write(
425
- `${JSON.stringify({
426
- ...result,
427
- ...(installerBundleSeal ? { installerBundleSeal } : {}),
428
- })}\n`,
429
- );
430
- }
431
-
432
- if (
433
- process.argv[1] &&
434
- import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
435
- ) {
436
- main(process.argv.slice(2)).catch((error) => {
437
- console.error(
438
- `publication commit evidence failed: ${
439
- error instanceof Error ? error.message : String(error)
440
- }`,
441
- );
442
- process.exit(1);
443
- });
444
- }
@@ -1,201 +0,0 @@
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
- });