@kungfu-tech/buildchain 4.1.3-alpha.1 → 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 (263) 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 +266 -10
  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 +362 -7
  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 +328 -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 +4 -4
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/dist/readers/business-attempt.cjs +809 -0
  26. package/dist/site/buildchain-contract.json +16 -68
  27. package/dist/site/buildchain-site.json +40 -25
  28. package/dist/site/capability-registry.json +4 -4
  29. package/dist/site/controller-registry.json +1 -47
  30. package/dist/site/kfd-claims.json +499 -494
  31. package/dist/site/kfd-upstream-aggregate.json +1 -1
  32. package/dist/site/manual-registry.json +3 -3
  33. package/dist/site/node-api-registry.json +54 -54
  34. package/dist/site/page-registry.json +33 -18
  35. package/dist/site/public-surface-audit.json +286 -349
  36. package/dist/site/publication-authority-registry.json +203 -523
  37. package/dist/site/publication-registry.json +4 -4
  38. package/dist/site/site-manifest.json +7 -7
  39. package/dist/site/workflow-registry.json +590 -940
  40. package/docs/code-organization.md +9 -6
  41. package/docs/node-api-reference.md +102 -102
  42. package/docs/release-governance.md +10 -12
  43. package/docs/runtime-entry.md +73 -5
  44. package/docs/runtime-train-validation.md +39 -11
  45. package/docs/stable-candidate-patrol.md +6 -5
  46. package/docs/workflow-catalog.md +9 -32
  47. package/package.json +4 -3
  48. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  49. package/packages/core/build/standalone/build.js +7 -1
  50. package/packages/core/consumer/buildchain-config.js +2 -21
  51. package/packages/core/consumer/contract/entries.js +85 -0
  52. package/packages/core/consumer/contract/examples.js +96 -0
  53. package/packages/core/consumer/contract/identity.js +65 -0
  54. package/packages/core/consumer/contract/inspection.js +45 -0
  55. package/packages/core/consumer/contract/plan.js +149 -0
  56. package/packages/core/consumer/contract/products.js +119 -0
  57. package/packages/core/consumer/contract/reader.js +47 -0
  58. package/packages/core/consumer/contract/shape.js +46 -0
  59. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  60. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  61. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  62. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  63. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  64. package/packages/core/dev-delivery/native/actions.js +9 -1
  65. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  66. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  67. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  68. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  69. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  70. package/packages/core/dev-delivery/warrant/service.js +7 -0
  71. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  72. package/packages/core/governance/github-governance-authority.js +1 -0
  73. package/packages/core/observability/controller-evidence.js +0 -8
  74. package/packages/core/paper/operations/bootstrap.js +2 -0
  75. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  76. package/packages/core/providers/github/attempt-index.js +60 -0
  77. package/packages/core/providers/github/attempt-journal.js +154 -0
  78. package/packages/core/providers/github/discussions/materials.js +19 -6
  79. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  80. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  81. package/packages/core/providers/github/pipeline-events.js +131 -0
  82. package/packages/core/providers/github/pipeline-integration.js +95 -0
  83. package/packages/core/providers/github/pipeline-policy.js +240 -0
  84. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  85. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  86. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  87. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  88. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  89. package/packages/core/providers/github/pipeline-runs.js +132 -0
  90. package/packages/core/providers/github/pipeline-source.js +195 -0
  91. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  92. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  93. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  94. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  95. package/packages/core/providers/github/pipeline-version.js +199 -0
  96. package/packages/core/providers/github/pipeline-worker.js +108 -0
  97. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  98. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  99. package/packages/core/publication/npm/pack-preview.js +2 -2
  100. package/packages/core/publication/npm/pack-result.js +31 -0
  101. package/packages/core/publication/npm/package.js +3 -6
  102. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  103. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  104. package/packages/core/publication/npm/registry.js +12 -9
  105. package/packages/core/publication/pipeline/actions.js +116 -0
  106. package/packages/core/publication/pipeline/apply.js +189 -0
  107. package/packages/core/publication/pipeline/build-segments.js +107 -0
  108. package/packages/core/publication/pipeline/capsules.js +92 -0
  109. package/packages/core/publication/pipeline/context.js +39 -0
  110. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  111. package/packages/core/publication/pipeline/development-current.js +63 -0
  112. package/packages/core/publication/pipeline/development-pr.js +112 -0
  113. package/packages/core/publication/pipeline/development-proof.js +76 -0
  114. package/packages/core/publication/pipeline/development-transition.js +112 -0
  115. package/packages/core/publication/pipeline/distribution.js +130 -0
  116. package/packages/core/publication/pipeline/documents.js +192 -0
  117. package/packages/core/publication/pipeline/effects.js +192 -0
  118. package/packages/core/publication/pipeline/files.js +66 -0
  119. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  120. package/packages/core/publication/pipeline/journal.js +122 -0
  121. package/packages/core/publication/pipeline/next-development.js +117 -0
  122. package/packages/core/publication/pipeline/pack.js +180 -0
  123. package/packages/core/publication/pipeline/package-policy.js +22 -0
  124. package/packages/core/publication/pipeline/plan.js +136 -0
  125. package/packages/core/publication/pipeline/prepare.js +139 -0
  126. package/packages/core/publication/pipeline/qualification.js +175 -0
  127. package/packages/core/publication/pipeline/qualify.js +104 -0
  128. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  129. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  130. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  131. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  132. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  133. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  135. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  136. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  137. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  138. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  139. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  140. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  141. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  142. package/packages/core/publication/pipeline/settle.js +42 -0
  143. package/packages/core/publication/pipeline/signing.js +140 -0
  144. package/packages/core/publication/pipeline/source-plan.js +83 -0
  145. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  146. package/packages/core/publication/pipeline/stable-products.js +317 -0
  147. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  148. package/packages/core/publication/pipeline/stable.js +82 -0
  149. package/packages/core/publication/pipeline/version-actions.js +93 -0
  150. package/packages/core/publication/pipeline/version-build.js +130 -0
  151. package/packages/core/publication/pipeline/version-context.js +143 -0
  152. package/packages/core/publication/pipeline/version-material.js +36 -0
  153. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  154. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  155. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  156. package/packages/core/publication/pipeline/version.js +81 -0
  157. package/packages/core/publication/pipeline/worker.js +72 -0
  158. package/packages/core/publication/publication-reproducibility.js +2 -2
  159. package/packages/core/release/next-development-transition.js +15 -15
  160. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  161. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  162. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  163. package/packages/core/release/stable-release-gate.js +11 -0
  164. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  165. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  166. package/packages/core/runtime/entry/actions.js +27 -25
  167. package/packages/core/runtime/entry/attempt.js +25 -0
  168. package/packages/core/runtime/entry/selection.js +14 -0
  169. package/packages/core/runtime/entry/source.js +62 -0
  170. package/packages/core/workflow/attempt/identity.js +170 -0
  171. package/packages/core/workflow/attempt/journal.js +89 -0
  172. package/packages/core/workflow/attempt/materials.js +72 -0
  173. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  174. package/packages/core/workflow/attempt/reader.js +149 -0
  175. package/packages/core/workflow/attempt/records.js +153 -0
  176. package/packages/core/workflow/attempt/store.js +75 -0
  177. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  178. package/packages/core/workflow/pipeline/action-output.js +11 -0
  179. package/packages/core/workflow/pipeline/actions.js +52 -0
  180. package/packages/core/workflow/pipeline/build-control.js +140 -0
  181. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  182. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  183. package/packages/core/workflow/pipeline/build-result.js +78 -0
  184. package/packages/core/workflow/pipeline/build.js +142 -0
  185. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  186. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  187. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  188. package/packages/core/workflow/pipeline/controller.js +141 -0
  189. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  190. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  191. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  192. package/packages/core/workflow/pipeline/events.js +84 -0
  193. package/packages/core/workflow/pipeline/fence.js +42 -0
  194. package/packages/core/workflow/pipeline/group-control.js +59 -0
  195. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  196. package/packages/core/workflow/pipeline/guard.js +158 -0
  197. package/packages/core/workflow/pipeline/host.js +144 -0
  198. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  199. package/packages/core/workflow/pipeline/materials.js +58 -0
  200. package/packages/core/workflow/pipeline/notifications.js +36 -0
  201. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  202. package/packages/core/workflow/pipeline/platforms.js +21 -0
  203. package/packages/core/workflow/pipeline/progress.js +108 -0
  204. package/packages/core/workflow/pipeline/projection.js +20 -0
  205. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  206. package/packages/core/workflow/pipeline/record-action.js +27 -0
  207. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  208. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  209. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  210. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  211. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  212. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  213. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  214. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  215. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  216. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  217. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  218. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  219. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  220. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  221. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  222. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  223. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  224. package/packages/core/workflow/pipeline/selection.js +111 -0
  225. package/packages/core/workflow/pipeline/session.js +72 -0
  226. package/packages/core/workflow/pipeline/settlement.js +137 -0
  227. package/packages/core/workflow/pipeline/wake.js +38 -0
  228. package/packages/core/workflow/pipeline/web-status.js +108 -0
  229. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  230. package/scripts/build-release-discussion-reader.mjs +11 -7
  231. package/scripts/check-inventory.mjs +7 -70
  232. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  233. package/scripts/check-public-dogfood-contract.mjs +38 -51
  234. package/scripts/check-release-topology.mjs +15 -8
  235. package/scripts/check-self-consumer-contract.mjs +133 -0
  236. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  237. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  238. package/scripts/generate-site-bundle.mjs +1 -1
  239. package/scripts/inventory/binary.mjs +9 -19
  240. package/scripts/inventory/promotion.mjs +2 -13
  241. package/scripts/prepare-product-toolchain.mjs +10 -0
  242. package/scripts/verify-golden-path.mjs +5 -1
  243. package/scripts/verify-product-platform.mjs +65 -0
  244. package/scripts/verify-standalone-product.mjs +42 -0
  245. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  246. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  247. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  248. package/templates/minimal-consumer/binary/package.json +6 -0
  249. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  250. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  251. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  252. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  253. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  254. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  255. package/templates/minimal-consumer/npm/package.json +6 -0
  256. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  257. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  258. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  259. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  260. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  261. package/templates/minimal-consumer/paper/package.json +6 -0
  262. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  263. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,59 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { object } from "../../consumer/contract/shape.js";
