@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,86 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ PIPELINE_ENTRY,
4
+ RECOVERY_ENTRY,
5
+ consumerWorkflows,
6
+ } from "../../consumer/contract/entries.js";
7
+
8
+ export function pipelineRunEntry(run) {
9
+ const definitions = (run.referenced_workflows || []).filter((item) =>
10
+ [PIPELINE_ENTRY, RECOVERY_ENTRY].some((entry) =>
11
+ item.path?.startsWith(`kungfu-systems/buildchain/${entry}@`),
12
+ ),
13
+ );
14
+ if (
15
+ definitions.length !== 1 ||
16
+ !/^[0-9a-f]{40}$/u.test(definitions[0].sha || "")
17
+ )
18
+ throw new Error(
19
+ "Build provider run has no unique exact pipeline or recovery entry",
20
+ );
21
+ const definition = definitions[0];
22
+ const recovery = definition.path.startsWith(
23
+ `kungfu-systems/buildchain/${RECOVERY_ENTRY}@`,
24
+ );
25
+ const caller = `.github/workflows/${recovery ? "buildchain-recover" : "buildchain"}.yml`;
26
+ const channel = definition.path
27
+ .split("@")
28
+ .at(-1)
29
+ .replace(/^refs\/tags\//u, "");
30
+ if (
31
+ !["v4", "v4-alpha"].includes(channel) ||
32
+ run.path?.split("@")[0] !== caller ||
33
+ (recovery
34
+ ? run.event !== "workflow_dispatch"
35
+ : ![
36
+ "pull_request",
37
+ "repository_dispatch",
38
+ "pull_request_review",
39
+ "merge_group",
40
+ ].includes(run.event))
41
+ )
42
+ throw new Error(
43
+ "Build run did not enter through its trusted published consumer workflow",
44
+ );
45
+ return { definition, channel, caller, recovery };
46
+ }
47
+
48
+ // Entry presence and a matching job name alone are insufficient: an altered
49
+ // caller could add a lookalike job beside a failed reusable-workflow call.
50
+ export async function readPipelineCaller(run, configPath, request, repository) {
51
+ const entry = pipelineRunEntry(run);
52
+ if (
53
+ run.repository?.full_name !== repository ||
54
+ run.head_repository?.full_name !== repository ||
55
+ !/^[0-9a-f]{40}$/u.test(run.head_sha || "")
56
+ )
57
+ throw new Error("Pipeline execution crossed a repository or fork boundary");
58
+ const file = await request(
59
+ `/repos/${repository}/contents/${entry.caller}?ref=${run.head_sha}`,
60
+ );
61
+ if (
62
+ file.type !== "file" ||
63
+ file.encoding !== "base64" ||
64
+ !Number.isSafeInteger(file.size) ||
65
+ file.size < 1 ||
66
+ file.size > 64000 ||
67
+ typeof file.content !== "string" ||
68
+ file.content.length > 128000
69
+ )
70
+ throw new Error("Pipeline caller workflow is absent or exceeds its bound");
71
+ const bytes = Buffer.from(file.content, "base64");
72
+ const blob = createHash("sha1")
73
+ .update(`blob ${bytes.length}\0`)
74
+ .update(bytes)
75
+ .digest("hex");
76
+ if (
77
+ bytes.length !== file.size ||
78
+ blob !== file.sha ||
79
+ bytes.toString("utf8") !==
80
+ consumerWorkflows(entry.channel, configPath)[entry.caller]
81
+ )
82
+ throw new Error(
83
+ "Pipeline caller differs from the trusted minimal workflow contract",
84
+ );
85
+ return { commit: run.head_sha, path: entry.caller, blob };
86
+ }
@@ -0,0 +1,132 @@
1
+ import { providerWriter } from "../../workflow/attempt/identity.js";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { pipelineRunEntry, readPipelineCaller } from "./pipeline-run-entry.js";
4
+
5
+ function uniqueJob(jobs, name) {
6
+ const matches = jobs.filter(
7
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
8
+ );
9
+ if (matches.length !== 1)
10
+ throw new Error(`Pipeline needs one exact provider job: ${name}`);
11
+ return matches[0];
12
+ }
13
+
14
+ export function githubPipelineRuns(request, repository) {
15
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
16
+ throw new Error("Invalid pipeline run repository");
17
+ const base = `/repos/${repository}`;
18
+ async function read(runId, runAttempt) {
19
+ for (const value of [runId, runAttempt])
20
+ if (!Number.isSafeInteger(value) || value < 1)
21
+ throw new Error("Invalid pipeline run coordinates");
22
+ const run = await request(`${base}/actions/runs/${runId}`);
23
+ if (
24
+ run.id !== runId ||
25
+ run.run_attempt !== runAttempt ||
26
+ run.repository?.full_name !== repository
27
+ )
28
+ throw new Error("Pipeline provider run attempt changed");
29
+ const jobs = [];
30
+ for (let page = 1; page <= 100; page++) {
31
+ const response = await request(
32
+ `${base}/actions/runs/${runId}/attempts/${runAttempt}/jobs?per_page=100&page=${page}`,
33
+ );
34
+ if (!Array.isArray(response.jobs))
35
+ throw new Error("Missing pipeline provider jobs");
36
+ jobs.push(...response.jobs);
37
+ if (response.jobs.length < 100) return { run, jobs };
38
+ }
39
+ throw new Error("Pipeline job inventory exceeds its bound");
40
+ }
41
+ async function writer(runId, runAttempt, jobName) {
42
+ const { jobs } = await read(runId, runAttempt);
43
+ const job = uniqueJob(jobs, jobName);
44
+ if (
45
+ job.run_id !== runId ||
46
+ job.run_attempt !== runAttempt ||
47
+ job.status !== "in_progress"
48
+ )
49
+ throw new Error("Pipeline writer is not the exact active provider job");
50
+ const value = {
51
+ repository,
52
+ runId: String(runId),
53
+ runAttempt: String(runAttempt),
54
+ jobId: String(job.id),
55
+ };
56
+ providerWriter(value, repository);
57
+ return value;
58
+ }
59
+ async function build(runId, runAttempt, source, platforms) {
60
+ const { run, jobs } = await read(runId, runAttempt);
61
+ if (
62
+ ![
63
+ "pull_request",
64
+ "repository_dispatch",
65
+ "pull_request_review",
66
+ "merge_group",
67
+ "workflow_dispatch",
68
+ ].includes(run.event) ||
69
+ (["pull_request", "merge_group"].includes(run.event) &&
70
+ run.head_sha !== source.commit) ||
71
+ source.repository !== repository
72
+ )
73
+ throw new Error("Build provider run does not match admitted PR source");
74
+ const entry = pipelineRunEntry(run);
75
+ await readPipelineCaller(run, source.configPath, request, repository);
76
+ if (
77
+ !Array.isArray(platforms) ||
78
+ !platforms.length ||
79
+ new Set(platforms).size !== platforms.length
80
+ )
81
+ throw new Error(
82
+ "Build provider receipt requires declared unique platforms",
83
+ );
84
+ const selected = platforms.map((platform) =>
85
+ uniqueJob(jobs, `Build product (${platform})`),
86
+ );
87
+ if (
88
+ selected.some(
89
+ (job) =>
90
+ job.run_id !== runId ||
91
+ job.run_attempt !== runAttempt ||
92
+ job.status !== "completed" ||
93
+ !job.conclusion,
94
+ )
95
+ )
96
+ throw new Error("Product build jobs are not provider-terminal");
97
+ const again = await request(`${base}/actions/runs/${runId}`);
98
+ if (
99
+ again.id !== run.id ||
100
+ again.run_attempt !== runAttempt ||
101
+ again.head_sha !== run.head_sha
102
+ )
103
+ throw new Error("Product build run changed during readback");
104
+ const body = {
105
+ schema: "buildchain.pipeline-build-readback/v1",
106
+ source,
107
+ runId,
108
+ runAttempt,
109
+ entry: entry.definition,
110
+ jobs: selected,
111
+ outcome: selected.every((job) => job.conclusion === "success")
112
+ ? "success"
113
+ : "failure",
114
+ };
115
+ return { ...body, root: recordDigest(body) };
116
+ }
117
+ async function completed(
118
+ runId,
119
+ runAttempt,
120
+ { sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) } = {},
121
+ ) {
122
+ for (let attempt = 0; attempt < 20; attempt++) {
123
+ const { run } = await read(runId, runAttempt);
124
+ if (run.status === "completed") return run;
125
+ await sleep(3000);
126
+ }
127
+ throw new Error(
128
+ "Predecessor workflow did not become terminal within the bounded provider readback window",
129
+ );
130
+ }
131
+ return { read, writer, build, completed };
132
+ }
@@ -0,0 +1,195 @@
1
+ import { bindConsumerSource } from "../../consumer/contract/identity.js";
2
+ import { normalInputs } from "../../consumer/contract/entries.js";
3
+ import { channelRoute } from "../../workflow/pipeline/events.js";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import { validateGeneration } from "../../workflow/attempt/identity.js";
6
+
7
+ function exactSha(value) {
8
+ if (!/^[0-9a-f]{40}$/u.test(value || ""))
9
+ throw new Error("Pipeline source requires an exact Git commit");
10
+ return value;
11
+ }
12
+
13
+ function sourceRefBinding(reference) {
14
+ return {
15
+ ref: reference?.ref ?? null,
16
+ sha: reference?.sha ?? null,
17
+ repository: {
18
+ id: reference?.repo?.id ?? null,
19
+ nodeId: reference?.repo?.node_id ?? null,
20
+ fullName: reference?.repo?.full_name ?? null,
21
+ },
22
+ };
23
+ }
24
+
25
+ function mergedCommit(pr) {
26
+ return pr.merged === true ? pr.merge_commit_sha : null;
27
+ }
28
+
29
+ function pullRequestBinding(pr) {
30
+ // Provider timestamps and repository metadata can change during our own
31
+ // evidence writes. Bind source and delivery state, not those unrelated fields.
32
+ return {
33
+ number: pr.number,
34
+ state: pr.state,
35
+ merged: pr.merged,
36
+ mergeCommit: mergedCommit(pr),
37
+ head: sourceRefBinding(pr.head),
38
+ base: sourceRefBinding(pr.base),
39
+ draft: pr.draft,
40
+ ready: pr.labels.some((label) => label.name === "ready"),
41
+ };
42
+ }
43
+
44
+ export function githubPipelineSource(request, repository) {
45
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
46
+ throw new Error("Invalid pipeline repository");
47
+ const base = `/repos/${repository}`;
48
+ async function source(commitInput, configPath) {
49
+ const commit = exactSha(commitInput);
50
+ const git = await request(`${base}/git/commits/${commit}`);
51
+ const config = await request(
52
+ `${base}/contents/${configPath}?ref=${commit}`,
53
+ );
54
+ if (
55
+ config.type !== "file" ||
56
+ config.encoding !== "base64" ||
57
+ !Number.isSafeInteger(config.size) ||
58
+ config.size < 1 ||
59
+ config.size > 256_000 ||
60
+ typeof config.content !== "string" ||
61
+ config.content.length > 512_000
62
+ )
63
+ throw new Error("Pipeline TOML is missing or exceeds its byte bound");
64
+ const bytes = Buffer.from(config.content, "base64");
65
+ if (bytes.length !== config.size)
66
+ throw new Error("Pipeline TOML size drift");
67
+ return bindConsumerSource(
68
+ {
69
+ repository,
70
+ commit,
71
+ tree: exactSha(git.tree?.sha),
72
+ configPath,
73
+ configBlob: config.sha,
74
+ },
75
+ bytes,
76
+ );
77
+ }
78
+ async function branchHead(branch) {
79
+ if (
80
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
81
+ branch || "",
82
+ ) ||
83
+ branch.includes("..")
84
+ )
85
+ throw new Error("Invalid protected branch");
86
+ const pointer = await request(`${base}/git/ref/heads/${branch}`);
87
+ if (pointer.object?.type !== "commit")
88
+ throw new Error("Protected ref is not a commit");
89
+ return exactSha(pointer.object.sha);
90
+ }
91
+ async function observe(pullRequest, inputs = {}) {
92
+ if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
93
+ throw new Error("Pipeline requires an exact PR number");
94
+ const { configPath } = normalInputs(inputs);
95
+ const pr = await request(`${base}/pulls/${pullRequest}`);
96
+ const repositoryInfo = await request(base);
97
+ if (pr.number !== pullRequest || !repositoryInfo.node_id)
98
+ throw new Error("Pipeline PR provider identity drift");
99
+ // Read delivery policy from the protected base. A PR may change its own
100
+ // product commands, but cannot lower review or grant itself a channel route.
101
+ const baseCommit = await branchHead(pr.base?.ref);
102
+ const protectedSource = await source(baseCommit, configPath);
103
+ const route = channelRoute(protectedSource.plan, pr, repository);
104
+ if (!route) {
105
+ if (baseCommit !== (await branchHead(pr.base.ref)))
106
+ throw new Error("Protected policy changed during route observation");
107
+ return { eligible: false, reason: "channel-not-enabled" };
108
+ }
109
+ const admitted = await source(pr.head.sha, configPath);
110
+ const again = await request(`${base}/pulls/${pullRequest}`);
111
+ if (
112
+ recordDigest(pullRequestBinding(pr)) !==
113
+ recordDigest(pullRequestBinding(again)) ||
114
+ baseCommit !== (await branchHead(pr.base.ref))
115
+ )
116
+ throw new Error("Pipeline source changed during observation; reobserve");
117
+ return {
118
+ eligible: true,
119
+ repositoryId: repositoryInfo.node_id,
120
+ route,
121
+ plan: admitted.plan,
122
+ protectedPlan: protectedSource.plan,
123
+ protectedSource: protectedSource.identity,
124
+ live: {
125
+ pullRequest,
126
+ observedHead: pr.head.sha,
127
+ targetBranch: pr.base.ref,
128
+ routeEnabled: true,
129
+ source: admitted.identity,
130
+ baseCommit,
131
+ state: pr.state,
132
+ merged: pr.merged === true,
133
+ mergeCommit: mergedCommit(pr),
134
+ draft: pr.draft === true,
135
+ ready: pr.labels.some((label) => label.name === "ready"),
136
+ },
137
+ };
138
+ }
139
+ async function observeIntent(intent, generation, inputs = {}) {
140
+ validateGeneration(generation, intent);
141
+ if (intent.repository !== repository)
142
+ throw new Error("Recorded intent repository drift");
143
+ const number = intent.source.pullRequest;
144
+ const pr = await request(`${base}/pulls/${number}`);
145
+ const repositoryInfo = await request(base);
146
+ if (
147
+ repositoryInfo.node_id !== intent.source.repositoryId ||
148
+ pr.number !== number ||
149
+ pr.base?.repo?.full_name !== repository
150
+ )
151
+ throw new Error("Recorded pipeline intent provider identity drift");
152
+ const sameHead =
153
+ pr.head?.repo?.full_name === repository &&
154
+ pr.head.sha === generation.source.commit;
155
+ const live = {
156
+ pullRequest: number,
157
+ observedHead: exactSha(pr.head?.sha),
158
+ targetBranch: pr.base.ref,
159
+ state: pr.state,
160
+ merged: pr.merged === true,
161
+ mergeCommit: mergedCommit(pr),
162
+ source: sameHead ? generation.source : null,
163
+ baseCommit: null,
164
+ ready: pr.labels.some((label) => label.name === "ready"),
165
+ draft: pr.draft === true,
166
+ };
167
+ // Closing, updating or retargeting an admitted PR must remain recoverable
168
+ // even when its new TOML is invalid, its old route disappeared or it is now a fork.
169
+ if (
170
+ pr.state !== "open" ||
171
+ !sameHead ||
172
+ pr.base.ref !== intent.source.targetBranch
173
+ ) {
174
+ const again = await request(`${base}/pulls/${number}`);
175
+ if (
176
+ recordDigest(pullRequestBinding(pr)) !==
177
+ recordDigest(pullRequestBinding(again))
178
+ )
179
+ throw new Error("Recorded intent changed during terminal observation");
180
+ return { eligible: true, cleanupOnly: true, live };
181
+ }
182
+ const observed = await observe(number, inputs);
183
+ if (observed.eligible) return observed;
184
+ return {
185
+ eligible: true,
186
+ cleanupOnly: true,
187
+ live: {
188
+ ...live,
189
+ baseCommit: await branchHead(intent.source.targetBranch),
190
+ routeEnabled: false,
191
+ },
192
+ };
193
+ }
194
+ return { observe, observeIntent, source, branchHead };
195
+ }
@@ -0,0 +1,86 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ SETTLEMENT_ASSET,
4
+ verifyPublicationSettlement,
5
+ } from "../../publication/settlement/transaction.js";
6
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
7
+
8
+ // Earlier publications materialized the floating entry after the exact tag.
9
+ // Only their completed immutable settlement can bind those two commits.
10
+ export async function readPipelineStableBaseline(plan, host, release, prior) {
11
+ if ((prior?.commit || null) === plan.previousChannelCommit) return null;
12
+ if (!release || !prior || !plan.previousChannelCommit)
13
+ throw new Error(
14
+ "Stable comparison differs from the retained published channel",
15
+ );
16
+ const evidence = await readReleaseEvidenceAssets(host, release, [
17
+ SETTLEMENT_ASSET,
18
+ "buildchain.release.json",
19
+ ]);
20
+ if (evidence.status !== "present")
21
+ throw new Error(
22
+ "Stable channel differs without a completed publication settlement",
23
+ );
24
+ const read = (name) =>
25
+ JSON.parse(
26
+ new TextDecoder("utf-8", { fatal: true }).decode(evidence.values[name]),
27
+ );
28
+ const settlement = read(SETTLEMENT_ASSET),
29
+ publicPassport = read("buildchain.release.json"),
30
+ repository = plan.source.repository,
31
+ tag = release.tag,
32
+ sourceSha = prior.commit;
33
+ if (
34
+ settlement.schemaVersion !== 1 ||
35
+ settlement.contract !== "buildchain-v4-publication-settlement/v1" ||
36
+ settlement.release?.tag !== tag ||
37
+ settlement.release?.sourceSha !== sourceSha ||
38
+ settlement.release?.channel !== "stable"
39
+ )
40
+ throw new Error(
41
+ "Stable baseline settlement has different release coordinates",
42
+ );
43
+ const verified = verifyPublicationSettlement(settlement.documents, {
44
+ repository,
45
+ tag,
46
+ sourceSha,
47
+ publicPassport,
48
+ });
49
+ const { product, providerState, productState, invocation, passport } =
50
+ settlement.documents;
51
+ const ref = `refs/tags/v${plan.version.split(".")[0]}`,
52
+ updates = product.updates.filter((value) => value.ref === ref);
53
+ const subject = {
54
+ repository,
55
+ sourceSha,
56
+ tag,
57
+ version: tag.slice(1),
58
+ channel: "stable",
59
+ };
60
+ if (
61
+ invocation.target.channel !== "stable" ||
62
+ passport.release.channel !== "stable" ||
63
+ product.promotedSha !== plan.previousChannelCommit ||
64
+ updates.length !== 1 ||
65
+ updates[0].action !== "converged-release-tag" ||
66
+ updates[0].sha !== plan.previousChannelCommit ||
67
+ recordDigest(productState.subject) !== recordDigest(subject) ||
68
+ recordDigest(providerState.subject) !==
69
+ recordDigest({
70
+ ...subject,
71
+ sourceSha: plan.previousChannelCommit,
72
+ })
73
+ )
74
+ throw new Error(
75
+ "Stable baseline settlement does not bind the retained floating channel",
76
+ );
77
+ return {
78
+ releaseId: release.id,
79
+ tag,
80
+ sourceSha,
81
+ channelRef: ref,
82
+ channelCommit: plan.previousChannelCommit,
83
+ receiptRoot: verified.receiptRoot,
84
+ assets: evidence.assets,
85
+ };
86
+ }