@kungfu-tech/buildchain 3.0.2 → 3.0.3-alpha.1

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +20 -2
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +56 -2
  9. package/bin/internal/command-registry.mjs +82 -0
  10. package/bin/internal/trust-release-cli.mjs +1 -0
  11. package/contracts/auditable-demo-media-profiles-v1.json +116 -1
  12. package/contracts/buildchain-v2-residuals-v1.json +146 -0
  13. package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +143 -0
  14. package/contracts/evidence/auditable-demo-web-delivery-v1.json +7 -3
  15. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/complete-transcript.txt +2 -0
  16. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/public-projection.json +16 -0
  17. package/contracts/fixtures/auditable-demo-responsive-web-delivery-v1/scene.json +12 -0
  18. package/dist/site/buildchain-contract.json +188 -46
  19. package/dist/site/buildchain-site.json +181 -90
  20. package/dist/site/capability-registry.json +5 -5
  21. package/dist/site/cli-registry.json +860 -98
  22. package/dist/site/controller-registry.json +118 -5
  23. package/dist/site/kfd-claims.json +233 -19
  24. package/dist/site/kfd-upstream-aggregate.json +1 -1
  25. package/dist/site/manual-registry.json +20 -20
  26. package/dist/site/node-api-registry.json +374 -59
  27. package/dist/site/page-registry.json +156 -65
  28. package/dist/site/public-surface-audit.json +231 -22
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/release-provenance.json +3 -0
  31. package/dist/site/site-manifest.json +24 -24
  32. package/dist/site/workflow-registry.json +575 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +54 -13
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +119 -12
  37. package/docs/consumer-issue-reporting.md +20 -2
  38. package/docs/dev-alpha-candidate-patrol.md +24 -10
  39. package/docs/github-governance-authority.md +24 -31
  40. package/docs/lifecycle-protocol.md +20 -2
  41. package/docs/migration-inventory.md +24 -5
  42. package/docs/observed-evidence-patrol.md +28 -12
  43. package/docs/ownership.md +19 -1
  44. package/docs/publication-artifacts.md +158 -18
  45. package/docs/publication-authority.md +4 -4
  46. package/docs/publish-transaction.md +78 -31
  47. package/docs/release-candidate.md +24 -0
  48. package/docs/release-flow.md +50 -32
  49. package/docs/release-governance.md +39 -32
  50. package/docs/release-passport.md +4 -0
  51. package/docs/release-propagation.md +57 -12
  52. package/docs/reusable-build-surface.md +218 -90
  53. package/docs/runtime-train-validation.md +20 -2
  54. package/docs/shifu-gate-profiles.md +7 -1
  55. package/docs/stable-candidate-patrol.md +6 -6
  56. package/docs/toolkit-observability.md +22 -0
  57. package/docs/versioning.md +46 -24
  58. package/docs/web-surface-deployments.md +82 -1
  59. package/package.json +7 -5
  60. package/packages/core/README.md +60 -4
  61. package/packages/core/artifact-signing.js +1 -0
  62. package/packages/core/buildchain-contract.js +9 -4
  63. package/packages/core/cache-evidence.js +1 -0
  64. package/packages/core/controller-evidence.js +2 -0
  65. package/packages/core/diagnostics.js +105 -30
  66. package/packages/core/github-governance-authority.js +25 -17
  67. package/packages/core/index.js +40 -0
  68. package/packages/core/paper.js +3226 -0
  69. package/packages/core/public-surface-audit.js +3 -0
  70. package/packages/core/publication-artifact.js +1 -6
  71. package/packages/core/publication-authority.js +1 -0
  72. package/packages/core/publication-control-plane-audit.js +28 -0
  73. package/packages/core/publication-reproducibility.js +947 -0
  74. package/packages/core/publication-sealed-bundle.js +190 -0
  75. package/packages/core/publish-transaction.js +147 -17
  76. package/packages/core/release-candidate.js +234 -0
  77. package/packages/core/release-passport.js +519 -228
  78. package/packages/core/release-propagation.js +175 -1
  79. package/packages/core/stable-release-gate.js +4 -1
  80. package/scripts/artifact-signing-delegation.mjs +268 -0
  81. package/scripts/audit-github-governance.mjs +32 -13
  82. package/scripts/audit-publication-control-plane.mjs +17 -8
  83. package/scripts/auditable-demo.mjs +266 -7
  84. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  85. package/scripts/aws-macos-jit-core.mjs +378 -0
  86. package/scripts/aws-macos-jit.mjs +123 -0
  87. package/scripts/aws-runner-burst-core.mjs +248 -0
  88. package/scripts/aws-runner-burst.mjs +79 -0
  89. package/scripts/aws-windows-jit-core.mjs +374 -0
  90. package/scripts/aws-windows-jit.mjs +121 -0
  91. package/scripts/build-contract-core.mjs +69 -6
  92. package/scripts/build-standalone-binary.mjs +23 -0
  93. package/scripts/buildchain-contract-lock.mjs +1 -1
  94. package/scripts/buildchain-patrol.mjs +1 -1
  95. package/scripts/check-internal-architecture.mjs +69 -2
  96. package/scripts/check-inventory.mjs +70 -2
  97. package/scripts/check-javascript-syntax.mjs +31 -0
  98. package/scripts/check-maintainability.mjs +371 -0
  99. package/scripts/compiler-cache-evidence.mjs +221 -0
  100. package/scripts/dev-alpha-candidate-patrol.mjs +288 -43
  101. package/scripts/dispatch-artifact-signing-authority.mjs +7 -1
  102. package/scripts/finalize-native-artifact-signing-result.mjs +185 -31
  103. package/scripts/gate-profile-core.mjs +6 -1
  104. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  105. package/scripts/generate-release-candidate-passport.mjs +4 -0
  106. package/scripts/generate-site-bundle.mjs +84 -18
  107. package/scripts/import-artifact-signing-results.mjs +64 -5
  108. package/scripts/inspect-artifact-signing-requests.mjs +52 -14
  109. package/scripts/maintainability-metrics.mjs +339 -0
  110. package/scripts/npm-publish-transaction.mjs +57 -6
  111. package/scripts/observed-evidence.mjs +151 -33
  112. package/scripts/paper.mjs +645 -0
  113. package/scripts/promotion-channel-router.mjs +16 -4
  114. package/scripts/publication-reproducibility.mjs +62 -0
  115. package/scripts/reconcile-github-governance.mjs +8 -1
  116. package/scripts/release-propagation.mjs +39 -0
  117. package/scripts/resolve-artifact-signing-upload-route.mjs +55 -0
  118. package/scripts/resolve-build-contract.mjs +6 -0
  119. package/scripts/route-offline-runners.mjs +236 -0
  120. package/scripts/run-candidate-body-prefix-renderer.mjs +187 -0
  121. package/scripts/run-lifecycle-core.mjs +62 -1
  122. package/scripts/runtime-ref-core.mjs +36 -4
  123. package/scripts/seal-artifact-signing-requests.mjs +64 -12
  124. package/scripts/stable-candidate-qualification.mjs +50 -0
  125. package/scripts/verify-artifact-signing-results.mjs +26 -1
  126. package/scripts/web-surface-core.mjs +272 -86
  127. package/scripts/web-surface-production-decision.mjs +19 -3
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  export const RELEASE_PROPAGATION_GRAPH_CONTRACT = "kungfu-buildchain-release-propagation-graph";
6
6
  export const RELEASE_PROPAGATION_PLAN_CONTRACT = "kungfu-buildchain-release-propagation-plan";
