@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,75 @@
1
+ import { releaseDiscussionStore } from "../../release/discussion/store.js";
2
+ import { validateAttemptRecord } from "./records.js";
3
+ import { readBusinessAttempt } from "./reader.js";
4
+
5
+ // exclusive.run owns the repository/intent-wide writer scope, not merely this
6
+ // process. assertOwner rechecks the live provider fence before every mutation.
7
+ // No default mutex: an in-memory lock cannot serialize independent workflows.
8
+ export function businessAttemptStore(transport, exclusive, options = {}) {
9
+ if (typeof exclusive?.run !== "function")
10
+ throw new Error("Attempt writes require an exclusive provider writer");
11
+ const journal = releaseDiscussionStore(transport, options);
12
+ async function read(session) {
13
+ const observed = await journal.read(session);
14
+ return { ...observed, ...readBusinessAttempt(observed) };
15
+ }
16
+ async function scoped(intent, operation) {
17
+ return exclusive.run(
18
+ { repository: intent.repository, intent: intent.id },
19
+ async (owner) => {
20
+ if (typeof owner?.assertOwner !== "function")
21
+ throw new Error("Missing live writer fence");
22
+ await owner.assertOwner();
23
+ return operation(owner);
24
+ },
25
+ );
26
+ }
27
+ async function initialize(request) {
28
+ readBusinessAttempt({ intent: request.intent, records: [] });
29
+ return scoped(request.intent, async (owner) => {
30
+ const repository = await transport.repository(request.intent.repository);
31
+ if (repository.id !== request.intent.source.repositoryId)
32
+ throw new Error("Intent repository identity changed");
33
+ const fenced = { ...transport };
34
+ for (const name of ["create", "append"])
35
+ fenced[name] = async (...args) => {
36
+ await owner.assertOwner();
37
+ return transport[name](...args);
38
+ };
39
+ return releaseDiscussionStore(fenced, options).initialize(request);
40
+ });
41
+ }
42
+ async function append(session, record, expectedHead) {
43
+ return scoped(session.intent, async (owner) => {
44
+ validateAttemptRecord(record, session.intent);
45
+ const observed = await read(session);
46
+ const existing = observed.records.find((event) => event.id === record.id);
47
+ if (existing) return observed;
48
+ if ((observed.head || "") !== expectedHead)
49
+ throw new Error("Stale attempt head; reobserve before writing");
50
+ if (record.node === "attempt") {
51
+ if (record.predecessor !== (observed.attempt || ""))
52
+ throw new Error("Recovery must succeed the current attempt");
53
+ } else if (record.attempt !== observed.attempt)
54
+ throw new Error("A superseded attempt cannot append new results");
55
+ readBusinessAttempt({
56
+ intent: observed.intent,
57
+ records: [...observed.records, record],
58
+ });
59
+ await owner.assertOwner();
60
+ const fenced = {
61
+ ...transport,
62
+ append: async (...args) => {
63
+ await owner.assertOwner();
64
+ return transport.append(...args);
65
+ },
66
+ };
67
+ await releaseDiscussionStore(fenced, options).append(session, record);
68
+ const result = await read(session);
69
+ if (result.head !== record.id)
70
+ throw new Error("Attempt append readback has a conflicting writer");
71
+ return result;
72
+ });
73
+ }
74
+ return { initialize, read, append };
75
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { normalInputs } from "../../consumer/contract/entries.js";
5
+ import { compileConsumerPlan } from "../../consumer/contract/plan.js";
6
+ import { buildPipelineProducts } from "../pipeline/build.js";
7
+
8
+ const args = process.argv.slice(2);
9
+ if (
10
+ args.length !== 4 ||
11
+ args[0] !== "--platform" ||
12
+ args[2] !== "--config-path"
13
+ )
14
+ throw new Error(
15
+ "usage: pipeline-build --platform <platform> --config-path <TOML>",
16
+ );
17
+ const { configPath } = normalInputs({ "config-path": args[3] });
18
+ const cwd = fs.realpathSync(process.cwd());
19
+ const config = fs.realpathSync(path.join(cwd, configPath));
20
+ if (!config.startsWith(`${cwd}${path.sep}`))
21
+ throw new Error("Candidate TOML escapes its checkout");
22
+ const plan = compileConsumerPlan(fs.readFileSync(config, "utf8"));
23
+ await buildPipelineProducts({ cwd, plan, platform: args[1] });
@@ -0,0 +1,11 @@
1
+ export function pipelineActionOutputs(core, result) {
2
+ core.setOutput("operation", result.operation);
3
+ core.setOutput("context", JSON.stringify(result.context || {}));
4
+ core.setOutput(
5
+ "matrix",
6
+ JSON.stringify({ include: result.context?.platforms || [] }),
7
+ );
8
+ core.setOutput("request", JSON.stringify(result.request || {}));
9
+ core.setOutput("attempt", result.attempt || result.context?.attempt || "");
10
+ core.info(result.reason || result.operation);
11
+ }
@@ -0,0 +1,52 @@
1
+ import { pipelineActionOutputs } from "./action-output.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { normalInputs } from "../../consumer/contract/entries.js";
5
+ import { pipelineHost } from "./host.js";
6
+ import { controlPipeline } from "./controller.js";
7
+ import { buildPipelineSource } from "./build.js";
8
+ import { wakePipelineStableWait } from "../../publication/pipeline/stable-wait.js";
9
+
10
+ export async function controlPipelineAction(core, env) {
11
+ const { configPath } = normalInputs({
12
+ "config-path": core.getInput("config-path") || undefined,
13
+ });
14
+ const host = await pipelineHost(core, env, "Buildchain pipeline controller");
15
+ const result = await controlPipeline(
16
+ env.GITHUB_EVENT_NAME,
17
+ JSON.parse(fs.readFileSync(env.GITHUB_EVENT_PATH, "utf8")),
18
+ { "config-path": configPath },
19
+ host,
20
+ );
21
+ pipelineActionOutputs(core, result);
22
+ }
23
+
24
+ export async function buildPipelineAction(core, env) {
25
+ const context = JSON.parse(core.getInput("context", { required: true }));
26
+ const result = await buildPipelineSource({
27
+ cwd: path.join(env.GITHUB_WORKSPACE, ".buildchain/product"),
28
+ source: context.source,
29
+ platform: core.getInput("platform", { required: true }),
30
+ environment: env,
31
+ });
32
+ core.info(
33
+ `Verified ${result.products.length} products on ${result.platform}`,
34
+ );
35
+ }
36
+
37
+ export async function wakePipelineAction(core, env) {
38
+ const waiting = core.getInput("stable-wait");
39
+ const host = await pipelineHost(
40
+ core,
41
+ env,
42
+ waiting ? "Recheck stable publication" : "Record delivery attempt",
43
+ );
44
+ if (waiting) {
45
+ const wait = JSON.parse(waiting);
46
+ if (wait.attempt !== core.getInput("attempt", { required: true }))
47
+ throw new Error("Stable wake changed its exact business attempt");
48
+ core.info(JSON.stringify(await wakePipelineStableWait(wait, host)));
49
+ return;
50
+ }
51
+ await host.wake(core.getInput("attempt", { required: true }));
52
+ }
@@ -0,0 +1,140 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { object } from "../../consumer/contract/shape.js";
3
+ import { pipelinePlatforms } from "./platforms.js";
4
+ import { resumePipelineSession } from "./session.js";
5
+ import { retainPipelineBuildResult } from "./build-result.js";
6
+
7
+ export async function beginPipelineBuild(session, admission, host) {
8
+ const observed = await session.journal.read();
9
+ const attempt = observed.attempt;
10
+ const previous = observed.phases.build;
11
+ if (
12
+ previous &&
13
+ (previous.payload.writer.runId !== String(host.runId) ||
14
+ previous.payload.writer.runAttempt !== String(host.runAttempt))
15
+ ) {
16
+ const { run } = await host.runs.read(
17
+ Number(previous.payload.writer.runId),
18
+ Number(previous.payload.writer.runAttempt),
19
+ );
20
+ if (run.status !== "completed") return null;
21
+ const material = await host
22
+ .materialStore(session)
23
+ .retain(
24
+ `build/incomplete-${run.id}-${run.run_attempt}`,
25
+ run,
26
+ "provider-readback",
27
+ );
28
+ await session.progress.progress({
29
+ attempt,
30
+ phase: "build",
31
+ state: "failure",
32
+ eventKey: `build-incomplete:${run.id}:${run.run_attempt}`,
33
+ reason: "build-recording-incomplete",
34
+ materials: [material],
35
+ expectedHead: observed.head,
36
+ });
37
+ return null;
38
+ }
39
+ if (!observed.phases.admission) {
40
+ const receipt = {
41
+ schema: "buildchain.pipeline-source-observation/v1",
42
+ repositoryId: admission.repositoryId,
43
+ source: admission.live.source,
44
+ baseCommit: admission.live.baseCommit,
45
+ protectedSource: admission.protectedSource,
46
+ route: admission.route,
47
+ runtime: host.runtime,
48
+ };
49
+ const reference = await host
50
+ .materialStore(session)
51
+ .retain("source/admission", receipt, "provider-readback");
52
+ await session.progress.progress({
53
+ attempt,
54
+ phase: "admission",
55
+ state: "success",
56
+ eventKey: `admission:${observed.generation}`,
57
+ materials: [reference],
58
+ });
59
+ }
60
+ const scheduling = await session.journal.read();
61
+ const scheduled = scheduling.phases.build?.payload.writer;
62
+ if (
63
+ scheduled &&
64
+ (scheduled.runId !== String(host.runId) ||
65
+ scheduled.runAttempt !== String(host.runAttempt))
66
+ )
67
+ return null;
68
+ await session.progress.progress({
69
+ attempt,
70
+ phase: "build",
71
+ state: "running",
72
+ eventKey: `build-start:${host.runId}:${host.runAttempt}`,
73
+ expectedHead: scheduling.head,
74
+ });
75
+ return {
76
+ schema: "buildchain.pipeline-build-context/v1",
77
+ attempt,
78
+ generation: observed.generation,
79
+ source: admission.live.source,
80
+ runId: host.runId,
81
+ runAttempt: host.runAttempt,
82
+ platforms: pipelinePlatforms(admission.plan),
83
+ };
84
+ }
85
+
86
+ export function validatePipelineBuildContext(context) {
87
+ object(context, [
88
+ "schema",
89
+ "attempt",
90
+ "generation",
91
+ "source",
92
+ "runId",
93
+ "runAttempt",
94
+ "platforms",
95
+ ]);
96
+ if (
97
+ context.schema !== "buildchain.pipeline-build-context/v1" ||
98
+ !/^attempt-[0-9a-f]{64}$/u.test(context.attempt) ||
99
+ !/^sha256:[0-9a-f]{64}$/u.test(context.generation) ||
100
+ ![context.runId, context.runAttempt].every(
101
+ (value) => Number.isSafeInteger(value) && value > 0,
102
+ )
103
+ )
104
+ throw new Error("Invalid internal pipeline build context");
105
+ return context;
106
+ }
107
+
108
+ export async function recordPipelineBuild(context, host) {
109
+ validatePipelineBuildContext(context);
110
+ if (context.runId !== host.runId || context.runAttempt !== host.runAttempt)
111
+ throw new Error("Build callback belongs to another provider execution");
112
+ const session = await resumePipelineSession(
113
+ { ...host, attempt: context.attempt },
114
+ host,
115
+ );
116
+ const current = session.observed.history.at(-1);
117
+ if (
118
+ context.generation !== current.generation.id ||
119
+ recordDigest(context.source) !== recordDigest(current.generation.source)
120
+ )
121
+ throw new Error("Build callback changed its admitted source generation");
122
+ const admitted = await host.source.source(
123
+ context.source.commit,
124
+ context.source.configPath,
125
+ );
126
+ if (
127
+ recordDigest(context.platforms) !==
128
+ recordDigest(pipelinePlatforms(admitted.plan))
129
+ )
130
+ throw new Error(
131
+ "Build callback omitted or changed a declared product platform",
132
+ );
133
+ const readback = await host.runs.build(
134
+ context.runId,
135
+ context.runAttempt,
136
+ context.source,
137
+ context.platforms.map((platform) => platform.platform),
138
+ );
139
+ return retainPipelineBuildResult(context, readback, session, host);
140
+ }
@@ -0,0 +1,210 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ PIPELINE_BUILD_QUALIFICATION,
4
+ verifyPipelineBuildQualification,
5
+ } from "./build-qualification.js";
6
+ import { pipelinePlatforms } from "./platforms.js";
7
+
8
+ const RECOVERY_CHECK = "Buildchain recovery verification";
9
+
10
+ // Dispatch-created Actions checks do not satisfy protected PR checks. Preserve
11
+ // their evidence under a diagnostic name instead of shadowing normal PR checks.
12
+ export async function observePipelineBuildChecks(source, request, repository) {
13
+ if (
14
+ source.repository !== repository ||
15
+ !/^[0-9a-f]{40}$/u.test(source.commit)
16
+ )
17
+ throw new Error("Recovery check repository or source identity drift");
18
+ const base = `/repos/${repository}`,
19
+ renames = [],
20
+ eligible = [];
21
+ for (let page = 1; page <= 20; page++) {
22
+ const result = await request(
23
+ `${base}/commits/${source.commit}/check-runs?filter=all&per_page=100&page=${page}`,
24
+ );
25
+ if (!Array.isArray(result.check_runs) || result.check_runs.length > 100)
26
+ throw new Error("Recovery check inventory is incomplete");
27
+ for (const check of result.check_runs) {
28
+ const match = /^buildchain:(attempt-[0-9a-f]{64}):(\d+):(\d+)$/u.exec(
29
+ check.external_id || "",
30
+ );
31
+ if (
32
+ check.name !== "check" ||
33
+ check.app?.slug !== "github-actions" ||
34
+ !match
35
+ )
36
+ continue;
37
+ const runId = Number(match[2]),
38
+ runAttempt = Number(match[3]);
39
+ if (
40
+ check.head_sha !== source.commit ||
41
+ ![check.id, runId, runAttempt].every(
42
+ (value) => Number.isSafeInteger(value) && value > 0,
43
+ )
44
+ )
45
+ throw new Error("Recovery check provider identity drift");
46
+ const run = await request(
47
+ `${base}/actions/runs/${runId}/attempts/${runAttempt}`,
48
+ );
49
+ if (
50
+ run.id !== runId ||
51
+ run.run_attempt !== runAttempt ||
52
+ run.repository?.full_name !== repository
53
+ )
54
+ throw new Error("Recovery check execution identity drift");
55
+ if (["workflow_dispatch", "repository_dispatch"].includes(run.event))
56
+ renames.push({
57
+ id: check.id,
58
+ externalId: check.external_id,
59
+ sourceHead: source.commit,
60
+ runId,
61
+ runAttempt,
62
+ name: RECOVERY_CHECK,
63
+ });
64
+ else if (
65
+ [
66
+ "push",
67
+ "pull_request",
68
+ "pull_request_review",
69
+ "pull_request_target",
70
+ ].includes(run.event) &&
71
+ run.head_sha === source.commit
72
+ )
73
+ eligible.push({
74
+ id: check.id,
75
+ runId,
76
+ runAttempt,
77
+ conclusion: check.conclusion,
78
+ });
79
+ }
80
+ if (result.check_runs.length < 100) return { renames, eligible };
81
+ }
82
+ throw new Error("Recovery check inventory exceeds its complete-read bound");
83
+ }
84
+
85
+ // The archive reference is already validated against the current attempt. A
86
+ // build result is still re-read from the provider before it can admit delivery.
87
+ export async function pipelineBuildEvidence(session, host) {
88
+ const observed = await session.journal.read();
89
+ const current = observed.history.at(-1);
90
+ const phase = current.phases.build;
91
+ if (phase?.payload.state !== "success") return null;
92
+ const references = phase.payload.materials.filter(
93
+ (material) =>
94
+ material.id.startsWith("build/provider-") ||
95
+ material.id.startsWith("build/qualified-"),
96
+ );
97
+ if (references.length !== 1)
98
+ throw new Error("Pipeline build requires one retained provider receipt");
99
+ const retained = await host.materialStore(session).read(references[0]);
100
+ let { run } = await host.runs.read(retained.runId, retained.runAttempt);
101
+ if (run.status !== "completed" && run.id !== host.runId)
102
+ run = await host.runs.completed(retained.runId, retained.runAttempt);
103
+ if (retained.schema === PIPELINE_BUILD_QUALIFICATION) {
104
+ const source = await host.source.source(
105
+ current.generation.source.commit,
106
+ current.generation.source.configPath,
107
+ );
108
+ if (
109
+ recordDigest(source.identity) !== recordDigest(current.generation.source)
110
+ )
111
+ throw new Error("Recovered build source changed during qualification");
112
+ await verifyPipelineBuildQualification(
113
+ retained,
114
+ source.identity,
115
+ pipelinePlatforms(source.plan).map(({ platform }) => platform),
116
+ host.runs,
117
+ );
118
+ } else {
119
+ const verified = await host.runs.build(
120
+ retained.runId,
121
+ retained.runAttempt,
122
+ current.generation.source,
123
+ retained.jobs.map(
124
+ (job) => job.name.match(/Build product \(([^)]+)\)$/u)?.[1],
125
+ ),
126
+ );
127
+ if (recordDigest(verified) !== recordDigest(retained))
128
+ throw new Error("Retained product build provider result changed");
129
+ }
130
+ return {
131
+ run,
132
+ readback: retained,
133
+ sourceHead: current.generation.source.commit,
134
+ outcome: retained.outcome,
135
+ };
136
+ }
137
+
138
+ export async function publishPipelineBuildCheck(
139
+ context,
140
+ readback,
141
+ request,
142
+ repository,
143
+ ) {
144
+ if (
145
+ context.source.repository !== repository ||
146
+ recordDigest(context.source) !== recordDigest(readback.source) ||
147
+ context.runId !== readback.runId ||
148
+ context.runAttempt !== readback.runAttempt
149
+ )
150
+ throw new Error(
151
+ "Product check source differs from independently verified build",
152
+ );
153
+ const externalId = `buildchain:${context.attempt}:${context.runId}:${context.runAttempt}`;
154
+ const recovered = readback.schema === PIPELINE_BUILD_QUALIFICATION;
155
+ let checkName = recovered ? RECOVERY_CHECK : "check";
156
+ const output = {
157
+ title: recovered
158
+ ? "Buildchain recovered product verification"
159
+ : "Buildchain product verification",
160
+ summary: `Exact product jobs: ${readback.root}\nAttempt: ${context.attempt}`,
161
+ };
162
+ if (recovered) {
163
+ const checks = await observePipelineBuildChecks(
164
+ context.source,
165
+ request,
166
+ repository,
167
+ );
168
+ for (const change of checks.renames)
169
+ await request(`/repos/${repository}/check-runs/${change.id}`, {
170
+ method: "PATCH",
171
+ body: { name: change.name },
172
+ });
173
+ const original = checks.eligible.find((check) =>
174
+ readback.segments.some(
175
+ (segment) =>
176
+ segment.readback.runId === check.runId &&
177
+ segment.readback.runAttempt === check.runAttempt,
178
+ ),
179
+ );
180
+ // A complete replacement has no reusable original check to project onto.
181
+ // Retain that failed execution and publish this independently qualified
182
+ // aggregate as the required check. Protected landing still requalifies it
183
+ // and publishes the App-bound commit status before queue admission.
184
+ if (!original) checkName = "check";
185
+ // Keep the eligible normal-event check identity. Only a fully requalified
186
+ // source aggregate can repair its failed projection; original runs and
187
+ // immutable build receipts still retain their original failure outcomes.
188
+ if (
189
+ original &&
190
+ original.conclusion !== "success" &&
191
+ readback.outcome === "success"
192
+ )
193
+ await request(`/repos/${repository}/check-runs/${original.id}`, {
194
+ method: "PATCH",
195
+ body: { status: "completed", conclusion: "success", output },
196
+ });
197
+ }
198
+ return request(`/repos/${repository}/check-runs`, {
199
+ method: "POST",
200
+ body: {
201
+ name: checkName,
202
+ head_sha: context.source.commit,
203
+ external_id: externalId,
204
+ status: "completed",
205
+ conclusion: readback.outcome === "success" ? "success" : "failure",
206
+ output,
207
+ details_url: `https://github.com/${repository}/actions/runs/${context.runId}/attempts/${context.runAttempt}`,
208
+ },
209
+ });
210
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ recordDigest,
3
+ validateRuntime,
4
+ } from "../../release/discussion/envelope.js";
5
+ import { object } from "../../consumer/contract/shape.js";
6
+
7
+ export const PIPELINE_BUILD_QUALIFICATION =
8
+ "buildchain.pipeline-build-qualification/v1";
9
+
10
+ export function buildReadbackPlatforms(readback) {
11
+ const { root, ...body } = readback;
12
+ if (
13
+ body.schema !== "buildchain.pipeline-build-readback/v1" ||
14
+ root !== recordDigest(body) ||
15
+ !Array.isArray(body.jobs) ||
16
+ !body.jobs.length
17
+ )
18
+ throw new Error(
19
+ "Build qualification requires immutable provider job readback",
20
+ );
21
+ const platforms = body.jobs.map(
22
+ (job) => job.name?.match(/Build product \(([^)]+)\)$/u)?.[1],
23
+ );
24
+ if (
25
+ platforms.some((platform) => !platform) ||
26
+ new Set(platforms).size !== platforms.length
27
+ )
28
+ throw new Error(
29
+ "Provider build has ambiguous or unsupported platform jobs",
30
+ );
31
+ return platforms;
32
+ }
33
+
34
+ function validateBuildInventory(platforms, segments, runId, runAttempt) {
35
+ if (
36
+ !Array.isArray(platforms) ||
37
+ !platforms.length ||
38
+ platforms.length > 5 ||
39
+ new Set(platforms).size !== platforms.length ||
40
+ !Array.isArray(segments) ||
41
+ !segments.length ||
42
+ segments.length > 5 ||
43
+ ![runId, runAttempt].every(
44
+ (value) => Number.isSafeInteger(value) && value > 0,
45
+ )
46
+ )
47
+ throw new Error(
48
+ "Build qualification requires bounded exact platform and execution coordinates",
49
+ );
50
+ }
51
+
52
+ export function qualifyPipelineBuild({
53
+ source,
54
+ platforms,
55
+ segments,
56
+ runId,
57
+ runAttempt,
58
+ }) {
59
+ validateBuildInventory(platforms, segments, runId, runAttempt);
60
+ const selected = [];
61
+ let successful = true;
62
+ for (const segment of segments) {
63
+ object(segment, ["readback", "platforms", "runtime"]);
64
+ validateRuntime(segment.runtime);
65
+ if (
66
+ segment.runtime.repository !== "kungfu-systems/buildchain" ||
67
+ !Array.isArray(segment.platforms) ||
68
+ !segment.platforms.length ||
69
+ recordDigest(segment.readback.source) !== recordDigest(source)
70
+ )
71
+ throw new Error(
72
+ "Build segment changed its exact source or runtime repository",
73
+ );
74
+ const available = buildReadbackPlatforms(segment.readback);
75
+ for (const platform of segment.platforms) {
76
+ const index = available.indexOf(platform),
77
+ job = segment.readback.jobs[index];
78
+ if (
79
+ index < 0 ||
80
+ !platforms.includes(platform) ||
81
+ selected.includes(platform) ||
82
+ job.status !== "completed" ||
83
+ !job.conclusion ||
84
+ job.run_id !== segment.readback.runId ||
85
+ job.run_attempt !== segment.readback.runAttempt ||
86
+ !Number.isSafeInteger(job.id) ||
87
+ job.id < 1
88
+ )
89
+ throw new Error(
90
+ "Build segments omitted, duplicated or changed a real platform execution",
91
+ );
92
+ selected.push(platform);
93
+ successful &&= job.conclusion === "success";
94
+ }
95
+ }
96
+ if (selected.length !== platforms.length)
97
+ throw new Error("Build qualification is missing declared platforms");
98
+ const body = {
99
+ schema: PIPELINE_BUILD_QUALIFICATION,
100
+ source,
101
+ platforms: [...platforms].sort(),
102
+ runId,
103
+ runAttempt,
104
+ segments: structuredClone(segments),
105
+ outcome: successful ? "success" : "failure",
106
+ };
107
+ return { ...body, root: recordDigest(body) };
108
+ }
109
+
110
+ export async function verifyPipelineBuildQualification(
111
+ value,
112
+ source,
113
+ platforms,
114
+ runs,
115
+ ) {
116
+ object(value, [
117
+ "schema",
118
+ "source",
119
+ "platforms",
120
+ "runId",
121
+ "runAttempt",
122
+ "segments",
123
+ "outcome",
124
+ "root",
125
+ ]);
126
+ const expected = qualifyPipelineBuild({ ...value, source, platforms });
127
+ if (recordDigest(value) !== recordDigest(expected))
128
+ throw new Error(
129
+ "Retained build qualification changed its source or exact platform inventory",
130
+ );
131
+ for (const segment of value.segments) {
132
+ const old = segment.readback;
133
+ const fresh = await runs.build(
134
+ old.runId,
135
+ old.runAttempt,
136
+ source,
137
+ buildReadbackPlatforms(old),
138
+ );
139
+ if (recordDigest(fresh) !== recordDigest(old))
140
+ throw new Error(
141
+ "A recovered platform result changed during provider requalification",
142
+ );
143
+ }
144
+ return value;
145
+ }