@kungfu-tech/buildchain 3.0.3-alpha.0 → 3.0.3-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 (115) hide show
  1. package/AGENTS.md +1 -1
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +32 -1
  4. package/actions/promote-buildchain-ref/README.md +90 -19
  5. package/actions/report-buildchain-issue/README.md +19 -1
  6. package/actions/run-lifecycle/README.md +5 -0
  7. package/actions/validate-config/README.md +19 -1
  8. package/bin/buildchain.mjs +41 -241
  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 +0 -45
  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 +161 -42
  19. package/dist/site/buildchain-site.json +180 -84
  20. package/dist/site/capability-registry.json +6 -6
  21. package/dist/site/cli-registry.json +932 -98
  22. package/dist/site/controller-registry.json +97 -5
  23. package/dist/site/kfd-claims.json +316 -23
  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 +371 -56
  27. package/dist/site/page-registry.json +155 -59
  28. package/dist/site/public-surface-audit.json +286 -24
  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 +570 -79
  33. package/docs/MAP.md +22 -4
  34. package/docs/auditable-demo.md +35 -12
  35. package/docs/aws-us-elastic-runner-burst-plane.md +381 -0
  36. package/docs/cli.md +143 -12
  37. package/docs/consumer-issue-reporting.md +19 -1
  38. package/docs/github-governance-authority.md +2 -2
  39. package/docs/lifecycle-protocol.md +20 -2
  40. package/docs/migration-inventory.md +24 -5
  41. package/docs/ownership.md +19 -1
  42. package/docs/publication-artifacts.md +185 -18
  43. package/docs/publication-authority.md +4 -4
  44. package/docs/publish-transaction.md +78 -31
  45. package/docs/release-candidate.md +24 -0
  46. package/docs/release-flow.md +50 -32
  47. package/docs/release-governance.md +31 -31
  48. package/docs/release-passport.md +4 -0
  49. package/docs/release-propagation.md +57 -12
  50. package/docs/reusable-build-surface.md +95 -27
  51. package/docs/runtime-train-validation.md +20 -2
  52. package/docs/stable-candidate-patrol.md +6 -6
  53. package/docs/toolkit-observability.md +22 -0
  54. package/docs/versioning.md +6 -4
  55. package/docs/web-surface-deployments.md +62 -0
  56. package/package.json +7 -5
  57. package/packages/core/README.md +61 -4
  58. package/packages/core/buildchain-contract.js +8 -4
  59. package/packages/core/cache-evidence.js +1 -0
  60. package/packages/core/diagnostics.js +105 -30
  61. package/packages/core/github-governance-authority.js +10 -25
  62. package/packages/core/index.js +40 -0
  63. package/packages/core/paper-fleet.js +260 -0
  64. package/packages/core/paper-repository.js +210 -0
  65. package/packages/core/paper-work.js +365 -0
  66. package/packages/core/paper.js +3180 -0
  67. package/packages/core/public-surface-audit.js +26 -75
  68. package/packages/core/public-surface-cli.js +117 -0
  69. package/packages/core/publication-artifact.js +1 -6
  70. package/packages/core/publication-authority.js +1 -0
  71. package/packages/core/publication-reproducibility.js +947 -0
  72. package/packages/core/publication-sealed-bundle.js +190 -0
  73. package/packages/core/publish-transaction.js +147 -17
  74. package/packages/core/release-candidate.js +234 -0
  75. package/packages/core/release-passport.js +483 -201
  76. package/packages/core/release-propagation.js +175 -1
  77. package/scripts/auditable-demo.mjs +119 -5
  78. package/scripts/aws-codebuild-toolchain.mjs +285 -0
  79. package/scripts/aws-macos-jit-core.mjs +378 -0
  80. package/scripts/aws-macos-jit.mjs +123 -0
  81. package/scripts/aws-runner-burst-core.mjs +248 -0
  82. package/scripts/aws-runner-burst.mjs +79 -0
  83. package/scripts/aws-windows-jit-core.mjs +374 -0
  84. package/scripts/aws-windows-jit.mjs +121 -0
  85. package/scripts/build-contract-core.mjs +69 -6
  86. package/scripts/build-standalone-binary.mjs +23 -0
  87. package/scripts/buildchain-cli-help.mjs +256 -0
  88. package/scripts/buildchain-contract-lock.mjs +1 -1
  89. package/scripts/check-internal-architecture.mjs +69 -2
  90. package/scripts/check-inventory.mjs +69 -2
  91. package/scripts/check-javascript-syntax.mjs +31 -0
  92. package/scripts/check-maintainability.mjs +371 -0
  93. package/scripts/compiler-cache-evidence.mjs +221 -0
  94. package/scripts/dev-alpha-candidate-patrol.mjs +268 -43
  95. package/scripts/finalize-native-artifact-signing-result.mjs +75 -4
  96. package/scripts/generate-channel-promotion-workflow.mjs +3 -12
  97. package/scripts/generate-release-candidate-passport.mjs +4 -0
  98. package/scripts/generate-site-bundle.mjs +68 -188
  99. package/scripts/import-artifact-signing-results.mjs +64 -5
  100. package/scripts/inspect-artifact-signing-requests.mjs +7 -0
  101. package/scripts/maintainability-metrics.mjs +339 -0
  102. package/scripts/npm-publish-transaction.mjs +57 -6
  103. package/scripts/paper-work-fleet-cli.mjs +563 -0
  104. package/scripts/paper.mjs +631 -0
  105. package/scripts/promotion-channel-router.mjs +16 -4
  106. package/scripts/publication-reproducibility.mjs +62 -0
  107. package/scripts/release-propagation.mjs +39 -0
  108. package/scripts/resolve-build-contract.mjs +6 -0
  109. package/scripts/route-offline-runners.mjs +236 -0
  110. package/scripts/run-lifecycle-core.mjs +62 -1
  111. package/scripts/runtime-ref-core.mjs +23 -2
  112. package/scripts/seal-artifact-signing-requests.mjs +13 -6
  113. package/scripts/site-capability-metadata.mjs +202 -0
  114. package/scripts/stable-candidate-qualification.mjs +42 -0
  115. package/scripts/web-surface-core.mjs +272 -86
