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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/action-taxonomy.json +24 -0
  4. package/architecture/agent-change-map.md +272 -12
  5. package/architecture/capability-state-machine-manifest.json +1 -4
  6. package/architecture/ci-lane-change-budget.json +190 -807
  7. package/architecture/core-mechanisms.json +3 -6
  8. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  9. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  10. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  11. package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
  12. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  13. package/architecture/internal-capabilities.json +371 -9
  14. package/architecture/maintainability-debt.json +50 -34
  15. package/architecture/maintainability-policy.json +8 -8
  16. package/architecture/minimal-consumer-migration.json +5670 -0
  17. package/architecture/platform-stage-checkpoints.json +31 -7
  18. package/architecture/product-upstreams.json +20 -0
  19. package/architecture/release-topology.json +331 -74
  20. package/architecture/stage-capsule-qualification.json +3 -1
  21. package/architecture/universal-workflow-bootstrap.json +15 -1
  22. package/architecture/universal-workflow-capability-policy.json +9 -9
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/contracts/release-discussion-v1.schema.json +3 -0
  26. package/dist/readers/business-attempt.cjs +809 -0
  27. package/dist/readers/release-discussion.cjs +4 -0
  28. package/dist/site/buildchain-contract.json +16 -68
  29. package/dist/site/buildchain-site.json +47 -27
  30. package/dist/site/capability-registry.json +4 -4
  31. package/dist/site/controller-registry.json +1 -47
  32. package/dist/site/kfd-claims.json +499 -494
  33. package/dist/site/kfd-upstream-aggregate.json +1 -1
  34. package/dist/site/manual-registry.json +3 -3
  35. package/dist/site/node-api-registry.json +56 -56
  36. package/dist/site/page-registry.json +40 -20
  37. package/dist/site/public-surface-audit.json +286 -349
  38. package/dist/site/publication-authority-registry.json +203 -523
  39. package/dist/site/publication-registry.json +4 -4
  40. package/dist/site/site-manifest.json +7 -7
  41. package/dist/site/workflow-registry.json +590 -940
  42. package/docs/code-organization.md +9 -6
  43. package/docs/node-api-reference.md +103 -103
  44. package/docs/release-discussions.md +44 -5
  45. package/docs/release-governance.md +10 -12
  46. package/docs/runtime-entry.md +73 -5
  47. package/docs/runtime-train-validation.md +39 -11
  48. package/docs/stable-candidate-patrol.md +6 -5
  49. package/docs/workflow-catalog.md +9 -32
  50. package/package.json +4 -3
  51. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  52. package/packages/core/build/standalone/build.js +7 -1
  53. package/packages/core/consumer/buildchain-config.js +2 -21
  54. package/packages/core/consumer/contract/entries.js +85 -0
  55. package/packages/core/consumer/contract/examples.js +96 -0
  56. package/packages/core/consumer/contract/identity.js +65 -0
  57. package/packages/core/consumer/contract/inspection.js +45 -0
  58. package/packages/core/consumer/contract/plan.js +149 -0
  59. package/packages/core/consumer/contract/products.js +119 -0
  60. package/packages/core/consumer/contract/reader.js +47 -0
  61. package/packages/core/consumer/contract/shape.js +46 -0
  62. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  63. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  64. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  65. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  66. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  67. package/packages/core/dev-delivery/native/actions.js +9 -1
  68. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  69. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  70. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  71. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  72. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  73. package/packages/core/dev-delivery/warrant/service.js +7 -0
  74. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  75. package/packages/core/governance/github-governance-authority.js +1 -0
  76. package/packages/core/observability/controller-evidence.js +0 -8
  77. package/packages/core/paper/operations/bootstrap.js +2 -0
  78. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  79. package/packages/core/providers/github/attempt-index.js +60 -0
  80. package/packages/core/providers/github/attempt-journal.js +154 -0
  81. package/packages/core/providers/github/discussions/materials.js +35 -11
  82. package/packages/core/providers/github/discussions/transport.js +16 -5
  83. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  84. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  85. package/packages/core/providers/github/pipeline-events.js +131 -0
  86. package/packages/core/providers/github/pipeline-integration.js +95 -0
  87. package/packages/core/providers/github/pipeline-policy.js +240 -0
  88. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  89. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  90. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  91. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  92. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  93. package/packages/core/providers/github/pipeline-runs.js +132 -0
  94. package/packages/core/providers/github/pipeline-source.js +195 -0
  95. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  96. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  97. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  98. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  99. package/packages/core/providers/github/pipeline-version.js +199 -0
  100. package/packages/core/providers/github/pipeline-worker.js +108 -0
  101. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  102. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  103. package/packages/core/publication/npm/pack-preview.js +2 -2
  104. package/packages/core/publication/npm/pack-result.js +31 -0
  105. package/packages/core/publication/npm/package.js +3 -6
  106. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  107. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  108. package/packages/core/publication/npm/registry.js +12 -9
  109. package/packages/core/publication/pipeline/actions.js +116 -0
  110. package/packages/core/publication/pipeline/apply.js +189 -0
  111. package/packages/core/publication/pipeline/build-segments.js +107 -0
  112. package/packages/core/publication/pipeline/capsules.js +92 -0
  113. package/packages/core/publication/pipeline/context.js +39 -0
  114. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  115. package/packages/core/publication/pipeline/development-current.js +63 -0
  116. package/packages/core/publication/pipeline/development-pr.js +112 -0
  117. package/packages/core/publication/pipeline/development-proof.js +76 -0
  118. package/packages/core/publication/pipeline/development-transition.js +112 -0
  119. package/packages/core/publication/pipeline/distribution.js +130 -0
  120. package/packages/core/publication/pipeline/documents.js +192 -0
  121. package/packages/core/publication/pipeline/effects.js +192 -0
  122. package/packages/core/publication/pipeline/files.js +66 -0
  123. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  124. package/packages/core/publication/pipeline/journal.js +122 -0
  125. package/packages/core/publication/pipeline/next-development.js +117 -0
  126. package/packages/core/publication/pipeline/pack.js +180 -0
  127. package/packages/core/publication/pipeline/package-policy.js +22 -0
  128. package/packages/core/publication/pipeline/plan.js +136 -0
  129. package/packages/core/publication/pipeline/prepare.js +139 -0
  130. package/packages/core/publication/pipeline/qualification.js +175 -0
  131. package/packages/core/publication/pipeline/qualify.js +104 -0
  132. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  133. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  135. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  136. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  137. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  138. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  139. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  140. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  141. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  142. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  143. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  144. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  145. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  146. package/packages/core/publication/pipeline/settle.js +42 -0
  147. package/packages/core/publication/pipeline/signing.js +140 -0
  148. package/packages/core/publication/pipeline/source-plan.js +83 -0
  149. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  150. package/packages/core/publication/pipeline/stable-products.js +317 -0
  151. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  152. package/packages/core/publication/pipeline/stable.js +82 -0
  153. package/packages/core/publication/pipeline/version-actions.js +93 -0
  154. package/packages/core/publication/pipeline/version-build.js +130 -0
  155. package/packages/core/publication/pipeline/version-context.js +143 -0
  156. package/packages/core/publication/pipeline/version-material.js +36 -0
  157. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  158. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  159. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  160. package/packages/core/publication/pipeline/version.js +81 -0
  161. package/packages/core/publication/pipeline/worker.js +72 -0
  162. package/packages/core/publication/publication-reproducibility.js +2 -2
  163. package/packages/core/release/discussion/binary.js +11 -0
  164. package/packages/core/release/discussion/checkpoints.js +45 -4
  165. package/packages/core/release/discussion/envelope.js +1 -0
  166. package/packages/core/release/discussion/evidence.js +51 -0
  167. package/packages/core/release/discussion/presentation.js +92 -0
  168. package/packages/core/release/discussion/publication.js +3 -1
  169. package/packages/core/release/discussion/qualification.js +1 -0
  170. package/packages/core/release/discussion/reader.js +9 -0
  171. package/packages/core/release/discussion/session.js +2 -0
  172. package/packages/core/release/discussion/store.js +28 -31
  173. package/packages/core/release/discussion/threads.js +83 -0
  174. package/packages/core/release/next-development-transition.js +15 -15
  175. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  176. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  177. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  178. package/packages/core/release/stable-release-gate.js +11 -0
  179. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  180. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  181. package/packages/core/runtime/entry/actions.js +27 -25
  182. package/packages/core/runtime/entry/attempt.js +25 -0
  183. package/packages/core/runtime/entry/selection.js +14 -0
  184. package/packages/core/runtime/entry/source.js +62 -0
  185. package/packages/core/workflow/attempt/identity.js +170 -0
  186. package/packages/core/workflow/attempt/journal.js +89 -0
  187. package/packages/core/workflow/attempt/materials.js +72 -0
  188. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  189. package/packages/core/workflow/attempt/reader.js +149 -0
  190. package/packages/core/workflow/attempt/records.js +153 -0
  191. package/packages/core/workflow/attempt/store.js +75 -0
  192. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  193. package/packages/core/workflow/pipeline/action-output.js +11 -0
  194. package/packages/core/workflow/pipeline/actions.js +52 -0
  195. package/packages/core/workflow/pipeline/build-control.js +140 -0
  196. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  197. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  198. package/packages/core/workflow/pipeline/build-result.js +78 -0
  199. package/packages/core/workflow/pipeline/build.js +142 -0
  200. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  201. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  202. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  203. package/packages/core/workflow/pipeline/controller.js +141 -0
  204. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  205. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  206. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  207. package/packages/core/workflow/pipeline/events.js +84 -0
  208. package/packages/core/workflow/pipeline/fence.js +42 -0
  209. package/packages/core/workflow/pipeline/group-control.js +59 -0
  210. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  211. package/packages/core/workflow/pipeline/guard.js +158 -0
  212. package/packages/core/workflow/pipeline/host.js +144 -0
  213. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  214. package/packages/core/workflow/pipeline/materials.js +58 -0
  215. package/packages/core/workflow/pipeline/notifications.js +36 -0
  216. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  217. package/packages/core/workflow/pipeline/platforms.js +21 -0
  218. package/packages/core/workflow/pipeline/progress.js +108 -0
  219. package/packages/core/workflow/pipeline/projection.js +20 -0
  220. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  221. package/packages/core/workflow/pipeline/record-action.js +27 -0
  222. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  223. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  224. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  225. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  226. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  227. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  228. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  229. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  230. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  231. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  232. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  233. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  234. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  235. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  236. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  237. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  238. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  239. package/packages/core/workflow/pipeline/selection.js +111 -0
  240. package/packages/core/workflow/pipeline/session.js +72 -0
  241. package/packages/core/workflow/pipeline/settlement.js +137 -0
  242. package/packages/core/workflow/pipeline/wake.js +38 -0
  243. package/packages/core/workflow/pipeline/web-status.js +108 -0
  244. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  245. package/scripts/build-release-discussion-reader.mjs +11 -7
  246. package/scripts/check-inventory.mjs +7 -70
  247. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  248. package/scripts/check-public-dogfood-contract.mjs +38 -51
  249. package/scripts/check-release-topology.mjs +15 -8
  250. package/scripts/check-self-consumer-contract.mjs +133 -0
  251. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  252. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  253. package/scripts/generate-site-bundle.mjs +1 -1
  254. package/scripts/inventory/binary.mjs +9 -19
  255. package/scripts/inventory/promotion.mjs +2 -13
  256. package/scripts/prepare-product-toolchain.mjs +10 -0
  257. package/scripts/verify-golden-path.mjs +5 -1
  258. package/scripts/verify-product-platform.mjs +65 -0
  259. package/scripts/verify-standalone-product.mjs +42 -0
  260. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  261. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  262. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  263. package/templates/minimal-consumer/binary/package.json +6 -0
  264. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  265. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  266. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  267. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  268. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  269. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  270. package/templates/minimal-consumer/npm/package.json +6 -0
  271. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  272. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  273. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  274. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  275. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  276. package/templates/minimal-consumer/paper/package.json +6 -0
  277. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  278. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,73 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { retainedRecoveryEvidence } from "../../workflow/pipeline/recovery-session.js";
