@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,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
+ }
@@ -0,0 +1,78 @@
1
+ import { publishPipelineBuildCheck } from "./build-evidence.js";
2
+ import { PIPELINE_BUILD_QUALIFICATION } from "./build-qualification.js";
3
+ import { recordDigest } from "../../release/discussion/envelope.js";
4
+ import { replayRecoveryIntegration } from "./recovery-integration.js";
5
+
6
+ async function resultReference(context, readback, session, host) {
7
+ const observed = await session.journal.read();
8
+ if (observed.attempt !== context.attempt)
9
+ throw new Error("Late build result belongs to a superseded attempt");
10
+ const prior = observed.history
11
+ .at(-1)
12
+ .events.find(
13
+ (event) =>
14
+ event.payload.eventKey ===
15
+ `build-result:${context.runId}:${context.runAttempt}`,
16
+ );
17
+ const store = host.materialStore(session);
18
+ if (prior) {
19
+ const references = prior.payload.materials;
20
+ if (
21
+ references.length !== 1 ||
22
+ recordDigest(await store.read(references[0])) !== recordDigest(readback)
23
+ )
24
+ throw new Error("Repeated build callback changed its immutable result");
25
+ return references[0];
26
+ }
27
+ return store.retain(
28
+ `build/${readback.schema === PIPELINE_BUILD_QUALIFICATION ? "qualified" : "provider"}-${context.runId}-${context.runAttempt}`,
29
+ readback,
30
+ "provider-readback",
31
+ );
32
+ }
33
+
34
+ export async function retainPipelineBuildResult(
35
+ context,
36
+ readback,
37
+ session,
38
+ host,
39
+ ) {
40
+ if (
41
+ readback.outcome === "success" &&
42
+ session.intent.expectedNodes.includes("publish")
43
+ )
44
+ await host.qualifyChannel(
45
+ context.source,
46
+ session.intent.source.targetBranch,
47
+ );
48
+ const reference = await resultReference(context, readback, session, host);
49
+ await session.progress.progress({
50
+ attempt: context.attempt,
51
+ phase: "build",
52
+ state: readback.outcome === "success" ? "success" : "failure",
53
+ eventKey: `build-result:${context.runId}:${context.runAttempt}`,
54
+ reason: readback.outcome === "success" ? "" : "product-build-failed",
55
+ materials: [reference],
56
+ });
57
+ await host.project(session);
58
+ await publishPipelineBuildCheck(
59
+ context,
60
+ readback,
61
+ host.request,
62
+ host.repository,
63
+ );
64
+ await host.publishBuildStatus?.(context, readback);
65
+ if (
66
+ readback.schema === PIPELINE_BUILD_QUALIFICATION &&
67
+ readback.outcome === "success"
68
+ )
69
+ await replayRecoveryIntegration(session, host);
70
+ try {
71
+ await host.wake(context.attempt);
72
+ return { outcome: readback.outcome, wakePending: false };
73
+ } catch {
74
+ // The source build remains qualified if only notification failed. Its
75
+ // durable result is reobserved on the next PR event or exact attempt wake.
76
+ return { outcome: readback.outcome, wakePending: true };
77
+ }
78
+ }