3
+
4
+ export async function recordPipelineGroup(context, host) {
5
+ object(context, [
6
+ "schema",
7
+ "source",
8
+ "branch",
9
+ "baseCommit",
10
+ "runId",
11
+ "runAttempt",
12
+ "platforms",
13
+ ]);
14
+ if (
15
+ context.schema !== "buildchain.pipeline-group-build-context/v1" ||
16
+ context.runId !== host.runId ||
17
+ context.runAttempt !== host.runAttempt
18
+ )
19
+ throw new Error("Merge group callback execution drift");
20
+ const { run } = await host.runs.read(context.runId, context.runAttempt);
21
+ if (run.event !== "merge_group" || run.head_sha !== context.source.commit)
22
+ throw new Error("Merge group build is not the exact provider execution");
23
+ const queue = await host.queue.getMergeQueueState(context.branch);
24
+ if (
25
+ !queue.enabled ||
26
+ !queue.entries.some(
27
+ (entry) =>
28
+ entry.headSha === context.source.commit &&
29
+ entry.baseSha === context.baseCommit,
30
+ )
31
+ )
32
+ throw new Error("Merge group was removed or changed before verification");
33
+ const readback = await host.runs.build(
34
+ context.runId,
35
+ context.runAttempt,
36
+ context.source,
37
+ context.platforms.map((platform) => platform.platform),
38
+ );
39
+ if (readback.outcome === "success")
40
+ await host.qualifyChannel(context.source, context.branch);
41
+ await host.publishBuildStatus?.(context, readback);
42
+ await host.request(`/repos/${host.repository}/check-runs`, {
43
+ method: "POST",
44
+ body: {
45
+ name: "check",
46
+ head_sha: context.source.commit,
47
+ external_id: `buildchain:merge-group:${recordDigest(context)}`,
48
+ status: "completed",
49
+ conclusion: readback.outcome,
50
+ output: {
51
+ title: "Buildchain exact merge-group verification",
52
+ summary: `Provider product jobs: ${readback.root}`,
53
+ },
54
+ },
55
+ });
56
+ if (readback.outcome !== "success")
57
+ throw new Error("Merge-group products failed verification");
58
+ return readback;
59
+ }
@@ -0,0 +1,74 @@
1
+ import { githubPipelineRuns } from "../../providers/github/pipeline-runs.js";
2
+ import { readPipelineCaller } from "../../providers/github/pipeline-run-entry.js";
3
+ import { pipelineBuildEvidence } from "./build-evidence.js";
4
+ import { qualifyPipelineSourceRun } from "./delivery-request.js";
5
+
6
+ export async function guardPipelineBuild(
7
+ input,
8
+ observed,
9
+ {
10
+ material,
11
+ source,
12
+ request,
13
+ repository,
14
+ runs = githubPipelineRuns(request, repository),
15
+ },
16
+ ) {
17
+ const session = { observed, journal: { read: async () => observed } };
18
+ const build = await pipelineBuildEvidence(session, {
19
+ runId: 0,
20
+ runs,
21
+ source,
22
+ materialStore: () => material,
23
+ });
24
+ if (!build || build.run.id !== Number(input["source-workflow-run-id"]))
25
+ throw new Error(
26
+ "Delivery requires its independently qualified exact product build",
27
+ );
28
+ const current = { ...observed.history.at(-1), intent: observed.intent };
29
+ qualifyPipelineSourceRun(build.run, current, build.readback);
30
+ await readPipelineCaller(
31
+ build.run,
32
+ current.generation.source.configPath,
33
+ request,
34
+ repository,
35
+ );
36
+ return build;
37
+ }
38
+
39
+ // A commit status is source-bound and survives later workflow suites. Publish
40
+ // only the independently reread build at the protected landing boundary, whose
41
+ // existing Actions credential owns statuses:write. Retained build receipts
42
+ // preserve the original executions and their outcomes.
43
+ export async function publishPipelineBuildStatus(
44
+ input,
45
+ build,
46
+ request,
47
+ repository,
48
+ ) {
49
+ if (
50
+ build.readback.source.repository !== repository ||
51
+ build.readback.source.commit !== input["expected-head-sha"] ||
52
+ build.sourceHead !== input["expected-head-sha"] ||
53
+ build.run.id !== Number(input["source-workflow-run-id"]) ||
54
+ build.readback.runId !== build.run.id ||
55
+ build.readback.runAttempt !== build.run.run_attempt ||
56
+ build.outcome !== "success" ||
57
+ build.readback.outcome !== "success"
58
+ )
59
+ throw new Error(
60
+ "Landing status differs from its qualified exact-source build",
61
+ );
62
+ return request(`/repos/${repository}/statuses/${build.sourceHead}`, {
63
+ method: "POST",
64
+ body: {
65
+ context: "check",
66
+ state: "success",
67
+ description: `Qualified product build: ${build.readback.root}`.slice(
68
+ 0,
69
+ 140,
70
+ ),
71
+ target_url: `https://github.com/${repository}/actions/runs/${build.run.id}/attempts/${build.run.run_attempt}`,
72
+ },
73
+ });
74
+ }
@@ -0,0 +1,158 @@
1
+ import { githubJsonClient } from "../../providers/github/json-client.js";
2
+ import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
3
+ import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
4
+ import { readBusinessAttempt } from "../attempt/reader.js";
5
+ import { assertPipelineExecution } from "./fence.js";
6
+ import { recordDigest } from "../../release/discussion/envelope.js";
7
+ import { githubPipelineSource } from "../../providers/github/pipeline-source.js";
8
+ import { githubPipelinePolicy } from "../../providers/github/pipeline-policy.js";
9
+ import { getOctokit } from "@actions/github";
10
+ import { discussionMaterials } from "../../providers/github/discussions/materials.js";
11
+ import { pipelineMaterials } from "./materials.js";
12
+ import { guardPipelineBuild } from "./guard-build.js";
13
+
14
+ async function assertRetainedRequest(input, connection, observed) {
15
+ const current = observed.history.at(-1);
16
+ const event = [...current.events]
17
+ .reverse()
18
+ .find((entry) =>
19
+ entry.payload.materials.some((material) =>
20
+ material.id.startsWith("delivery/execution-"),
21
+ ),
22
+ );
23
+ const reference = event?.payload.materials.find((material) =>
24
+ material.id.startsWith("delivery/execution-"),
25
+ );
26
+ if (!reference)
27
+ throw new Error("Pipeline delivery has no admitted execution request");
28
+ const archive = discussionMaterials({
29
+ octokit: getOctokit(connection.token),
30
+ repository: connection.repository,
31
+ intentId: observed.intent.id,
32
+ });
33
+ const material = pipelineMaterials(archive, {
34
+ repository: connection.repository,
35
+ attempt: current.identity,
36
+ });
37
+ const execution = await material.read(reference);
38
+ validatePipelineExecutionRequest(execution, input, observed, {
39
+ runId: Number(process.env.GITHUB_RUN_ID),
40
+ runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
41
+ });
42
+ return material;
43
+ }
44
+
45
+ export function validatePipelineExecutionRequest(
46
+ execution,
47
+ input,
48
+ observed,
49
+ coordinates,
50
+ ) {
51
+ if (execution.schema !== "buildchain.pipeline-delivery-execution/v1")
52
+ throw new Error("Pipeline delivery request schema is unsupported");
53
+ if (
54
+ execution.attempt !== observed.attempt ||
55
+ execution.generation !== observed.generation ||
56
+ execution.runId !== coordinates.runId ||
57
+ execution.runAttempt !== coordinates.runAttempt
58
+ )
59
+ throw new Error(
60
+ "Pipeline delivery is not the exact admitted provider execution",
61
+ );
62
+ for (const [key, value] of Object.entries(execution.request))
63
+ if (
64
+ !Object.hasOwn(input, key) ||
65
+ recordDigest(input[key]) !== recordDigest(value)
66
+ )
67
+ throw new Error(
68
+ `Pipeline delivery changed its retained request field: ${key}`,
69
+ );
70
+ }
71
+
72
+ export async function guardPipelineAdmission(input, connection) {
73
+ const observed = await guardPipelineDelivery(input, connection);
74
+ if (!observed) return null;
75
+ const material = await assertRetainedRequest(input, connection, observed);
76
+ const request = githubJsonClient({
77
+ token: connection.token,
78
+ userAgent: "buildchain-pipeline-admission",
79
+ });
80
+ const current = { ...observed.history.at(-1), intent: observed.intent };
81
+ const source = githubPipelineSource(request, connection.repository);
82
+ const admission = await source.observeIntent(
83
+ observed.intent,
84
+ current.generation,
85
+ { "config-path": current.generation.source.configPath },
86
+ );
87
+ const { live } = admission;
88
+ if (
89
+ admission.cleanupOnly ||
90
+ !live.source ||
91
+ live.state !== "open" ||
92
+ live.draft ||
93
+ !live.ready ||
94
+ live.baseCommit !== current.generation.baseCommit ||
95
+ recordDigest(live.source) !== recordDigest(current.generation.source)
96
+ )
97
+ throw new Error(
98
+ "Pipeline source is no longer admitted for a delivery effect",
99
+ );
100
+ const policy = await githubPipelinePolicy(
101
+ request,
102
+ connection.repository,
103
+ ).observe(current, admission.protectedPlan.review);
104
+ if (!policy.review || !policy.checksPassing)
105
+ throw new Error(
106
+ "Pipeline protected review or required checks changed before delivery effect",
107
+ );
108
+ const build = await guardPipelineBuild(input, observed, {
109
+ material,
110
+ source,
111
+ request,
112
+ repository: connection.repository,
113
+ });
114
+ const fresh = await guardPipelineDelivery(input, connection);
115
+ await connection.publishBuildStatus?.(build);
116
+ return fresh;
117
+ }
118
+
119
+ export async function guardPipelineDelivery(input, connection, lookup) {
120
+ const attempt = input["pipeline-attempt"];
121
+ // Existing advanced delivery callers retain their domain authority. The new
122
+ // normal entry always supplies its internally selected business attempt.
123
+ if (!attempt) return null;
124
+ if (!connection.token)
125
+ throw new Error("Pipeline controller requires its scoped read credential");
126
+ const request = githubJsonClient({
127
+ token: connection.token,
128
+ userAgent: "buildchain-pipeline",
129
+ });
130
+ const index =
131
+ lookup ||
132
+ githubAttemptIndex(
133
+ request,
134
+ githubAttemptJournal(request),
135
+ connection.repository,
136
+ );
137
+ const loaded = await index.resolve(attempt);
138
+ const observed = readBusinessAttempt(loaded.snapshot);
139
+ const current = observed.history.at(-1);
140
+ if (
141
+ !current ||
142
+ observed.intent.repository !== connection.repository ||
143
+ observed.intent.source.targetBranch !== input["target-branch"] ||
144
+ observed.intent.source.pullRequest !==
145
+ Number(input["expected-pr-number"]) ||
146
+ current.generation.source.commit !== input["expected-head-sha"]
147
+ )
148
+ throw new Error(
149
+ "Pipeline delivery request changed its admitted source intent",
150
+ );
151
+ assertPipelineExecution(observed, {
152
+ attempt,
153
+ intent: observed.intent.id,
154
+ generation: current.generation.id,
155
+ sourceRoot: recordDigest(current.generation.source),
156
+ });
157
+ return observed;
158
+ }
@@ -0,0 +1,144 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { getOctokit } from "@actions/github";
5
+ import { installationRoot } from "../../runtime/installation-root.js";
6
+ import { preparedRuntimeSelection } from "../../runtime/entry/selection.js";
7
+ import { githubJsonClient } from "../../providers/github/json-client.js";
8
+ import { githubAttemptJournal } from "../../providers/github/attempt-journal.js";
9
+ import { githubAttemptIndex } from "../../providers/github/attempt-index.js";
10
+ import { githubPipelineSource } from "../../providers/github/pipeline-source.js";
11
+ import { githubPipelineRuns } from "../../providers/github/pipeline-runs.js";
12
+ import { githubPipelineWorker } from "../../providers/github/pipeline-worker.js";
13
+ import { githubPipelinePolicy } from "../../providers/github/pipeline-policy.js";
14
+ import { discussionMaterials } from "../../providers/github/discussions/materials.js";
15
+ import { pipelineMaterials } from "./materials.js";
16
+ import { GitHubClient } from "../../dev-delivery/admission/github-client.js";
17
+ import { githubPipelineIntegration } from "../../providers/github/pipeline-integration.js";
18
+ import { recordPipelineGroup } from "./group-control.js";
19
+ import { controlPipelineChannel } from "./channel-control.js";
20
+ import { qualifyPipelineChannelSource } from "./channel-source.js";
21
+ import { settlePipeline } from "./settlement.js";
22
+ import { pipelineProjection } from "./projection.js";
23
+ import { resumePipelineNotifications } from "./notifications.js";
24
+ import { projectPipelineStatus } from "./web-status.js";
25
+ import { inspectRecoveryPublication } from "../../publication/pipeline/recovery-inspection.js";
26
+
27
+ export function pipelineQueue({ queueToken, ...options }) {
28
+ const reader = new GitHubClient(options);
29
+ return {
30
+ getMergeQueueState: (branch) => reader.getMergeQueueState(branch),
31
+ enqueuePullRequest: async (input) => {
32
+ // GITHUB_TOKEN enqueue suppresses the merge_group workflow it requires.
33
+ if (!queueToken || queueToken === options.token)
34
+ throw new Error(
35
+ "Channel enqueue requires a distinct App or automation credential",
36
+ );
37
+ return new GitHubClient({
38
+ ...options,
39
+ token: queueToken,
40
+ }).enqueuePullRequest(input);
41
+ },
42
+ };
43
+ }
44
+
45
+ export async function pipelineHost(core, env, jobName) {
46
+ const token = core.getInput("token", { required: true });
47
+ const repository = env.GITHUB_REPOSITORY;
48
+ const selection = preparedRuntimeSelection(
49
+ JSON.parse(env.BUILDCHAIN_RUNTIME_SELECTION),
50
+ );
51
+ const runtimeRoot = installationRoot(import.meta.url);
52
+ const reader = fs.readFileSync(
53
+ path.join(runtimeRoot, "dist/readers/business-attempt.cjs"),
54
+ );
55
+ const runtime = {
56
+ repository: selection.repository,
57
+ sha: selection.sha,
58
+ readerDigest: `sha256:${createHash("sha256").update(reader).digest("hex")}`,
59
+ };
60
+ const request = githubJsonClient({ token, userAgent: "buildchain-pipeline" });
61
+ const github = getOctokit(token);
62
+ const provider = githubAttemptJournal(request);
63
+ const index = githubAttemptIndex(request, provider, repository);
64
+ const runs = githubPipelineRuns(request, repository);
65
+ const runId = Number(env.GITHUB_RUN_ID),
66
+ runAttempt = Number(env.GITHUB_RUN_ATTEMPT);
67
+ const writer = await runs.writer(runId, runAttempt, jobName);
68
+ const source = githubPipelineSource(request, repository);
69
+ const [owner, repo] = repository.split("/");
70
+ const archives = new Map();
71
+ function archive(session) {
72
+ if (!archives.has(session.intent.id))
73
+ archives.set(
74
+ session.intent.id,
75
+ discussionMaterials({
76
+ octokit: github,
77
+ repository,
78
+ intentId: session.intent.id,
79
+ // Storage is partitioned by verified provider job, not attempt authority.
80
+ // Journal references still select exact immutable asset IDs and digests.
81
+ partition: `${writer.runId}-${writer.runAttempt}-${writer.jobId}`,
82
+ authorityDescription:
83
+ "The canonical Buildchain intent Git journal owns transaction state.",
84
+ }),
85
+ );
86
+ return archives.get(session.intent.id);
87
+ }
88
+ const host = {
89
+ repository,
90
+ token,
91
+ github,
92
+ selection,
93
+ runtime,
94
+ runtimeRoot,
95
+ request,
96
+ provider,
97
+ index,
98
+ runs,
99
+ runId,
100
+ runAttempt,
101
+ writer,
102
+ source,
103
+ workers: githubPipelineWorker(request),
104
+ policy: githubPipelinePolicy(request, repository),
105
+ queue: pipelineQueue({
106
+ repository: { owner, repo },
107
+ token,
108
+ queueToken: core.getInput("queue-token"),
109
+ }),
110
+ integration: githubPipelineIntegration(request, repository, source, runs),
111
+ groupRecord: (context) => recordPipelineGroup(context, host),
112
+ channel: (session, admission, inputs) =>
113
+ controlPipelineChannel(session, admission, inputs, host),
114
+ qualifyChannel: (source, branch) =>
115
+ qualifyPipelineChannelSource(source, branch, host),
116
+ settle: (session, fresh, delivery) =>
117
+ settlePipeline(session, fresh, delivery, host),
118
+ project: (session) =>
119
+ projectPipelineStatus(session, {
120
+ project: pipelineProjection(github.graphql),
121
+ core,
122
+ }),
123
+ notifyTerminal: (session) => resumePipelineNotifications(session, host),
124
+ recoverPublication: (session, admitted) =>
125
+ inspectRecoveryPublication(session, admitted, host),
126
+ materialStore: (session) =>
127
+ pipelineMaterials(archive(session), {
128
+ repository,
129
+ attempt: session.observed.history.at(-1).identity,
130
+ }),
131
+ productArchive: archive,
132
+ wake: async (attempt) => {
133
+ await index.resolve(attempt);
134
+ await request(`/repos/${repository}/dispatches`, {
135
+ method: "POST",
136
+ body: {
137
+ event_type: "buildchain-attempt-wake",
138
+ client_payload: { attempt },
139
+ },
140
+ });
141
+ },
142
+ };
143
+ return host;
144
+ }
@@ -0,0 +1,113 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ // The landing job already owns statuses:write. Keep its exact admission alive
4
+ // through the actual queue result so later channel PRs need not borrow the
5
+ // temporary queue branch's Actions check suite.
6
+ async function observeProtectedQueue(current, ports) {
7
+ const { repository, request, queue } = ports;
8
+ const now = ports.now || Date.now;
9
+ const pause =
10
+ ports.pause || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
11
+ const deadline = now() + 120 * 60 * 1000;
12
+ const number = current.intent.source.pullRequest;
13
+ const branch = current.intent.source.targetBranch;
14
+ let queueExitAt;
15
+ while (true) {
16
+ const pr = await request(`/repos/${repository}/pulls/${number}`);
17
+ if (
18
+ pr.head?.sha !== current.generation.source.commit ||
19
+ pr.head?.repo?.full_name !== repository ||
20
+ pr.base?.repo?.full_name !== repository ||
21
+ pr.base?.ref !== branch
22
+ )
23
+ throw new Error("Integrated status lost its exact admitted PR source");
24
+ if (pr.merged) return { pr, deadline, now, pause };
25
+ if (now() >= deadline)
26
+ throw new Error(
27
+ "Protected queue did not complete the admitted integration",
28
+ );
29
+ const pending = await queue(branch);
30
+ if (
31
+ pr.state !== "open" ||
32
+ !pending.enabled ||
33
+ !pending.entries.some(
34
+ (entry) =>
35
+ entry.pullRequestNumber === number &&
36
+ entry.pullRequestHeadSha === current.generation.source.commit,
37
+ )
38
+ ) {
39
+ // Queue removal can precede the PR endpoint's merged readback. Bound
40
+ // convergence without treating absence or a closed PR as merge evidence.
41
+ queueExitAt ??= now();
42
+ if (now() - queueExitAt >= 30_000)
43
+ throw new Error("Admitted PR left its protected merge queue");
44
+ } else queueExitAt = undefined;
45
+ await pause(10000);
46
+ }
47
+ }
48
+
49
+ async function observeIntegrationWorkflow(merged, ports) {
50
+ const { pr, deadline, now, pause } = merged;
51
+ if (!/^[0-9a-f]{40}$/u.test(pr.merge_commit_sha || ""))
52
+ throw new Error("Merged PR has no exact integration commit");
53
+ while (true) {
54
+ const result = await ports.request(
55
+ `/repos/${ports.repository}/actions/runs?event=merge_group&head_sha=${pr.merge_commit_sha}&per_page=100`,
56
+ );
57
+ if (!Array.isArray(result.workflow_runs) || result.total_count > 100)
58
+ throw new Error("Integration workflow inventory exceeds its bound");
59
+ const runs = result.workflow_runs.filter(
60
+ (run) =>
61
+ run.path?.split("@")[0] === ".github/workflows/buildchain.yml" &&
62
+ run.event === "merge_group" &&
63
+ run.head_sha === pr.merge_commit_sha,
64
+ );
65
+ if (
66
+ runs.some(
67
+ (run) => run.status === "completed" && run.conclusion === "success",
68
+ )
69
+ )
70
+ return;
71
+ if (!runs.some((run) => run.status !== "completed") || now() >= deadline)
72
+ throw new Error("Merged integration has no successful queue workflow");
73
+ await pause(10000);
74
+ }
75
+ }
76
+
77
+ function validateIntegratedStatus(proof, current, repository) {
78
+ const { root, ...body } = proof;
79
+ if (
80
+ recordDigest(body) !== root ||
81
+ proof.repository !== repository ||
82
+ proof.pullRequest !== current.intent.source.pullRequest ||
83
+ proof.sourceHead !== current.generation.source.commit ||
84
+ proof.branch !== current.intent.source.targetBranch ||
85
+ proof.build.source.commit !== proof.mergeCommit ||
86
+ proof.build.source.repository !== repository ||
87
+ proof.build.outcome !== "success" ||
88
+ proof.run.conclusion !== "success" ||
89
+ proof.run.status !== "completed" ||
90
+ proof.run.event !== "merge_group" ||
91
+ proof.run.head_sha !== proof.mergeCommit
92
+ )
93
+ throw new Error("Integrated status differs from its verified queue result");
94
+ }
95
+
96
+ export async function publishIntegratedPipelineStatus(current, ports) {
97
+ const merged = await observeProtectedQueue(current, ports);
98
+ await observeIntegrationWorkflow(merged, ports);
99
+ const { repository, integration, status } = ports;
100
+ const proof = await integration.observe(current);
101
+ validateIntegratedStatus(proof, current, repository);
102
+ const { root } = proof;
103
+ await status(`/repos/${repository}/statuses/${proof.mergeCommit}`, {
104
+ method: "POST",
105
+ body: {
106
+ context: "check",
107
+ state: "success",
108
+ description: `Qualified protected integration: ${root}`.slice(0, 140),
109
+ target_url: `https://github.com/${repository}/actions/runs/${proof.run.id}/attempts/${proof.run.run_attempt}`,
110
+ },
111
+ });
112
+ return proof;
113
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ canonicalJson,
3
+ recordDigest,
4
+ } from "../../release/discussion/envelope.js";
5
+ import {
6
+ materialReference,
7
+ verifyMaterialBytes,
8
+ } from "../attempt/materials.js";
9
+
10
+ // Receipts are retained before the atomic journal references them. An interrupted
11
+ // upload can leave unreferenced material; it cannot advance attempt authority.
12
+ export function pipelineMaterials(archive, { repository, attempt }) {
13
+ async function retain(id, receipt, kind = "receipt") {
14
+ const bytes = Buffer.from(`${canonicalJson(receipt)}\n`);
15
+ const handle = await archive.put(bytes, {
16
+ name: `${id.replaceAll("/", "-")}.json`,
17
+ mediaType: "application/json",
18
+ });
19
+ const reference = materialReference(
20
+ {
21
+ id,
22
+ kind,
23
+ digest: handle.digest,
24
+ bytes: handle.size,
25
+ url: `https://api.github.com/repos/${repository}/releases/assets/${handle.id}`,
26
+ producerAttempt: attempt.id,
27
+ generation: attempt.generation,
28
+ },
29
+ attempt,
30
+ repository,
31
+ );
32
+ verifyMaterialBytes(reference, await archive.read(handle));
33
+ return reference;
34
+ }
35
+ async function read(reference) {
36
+ materialReference(reference, attempt, repository);
37
+ const prefix = `https://api.github.com/repos/${repository}/releases/assets/`;
38
+ if (!reference.url.startsWith(prefix))
39
+ throw new Error(
40
+ "Pipeline receipt is not in its retained material archive",
41
+ );
42
+ const id = reference.url.slice(prefix.length);
43
+ if (!/^[1-9][0-9]*$/u.test(id) || !Number.isSafeInteger(Number(id)))
44
+ throw new Error("Pipeline receipt asset identity is invalid");
45
+ const bytes = await archive.read({
46
+ id: Number(id),
47
+ size: reference.bytes,
48
+ digest: reference.digest,
49
+ });
50
+ verifyMaterialBytes(reference, bytes);
51
+ const receipt = JSON.parse(bytes.toString("utf8"));
52
+ // Normalize the supported JSON value domain, rejecting undefined or NaN
53
+ // from alternate injected archive implementations before returning it.
54
+ recordDigest(receipt);
55
+ return receipt;
56
+ }
57
+ return { retain, read };
58
+ }
@@ -0,0 +1,36 @@
1
+ import { wakePipelineCandidate } from "./wake.js";
2
+ import { pipelineCandidate } from "./reconcile.js";
3
+ import { notifyPipelineParent } from "./parent-notification.js";
4
+
5
+ export async function resumePipelineNotifications(session, host) {
6
+ await notifyPipelineParent(session, host);
7
+ const observed = await session.journal.read();
8
+ const current = { ...observed.history.at(-1), intent: session.intent };
9
+ const terminal = Object.values(current.phases).filter((phase) =>
10
+ ["success", "cancelled", "failure"].includes(phase.payload.state),
11
+ );
12
+ for (const phase of terminal) {
13
+ for (const reference of phase.payload.materials.filter((material) =>
14
+ /^(?:merge\/settlement-|cancellation\/receipt-|failure\/)/u.test(
15
+ material.id,
16
+ ),
17
+ )) {
18
+ const receipt = await host.materialStore(session).read(reference);
19
+ const successor =
20
+ receipt.receipt?.successorWake ||
21
+ (Array.isArray(receipt.candidates)
22
+ ? pipelineCandidate(receipt, current)?.terminal?.successorWake
23
+ : null);
24
+ if (successor)
25
+ await wakePipelineCandidate(
26
+ successor,
27
+ {
28
+ repository: host.repository,
29
+ token: host.token,
30
+ branch: session.intent.source.targetBranch,
31
+ },
32
+ host,
33
+ );
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,27 @@
1
+ import { readBusinessAttempt } from "../attempt/reader.js";
2
+
3
+ export async function notifyPipelineParent(session, host) {
4
+ const pr = await host.request(
5
+ `/repos/${host.repository}/pulls/${session.intent.source.pullRequest}`,
6
+ );
7
+ if (!/^feature\/buildchain-next\/[0-9a-f]{64}$/u.test(pr.head?.ref || ""))
8
+ return;
9
+ const matches = [
10
+ ...String(pr.body || "").matchAll(
11
+ /^Buildchain-Parent-Attempt: (attempt-[0-9a-f]{64})$/gmu,
12
+ ),
13
+ ];
14
+ if (matches.length !== 1) return;
15
+ const attempt = matches[0][1];
16
+ const parent = await host.index.resolve(attempt);
17
+ const state = readBusinessAttempt(parent.snapshot);
18
+ if (
19
+ state.attempt !== attempt ||
20
+ state.status === "complete" ||
21
+ !state.intent.expectedNodes.includes("next-development")
22
+ )
23
+ return;
24
+ // The body is only a bounded wake selector. Current journal source and exact
25
+ // generated PR readback are re-admitted before any parent mutation.
26
+ await host.wake(attempt);
27
+ }