3
+ import { recoveryPublicationMaterial } from "./recovery-materials.js";
4
+
5
+ import { importRecoveredPublication } from "./recovery-import.js";
6
+
7
+ export async function prepareRecoveredPublication(
8
+ session,
9
+ publisherSha,
10
+ host,
11
+ journal,
12
+ phase,
13
+ ) {
14
+ if (
15
+ !session.observed.history.at(-1).identity.requestKey.startsWith("recover:")
16
+ )
17
+ return null;
18
+ const { plan: recoveryPlan, evidence } = await retainedRecoveryEvidence(
19
+ session,
20
+ host,
21
+ );
22
+ const publication = evidence.publication;
23
+ if (!publication || publication.mode === "unprepared") return null;
24
+ const plan = recoveryPublicationMaterial(
25
+ publication.materials,
26
+ "publication/plan/",
27
+ true,
28
+ );
29
+ if (recordDigest(host.runtime) !== recordDigest(recoveryPlan.runtime))
30
+ throw new Error(
31
+ "Recovered publication executor differs from the admitted repair",
32
+ );
33
+ const runtime = await host.request(
34
+ `/repos/${host.runtime.repository}/git/commits/${host.runtime.sha}`,
35
+ );
36
+ if (
37
+ runtime.sha !== host.runtime.sha ||
38
+ !/^[0-9a-f]{40}$/u.test(runtime.tree?.sha || "")
39
+ )
40
+ throw new Error("Recovery publisher runtime has no exact admitted tree");
41
+ const execution = {
42
+ attempt: session.observed.attempt,
43
+ runId: host.runId,
44
+ runAttempt: host.runAttempt,
45
+ publisher: {
46
+ repository: "kungfu-systems/buildchain",
47
+ workflow: ".github/workflows/.release-pipeline-products.yml",
48
+ workflowSha: publisherSha,
49
+ job: "apply",
50
+ },
51
+ runtime: {
52
+ repository: host.runtime.repository,
53
+ commit: runtime.sha,
54
+ tree: runtime.tree.sha,
55
+ },
56
+ };
57
+ await importRecoveredPublication(
58
+ publication,
59
+ execution,
60
+ recoveryPlan.root,
61
+ journal,
62
+ phase,
63
+ );
64
+ return {
65
+ planRoot: recoveryPlan.root,
66
+ predecessor: recoveryPlan.predecessor,
67
+ originalPlanRoot: plan.root,
68
+ mode: publication.mode,
69
+ build: publication.build || null,
70
+ preserveTransaction: publication.mode === "qualified",
71
+ execution,
72
+ };
73
+ }
@@ -0,0 +1,128 @@
1
+ import path from "node:path";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { createDomainPublicationQualificationReceipt } from "../publication-qualification.js";
4
+ import { restorePipelineProducts } from "./sealed-products.js";
5
+ import { inspectPipelinePublicationArtifacts } from "./qualification.js";
6
+ import { verifyPipelineQualification } from "./documents.js";
7
+ import { reobservePublicationBuild } from "./recovery-build-readback.js";
8
+ import { requalifyPublicationCapsules } from "./recovery-capsules.js";
9
+ import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
10
+
11
+ function originalManifests(qualified) {
12
+ return [...new Set(qualified.artifacts.map((item) => item.manifestRoot))].map(
13
+ (root) => {
14
+ const artifacts = qualified.artifacts
15
+ .filter((item) => item.manifestRoot === root)
16
+ .map(({ manifestRoot, providerArtifactId, ...item }) => item);
17
+ const body = {
18
+ schema: "buildchain.pipeline-publication-products/v1",
19
+ planRoot: qualified.planRoot,
20
+ source: qualified.source,
21
+ platform: artifacts[0].platform,
22
+ artifacts,
23
+ };
24
+ if (recordDigest(body) !== root)
25
+ throw new Error(
26
+ "Recovered artifact descriptors cannot reconstruct their original manifest bytes",
27
+ );
28
+ return { ...body, root };
29
+ },
30
+ );
31
+ }
32
+
33
+ export async function requalifySealedPublication({
34
+ context,
35
+ prepared,
36
+ originalPlan,
37
+ originalMaterialization,
38
+ archive,
39
+ host,
40
+ directory,
41
+ now = new Date(),
42
+ }) {
43
+ const previous = prepared.qualified;
44
+ verifyPipelineQualification({
45
+ plan: originalPlan,
46
+ materialization: originalMaterialization,
47
+ qualified: previous,
48
+ evaluatedAt: previous.qualification.issuedAt,
49
+ });
50
+ const { plan, materialization } = context;
51
+ if (
52
+ recordDigest(plan.outputs) !== recordDigest(originalPlan.outputs) ||
53
+ plan.version !== originalPlan.version ||
54
+ plan.contractRoot !== originalPlan.contractRoot ||
55
+ recordDigest(materialization.source) !== recordDigest(previous.source)
56
+ )
57
+ throw new Error(
58
+ "Signing recovery cannot change the already sealed product contract or source",
59
+ );
60
+ await reobservePublicationBuild(previous.build, previous.source, host);
61
+ const products = await restorePipelineProducts(
62
+ archive,
63
+ previous,
64
+ prepared.sealed,
65
+ path.join(directory, "requalified-products"),
66
+ );
67
+ for (const manifest of originalManifests(previous))
68
+ inspectPipelinePublicationArtifacts(products, manifest, plan);
69
+ const { run } = await host.runs.read(context.runId, context.runAttempt);
70
+ await readPipelineCaller(
71
+ run,
72
+ previous.source.configPath,
73
+ host.request,
74
+ host.repository,
75
+ );
76
+ const buildBody = {
77
+ schema: "buildchain.pipeline-publication-requalification/v1",
78
+ operation: "requalify-sealed-bytes",
79
+ outcome: "success",
80
+ planRoot: plan.root,
81
+ source: previous.source,
82
+ runId: context.runId,
83
+ runAttempt: context.runAttempt,
84
+ providerSource: run.head_sha,
85
+ predecessorBuild: previous.build,
86
+ sealedRoot: prepared.sealed.root,
87
+ };
88
+ const build = { ...buildBody, root: recordDigest(buildBody) };
89
+ const qualification = createDomainPublicationQualificationReceipt({
90
+ ...previous.qualification,
91
+ candidateRoot: recordDigest({
92
+ planRoot: plan.root,
93
+ source: previous.source,
94
+ artifacts: previous.artifacts,
95
+ }),
96
+ issuedAt: now.toISOString(),
97
+ expiresAt: new Date(now.getTime() + 3600000).toISOString(),
98
+ });
99
+ const body = {
100
+ schema: "buildchain.pipeline-publication-qualification/v1",
101
+ planRoot: plan.root,
102
+ source: previous.source,
103
+ artifacts: previous.artifacts,
104
+ build,
105
+ qualification,
106
+ predecessorRoot: previous.root,
107
+ };
108
+ const qualified = { ...body, root: recordDigest(body) };
109
+ const capsules = requalifyPublicationCapsules(
110
+ prepared.capsules,
111
+ qualified,
112
+ plan,
113
+ );
114
+ const { root, ...sealedBody } = prepared.sealed;
115
+ const sealedValue = {
116
+ ...sealedBody,
117
+ qualificationRoot: qualified.root,
118
+ predecessorRoot: root,
119
+ };
120
+ const sealed = { ...sealedValue, root: recordDigest(sealedValue) };
121
+ return {
122
+ schema: "buildchain.pipeline-qualification-preparation/v1",
123
+ contextRoot: recordDigest(context),
124
+ qualified,
125
+ capsules,
126
+ sealed,
127
+ };
128
+ }
@@ -0,0 +1,56 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ export async function observeRecoveryPublicationEffects({
4
+ effects,
5
+ receipts,
6
+ retained,
7
+ provider,
8
+ fence,
9
+ }) {
10
+ const transactionRoot = retained.documents.transaction.transactionRoot;
11
+ const observations = [];
12
+ for (const effect of effects) {
13
+ const { root, ...body } = effect;
14
+ if (root !== recordDigest(body))
15
+ throw new Error(
16
+ "Recovery publication effect changed its immutable declaration",
17
+ );
18
+ const prior = receipts.filter((receipt) => receipt.effectId === effect.id);
19
+ for (const receipt of prior) {
20
+ const { root: receiptRoot, ...value } = receipt;
21
+ if (
22
+ receiptRoot !== recordDigest(value) ||
23
+ value.schema !== "buildchain.pipeline-publication-effect/v1" ||
24
+ receipt.transactionRoot !== transactionRoot ||
25
+ receipt.effectRoot !== effect.root
26
+ )
27
+ throw new Error(
28
+ "Recovery cannot rewrite a prior publication receipt or transaction",
29
+ );
30
+ }
31
+ await fence();
32
+ const observed = await provider.observe(effect);
33
+ const matched = provider.matches(effect, observed);
34
+ if (
35
+ (!matched && prior.some((item) => item.state === "success")) ||
36
+ (observed.state !== "absent" &&
37
+ !matched &&
38
+ !provider.canApply?.(effect, observed))
39
+ )
40
+ throw new Error(
41
+ "Recovery publication destination conflicts with retained bytes",
42
+ );
43
+ observations.push({ effect, observed, matched });
44
+ }
45
+ const body = {
46
+ schema: "buildchain.pipeline-recovery-publication-readback/v1",
47
+ transactionRoot,
48
+ qualificationRoot: retained.qualified.root,
49
+ signingRoot: recordDigest(retained.signing),
50
+ sealedRoot: retained.sealed.root,
51
+ effectsRoot: recordDigest(effects),
52
+ receiptRoots: receipts.map((receipt) => receipt.root),
53
+ observations,
54
+ };
55
+ return { ...body, root: recordDigest(body) };
56
+ }
@@ -0,0 +1,58 @@
1
+ import path from "node:path";
2
+ import { uniquePublicationMaterial } from "./context.js";
3
+ import { requalifySealedPublication } from "./recovery-qualification.js";
4
+ import { preparePipelineSigning } from "./signing.js";
5
+
6
+ async function originalMaterial(journal, prefix, accepts) {
7
+ const matches = (await journal.materials(prefix)).filter(accepts);
8
+ if (matches.length !== 1)
9
+ throw new Error(
10
+ `Signing recovery has no unique original material: ${prefix}`,
11
+ );
12
+ return matches[0];
13
+ }
14
+
15
+ export async function prepareRecoveredSigning(
16
+ context,
17
+ host,
18
+ journal,
19
+ archive,
20
+ directory,
21
+ ) {
22
+ const prepared = await uniquePublicationMaterial(
23
+ journal,
24
+ "publication/predecessor-prepared/",
25
+ );
26
+ const originalPlan = await originalMaterial(
27
+ journal,
28
+ "publication/predecessor-plan/",
29
+ (plan) => plan.root === prepared.qualified.planRoot,
30
+ );
31
+ const originalMaterialization = await originalMaterial(
32
+ journal,
33
+ "publication/predecessor-materialization/",
34
+ (value) =>
35
+ value.planRoot === originalPlan.root &&
36
+ value.source.commit === prepared.qualified.source.commit,
37
+ );
38
+ const result = await requalifySealedPublication({
39
+ context,
40
+ prepared,
41
+ originalPlan,
42
+ originalMaterialization,
43
+ archive,
44
+ host,
45
+ directory,
46
+ });
47
+ await journal.fence();
48
+ await journal.record(
49
+ `publication/qualification-prepared/${context.runId}-${context.runAttempt}`,
50
+ result,
51
+ );
52
+ return preparePipelineSigning({
53
+ plan: context.plan,
54
+ materialization: context.materialization,
55
+ qualified: result.qualified,
56
+ directory: path.join(directory, "signing"),
57
+ });
58
+ }
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import {
6
+ publicationFile,
7
+ publicationPath,
8
+ writeImmutablePublicationFile,
9
+ } from "./files.js";
10
+
11
+ export async function retainPipelineProducts(archive, qualified, bundles) {
12
+ const retained = [];
13
+ for (const artifact of qualified.artifacts) {
14
+ const bundle = bundles.find(
15
+ ({ manifest }) => manifest.root === artifact.manifestRoot,
16
+ );
17
+ if (!bundle)
18
+ throw new Error("Qualified product has no exact source manifest");
19
+ const file = publicationFile(
20
+ publicationPath(bundle.directory, artifact.file),
21
+ );
22
+ if (file.digest !== artifact.digest || file.size !== artifact.size)
23
+ throw new Error("Qualified product changed before durable retention");
24
+ const handle = await archive.put(file.bytes, {
25
+ name: `${artifact.digest.slice(7)}-${path.basename(artifact.file)}`,
26
+ mediaType: "application/octet-stream",
27
+ });
28
+ await verifiedBytes(archive, handle, artifact);
29
+ retained.push({ id: artifact.id, handle });
30
+ }
31
+ const body = {
32
+ schema: "buildchain.pipeline-sealed-products/v1",
33
+ qualificationRoot: qualified.root,
34
+ products: retained,
35
+ };
36
+ return { ...body, root: recordDigest(body) };
37
+ }
38
+
39
+ async function verifiedBytes(archive, handle, artifact) {
40
+ if (handle.digest !== artifact.digest || handle.size !== artifact.size)
41
+ throw new Error("Retained product handle does not bind qualified bytes");
42
+ const bytes = Buffer.from(await archive.read(handle));
43
+ if (
44
+ bytes.length !== artifact.size ||
45
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
46
+ artifact.digest
47
+ )
48
+ throw new Error("Retained product readback differs from qualified bytes");
49
+ return bytes;
50
+ }
51
+
52
+ export async function restorePipelineProducts(
53
+ archive,
54
+ qualified,
55
+ sealed,
56
+ directory,
57
+ ) {
58
+ const { root, ...body } = sealed;
59
+ if (
60
+ root !== recordDigest(body) ||
61
+ sealed.qualificationRoot !== qualified.root ||
62
+ recordDigest(sealed.products.map(({ id }) => id)) !==
63
+ recordDigest(qualified.artifacts.map(({ id }) => id))
64
+ )
65
+ throw new Error(
66
+ "Retained products must cover the exact qualification inventory",
67
+ );
68
+ fs.mkdirSync(directory, { recursive: true });
69
+ for (const artifact of qualified.artifacts) {
70
+ const { handle } = sealed.products.find(({ id }) => id === artifact.id);
71
+ const bytes = await verifiedBytes(archive, handle, artifact);
72
+ const target = path.resolve(directory, artifact.file);
73
+ if (!target.startsWith(`${path.resolve(directory)}${path.sep}`))
74
+ throw new Error("Retained product path escapes its directory");
75
+ writeImmutablePublicationFile(target, bytes);
76
+ publicationPath(directory, artifact.file);
77
+ }
78
+ return directory;
79
+ }
@@ -0,0 +1,42 @@
1
+ import fs from "node:fs";
2
+ import { publicationContext, uniquePublicationMaterial } from "./context.js";
3
+ import { distributePipelineProducts } from "./distribution.js";
4
+ import { nextPipelineDevelopment } from "./next-development.js";
5
+
6
+ export async function settlePipelinePublication(
7
+ context,
8
+ host,
9
+ directory,
10
+ environment,
11
+ ) {
12
+ const { session, journal } = await publicationContext(context, host);
13
+ if (session.observed.phases.publish?.payload.state !== "success")
14
+ throw new Error(
15
+ "Publication must retain success before distribution and next-development",
16
+ );
17
+ fs.mkdirSync(directory, { recursive: true });
18
+ const retained = await uniquePublicationMaterial(
19
+ journal,
20
+ "publication/qualified/",
21
+ );
22
+ if (session.observed.phases.distribution?.payload.state !== "success") {
23
+ const receipt = await distributePipelineProducts(
24
+ context,
25
+ host,
26
+ journal,
27
+ retained,
28
+ environment,
29
+ );
30
+ await journal.record("publication/distribution-complete", receipt, {
31
+ phase: "distribution",
32
+ state: "success",
33
+ });
34
+ }
35
+ const next = await nextPipelineDevelopment(context, host, journal);
36
+ await journal.record("publication/next-development-result", next, {
37
+ phase: "next-development",
38
+ state: next.state,
39
+ });
40
+ await host.project(session);
41
+ return next;
42
+ }
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
5
+ import {
6
+ canonicalJson,
7
+ recordDigest,
8
+ } from "../../release/discussion/envelope.js";
9
+ import { verifyPipelineQualification } from "./documents.js";
10
+ import { writeImmutablePublicationFile } from "./files.js";
11
+
12
+ export const PIPELINE_PRODUCT_PREDICATE =
13
+ "https://buildchain.libkungfu.dev/attestations/pipeline-products/v1";
14
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+
16
+ export function preparePipelineSigning({
17
+ plan,
18
+ materialization,
19
+ qualified,
20
+ directory,
21
+ evaluatedAt,
22
+ }) {
23
+ verifyPipelineQualification({
24
+ plan,
25
+ materialization,
26
+ qualified,
27
+ evaluatedAt,
28
+ });
29
+ const predicate = {
30
+ schema: "buildchain.pipeline-product-attestation/v1",
31
+ planRoot: plan.root,
32
+ materializationRoot: materialization.root,
33
+ qualificationRoot: qualified.root,
34
+ source: qualified.source,
35
+ protectedSource: plan.source,
36
+ publisher: plan.publisher,
37
+ runtime: plan.runtime,
38
+ provider: {
39
+ runId: qualified.build.runId,
40
+ runAttempt: qualified.build.runAttempt,
41
+ source: qualified.build.providerSource,
42
+ },
43
+ };
44
+ const subjectBytes = Buffer.from(`${canonicalJson(qualified)}\n`);
45
+ const subjectPath = writeImmutablePublicationFile(
46
+ path.join(directory, "qualified-products.json"),
47
+ subjectBytes,
48
+ );
49
+ const predicatePath = writeImmutablePublicationFile(
50
+ path.join(directory, "predicate.json"),
51
+ `${canonicalJson(predicate)}\n`,
52
+ );
53
+ return {
54
+ subjectPath,
55
+ predicatePath,
56
+ predicate,
57
+ subjectDigest: digest(subjectBytes),
58
+ predicateType: PIPELINE_PRODUCT_PREDICATE,
59
+ };
60
+ }
61
+
62
+ export function verifyPipelineSigning({
63
+ plan,
64
+ materialization,
65
+ qualified,
66
+ bundlePath,
67
+ directory,
68
+ token,
69
+ execute = execFileSync,
70
+ evaluatedAt,
71
+ }) {
72
+ const prepared = preparePipelineSigning({
73
+ plan,
74
+ materialization,
75
+ qualified,
76
+ directory,
77
+ evaluatedAt,
78
+ });
79
+ const output = execute(
80
+ "gh",
81
+ [
82
+ "attestation",
83
+ "verify",
84
+ prepared.subjectPath,
85
+ "--repo",
86
+ qualified.source.repository,
87
+ "--signer-workflow",
88
+ `${plan.publisher.repository}/${plan.publisher.workflow}`,
89
+ "--signer-digest",
90
+ plan.publisher.workflowSha,
91
+ "--source-digest",
92
+ qualified.build.providerSource,
93
+ "--predicate-type",
94
+ PIPELINE_PRODUCT_PREDICATE,
95
+ "--bundle",
96
+ bundlePath,
97
+ "--deny-self-hosted-runners",
98
+ "--format",
99
+ "json",
100
+ ],
101
+ {
102
+ encoding: "utf8",
103
+ stdio: ["ignore", "pipe", "pipe"],
104
+ maxBuffer: 16 * 1024 * 1024,
105
+ env: { ...process.env, GH_TOKEN: token },
106
+ },
107
+ );
108
+ const results = JSON.parse(output);
109
+ const statements = Array.isArray(results)
110
+ ? results
111
+ .map((result) => result.verificationResult?.statement)
112
+ .filter(Boolean)
113
+ : [];
114
+ if (
115
+ statements.length !== 1 ||
116
+ statements[0].predicateType !== prepared.predicateType ||
117
+ recordDigest(statements[0].predicate) !==
118
+ recordDigest(prepared.predicate) ||
119
+ statements[0].subject?.length !== 1 ||
120
+ statements[0].subject[0].digest?.sha256 !== prepared.subjectDigest
121
+ )
122
+ throw new Error(
123
+ "Cryptographically verified attestation does not bind the exact publication predicate and subject",
124
+ );
125
+ if (digest(fs.readFileSync(prepared.subjectPath)) !== prepared.subjectDigest)
126
+ throw new Error("Signed publication subject changed during verification");
127
+ const bundleBytes = fs.readFileSync(bundlePath);
128
+ return {
129
+ schema: "buildchain.pipeline-product-signing/v1",
130
+ verified: true,
131
+ planRoot: plan.root,
132
+ materializationRoot: materialization.root,
133
+ qualificationRoot: qualified.root,
134
+ subjectDigest: `sha256:${prepared.subjectDigest}`,
135
+ predicateRoot: recordDigest(prepared.predicate),
136
+ bundleDigest: `sha256:${digest(bundleBytes)}`,
137
+ signer: plan.publisher,
138
+ providerSource: qualified.build.providerSource,
139
+ };
140
+ }
@@ -0,0 +1,83 @@
1
+ import { planPipelinePublication } from "./plan.js";
2
+
3
+ export async function createPipelinePublicationPlan(
4
+ session,
5
+ publisherSha,
6
+ host,
7
+ version,
8
+ ) {
9
+ const observed = session.observed;
10
+ const attempt = observed.attempt;
11
+ const integration = await host.integration.observe({
12
+ ...observed.history.at(-1),
13
+ intent: session.intent,
14
+ });
15
+ const source = await host.source.source(
16
+ integration.mergeCommit,
17
+ observed.history.at(-1).generation.source.configPath,
18
+ );
19
+ const route = source.plan.channels.find(
20
+ (entry) =>
21
+ entry.to === session.intent.source.targetBranch &&
22
+ entry.operation !== "develop",
23
+ );
24
+ if (!route)
25
+ throw new Error(
26
+ "Protected source does not declare this publication channel",
27
+ );
28
+ const input = await version.inspect(source.identity, source.plan.version);
29
+ const runtimeCommit = await host.request(
30
+ `/repos/${host.runtime.repository}/git/commits/${host.runtime.sha}`,
31
+ );
32
+ if (
33
+ runtimeCommit.sha !== host.runtime.sha ||
34
+ !/^[0-9a-f]{40}$/u.test(runtimeCommit.tree?.sha || "")
35
+ )
36
+ throw new Error("Publication runtime has no exact source tree");
37
+ const definition = {
38
+ repository: "kungfu-systems/buildchain",
39
+ workflow: ".github/workflows/.release-pipeline-products.yml",
40
+ workflowSha: publisherSha,
41
+ job: "apply",
42
+ };
43
+ const parameters = {
44
+ attempt,
45
+ generation: observed.generation,
46
+ source: source.identity,
47
+ intentSource: observed.history.at(-1).generation.source,
48
+ runtime: {
49
+ repository: host.runtime.repository,
50
+ commit: runtimeCommit.sha,
51
+ tree: runtimeCommit.tree.sha,
52
+ },
53
+ publisher: definition,
54
+ contract: source.plan,
55
+ route,
56
+ version: input.version,
57
+ sourceTimestamp: input.sourceTimestamp,
58
+ };
59
+ let plan = planPipelinePublication(parameters);
60
+ const tag = await host.request(
61
+ `/repos/${host.repository}/git/ref/tags/${encodeURIComponent(plan.tag)}`,
62
+ { allow404: true },
63
+ );
64
+ const channel = `v${plan.version.split(".")[0]}${plan.channel === "alpha" ? "-alpha" : ""}`;
65
+ const previous = await host.request(
66
+ `/repos/${host.repository}/git/ref/tags/${channel}`,
67
+ { allow404: true },
68
+ );
69
+ if (
70
+ previous &&
71
+ (previous.object?.type !== "commit" ||
72
+ !/^[0-9a-f]{40}$/u.test(previous.object.sha))
73
+ )
74
+ throw new Error(
75
+ "Publication channel requires an exact lightweight commit ref",
76
+ );
77
+ plan = planPipelinePublication({
78
+ ...parameters,
79
+ expectedTagSha: tag?.object?.sha || null,
80
+ previousChannelCommit: previous?.object?.sha || null,
81
+ });
82
+ return plan;
83
+ }