@@ -12,6 +12,8 @@ const STATE_MARKER_START = "<!-- buildchain-dev-alpha-candidate-state";
12
12
  const STATE_MARKER_END = "-->";
13
13
  const LEGACY_BODY_MARKER = "Buildchain exact-source channel candidate.";
14
14
  const EXACT_SHA = /^[0-9a-f]{40}$/u;
15
+ const EVIDENCE_ROOT = /^sha256:[0-9a-f]{64}$/u;
16
+ const ABSENT_STATE_ROOT = "absent";
15
17
 
16
18
  function text(value = "") {
17
19
  return String(value ?? "").trim();
@@ -72,6 +74,18 @@ function optionalExactSha(value, name) {
72
74
  return normalized;
73
75
  }
74
76
 
77
+ function optionalStateRoot(value, name) {
78
+ const normalized = text(value);
79
+ if (
80
+ normalized &&
81
+ normalized !== ABSENT_STATE_ROOT &&
82
+ !EVIDENCE_ROOT.test(normalized)
83
+ ) {
84
+ throw new Error(`${name} must be absent or an exact sha256 root`);
85
+ }
86
+ return normalized;
87
+ }
88
+
75
89
  function canonical(value) {
76
90
  if (Array.isArray(value)) return value.map(canonical);
77
91
  if (value && typeof value === "object") {
@@ -237,6 +251,31 @@ export function normalizeDevAlphaPatrolOptions(options = {}) {
237
251
  process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_SELECTED_SHA,
238
252
  "expectedSelectedSha",
239
253
  ),
254
+ expectedPriorStateRoot: optionalStateRoot(
255
+ options.expectedPriorStateRoot ??
256
+ process.env.BUILDCHAIN_CHANNEL_PATROL_EXPECTED_PRIOR_STATE_ROOT,
257
+ "expectedPriorStateRoot",
258
+ ),
259
+ reactivationAuthorized: bool(
260
+ options.reactivationAuthorized ??
261
+ process.env.BUILDCHAIN_CHANNEL_PATROL_REACTIVATION_AUTHORIZED,
262
+ false,
263
+ ),
264
+ transitionAuthority: {
265
+ actor: text(
266
+ options.transitionAuthority?.actor ?? process.env.GITHUB_ACTOR,
267
+ ),
268
+ workflow: text(
269
+ options.transitionAuthority?.workflow ?? process.env.GITHUB_WORKFLOW,
270
+ ),
271
+ runId: text(
272
+ options.transitionAuthority?.runId ?? process.env.GITHUB_RUN_ID,
273
+ ),
274
+ runAttempt: text(
275
+ options.transitionAuthority?.runAttempt ??
276
+ process.env.GITHUB_RUN_ATTEMPT,
277
+ ),
278
+ },
240
279
  createPullRequest,
241
280
  settlementAuthorized: bool(
242
281
  options.settlementAuthorized ??
@@ -396,6 +435,10 @@ function candidateFromDecision(decision) {
396
435
  sourceLockRef: decision.sourceLockRef,
397
436
  decisionRoot: decision.decisionRoot,
398
437
  workflowEvidence: decision.workflowEvidence,
438
+ qualificationRoot: evidenceRoot({
439
+ sourceSha: decision.source.sha,
440
+ workflowEvidence: decision.workflowEvidence,
441
+ }),
399
442
  };
400
443
  }
401
444
 
@@ -406,6 +449,9 @@ function candidateStateBody({
406
449
  activeCandidate,
407
450
  nextCandidate,
408
451
  supersededCandidate,
452
+ priorStateRoot = ABSENT_STATE_ROOT,
453
+ generation = 1,
454
+ tombstones = [],
409
455
  }) {
410
456
  const body = {
411
457
  schema: DEV_ALPHA_CANDIDATE_STATE_SCHEMA,
@@ -413,8 +459,11 @@ function candidateStateBody({
413
459
  sourceBranch: options.sourceBranch,
414
460
  targetBranch: options.targetBranch,
415
461
  targetSha,
462
+ generation,
463
+ priorStateRoot,
416
464
  activeCandidate,
417
465
  nextCandidate,
466
+ tombstones,
418
467
  observationDecisionRoot: decision.decisionRoot || null,
419
468
  observedAt: options.now,
420
469
  ...(supersededCandidate ? { supersededCandidate } : {}),
@@ -422,6 +471,50 @@ function candidateStateBody({
422
471
  return { ...body, stateRoot: evidenceRoot(body) };
423
472
  }
424
473
 
474
+ function persistedStateRoot(candidate) {
475
+ return text(candidate?.state?.stateRoot) || ABSENT_STATE_ROOT;
476
+ }
477
+
478
+ function candidateTombstone({
479
+ options,
480
+ candidate,
481
+ generation,
482
+ priorStateRoot,
483
+ reason,
484
+ pullRequest,
485
+ }) {
486
+ const body = {
487
+ schema: "kungfu-buildchain-dev-alpha-candidate-tombstone/v1",
488
+ repository: options.repository,
489
+ targetBranch: options.targetBranch,
490
+ candidateSha: candidate.sourceSha,
491
+ qualificationRoot:
492
+ candidate.qualificationRoot ||
493
+ evidenceRoot({
494
+ sourceSha: candidate.sourceSha,
495
+ workflowEvidence: candidate.workflowEvidence || [],
496
+ }),
497
+ generation,
498
+ priorStateRoot,
499
+ reason,
500
+ transitionAuthority: options.transitionAuthority,
501
+ pullRequest: pullRequest
502
+ ? {
503
+ number: Number(pullRequest.number),
504
+ url: text(pullRequest.url || pullRequest.html_url),
505
+ }
506
+ : null,
507
+ workflowRuns: (candidate.workflowEvidence || []).map((row) => ({
508
+ workflowPath: row.workflowPath,
509
+ runId: row.runId,
510
+ runAttempt: row.runAttempt,
511
+ url: row.url,
512
+ })),
513
+ recordedAt: options.now,
514
+ };
515
+ return { ...body, tombstoneRoot: evidenceRoot(body) };
516
+ }
517
+
425
518
  function pullRequestBody({
426
519
  options,
427
520
  observedSourceHeadSha,
@@ -573,9 +666,24 @@ export async function runDevAlphaCandidatePatrol(
573
666
  );
574
667
  }
575
668
  let activeCandidate = managedCandidates[0] || null;
669
+ const priorStateRoot = persistedStateRoot(activeCandidate);
670
+ if (
671
+ options.expectedPriorStateRoot &&
672
+ options.expectedPriorStateRoot !== priorStateRoot
673
+ ) {
674
+ throw new Error(
675
+ `candidate controller compare-and-swap failed: expected prior state root ${options.expectedPriorStateRoot}, observed ${priorStateRoot}`,
676
+ );
677
+ }
678
+ const priorGeneration = Number(activeCandidate?.state?.generation || 0);
679
+ let generation = Math.max(priorGeneration, 1);
680
+ const priorTombstones = Array.isArray(activeCandidate?.state?.tombstones)
681
+ ? activeCandidate.state.tombstones
682
+ : [];
576
683
  const observedCandidate = candidateFromDecision(decision);
577
684
  let nextCandidate = null;
578
685
  let supersededCandidate = null;
686
+ let tombstones = [...priorTombstones];
579
687
  let controllerState = decision.eligible
580
688
  ? "eligible-for-settlement"
581
689
  : qualificationError
@@ -585,43 +693,100 @@ export async function runDevAlphaCandidatePatrol(
585
693
  : "observed";
586
694
  if (activeCandidate) {
587
695
  controllerState = "active";
696
+ nextCandidate = activeCandidate.nextCandidate || null;
588
697
  if (
589
698
  observedCandidate &&
590
699
  observedCandidate.sourceSha !== activeCandidate.sourceSha
591
700
  ) {
592
- nextCandidate = observedCandidate;
593
- controllerState = "retained-next";
594
- }
595
- if (
596
- activeCandidate.nextCandidate &&
597
- activeCandidate.nextCandidate.sourceSha !== nextCandidate?.sourceSha
701
+ const matchingTombstone = priorTombstones.find(
702
+ (row) => row.candidateSha === observedCandidate.sourceSha,
703
+ );
704
+ if (matchingTombstone && !options.reactivationAuthorized) {
705
+ controllerState = "tombstone-blocked";
706
+ } else {
707
+ if (
708
+ nextCandidate &&
709
+ nextCandidate.sourceSha !== observedCandidate.sourceSha
710
+ ) {
711
+ supersededCandidate = nextCandidate;
712
+ generation = priorGeneration + 1;
713
+ tombstones.push(
714
+ candidateTombstone({
715
+ options,
716
+ candidate: supersededCandidate,
717
+ generation,
718
+ priorStateRoot,
719
+ reason: "newer-qualified-next-candidate",
720
+ pullRequest: activeCandidate,
721
+ }),
722
+ );
723
+ }
724
+ nextCandidate = observedCandidate;
725
+ controllerState = "retained-next";
726
+ }
727
+ } else if (
728
+ !decision.eligible &&
729
+ nextCandidate &&
730
+ decision.source.sha === nextCandidate.sourceSha
598
731
  ) {
599
- supersededCandidate = activeCandidate.nextCandidate;
732
+ supersededCandidate = nextCandidate;
733
+ nextCandidate = null;
734
+ generation = priorGeneration + 1;
735
+ tombstones.push(
736
+ candidateTombstone({
737
+ options,
738
+ candidate: supersededCandidate,
739
+ generation,
740
+ priorStateRoot,
741
+ reason: "qualification-evidence-rejected",
742
+ pullRequest: activeCandidate,
743
+ }),
744
+ );
745
+ controllerState = "rejected-next";
600
746
  }
601
747
  }
602
- let state = candidateStateBody({
603
- options,
604
- targetSha,
605
- decision,
606
- activeCandidate: activeCandidate
607
- ? {
608
- sourceSha: activeCandidate.sourceSha,
609
- sourceLockRef: activeCandidate.sourceLockRef,
610
- decisionRoot: activeCandidate.decisionRoot || null,
611
- pullRequestNumber: activeCandidate.number,
612
- pullRequestUrl: activeCandidate.url,
613
- }
614
- : null,
615
- nextCandidate,
616
- supersededCandidate,
617
- });
748
+ const persistedNextSha = text(activeCandidate?.nextCandidate?.sourceSha);
749
+ const desiredNextSha = text(nextCandidate?.sourceSha);
750
+ const transitionNeeded =
751
+ !activeCandidate?.state ||
752
+ persistedNextSha !== desiredNextSha ||
753
+ tombstones.length !== priorTombstones.length;
754
+ if (transitionNeeded && activeCandidate && generation === priorGeneration) {
755
+ generation = priorGeneration + 1;
756
+ }
757
+ let state =
758
+ activeCandidate?.state && !transitionNeeded
759
+ ? activeCandidate.state
760
+ : candidateStateBody({
761
+ options,
762
+ targetSha,
763
+ decision,
764
+ activeCandidate: activeCandidate
765
+ ? {
766
+ sourceSha: activeCandidate.sourceSha,
767
+ sourceLockRef: activeCandidate.sourceLockRef,
768
+ decisionRoot: activeCandidate.decisionRoot || null,
769
+ pullRequestNumber: activeCandidate.number,
770
+ pullRequestUrl: activeCandidate.url,
771
+ }
772
+ : null,
773
+ nextCandidate,
774
+ supersededCandidate,
775
+ priorStateRoot,
776
+ generation,
777
+ tombstones,
778
+ });
618
779
  let pullRequest;
619
780
  let settlementAction = "none";
620
781
  if (options.settlementAuthorized && !options.dryRun) {
621
782
  if (activeCandidate) {
622
783
  const nextBody = replaceCandidateStateMarker(activeCandidate.body, state);
623
784
  if (nextBody !== activeCandidate.body) {
624
- await client.updatePullRequestBody(activeCandidate.number, nextBody);
785
+ await client.updatePullRequestBody(
786
+ activeCandidate.number,
787
+ nextBody,
788
+ priorStateRoot,
789
+ );
625
790
  settlementAction = nextCandidate
626
791
  ? supersededCandidate
627
792
  ? "supersede-next-candidate"
@@ -656,15 +821,53 @@ export async function runDevAlphaCandidatePatrol(
656
821
  state,
657
822
  }),
658
823
  });
659
- settlementAction = "create-active-candidate";
660
- controllerState = "active";
661
- activeCandidate = {
662
- number: Number(pullRequest.number || 0),
663
- url: text(pullRequest.html_url),
664
- sourceSha,
665
- sourceLockRef: decision.sourceLockRef,
666
- decisionRoot: decision.decisionRoot,
667
- };
824
+ if (pullRequest.state && pullRequest.state !== "open") {
825
+ const tombstone = candidateTombstone({
826
+ options,
827
+ candidate: observedCandidate,
828
+ generation,
829
+ priorStateRoot,
830
+ reason: pullRequest.merged_at
831
+ ? "candidate-pr-merged"
832
+ : "candidate-pr-closed",
833
+ pullRequest,
834
+ });
835
+ tombstones = [...priorTombstones, tombstone];
836
+ state = candidateStateBody({
837
+ options,
838
+ targetSha,
839
+ decision,
840
+ activeCandidate: null,
841
+ nextCandidate: null,
842
+ supersededCandidate: null,
843
+ priorStateRoot,
844
+ generation,
845
+ tombstones,
846
+ });
847
+ const nextBody = replaceCandidateStateMarker(pullRequest.body, state);
848
+ await client.updatePullRequestBody(
849
+ pullRequest.number,
850
+ nextBody,
851
+ persistedStateRoot({
852
+ state: parseCandidateStateMarker(pullRequest.body),
853
+ }),
854
+ );
855
+ settlementAction = "reuse-known-candidate-tombstone";
856
+ controllerState = "tombstoned";
857
+ activeCandidate = null;
858
+ } else {
859
+ settlementAction = pullRequest.reused
860
+ ? "reuse-active-candidate"
861
+ : "create-active-candidate";
862
+ controllerState = "active";
863
+ activeCandidate = {
864
+ number: Number(pullRequest.number || 0),
865
+ url: text(pullRequest.html_url),
866
+ sourceSha,
867
+ sourceLockRef: decision.sourceLockRef,
868
+ decisionRoot: decision.decisionRoot,
869
+ };
870
+ }
668
871
  }
669
872
  }
670
873
  return {
@@ -681,6 +884,9 @@ export async function runDevAlphaCandidatePatrol(
681
884
  supersededCandidate,
682
885
  settlementAction,
683
886
  stateRoot: state.stateRoot,
887
+ priorStateRoot,
888
+ generation: state.generation,
889
+ tombstones: state.tombstones || [],
684
890
  },
685
891
  pullRequest: pullRequest || null,
686
892
  };
@@ -809,18 +1015,36 @@ export function createGitHubChannelCandidateClient({
809
1015
  });
810
1016
  },
811
1017
  async ensurePullRequest({ head, base, title, body }) {
812
- const open = await api(
813
- `/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
814
- );
815
- return (
816
- open[0] ||
817
- api(`/repos/${owner}/${repo}/pulls`, {
818
- method: "POST",
819
- body: { head, base, title, body },
820
- })
1018
+ const known = await api(
1019
+ `/repos/${owner}/${repo}/pulls?state=all&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`,
821
1020
  );
1021
+ if (known.length > 1) {
1022
+ throw new Error(
1023
+ `multiple known candidate pull requests bind ${head}: ${known
1024
+ .map((row) => `#${row.number}`)
1025
+ .join(", ")}`,
1026
+ );
1027
+ }
1028
+ if (known[0]) return { ...known[0], reused: true };
1029
+ return api(`/repos/${owner}/${repo}/pulls`, {
1030
+ method: "POST",
1031
+ body: { head, base, title, body },
1032
+ });
822
1033
  },
823
- async updatePullRequestBody(number, body) {
1034
+ async updatePullRequestBody(
1035
+ number,
1036
+ body,
1037
+ expectedPriorStateRoot = ABSENT_STATE_ROOT,
1038
+ ) {
1039
+ const current = await api(`/repos/${owner}/${repo}/pulls/${number}`);
1040
+ const observedPriorStateRoot = persistedStateRoot({
1041
+ state: parseCandidateStateMarker(current.body),
1042
+ });
1043
+ if (observedPriorStateRoot !== expectedPriorStateRoot) {
1044
+ throw new Error(
1045
+ `candidate controller compare-and-swap failed: expected prior state root ${expectedPriorStateRoot}, observed ${observedPriorStateRoot}`,
1046
+ );
1047
+ }
824
1048
  return api(`/repos/${owner}/${repo}/pulls/${number}`, {
825
1049
  method: "PATCH",
826
1050
  body: { body },
@@ -866,6 +1090,7 @@ async function main() {
866
1090
  "active-candidate-pr": result.controller.activeCandidate?.url || "",
867
1091
  "next-candidate-sha": result.controller.nextCandidate?.sourceSha || "",
868
1092
  "settlement-action": result.controller.settlementAction,
1093
+ "prior-state-root": result.controller.priorStateRoot,
869
1094
  };
870
1095
  fs.appendFileSync(
871
1096
  process.env.GITHUB_OUTPUT,
@@ -37,6 +37,8 @@ export function finalizeNativeArtifactSigningResult({
37
37
  requestPath = process.env.BUILDCHAIN_SIGNING_REQUEST_PATH,
38
38
  signedPayload = process.env.BUILDCHAIN_SIGNED_PAYLOAD,
39
39
  evidencePath = process.env.BUILDCHAIN_SIGNING_EVIDENCE,
40
+ credentialArtifactRoot =
41
+ process.env.BUILDCHAIN_SIGNING_CREDENTIAL_ARTIFACT_ROOT,
40
42
  outputRoot = process.env.BUILDCHAIN_SIGNING_RESULT_ROOT,
41
43
  checks = process.env.BUILDCHAIN_SIGNING_VERIFICATION_CHECKS,
42
44
  } = {}) {
@@ -74,20 +76,79 @@ export function finalizeNativeArtifactSigningResult({
74
76
  const evidenceOutput = path.join(resultDirectory, "provider-evidence.json");
75
77
  fs.copyFileSync(evidenceSource, evidenceOutput, fs.constants.COPYFILE_EXCL);
76
78
  const evidenceDocument = JSON.parse(fs.readFileSync(evidenceOutput, "utf8"));
77
- if (
78
- evidenceDocument.status !== "passed" ||
79
- evidenceDocument.provider !== request.signature.provider
80
- ) {
79
+ const appBundleResult = request.artifact.kind === "app-bundle";
80
+ const providerEvidencePassed = appBundleResult
81
+ ? evidenceDocument.schema ===
82
+ "buildchain.macos-credential-island-evidence/v1" &&
83
+ evidenceDocument.status === "accepted" &&
84
+ evidenceDocument.source?.repository === request.source.repository &&
85
+ evidenceDocument.source?.sha === request.source.sha &&
86
+ evidenceDocument.source?.treeSha === request.source.treeSha &&
87
+ evidenceDocument.buildchain?.runtimeSha === request.runtime.sha &&
88
+ evidenceDocument.app?.architecture === request.artifact.arch &&
89
+ evidenceDocument.notarization?.application?.status === "Accepted" &&
90
+ evidenceDocument.notarization?.diskImage?.status === "Accepted"
91
+ : evidenceDocument.status === "passed" &&
92
+ evidenceDocument.provider === request.signature.provider;
93
+ if (!providerEvidencePassed) {
81
94
  throw new Error(
82
95
  "provider evidence does not prove the requested native signature",
83
96
  );
84
97
  }
98
+ let credentialOutput = "";
99
+ let credentialEvidence = [];
100
+ if (appBundleResult) {
101
+ const credentialSource = path.resolve(
102
+ required(
103
+ credentialArtifactRoot,
104
+ "credential artifact root for app-bundle result",
105
+ ),
106
+ );
107
+ const credentialManifest = path.join(credentialSource, "manifest.json");
108
+ if (!fs.statSync(credentialManifest).isFile())
109
+ throw new Error("credential artifact manifest is missing");
110
+ credentialOutput = path.join(resultDirectory, "credential-artifact");
111
+ fs.cpSync(credentialSource, credentialOutput, {
112
+ recursive: true,
113
+ force: false,
114
+ errorOnExist: true,
115
+ });
116
+ const manifestOutput = path.join(credentialOutput, "manifest.json");
117
+ const manifest = JSON.parse(fs.readFileSync(manifestOutput, "utf8"));
118
+ if (!Array.isArray(manifest.files) || manifest.files.length === 0)
119
+ throw new Error("credential artifact manifest contains no files");
120
+ credentialEvidence = [
121
+ {
122
+ kind: "credential-artifact-manifest",
123
+ path: "credential-artifact/manifest.json",
124
+ digest: sha256File(manifestOutput),
125
+ },
126
+ ...manifest.files.map((file, index) => {
127
+ const filePath = resolveBelow(
128
+ credentialOutput,
129
+ file.path,
130
+ `credential artifact file ${index}`,
131
+ );
132
+ const observed = sha256File(filePath);
133
+ if (observed !== `sha256:${String(file.sha256 || "").toLowerCase()}`)
134
+ throw new Error(
135
+ `credential artifact file digest mismatch: ${file.path}`,
136
+ );
137
+ return {
138
+ kind: `credential-artifact-${index}`,
139
+ path: `credential-artifact/${String(file.path).replaceAll("\\", "/")}`,
140
+ digest: observed,
141
+ };
142
+ }),
143
+ ];
144
+ }
85
145
  const evidence = [
86
146
  {
87
147
  kind: `${request.signature.profile}-verification`,
88
148
  path: "provider-evidence.json",
89
149
  digest: sha256File(evidenceOutput),
90
150
  },
151
+ ...credentialEvidence,
91
152
  ];
92
153
  const payloadDigest = sha256File(payloadPath);
93
154
  const receipt = createArtifactSigningReceipt({
@@ -118,6 +179,15 @@ export function finalizeNativeArtifactSigningResult({
118
179
  .filter(Boolean),
119
180
  );
120
181
  }
182
+ if (verificationChecks.length === 0 && appBundleResult) {
183
+ verificationChecks.push(
184
+ ...Object.entries(evidenceDocument.verification || {})
185
+ .filter(([, passed]) => passed === true)
186
+ .map(([name]) => name),
187
+ "application-notarytool-accepted",
188
+ "disk-image-notarytool-accepted",
189
+ );
190
+ }
121
191
  if (verificationChecks.length === 0)
122
192
  throw new Error("provider evidence contains no verification checks");
123
193
  const result = createArtifactSigningResult({
@@ -160,6 +230,7 @@ export function finalizeNativeArtifactSigningResult({
160
230
  writeGitHubOutputs({
161
231
  "result-root": resultDirectory,
162
232
  "result-index": path.join(resultDirectory, "index.json"),
233
+ "credential-artifact-root": credentialOutput,
163
234
  });
164
235
  return index;
165
236
  }
@@ -284,24 +284,15 @@ jobs:
284
284
  --publication-channel "\${{ inputs.channel }}"
285
285
  --target-ref "\${{ inputs.target-ref || github.ref_name }}"
286
286
  --router-ref "\${{ steps.router.outputs.ref }}"
287
+ --router-sha "\${{ steps.router.outputs.sha }}"
287
288
 
288
289
  - name: Authorize trusted runtime override
289
290
  if: \${{ steps.route.outputs.override-used == 'true' }}
290
291
  uses: actions/github-script@v8
291
292
  with:
292
293
  script: |
293
- if (context.eventName !== "workflow_dispatch") {
294
- throw new Error("promotion runtime override is only allowed for trusted workflow_dispatch runs");
295
- }
296
- const permission = await github.rest.repos.getCollaboratorPermissionLevel({
297
- owner: context.repo.owner,
298
- repo: context.repo.repo,
299
- username: context.actor,
300
- });
301
- const level = permission.data.user?.permissions || permission.data.permission || "none";
302
- if (!["write", "maintain", "admin"].includes(level)) {
303
- throw new Error(\`promotion runtime override requires write, maintain, or admin permission; actor has \${level}\`);
304
- }
294
+ const script = require(process.env.GITHUB_WORKSPACE + "/.buildchain/router/scripts/authorize-promotion-runtime-override.cjs");
295
+ await script.authorizePromotionRuntimeOverride({ github, context });
305
296
 
306
297
  - name: Resolve immutable promotion identities
307
298
  id: identities
@@ -29,6 +29,8 @@ export function generateReleaseCandidatePassportCli() {
29
29
  const gateAggregate = gateAggregateJson ? JSON.parse(gateAggregateJson) : undefined;
30
30
  const controllerReceiptJson = env("BUILDCHAIN_CONTROLLER_RECEIPT_JSON");
31
31
  const controllerReceipts = controllerReceiptJson ? [JSON.parse(controllerReceiptJson)] : [];
32
+ const familyEvidenceJson = env("BUILDCHAIN_RC_FAMILY_EVIDENCE_JSON");
33
+ const familyEvidence = familyEvidenceJson ? JSON.parse(familyEvidenceJson) : undefined;
32
34
  const passport = createReleaseCandidatePassport({
33
35
  repository: env("GITHUB_REPOSITORY", buildSummary.git?.repository || ""),
34
36
  pullRequest: {
@@ -51,6 +53,7 @@ export function generateReleaseCandidatePassportCli() {
51
53
  workflowShellRef: env("BUILDCHAIN_WORKFLOW_SHELL_REF", buildSummary.runtime?.workflowShellRef || ""),
52
54
  },
53
55
  gateAggregate,
56
+ familyEvidence,
54
57
  controllerReceipts,
55
58
  workflow: {
56
59
  name: env("GITHUB_WORKFLOW"),
@@ -80,6 +83,7 @@ export function generateReleaseCandidatePassportCli() {
80
83
  candidateHash: passport.candidateHash,
81
84
  platformCount: passport.platformMatrix.length,
82
85
  gateProfileEvidence: passport.gateProfileEvidence,
86
+ familyEvidence: passport.familyEvidence,
83
87
  controllerReceipts: passport.controllerReceipts || [],
84
88
  }),
85
89
  });