@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,199 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { readPipelineVersion } from "../../publication/pipeline/version.js";
4
+ import { preparedPipelineVersionMaterial } from "../../publication/pipeline/version-material.js";
5
+ import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
6
+
7
+ function blobBytes(value, sha) {
8
+ if (
9
+ value.encoding !== "base64" ||
10
+ value.size > 4 * 1024 * 1024 ||
11
+ typeof value.content !== "string"
12
+ )
13
+ throw new Error("Version material requires a bounded Git blob");
14
+ const bytes = Buffer.from(value.content, "base64");
15
+ const digest = createHash("sha1")
16
+ .update(`blob ${bytes.length}\0`)
17
+ .update(bytes)
18
+ .digest("hex");
19
+ if (bytes.length !== value.size || digest !== sha || value.sha !== sha)
20
+ throw new Error("Version material blob identity drift");
21
+ return bytes.toString("utf8");
22
+ }
23
+
24
+ export function githubPipelineVersion(request, repository) {
25
+ const base = `/repos/${repository}`;
26
+ const post = (url, body) => request(url, { method: "POST", body });
27
+ async function inspect(source, policy) {
28
+ if (source.repository !== repository)
29
+ throw new Error("Version material repository mismatch");
30
+ const commit = await request(`${base}/git/commits/${source.commit}`);
31
+ if (commit.sha !== source.commit || commit.tree?.sha !== source.tree)
32
+ throw new Error("Version material source tree drift");
33
+ const tree = await request(`${base}/git/trees/${source.tree}?recursive=1`);
34
+ if (tree.truncated || tree.sha !== source.tree || !Array.isArray(tree.tree))
35
+ throw new Error("Version material requires a complete source tree");
36
+ const files = {},
37
+ entries = {};
38
+ for (const file of [
39
+ ...policy.files,
40
+ ...(policy.derived_files || []).map((path) => ({ path })),
41
+ ]) {
42
+ if (Object.hasOwn(files, file.path)) continue;
43
+ const found = tree.tree.filter((entry) => entry.path === file.path);
44
+ if (
45
+ found.length !== 1 ||
46
+ found[0].type !== "blob" ||
47
+ !["100644", "100755"].includes(found[0].mode)
48
+ )
49
+ throw new Error("Version material must be a tracked regular file");
50
+ const entry = found[0];
51
+ entries[file.path] = entry;
52
+ files[file.path] = blobBytes(
53
+ await request(`${base}/git/blobs/${entry.sha}`),
54
+ entry.sha,
55
+ );
56
+ }
57
+ return {
58
+ commit,
59
+ files,
60
+ entries,
61
+ version: readPipelineVersion(policy, files),
62
+ sourceTimestamp: commit.committer.date,
63
+ };
64
+ }
65
+ async function materialize(plan, regeneration) {
66
+ verifyPipelinePublicationPlan(plan);
67
+ const input = await inspect(plan.source, plan.versionPolicy);
68
+ if (input.version !== plan.candidateVersion)
69
+ throw new Error("Version material changed after planning");
70
+ const material = preparedPipelineVersionMaterial(
71
+ plan,
72
+ input.files,
73
+ regeneration,
74
+ );
75
+ let source = plan.source;
76
+ if (material.changes.length)
77
+ source = await createMaterializedSource(plan, input, material);
78
+ const body = {
79
+ schema: "buildchain.pipeline-version-materialization/v1",
80
+ planRoot: plan.root,
81
+ protectedSource: plan.source,
82
+ source,
83
+ material,
84
+ };
85
+ return { ...body, root: recordDigest(body) };
86
+ }
87
+ async function materializeDevelopment(plan, regeneration) {
88
+ const input = await inspect(plan.source, plan.versionPolicy);
89
+ const material = preparedPipelineVersionMaterial(
90
+ plan,
91
+ input.files,
92
+ regeneration,
93
+ );
94
+ const branch = `feature/buildchain-next/${plan.root.slice(7)}`;
95
+ const source = material.changes.length
96
+ ? await createMaterializedSource(plan, input, material, `heads/${branch}`)
97
+ : plan.source;
98
+ const body = {
99
+ schema: "buildchain.pipeline-development-materialization/v1",
100
+ transitionRoot: plan.root,
101
+ protectedSource: plan.source,
102
+ source,
103
+ branch,
104
+ material,
105
+ };
106
+ return { ...body, root: recordDigest(body) };
107
+ }
108
+ async function createMaterializedSource(plan, input, material, targetRef) {
109
+ const changes = material.changes.map(({ path, content }) => ({
110
+ path,
111
+ content,
112
+ mode: input.entries[path].mode,
113
+ type: "blob",
114
+ }));
115
+ const tree = await post(`${base}/git/trees`, {
116
+ base_tree: plan.source.tree,
117
+ tree: changes,
118
+ });
119
+ const identity = {
120
+ name: "Buildchain automation",
121
+ email: "noreply@github.com",
122
+ date: plan.sourceTimestamp,
123
+ };
124
+ const parents = [plan.source.commit];
125
+ // Preserve the prior published version overlay as real Git ancestry. The
126
+ // resulting tree still changes only the current protected version fields;
127
+ // no provider force-update is needed for the next stable floating channel.
128
+ if (
129
+ plan.previousChannelCommit &&
130
+ plan.previousChannelCommit !== plan.source.commit
131
+ ) {
132
+ if (!/^[0-9a-f]{40}$/u.test(plan.previousChannelCommit))
133
+ throw new Error(
134
+ "Version overlay parent must be an exact prior channel commit",
135
+ );
136
+ const previous = await request(
137
+ `${base}/git/commits/${plan.previousChannelCommit}`,
138
+ );
139
+ if (previous.sha !== plan.previousChannelCommit)
140
+ throw new Error("Prior version overlay commit changed");
141
+ parents.push(previous.sha);
142
+ }
143
+ const created = await post(`${base}/git/commits`, {
144
+ message: `chore(release): materialize ${plan.version}\n\nBuildchain-Plan: ${plan.root}\nSigned-off-by: Buildchain automation <noreply@github.com>`,
145
+ tree: tree.sha,
146
+ parents,
147
+ author: identity,
148
+ committer: identity,
149
+ });
150
+ const commit = await request(`${base}/git/commits/${created.sha}`);
151
+ if (
152
+ commit.tree?.sha !== tree.sha ||
153
+ recordDigest(commit.parents?.map(({ sha }) => sha)) !==
154
+ recordDigest(parents)
155
+ )
156
+ throw new Error("Materialized version commit readback mismatch");
157
+ const source = { ...plan.source, commit: commit.sha, tree: tree.sha };
158
+ const verified = await inspect(source, plan.versionPolicy);
159
+ if (verified.version !== plan.version)
160
+ throw new Error("Version material did not retain the planned version");
161
+ const expectedFiles = {
162
+ ...input.files,
163
+ ...Object.fromEntries(
164
+ material.changes.map(({ path, content }) => [path, content]),
165
+ ),
166
+ };
167
+ if (recordDigest(verified.files) !== recordDigest(expectedFiles))
168
+ throw new Error(
169
+ "Version material changed document bytes beyond the planned fields",
170
+ );
171
+ const comparison = await request(
172
+ `${base}/compare/${plan.source.commit}...${commit.sha}`,
173
+ );
174
+ const paths = comparison.files?.map((file) => file.filename).sort();
175
+ if (
176
+ comparison.status !== "ahead" ||
177
+ (parents.length === 1 && comparison.total_commits !== 1) ||
178
+ recordDigest(paths) !==
179
+ recordDigest(material.changes.map(({ path }) => path).sort()) ||
180
+ comparison.files.some((file) => file.status !== "modified")
181
+ )
182
+ throw new Error(
183
+ "Version material changed paths outside the declared version fields",
184
+ );
185
+ const ref =
186
+ targetRef || `heads/buildchain/publication-source/${plan.root.slice(7)}`;
187
+ try {
188
+ await post(`${base}/git/refs`, { ref: `refs/${ref}`, sha: commit.sha });
189
+ } catch (error) {
190
+ const observed = await request(`${base}/git/ref/${ref}`);
191
+ if (observed.object?.sha !== commit.sha) throw error;
192
+ }
193
+ const observed = await request(`${base}/git/ref/${ref}`);
194
+ if (observed.object?.sha !== commit.sha)
195
+ throw new Error("Version material ref readback mismatch");
196
+ return source;
197
+ }
198
+ return { inspect, materialize, materializeDevelopment };
199
+ }
@@ -0,0 +1,108 @@
1
+ import { object } from "../../consumer/contract/shape.js";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ export const PIPELINE_WORKER = "buildchain.pipeline-worker/v1";
5
+
6
+ export function validatePipelineWorker(binding, current, warrant) {
7
+ object(binding, [
8
+ "schema",
9
+ "repository",
10
+ "attempt",
11
+ "generation",
12
+ "sourceHead",
13
+ "candidateId",
14
+ "fencingToken",
15
+ "leaseGeneration",
16
+ "runId",
17
+ "runAttempt",
18
+ "nativeJobId",
19
+ "sealJobId",
20
+ ]);
21
+ if (
22
+ binding.schema !== PIPELINE_WORKER ||
23
+ binding.repository !== current.intent.repository ||
24
+ binding.attempt !== current.identity.id ||
25
+ binding.generation !== current.generation.id ||
26
+ binding.sourceHead !== current.generation.source.commit ||
27
+ binding.candidateId !== warrant.candidateId ||
28
+ binding.fencingToken !== warrant.fencingToken ||
29
+ binding.leaseGeneration !== warrant.generation
30
+ )
31
+ throw new Error("Native worker does not bind the active pipeline Warrant");
32
+ for (const key of [
33
+ "runId",
34
+ "runAttempt",
35
+ "nativeJobId",
36
+ "sealJobId",
37
+ "leaseGeneration",
38
+ ])
39
+ if (!Number.isSafeInteger(binding[key]) || binding[key] < 1)
40
+ throw new Error("Native worker provider identity is invalid");
41
+ if (binding.nativeJobId === binding.sealJobId)
42
+ throw new Error("Native execution and evidence seal must be separate jobs");
43
+ return binding;
44
+ }
45
+
46
+ export function githubPipelineWorker(request) {
47
+ async function jobs(base, binding) {
48
+ const result = [];
49
+ for (let page = 1; page <= 100; page++) {
50
+ const response = await request(
51
+ `${base}/actions/runs/${binding.runId}/attempts/${binding.runAttempt}/jobs?per_page=100&page=${page}`,
52
+ );
53
+ if (!Array.isArray(response.jobs))
54
+ throw new Error("Missing provider jobs readback");
55
+ result.push(...response.jobs);
56
+ if (response.jobs.length < 100) return result;
57
+ }
58
+ throw new Error("Pipeline worker jobs exceed the readback bound");
59
+ }
60
+ async function observe(binding, current, warrant) {
61
+ validatePipelineWorker(binding, current, warrant);
62
+ const base = `/repos/${binding.repository}`;
63
+ const run = await request(`${base}/actions/runs/${binding.runId}`);
64
+ if (
65
+ run.id !== binding.runId ||
66
+ run.run_attempt !== binding.runAttempt ||
67
+ run.repository?.full_name !== binding.repository
68
+ )
69
+ throw new Error(
70
+ "Native worker provider run changed; terminality is unproved",
71
+ );
72
+ const entries = await jobs(base, binding);
73
+ const selected = [binding.nativeJobId, binding.sealJobId].map((id) => {
74
+ const matches = entries.filter((job) => job.id === id);
75
+ if (
76
+ matches.length !== 1 ||
77
+ matches[0].run_id !== binding.runId ||
78
+ matches[0].run_attempt !== binding.runAttempt
79
+ )
80
+ throw new Error("Native worker exact job identity drift");
81
+ return matches[0];
82
+ });
83
+ const again = await request(`${base}/actions/runs/${binding.runId}`);
84
+ if (
85
+ again.id !== run.id ||
86
+ again.run_attempt !== run.run_attempt ||
87
+ again.status !== run.status ||
88
+ again.conclusion !== run.conclusion
89
+ )
90
+ throw new Error("Native worker changed during terminal readback");
91
+ const terminal =
92
+ run.status === "completed" &&
93
+ Boolean(run.conclusion) &&
94
+ selected.every(
95
+ (job) =>
96
+ job.status === "completed" && job.conclusion && job.completed_at,
97
+ );
98
+ const body = {
99
+ schema: "buildchain.pipeline-worker-readback/v1",
100
+ binding,
101
+ status: terminal ? "completed" : "in_progress",
102
+ run,
103
+ jobs: selected,
104
+ };
105
+ return { ...body, root: recordDigest(body) };
106
+ }
107
+ return { observe };
108
+ }
@@ -0,0 +1,97 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ const LIMIT = 8 * 1024 * 1024;
5
+
6
+ async function inventory(request, prefix) {
7
+ const assets = [],
8
+ ids = new Set(),
9
+ names = new Set();
10
+ for (let page = 1; page <= 20; page++) {
11
+ const values = await request(`${prefix}/assets?per_page=100&page=${page}`);
12
+ if (!Array.isArray(values) || values.length > 100)
13
+ throw new Error("Published evidence asset inventory is incomplete");
14
+ for (const value of values) {
15
+ if (
16
+ !Number.isSafeInteger(value.id) ||
17
+ value.id < 1 ||
18
+ typeof value.name !== "string" ||
19
+ !value.name ||
20
+ ids.has(value.id) ||
21
+ names.has(value.name)
22
+ )
23
+ throw new Error("Published evidence asset inventory is ambiguous");
24
+ ids.add(value.id);
25
+ names.add(value.name);
26
+ assets.push(value);
27
+ }
28
+ if (values.length < 100) return assets;
29
+ }
30
+ throw new Error(
31
+ "Published evidence inventory exceeds its complete-read bound",
32
+ );
33
+ }
34
+
35
+ function selectedAssets(assets, names) {
36
+ return names.map((name) => {
37
+ const asset = assets.find((value) => value.name === name);
38
+ if (!asset) return { name, missing: true };
39
+ if (
40
+ asset.state !== "uploaded" ||
41
+ !Number.isSafeInteger(asset.size) ||
42
+ asset.size < 1 ||
43
+ asset.size > LIMIT ||
44
+ !/^sha256:[0-9a-f]{64}$/u.test(asset.digest || "")
45
+ )
46
+ throw new Error("Published evidence asset lacks bounded immutable bytes");
47
+ return { id: asset.id, name, size: asset.size, digest: asset.digest };
48
+ });
49
+ }
50
+
51
+ // Download only fixed evidence names from the exact already-observed release.
52
+ // There is no caller-supplied URL, artifact selector or provider write here.
53
+ export async function readReleaseEvidenceAssets(host, candidate, names) {
54
+ const repository = host.repository;
55
+ if (
56
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
57
+ !Number.isSafeInteger(candidate.id) ||
58
+ candidate.id < 1
59
+ )
60
+ throw new Error("Published evidence requires exact release coordinates");
61
+ const prefix = `/repos/${repository}/releases/${candidate.id}`;
62
+ const selected = selectedAssets(await inventory(host.request, prefix), names);
63
+ const missing = selected
64
+ .filter((value) => value.missing)
65
+ .map(({ name }) => name);
66
+ if (missing.length) return { status: "missing", missing };
67
+ const [owner, repo] = repository.split("/");
68
+ const values = {};
69
+ for (const asset of selected) {
70
+ const result = await host.github.rest.repos.getReleaseAsset({
71
+ owner,
72
+ repo,
73
+ asset_id: asset.id,
74
+ headers: { accept: "application/octet-stream" },
75
+ });
76
+ const bytes = Buffer.from(result.data);
77
+ if (
78
+ bytes.length !== asset.size ||
79
+ bytes.length > LIMIT ||
80
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
81
+ asset.digest
82
+ )
83
+ throw new Error(
84
+ "Published evidence download differs from provider bytes",
85
+ );
86
+ values[asset.name] = bytes;
87
+ }
88
+ if (
89
+ recordDigest(
90
+ selectedAssets(await inventory(host.request, prefix), names),
91
+ ) !== recordDigest(selected)
92
+ )
93
+ throw new Error(
94
+ "Published evidence assets changed during provider readback",
95
+ );
96
+ return { status: "present", values, assets: selected };
97
+ }
@@ -1,3 +1,4 @@
1
+ import { readNpmPackResult } from "../npm/pack-result.js";
1
2
  import { execFileSync } from "node:child_process";