7
7
  export const RELEASE_PROPAGATION_LOCK_CONTRACT = "kungfu-buildchain-release-propagation-lock";
8
+ export const RELEASE_PROPAGATION_RECEIPT_CONTRACT = "kungfu-buildchain-release-propagation-receipt";
8
9
 
9
10
  const SUPPORTED_CHANNELS = new Set(["alpha", "release"]);
10
11
  const SUPPORTED_CHANNEL_POLICIES = new Set(["preserve", "explicit"]);
@@ -31,6 +32,49 @@ function sha256Json(value) {
31
32
  return crypto.createHash("sha256").update(stableJson(value)).digest("hex");
32
33
  }
33
34
 
35
+ function releaseVersion(upstreamRelease) {
36
+ return upstreamRelease.package?.version || upstreamRelease.publicationArtifact?.version || "";
37
+ }
38
+
39
+ function safeBranchPart(value) {
40
+ const normalized = String(value || "")
41
+ .trim()
42
+ .toLowerCase()
43
+ .replaceAll(/[^a-z0-9._-]+/g, "-")
44
+ .replaceAll(/^-+|-+$/g, "");
45
+ if (!normalized) {
46
+ throw new Error(`release propagation branch component is empty: ${value}`);
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ function releaseIdentity(upstreamRelease) {
52
+ const version = releaseVersion(upstreamRelease);
53
+ if (!version) {
54
+ throw new Error("upstream release does not expose a package or publication version");
55
+ }
56
+ return {
57
+ repository: upstreamRelease.repository,
58
+ version,
59
+ channel: upstreamRelease.channel,
60
+ };
61
+ }
62
+
63
+ function propagationIdentity({ upstreamRelease, targetNode }) {
64
+ return {
65
+ release: releaseIdentity(upstreamRelease),
66
+ downstreamRepository: targetNode.repository,
67
+ };
68
+ }
69
+
70
+ function propagationBranch({ upstreamRelease, targetNode }) {
71
+ const identity = propagationIdentity({ upstreamRelease, targetNode });
72
+ const digest = sha256Json(identity).slice(0, 12);
73
+ const repository = safeBranchPart(upstreamRelease.repository.replace("/", "-"));
74
+ const version = safeBranchPart(releaseVersion(upstreamRelease));
75
+ return `buildchain/release-propagation/${repository}/${version}-${upstreamRelease.channel}-${digest}`;
76
+ }
77
+
34
78
  function assertPlainObject(value, label) {
35
79
  if (!value || typeof value !== "object" || Array.isArray(value)) {
36
80
  throw new Error(`${label} must be an object`);
@@ -305,6 +349,8 @@ export function createReleasePropagationLock({
305
349
  upstreamRelease,
306
350
  downstreamChannel,
307
351
  } = {}) {
352
+ const identity = propagationIdentity({ upstreamRelease, targetNode });
353
+ const propagationKey = sha256Json(identity);
308
354
  const lock = {
309
355
  schemaVersion: 1,
310
356
  contract: RELEASE_PROPAGATION_LOCK_CONTRACT,
@@ -331,6 +377,9 @@ export function createReleasePropagationLock({
331
377
  edge: edge.id,
332
378
  channelPolicy: edge.channelPolicy,
333
379
  channelMap: edge.channelMap,
380
+ releaseIdentity: identity.release,
381
+ propagationKey,
382
+ branch: propagationBranch({ upstreamRelease, targetNode }),
334
383
  exact: true,
335
384
  floatingTags: false,
336
385
  },
@@ -374,6 +423,8 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
374
423
  channel: downstreamChannel,
375
424
  baseRef: lock.downstream.baseRef,
376
425
  lockPath: lock.downstream.lockPath,
426
+ propagationKey: lock.propagation.propagationKey,
427
+ branch: lock.propagation.branch,
377
428
  lock,
378
429
  });
379
430
  }
@@ -387,6 +438,7 @@ export function planReleasePropagation({ graph: graphInput, upstreamRelease: rel
387
438
  targetCount: targets.length,
388
439
  channels: [...new Set(targets.map((target) => target.channel))],
389
440
  repositories: targets.map((target) => target.repository),
441
+ releaseIdentity: releaseIdentity(upstreamRelease),
390
442
  },
391
443
  };
392
444
  }
@@ -411,13 +463,135 @@ export function writeReleasePropagationLock({ plan, target = "", cwd = process.c
411
463
  }
412
464
  const selected = matches[0];
413
465
  const outputPath = path.resolve(cwd, output || selected.lockPath);
466
+ const rendered = stableJson(selected.lock);
467
+ const previous = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, "utf8") : "";
414
468
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
415
- fs.writeFileSync(outputPath, stableJson(selected.lock));
469
+ if (previous !== rendered) {
470
+ fs.writeFileSync(outputPath, rendered);
471
+ }
416
472
  return {
417
473
  target: selected.target,
418
474
  repository: selected.repository,
419
475
  channel: selected.channel,
420
476
  path: outputPath,
421
477
  lockSha256: selected.lock.lockSha256,
478
+ propagationKey: selected.propagationKey || selected.lock.propagation?.propagationKey || "",
479
+ branch: selected.branch || selected.lock.propagation?.branch || "",
480
+ status: previous === rendered ? "reused" : "written",
481
+ changed: previous !== rendered,
482
+ };
483
+ }
484
+
485
+ const RECEIPT_VISIBILITY_STATES = new Set(["pending", "visible", "failed", "not-requested"]);
486
+ const RECEIPT_PR_STATES = new Set(["planned", "created", "updated", "reused", "no-change", "failed"]);
487
+
488
+ function receiptVisibilityState(value, label) {
489
+ const state = optionalString(value) || "pending";
490
+ if (!RECEIPT_VISIBILITY_STATES.has(state)) {
491
+ throw new Error(`${label} must be pending, visible, failed, or not-requested`);
492
+ }
493
+ return state;
494
+ }
495
+
496
+ function receiptPrOutcome(value = {}) {
497
+ const outcome = assertPlainObject(value, "release propagation PR outcome");
498
+ const state = optionalString(outcome.state) || "planned";
499
+ if (!RECEIPT_PR_STATES.has(state)) {
500
+ throw new Error("release propagation PR outcome state is unsupported");
501
+ }
502
+ return {
503
+ state,
504
+ number: outcome.number === undefined || outcome.number === null || outcome.number === ""
505
+ ? null
506
+ : Number(outcome.number),
507
+ url: optionalString(outcome.url),
508
+ branch: assertString(outcome.branch, "release propagation PR outcome branch"),
509
+ };
510
+ }
511
+
512
+ export function createReleasePropagationReceipt({
513
+ plan,
514
+ target = "",
515
+ lockResult,
516
+ prOutcome,
517
+ stagingState = "pending",
518
+ productionState = "not-requested",
519
+ observedAt = "",
520
+ } = {}) {
521
+ const selectedPlan = assertPlainObject(plan, "plan");
522
+ const matches = selectedPlan.targets.filter((entry) =>
523
+ !target || entry.target === target || entry.repository === target);
524
+ if (matches.length !== 1) {
525
+ throw new Error(`expected exactly one propagation target, found ${matches.length}`);
526
+ }
527
+ const selected = matches[0];
528
+ const result = assertPlainObject(lockResult, "release propagation lock result");
529
+ if (result.lockSha256 !== selected.lock.lockSha256) {
530
+ throw new Error("release propagation receipt lock result does not match the plan");
531
+ }
532
+ const propagationKey = selected.propagationKey || selected.lock.propagation?.propagationKey || "";
533
+ if (result.propagationKey && result.propagationKey !== propagationKey) {
534
+ throw new Error("release propagation receipt key does not match the lock result");
535
+ }
536
+ const branch = selected.branch || selected.lock.propagation?.branch || "";
537
+ const normalizedPr = receiptPrOutcome({ ...prOutcome, branch: prOutcome?.branch || branch });
538
+ if (normalizedPr.branch !== branch) {
539
+ throw new Error("release propagation PR branch does not match the exact plan");
540
+ }
541
+ const upstreamRelease = selectedPlan.upstreamRelease;
542
+ const release = releaseIdentity(upstreamRelease);
543
+ const states = {
544
+ "package-published": {
545
+ state: upstreamRelease.package ? "complete" : "not-applicable",
546
+ evidence: upstreamRelease.package
547
+ ? {
548
+ name: upstreamRelease.package.name,
549
+ version: upstreamRelease.package.version,
550
+ integrity: upstreamRelease.package.integrity,
551
+ }
552
+ : null,
553
+ },
554
+ "alpha-complete": {
555
+ state: release.channel === "alpha" ? "complete" : "not-applicable",
556
+ evidence: release.channel === "alpha"
557
+ ? {
558
+ releasePassportSha256: upstreamRelease.releasePassport.sha256,
559
+ tag: upstreamRelease.tag,
560
+ }
561
+ : null,
562
+ },
563
+ "staging-visible": {
564
+ state: receiptVisibilityState(stagingState, "stagingState"),
565
+ },
566
+ "production-visible": {
567
+ state: receiptVisibilityState(productionState, "productionState"),
568
+ },
569
+ };
570
+ const body = {
571
+ schemaVersion: 1,
572
+ contract: RELEASE_PROPAGATION_RECEIPT_CONTRACT,
573
+ propagationKey,
574
+ observedAt: optionalString(observedAt),
575
+ release,
576
+ upstream: {
577
+ repository: upstreamRelease.repository,
578
+ tag: upstreamRelease.tag,
579
+ sourceSha: upstreamRelease.sourceSha,
580
+ releasePassport: upstreamRelease.releasePassport,
581
+ },
582
+ downstream: {
583
+ repository: selected.repository,
584
+ channel: selected.channel,
585
+ baseRef: selected.baseRef,
586
+ lockPath: selected.lockPath,
587
+ lockSha256: result.lockSha256,
588
+ lockWriteState: result.status || (result.changed === false ? "reused" : "written"),
589
+ pullRequest: normalizedPr,
590
+ },
591
+ states,
592
+ };
593
+ return {
594
+ ...body,
595
+ receiptSha256: sha256Json(body),
422
596
  };
423
597
  }
@@ -194,12 +194,14 @@ export function evaluateStableReleaseGate({
194
194
  : undefined;
195
195
  const attestorAllowed = required.allowedAttestors.length === 0 ||
196
196
  required.allowedAttestors.includes(string(evidence?.attestor));
197
+ const completionTimingValid = required.source === "release-candidate" ||
198
+ (completedAt && completedAt.milliseconds >= candidatePublished.milliseconds);
197
199
  const valid = Boolean(
198
200
  evidence &&
199
201
  string(evidence.status) === "success" &&
200
202
  string(evidence.candidateSha) === candidateSha &&
201
203
  completedAt &&
202
- completedAt.milliseconds >= candidatePublished.milliseconds &&
204
+ completionTimingValid &&
203
205
  attestorAllowed,
204
206
  );
205
207
  if (completedAt) {
@@ -222,6 +224,7 @@ export function evaluateStableReleaseGate({
222
224
  evidenceUrl: string(evidence?.evidenceUrl),
223
225
  attestor: string(evidence?.attestor),
224
226
  attestorAllowed,
227
+ completionTimingValid: Boolean(completionTimingValid),
225
228
  },
226
229
  ));
227
230
  }
@@ -0,0 +1,268 @@
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
+ const CONTRACT = "kungfu-buildchain-artifact-signing-delegation/v1";
9
+
10
+ function required(value, label) {
11
+ const normalized = String(value || "").trim();
12
+ if (!normalized) throw new Error(`${label} is required`);
13
+ if (/[\r\n\0]/u.test(normalized))
14
+ throw new Error(`${label} contains control characters`);
15
+ return normalized;
16
+ }
17
+
18
+ function optional(value, label) {
19
+ const normalized = String(value || "").trim();
20
+ if (/[\r\n\0]/u.test(normalized))
21
+ throw new Error(`${label} contains control characters`);
22
+ return normalized;
23
+ }
24
+
25
+ function exactSha(value, label) {
26
+ const normalized = required(value, label);
27
+ if (!/^[0-9a-f]{40}$/u.test(normalized))
28
+ throw new Error(`${label} must be an exact SHA`);
29
+ return normalized;
30
+ }
31
+
32
+ function positiveInteger(value, label, { allowZero = false } = {}) {
33
+ const normalized = Number(value);
34
+ if (!Number.isSafeInteger(normalized) || normalized < (allowZero ? 0 : 1)) {
35
+ throw new Error(
36
+ `${label} must be ${allowZero ? "a non-negative" : "a positive"} integer`,
37
+ );
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ function repository(value, label) {
43
+ const normalized = required(value, label);
44
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(normalized)) {
45
+ throw new Error(`${label} must be owner/repository`);
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ function safeRelativePath(value, label) {
51
+ const normalized = required(value, label).replaceAll("\\", "/");
52
+ const resolved = path.posix.normalize(normalized);
53
+ if (
54
+ resolved === ".." ||
55
+ resolved.startsWith("../") ||
56
+ path.posix.isAbsolute(resolved)
57
+ ) {
58
+ throw new Error(`${label} must be a safe relative path`);
59
+ }
60
+ if (!/^[A-Za-z0-9._ /-]+$/u.test(resolved))
61
+ throw new Error(`${label} contains unsafe shell characters`);
62
+ return resolved;
63
+ }
64
+
65
+ export function validateArtifactSigningDelegation(value) {
66
+ if (!value || typeof value !== "object" || Array.isArray(value))
67
+ throw new Error("delegation must be an object");
68
+ if (value.schemaVersion !== 1 || value.contract !== CONTRACT)
69
+ throw new Error("artifact signing delegation contract mismatch");
70
+ const requestCount = positiveInteger(value.request?.count, "request.count", {
71
+ allowZero: true,
72
+ });
73
+ const delegation = {
74
+ schemaVersion: 1,
75
+ contract: CONTRACT,
76
+ source: {
77
+ repository: repository(value.source?.repository, "source.repository"),
78
+ runId: required(value.source?.runId, "source.runId"),
79
+ runAttempt: positiveInteger(
80
+ value.source?.runAttempt,
81
+ "source.runAttempt",
82
+ ),
83
+ sha: exactSha(value.source?.sha, "source.sha"),
84
+ treeSha: exactSha(value.source?.treeSha, "source.treeSha"),
85
+ },
86
+ runtime: {
87
+ repository: repository(value.runtime?.repository, "runtime.repository"),
88
+ sha: exactSha(value.runtime?.sha, "runtime.sha"),
89
+ },
90
+ platform: {
91
+ id: required(value.platform?.id, "platform.id"),
92
+ name: required(value.platform?.name, "platform.name"),
93
+ },
94
+ request: {
95
+ count: requestCount,
96
+ artifact:
97
+ requestCount > 0
98
+ ? required(value.request?.artifact, "request.artifact")
99
+ : optional(value.request?.artifact, "request.artifact"),
100
+ },
101
+ authority: {
102
+ runId:
103
+ requestCount > 0
104
+ ? required(value.authority?.runId, "authority.runId")
105
+ : optional(value.authority?.runId, "authority.runId"),
106
+ resultArtifact:
107
+ requestCount > 0
108
+ ? required(
109
+ value.authority?.resultArtifact,
110
+ "authority.resultArtifact",
111
+ )
112
+ : optional(
113
+ value.authority?.resultArtifact,
114
+ "authority.resultArtifact",
115
+ ),
116
+ },
117
+ artifact: {
118
+ name: required(value.artifact?.name, "artifact.name"),
119
+ manifestArtifact: required(
120
+ value.artifact?.manifestArtifact,
121
+ "artifact.manifestArtifact",
122
+ ),
123
+ diagnosticsArtifact: required(
124
+ value.artifact?.diagnosticsArtifact,
125
+ "artifact.diagnosticsArtifact",
126
+ ),
127
+ },
128
+ workingDirectory: safeRelativePath(
129
+ value.workingDirectory || ".",
130
+ "workingDirectory",
131
+ ),
132
+ };
133
+ if (
134
+ requestCount === 0 &&
135
+ (delegation.authority.runId || delegation.authority.resultArtifact)
136
+ ) {
137
+ throw new Error(
138
+ "unsigned delegation must not contain authority result coordinates",
139
+ );
140
+ }
141
+ return delegation;
142
+ }
143
+
144
+ export function createArtifactSigningDelegation({
145
+ sourceRepository = process.env.GITHUB_REPOSITORY,
146
+ sourceRunId = process.env.GITHUB_RUN_ID,
147
+ sourceRunAttempt = process.env.GITHUB_RUN_ATTEMPT || "1",
148
+ sourceSha = process.env.BUILDCHAIN_SOURCE_SHA,
149
+ sourceTreeSha = process.env.BUILDCHAIN_SOURCE_TREE_SHA,
150
+ runtimeRepository = process.env.BUILDCHAIN_RUNTIME_REPOSITORY,
151
+ runtimeSha = process.env.BUILDCHAIN_RUNTIME_SHA,
152
+ platformId = process.env.BUILDCHAIN_PLATFORM_ID,
153
+ platformName = process.env.BUILDCHAIN_PLATFORM_NAME,
154
+ requestCount = process.env.BUILDCHAIN_SIGNING_REQUEST_COUNT || "0",
155
+ requestArtifact = process.env.BUILDCHAIN_SIGNING_REQUEST_ARTIFACT || "",
156
+ authorityRunId = process.env.BUILDCHAIN_AUTHORITY_RUN_ID || "",
157
+ resultArtifact = process.env.BUILDCHAIN_SIGNING_RESULT_ARTIFACT || "",
158
+ artifactName = process.env.BUILDCHAIN_ARTIFACT_NAME,
159
+ manifestArtifact = process.env.BUILDCHAIN_MANIFEST_ARTIFACT_NAME,
160
+ diagnosticsArtifact = process.env.BUILDCHAIN_DIAGNOSTICS_ARTIFACT_NAME,
161
+ workingDirectory = process.env.BUILDCHAIN_SIGNING_CWD || ".",
162
+ } = {}) {
163
+ return validateArtifactSigningDelegation({
164
+ schemaVersion: 1,
165
+ contract: CONTRACT,
166
+ source: {
167
+ repository: sourceRepository,
168
+ runId: sourceRunId,
169
+ runAttempt: Number(sourceRunAttempt),
170
+ sha: sourceSha,
171
+ treeSha: sourceTreeSha,
172
+ },
173
+ runtime: { repository: runtimeRepository, sha: runtimeSha },
174
+ platform: { id: platformId, name: platformName },
175
+ request: { count: Number(requestCount), artifact: requestArtifact },
176
+ authority: { runId: authorityRunId, resultArtifact },
177
+ artifact: { name: artifactName, manifestArtifact, diagnosticsArtifact },
178
+ workingDirectory,
179
+ });
180
+ }
181
+
182
+ export function sealArtifactSigningDelegation({
183
+ outputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
184
+ ...values
185
+ } = {}) {
186
+ const delegation = createArtifactSigningDelegation(values);
187
+ const target = path.resolve(required(outputPath, "delegation output path"));
188
+ fs.mkdirSync(path.dirname(target), { recursive: true });
189
+ fs.writeFileSync(target, `${JSON.stringify(delegation, null, 2)}\n`);
190
+ return delegation;
191
+ }
192
+
193
+ export function readArtifactSigningDelegation(
194
+ inputPath = process.env.BUILDCHAIN_SIGNING_DELEGATION_PATH,
195
+ ) {
196
+ const target = path.resolve(required(inputPath, "delegation input path"));
197
+ return validateArtifactSigningDelegation(
198
+ JSON.parse(fs.readFileSync(target, "utf8")),
199
+ );
200
+ }
201
+
202
+ export function artifactSigningDelegationOutputs(delegation) {
203
+ const value = validateArtifactSigningDelegation(delegation);
204
+ return {
205
+ "request-count": String(value.request.count),
206
+ "request-artifact": value.request.artifact,
207
+ "authority-run-id": value.authority.runId,
208
+ "result-artifact": value.authority.resultArtifact,
209
+ "artifact-name": value.artifact.name,
210
+ "manifest-artifact-name": value.artifact.manifestArtifact,
211
+ "diagnostics-artifact-name": value.artifact.diagnosticsArtifact,
212
+ "working-directory": value.workingDirectory,
213
+ };
214
+ }
215
+
216
+ export function assertArtifactSigningDelegationContext(
217
+ delegation,
218
+ {
219
+ sourceRepository = process.env.BUILDCHAIN_EXPECTED_SOURCE_REPOSITORY || "",
220
+ sourceRunId = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ID || "",
221
+ sourceRunAttempt = process.env.BUILDCHAIN_EXPECTED_SOURCE_RUN_ATTEMPT || "",
222
+ sourceSha = process.env.BUILDCHAIN_EXPECTED_SOURCE_SHA || "",
223
+ runtimeRepository = process.env.BUILDCHAIN_EXPECTED_RUNTIME_REPOSITORY ||
224
+ "",
225
+ runtimeSha = process.env.BUILDCHAIN_EXPECTED_RUNTIME_SHA || "",
226
+ platformId = process.env.BUILDCHAIN_EXPECTED_PLATFORM_ID || "",
227
+ } = {},
228
+ ) {
229
+ const value = validateArtifactSigningDelegation(delegation);
230
+ const expectations = [
231
+ [sourceRepository, value.source.repository, "source repository"],
232
+ [sourceRunId, value.source.runId, "source run ID"],
233
+ [sourceRunAttempt, String(value.source.runAttempt), "source run attempt"],
234
+ [sourceSha, value.source.sha, "source SHA"],
235
+ [runtimeRepository, value.runtime.repository, "runtime repository"],
236
+ [runtimeSha, value.runtime.sha, "runtime SHA"],
237
+ [platformId, value.platform.id, "platform ID"],
238
+ ];
239
+ for (const [expected, actual, label] of expectations) {
240
+ if (expected && String(expected) !== actual)
241
+ throw new Error(`artifact signing delegation ${label} mismatch`);
242
+ }
243
+ return value;
244
+ }
245
+
246
+ if (
247
+ process.argv[1] &&
248
+ import.meta.url === pathToFileURL(process.argv[1]).href
249
+ ) {
250
+ try {
251
+ const mode = process.argv[2] || "seal";
252
+ if (mode === "seal") {
253
+ sealArtifactSigningDelegation();
254
+ } else if (mode === "outputs") {
255
+ const delegation = assertArtifactSigningDelegationContext(
256
+ readArtifactSigningDelegation(),
257
+ );
258
+ writeGitHubOutputs(artifactSigningDelegationOutputs(delegation));
259
+ } else {
260
+ throw new Error(`unsupported artifact signing delegation mode: ${mode}`);
261
+ }
262
+ } catch (error) {
263
+ console.error(
264
+ `::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
265
+ );
266
+ process.exitCode = 1;
267
+ }
268
+ }
@@ -254,15 +254,33 @@ function addMinutes(iso, minutes) {
254
254
  return new Date(Date.parse(iso) + minutes * 60_000).toISOString();
255
255
  }
256
256
 
257
- function repositorySelector(repositories, requested) {
258
- if (!requested) return repositories;
259
- const exact = repositories.filter((repository) =>
260
- repository.full_name === requested ||
261
- repository.name === requested);
262
- if (exact.length !== 1) {
263
- throw new Error(`repository selector must resolve exactly once: ${requested}`);
257
+ function repositoryVisibility(repository) {
258
+ return String(
259
+ repository.visibility || (repository.private ? "private" : "public"),
260
+ ).toLowerCase();
261
+ }
262
+
263
+ export function selectGithubGovernanceRepositories(
264
+ repositories,
265
+ requested = "",
266
+ descriptor = BUILDCHAIN_GITHUB_GOVERNANCE_AUTHORITY,
267
+ ) {
268
+ const exact = requested
269
+ ? repositories.filter((repository) =>
270
+ repository.full_name === requested || repository.name === requested)
271
+ : repositories;
272
+ if (requested && exact.length !== 1) {
273
+ throw new Error("repository selector must resolve exactly once");
264
274
  }
265
- return exact;
275
+ const managedVisibilities = new Set(
276
+ descriptor.repositoryAdmission.managedVisibilities || ["public"],
277
+ );
278
+ const selected = exact.filter((repository) =>
279
+ managedVisibilities.has(repositoryVisibility(repository)));
280
+ if (requested && selected.length !== 1) {
281
+ throw new Error("repository selector is outside managed governance scope");
282
+ }
283
+ return selected;
266
284
  }
267
285
 
268
286
  export function collectGithubGovernanceAudit({
@@ -291,7 +309,10 @@ export function collectGithubGovernanceAudit({
291
309
  if (!organizationState.ok || !repositoriesState.readable) {
292
310
  throw new Error("organization or managed repository inventory is unreadable; governance audit fails closed");
293
311
  }
294
- const selected = repositorySelector(repositoriesState.repositories, repository);
312
+ const selected = selectGithubGovernanceRepositories(
313
+ repositoriesState.repositories,
314
+ repository,
315
+ );
295
316
  if (targetRef && selected.length !== 1) {
296
317
  throw new Error("--target-ref requires exactly one selected repository");
297
318
  }
@@ -316,9 +337,7 @@ export function collectGithubGovernanceAudit({
316
337
  const diagnostics = [];
317
338
  for (const metadata of selected) {
318
339
  const fullName = String(metadata.full_name || "");
319
- const visibilityClass = String(
320
- metadata.visibility || (metadata.private ? "private" : "public"),
321
- );
340
+ const visibilityClass = repositoryVisibility(metadata);
322
341
  const repositoryIdentityRoot = githubRepositoryIdentityRoot({
323
342
  provider: "github",
324
343
  providerRepositoryId: String(metadata.node_id || metadata.id || ""),
@@ -420,7 +439,7 @@ export function collectGithubGovernanceAudit({
420
439
  }
421
440
  }
422
441
  const visibility = selected.reduce((counts, item) => {
423
- const key = String(item.visibility || (item.private ? "private" : "public"));
442
+ const key = repositoryVisibility(item);
424
443
  counts[key] = Number(counts[key] || 0) + 1;
425
444
  return counts;
426
445
  }, {});
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import {
7
7
  evaluateBuildchainReleaseReconciliation,
8
8
  evaluatePublicationControlPlaneSnapshot,
9
+ matchesGithubDeploymentPolicy,
9
10
  } from "../packages/core/publication-control-plane-audit.js";
10
11
 
11
12
  function flag(name, fallback = "") {
@@ -197,8 +198,10 @@ function main() {
197
198
  const requiredStatusCheck = flag("required-status-check", "check");
198
199
  const jobId = flag("job", "promote");
199
200
  const environment = flag("environment", "none");
200
- const providerEnvironment = environment === "none" ? "" : environment;
201
201
  const branch = flag("branch");
202
+ const environmentRef = flag("environment-ref", branch);
203
+ const environmentRefType = flag("environment-ref-type", "branch");
204
+ const providerEnvironment = environment === "none" ? "" : environment;
202
205
  const sourceSha = flag("source-sha").toLowerCase();
203
206
  const packageName = flag("package", "@kungfu-tech/buildchain");
204
207
  const publisherMode = flag("publisher-mode", "npm-trusted-publisher");
@@ -239,15 +242,19 @@ function main() {
239
242
  const deploymentBranches = environment !== "none" && environmentState.deployment_branch_policy?.custom_branch_policies === true
240
243
  ? githubJson(`repos/${repository}/environments/${encodeURIComponent(environment)}/deployment-branch-policies?per_page=100`, "Environment deployment branch policy")
241
244
  : { branch_policies: [] };
242
- const exactEnvironmentBranchPolicy = (deploymentBranches.branch_policies || []).find((entry) =>
243
- entry?.type === "branch" && entry?.name === branch
245
+ if (!["branch", "tag"].includes(environmentRefType)) {
246
+ throw new Error(`unsupported --environment-ref-type: ${environmentRefType}`);
247
+ }
248
+ const matchingEnvironmentPolicy = (deploymentBranches.branch_policies || []).find((entry) =>
249
+ matchesGithubDeploymentPolicy(entry, { ref: environmentRef, refType: environmentRefType })
244
250
  );
245
251
  const environmentBranchAuthorized = environment !== "none" && (
246
252
  (
253
+ environmentRefType === "branch" &&
247
254
  environmentState.deployment_branch_policy?.protected_branches === true &&
248
255
  branchState.protected === true
249
256
  ) ||
250
- Boolean(exactEnvironmentBranchPolicy)
257
+ Boolean(matchingEnvironmentPolicy)
251
258
  );
252
259
  const oidc = githubJson(`repos/${repository}/actions/oidc/customization/sub`, "OIDC subject policy");
253
260
  if (!["npm-trusted-publisher", "github-token", "oidc-role"].includes(publisherMode)) {
@@ -465,12 +472,14 @@ function main() {
465
472
  environmentState.deployment_branch_policy?.protected_branches === true ||
466
473
  (deploymentBranches.branch_policies || []).length > 0,
467
474
  branchAuthorized: environmentBranchAuthorized,
468
- branchPolicyMode: environmentState.deployment_branch_policy?.protected_branches === true
475
+ branchPolicyMode: environmentRefType === "branch" && environmentState.deployment_branch_policy?.protected_branches === true
469
476
  ? "protected-branches"
470
- : exactEnvironmentBranchPolicy
471
- ? "exact-custom-branch"
477
+ : matchingEnvironmentPolicy
478
+ ? `custom-${environmentRefType}-policy`
472
479
  : "unqualified",
473
- authorizedBranch: exactEnvironmentBranchPolicy?.name || "",
480
+ authorizedBranch: matchingEnvironmentPolicy?.name || "",
481
+ authorizedRef: environmentRef,
482
+ authorizedRefType: environmentRefType,
474
483
  reviewRequired: reviewRules.length > 0,
475
484
  preventSelfReview: reviewRules.some((rule) => rule.prevent_self_review === true),
476
485
  },