2
3
  import fs from "node:fs";
3
4
  import path from "node:path";
@@ -117,7 +118,7 @@ export function hydratePublishedPublicationRegistry({
117
118
  for (const version of publishedVersions) {
118
119
  const packageDir = path.join(resolvedOutputDir, `.package-${version}`);
119
120
  fs.mkdirSync(packageDir, { recursive: true });
120
- const packed = JSON.parse(
121
+ const pack = readNpmPackResult(
121
122
  commandRunner(
122
123
  "npm",
123
124
  [
@@ -132,7 +133,6 @@ export function hydratePublishedPublicationRegistry({
132
133
  { cwd: resolvedCwd },
133
134
  ),
134
135
  );
135
- const pack = Array.isArray(packed) ? packed[0] : packed;
136
136
  if (!pack?.filename || !pack?.integrity) {
137
137
  throw new Error(
138
138
  `npm pack did not report filename and integrity for ${packageName}@${version}`,
@@ -1,6 +1,6 @@
1
+ import { readNpmPackResult } from "./pack-result.js";
1
2
  export function summarizePackPreview(stdout) {
2
- const parsed = JSON.parse(stdout);
3
- const pack = Array.isArray(parsed) ? parsed[0] : parsed;
3
+ const pack = readNpmPackResult(stdout);
4
4
  const files = Array.isArray(pack?.files) ? pack.files : [];
5
5
  const bin = files.find((file) => file.path === "bin/buildchain.mjs");
6
6
  const fileEntries = files.map((file) => ({
@@ -0,0 +1,31 @@
1
+ import path from "node:path";
2
+
3
+ export function readNpmPackResult(stdout) {
4
+ const parsed = JSON.parse(stdout);
5
+ const entries = Array.isArray(parsed)
6
+ ? parsed
7
+ : typeof parsed?.name === "string"
8
+ ? [parsed]
9
+ : parsed && typeof parsed === "object"
10
+ ? Object.values(parsed)
11
+ : [];
12
+ if (entries.length !== 1)
13
+ throw new Error("npm pack must return exactly one package");
14
+ const pack = entries[0];
15
+ if (
16
+ typeof pack?.name !== "string" ||
17
+ !pack.name ||
18
+ typeof pack?.version !== "string" ||
19
+ !pack.version
20
+ ) {
21
+ throw new Error("npm pack did not return package name and version");
22
+ }
23
+ if (
24
+ typeof pack.filename !== "string" ||
25
+ !pack.filename.endsWith(".tgz") ||
26
+ /[/\\]/u.test(pack.filename) ||
27
+ path.basename(pack.filename) !== pack.filename
28
+ )
29
+ throw new Error("npm pack must return a local tarball filename");
30
+ return pack;
31
+ }
@@ -1,3 +1,4 @@
1
+ import { readNpmPackResult } from "./pack-result.js";
1
2
  import crypto from "node:crypto";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
@@ -11,12 +12,8 @@ export function readPackageJson(cwd) {
11
12
  return JSON.parse(fs.readFileSync(filePath, "utf8"));
12
13
  }
13
14
 
14
- function parsePackResult(stdout) {
15
- const parsed = JSON.parse(stdout);
16
- const pack = Array.isArray(parsed) ? parsed[0] : parsed;
17
- if (!pack?.name || !pack?.version) {
18
- throw new Error("npm pack did not return package name and version");
19
- }
15
+ export function parsePackResult(stdout) {
16
+ const pack = readNpmPackResult(stdout);
20
17
  return {
21
18
  name: pack.name,
22
19
  version: pack.version,
@@ -0,0 +1,36 @@
1
+ const REGISTRY = "https://registry.npmjs.org";
2
+
3
+ export function pipelineNpmChannel({
4
+ environment = process.env,
5
+ fetchImpl = globalThis.fetch,
6
+ }) {
7
+ async function json(url, options = {}) {
8
+ const response = await fetchImpl(url, { ...options, redirect: "error" });
9
+ if (!response.ok)
10
+ throw new Error(
11
+ `npm channel provider rejected the operation with HTTP ${response.status}`,
12
+ );
13
+ return response.json();
14
+ }
15
+ return {
16
+ async observe(effect) {
17
+ if (effect.access === "restricted" && !environment.NODE_AUTH_TOKEN)
18
+ throw new Error(
19
+ "Restricted npm readback requires a configured package read credential",
20
+ );
21
+ const headers = environment.NODE_AUTH_TOKEN
22
+ ? { authorization: `Bearer ${environment.NODE_AUTH_TOKEN}` }
23
+ : {};
24
+ const tags = await json(
25
+ `${REGISTRY}/-/package/${encodeURIComponent(effect.name)}/dist-tags`,
26
+ { headers },
27
+ );
28
+ const version = tags[effect.tag] || null;
29
+ if (version !== null && typeof version !== "string")
30
+ throw new Error("npm channel has no exact version");
31
+ return version
32
+ ? { state: "present", version }
33
+ : { state: "absent", version: null };
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { publishedDigest, runNpm } from "./registry.js";
5
+ import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
6
+ import {
7
+ publicationFile,
8
+ publicationPath,
9
+ writeImmutablePublicationFile,
10
+ } from "../pipeline/files.js";
11
+
12
+ function publicationFailureCode(result) {
13
+ const code = stripVTControlCharacters(String(result.stderr ?? "")).match(
14
+ /^npm (?:error|ERR!) code ([A-Z0-9_]+)\r?$/m,
15
+ )?.[1];
16
+ return [
17
+ "ENEEDAUTH",
18
+ "EOTP",
19
+ "E401",
20
+ "E403",
21
+ "E404",
22
+ "EPUBLISHCONFLICT",
23
+ "EPRIVATE",
24
+ "EINTEGRITY",
25
+ "EUSAGE",
26
+ "EINVALIDPACKAGENAME",
27
+ "ETARGET",
28
+ "ENOENT",
29
+ "ETIMEDOUT",
30
+ "ECONNRESET",
31
+ "EAI_AGAIN",
32
+ "ENETUNREACH",
33
+ "ENOTFOUND",
34
+ ].includes(code)
35
+ ? code
36
+ : "unclassified";
37
+ }
38
+
39
+ export function pipelineNpmEnvironment(directory, environment = process.env) {
40
+ const cwd = path.resolve(directory);
41
+ const config = path.join(cwd, ".buildchain-publisher.npmrc");
42
+ fs.mkdirSync(cwd, { recursive: true });
43
+ writeImmutablePublicationFile(
44
+ config,
45
+ environment.NODE_AUTH_TOKEN
46
+ ? "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}\n"
47
+ : "",
48
+ );
49
+ const env = {
50
+ ...createNativeChildEnvironment(environment),
51
+ NPM_CONFIG_USERCONFIG: config,
52
+ NPM_CONFIG_REGISTRY: "https://registry.npmjs.org/",
53
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
54
+ };
55
+ for (const key of [
56
+ "ACTIONS_ID_TOKEN_REQUEST_URL",
57
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
58
+ "NODE_AUTH_TOKEN",
59
+ "GITHUB_ACTIONS",
60
+ ])
61
+ if (environment[key]) env[key] = environment[key];
62
+ return env;
63
+ }
64
+
65
+ export function pipelineNpmProvider({
66
+ directory,
67
+ artifacts,
68
+ environment = process.env,
69
+ run = runNpm,
70
+ lookup = publishedDigest,
71
+ }) {
72
+ const cwd = path.resolve(directory);
73
+ const env = pipelineNpmEnvironment(directory, environment);
74
+ const registry = "https://registry.npmjs.org/";
75
+ const args = { cwd, env, registry };
76
+ function artifactFor(effect) {
77
+ const found = artifacts.filter(
78
+ (artifact) => artifact.id === effect.product,
79
+ );
80
+ if (
81
+ found.length !== 1 ||
82
+ found[0].package?.name !== effect.name ||
83
+ found[0].package?.version !== effect.version ||
84
+ found[0].package?.integrity !== effect.integrity
85
+ )
86
+ throw new Error("npm publication changed qualified package identity");
87
+ const artifact = found[0];
88
+ const file = publicationPath(cwd, artifact.file);
89
+ const bytes = publicationFile(file);
90
+ if (
91
+ bytes.integrity !== effect.integrity ||
92
+ bytes.digest !== artifact.digest
93
+ )
94
+ throw new Error("npm publication bytes changed after qualification");
95
+ return file;
96
+ }
97
+ return {
98
+ async observe(effect) {
99
+ artifactFor(effect);
100
+ if (effect.access === "restricted" && !env.NODE_AUTH_TOKEN)
101
+ throw new Error(
102
+ "Restricted npm readback requires a configured package read credential",
103
+ );
104
+ const integrity = lookup({
105
+ ...args,
106
+ name: effect.name,
107
+ version: effect.version,
108
+ });
109
+ return integrity ? { state: "present", integrity } : { state: "absent" };
110
+ },
111
+ matches: (effect, observed) =>
112
+ observed.state === "present" && observed.integrity === effect.integrity,
113
+ async apply(effect) {
114
+ const file = artifactFor(effect);
115
+ const result = run({
116
+ cwd,
117
+ env,
118
+ args: [
119
+ "publish",
120
+ file,
121
+ "--ignore-scripts",
122
+ "--provenance=false",
123
+ "--access",
124
+ effect.access,
125
+ "--tag",
126
+ effect.tag,
127
+ `--registry=${registry}`,
128
+ ],
129
+ allowFailure: true,
130
+ });
131
+ if (result.status !== 0)
132
+ throw new Error(
133
+ `Sealed npm publication failed with exit ${result.status} (npm code ${publicationFailureCode(result)})`,
134
+ );
135
+ },
136
+ };
137
+ }