@kungfu-tech/buildchain 4.1.3-alpha.0 → 4.1.3-alpha.2

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 (217) hide show
  1. package/architecture/action-taxonomy.json +16 -0
  2. package/architecture/agent-change-map.md +245 -4
  3. package/architecture/ci-lane-change-budget.json +289 -0
  4. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  5. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  6. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  7. package/architecture/decisions/0008-pipeline-product-publication.md +99 -0
  8. package/architecture/internal-capabilities.json +340 -4
  9. package/architecture/maintainability-debt.json +3 -1
  10. package/architecture/maintainability-policy.json +8 -8
  11. package/architecture/minimal-consumer-migration.json +7558 -0
  12. package/architecture/release-topology.json +248 -11
  13. package/architecture/universal-workflow-bootstrap.json +10 -0
  14. package/architecture/universal-workflow-capability-policy.json +6 -6
  15. package/architecture/workflow-taxonomy.json +55 -0
  16. package/contracts/release-discussion-v1.schema.json +3 -0
  17. package/dist/readers/business-attempt.cjs +805 -0
  18. package/dist/readers/release-discussion.cjs +4 -0
  19. package/dist/site/buildchain-contract.json +7 -5
  20. package/dist/site/buildchain-site.json +25 -15
  21. package/dist/site/capability-registry.json +3 -3
  22. package/dist/site/kfd-claims.json +311 -10
  23. package/dist/site/kfd-upstream-aggregate.json +1 -1
  24. package/dist/site/manual-registry.json +1 -1
  25. package/dist/site/node-api-registry.json +9 -9
  26. package/dist/site/page-registry.json +20 -10
  27. package/dist/site/public-surface-audit.json +201 -7
  28. package/dist/site/publication-authority-registry.json +121 -1
  29. package/dist/site/publication-registry.json +4 -4
  30. package/dist/site/site-manifest.json +5 -5
  31. package/dist/site/workflow-registry.json +416 -7
  32. package/docs/node-api-reference.md +12 -12
  33. package/docs/release-discussions.md +44 -5
  34. package/docs/runtime-entry.md +45 -5
  35. package/docs/workflow-catalog.md +5 -0
  36. package/package.json +4 -3
  37. package/packages/core/consumer/contract/entries.js +85 -0
  38. package/packages/core/consumer/contract/examples.js +96 -0
  39. package/packages/core/consumer/contract/identity.js +65 -0
  40. package/packages/core/consumer/contract/inspection.js +45 -0
  41. package/packages/core/consumer/contract/plan.js +99 -0
  42. package/packages/core/consumer/contract/products.js +101 -0
  43. package/packages/core/consumer/contract/shape.js +46 -0
  44. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  45. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  46. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  47. package/packages/core/dev-delivery/native/actions.js +7 -1
  48. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  49. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  50. package/packages/core/dev-delivery/queue/landing-action.js +5 -0
  51. package/packages/core/governance/buildchain-publication-authority.js +5 -0
  52. package/packages/core/paper/operations/bootstrap.js +2 -0
  53. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  54. package/packages/core/providers/github/attempt-index.js +60 -0
  55. package/packages/core/providers/github/attempt-journal.js +154 -0
  56. package/packages/core/providers/github/discussions/materials.js +23 -7
  57. package/packages/core/providers/github/discussions/transport.js +16 -5
  58. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  59. package/packages/core/providers/github/pipeline-events.js +52 -0
  60. package/packages/core/providers/github/pipeline-integration.js +95 -0
  61. package/packages/core/providers/github/pipeline-policy.js +213 -0
  62. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  63. package/packages/core/providers/github/pipeline-product-release.js +191 -0
  64. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  65. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  66. package/packages/core/providers/github/pipeline-runs.js +132 -0
  67. package/packages/core/providers/github/pipeline-source.js +178 -0
  68. package/packages/core/providers/github/pipeline-version.js +201 -0
  69. package/packages/core/providers/github/pipeline-worker.js +108 -0
  70. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  71. package/packages/core/publication/npm/pack-preview.js +2 -2
  72. package/packages/core/publication/npm/pack-result.js +31 -0
  73. package/packages/core/publication/npm/package.js +3 -6
  74. package/packages/core/publication/npm/pipeline-channel.js +87 -0
  75. package/packages/core/publication/npm/pipeline-provider.js +109 -0
  76. package/packages/core/publication/npm/registry.js +12 -9
  77. package/packages/core/publication/pipeline/actions.js +100 -0
  78. package/packages/core/publication/pipeline/apply.js +182 -0
  79. package/packages/core/publication/pipeline/build-segments.js +107 -0
  80. package/packages/core/publication/pipeline/capsules.js +89 -0
  81. package/packages/core/publication/pipeline/context.js +39 -0
  82. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  83. package/packages/core/publication/pipeline/development-current.js +63 -0
  84. package/packages/core/publication/pipeline/development-pr.js +85 -0
  85. package/packages/core/publication/pipeline/development-proof.js +76 -0
  86. package/packages/core/publication/pipeline/development-transition.js +112 -0
  87. package/packages/core/publication/pipeline/distribution.js +124 -0
  88. package/packages/core/publication/pipeline/documents.js +156 -0
  89. package/packages/core/publication/pipeline/effects.js +162 -0
  90. package/packages/core/publication/pipeline/files.js +66 -0
  91. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  92. package/packages/core/publication/pipeline/journal.js +110 -0
  93. package/packages/core/publication/pipeline/next-development.js +116 -0
  94. package/packages/core/publication/pipeline/pack.js +151 -0
  95. package/packages/core/publication/pipeline/package-policy.js +22 -0
  96. package/packages/core/publication/pipeline/plan.js +114 -0
  97. package/packages/core/publication/pipeline/prepare.js +151 -0
  98. package/packages/core/publication/pipeline/qualification.js +170 -0
  99. package/packages/core/publication/pipeline/qualify.js +104 -0
  100. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  101. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  102. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  103. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  104. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  105. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  106. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  107. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  108. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  109. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  110. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  111. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  112. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  113. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  114. package/packages/core/publication/pipeline/settle.js +43 -0
  115. package/packages/core/publication/pipeline/signing.js +140 -0
  116. package/packages/core/publication/pipeline/source-plan.js +83 -0
  117. package/packages/core/publication/pipeline/version.js +81 -0
  118. package/packages/core/publication/publication-reproducibility.js +2 -2
  119. package/packages/core/release/discussion/binary.js +11 -0
  120. package/packages/core/release/discussion/checkpoints.js +45 -4
  121. package/packages/core/release/discussion/envelope.js +1 -0
  122. package/packages/core/release/discussion/evidence.js +51 -0
  123. package/packages/core/release/discussion/presentation.js +92 -0
  124. package/packages/core/release/discussion/publication.js +3 -1
  125. package/packages/core/release/discussion/qualification.js +1 -0
  126. package/packages/core/release/discussion/reader.js +9 -0
  127. package/packages/core/release/discussion/session.js +2 -0
  128. package/packages/core/release/discussion/store.js +28 -31
  129. package/packages/core/release/discussion/threads.js +83 -0
  130. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  131. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  132. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  133. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  134. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  135. package/packages/core/runtime/entry/actions.js +27 -25
  136. package/packages/core/runtime/entry/attempt.js +25 -0
  137. package/packages/core/runtime/entry/selection.js +14 -0
  138. package/packages/core/runtime/entry/source.js +62 -0
  139. package/packages/core/workflow/attempt/identity.js +165 -0
  140. package/packages/core/workflow/attempt/journal.js +87 -0
  141. package/packages/core/workflow/attempt/materials.js +72 -0
  142. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  143. package/packages/core/workflow/attempt/reader.js +149 -0
  144. package/packages/core/workflow/attempt/records.js +153 -0
  145. package/packages/core/workflow/attempt/store.js +75 -0
  146. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  147. package/packages/core/workflow/pipeline/action-output.js +11 -0
  148. package/packages/core/workflow/pipeline/actions.js +62 -0
  149. package/packages/core/workflow/pipeline/build-control.js +140 -0
  150. package/packages/core/workflow/pipeline/build-evidence.js +92 -0
  151. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  152. package/packages/core/workflow/pipeline/build-result.js +69 -0
  153. package/packages/core/workflow/pipeline/build.js +95 -0
  154. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  155. package/packages/core/workflow/pipeline/channel-control.js +127 -0
  156. package/packages/core/workflow/pipeline/controller.js +133 -0
  157. package/packages/core/workflow/pipeline/delivery-control.js +204 -0
  158. package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
  159. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  160. package/packages/core/workflow/pipeline/events.js +84 -0
  161. package/packages/core/workflow/pipeline/fence.js +42 -0
  162. package/packages/core/workflow/pipeline/group-control.js +56 -0
  163. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  164. package/packages/core/workflow/pipeline/guard.js +156 -0
  165. package/packages/core/workflow/pipeline/host.js +114 -0
  166. package/packages/core/workflow/pipeline/materials.js +58 -0
  167. package/packages/core/workflow/pipeline/notifications.js +36 -0
  168. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  169. package/packages/core/workflow/pipeline/platforms.js +21 -0
  170. package/packages/core/workflow/pipeline/progress.js +108 -0
  171. package/packages/core/workflow/pipeline/projection.js +20 -0
  172. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  173. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  174. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  175. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  176. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  177. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  178. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  179. package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
  180. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  181. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  182. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  183. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  184. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  185. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  186. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  187. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  188. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  189. package/packages/core/workflow/pipeline/selection.js +106 -0
  190. package/packages/core/workflow/pipeline/session.js +72 -0
  191. package/packages/core/workflow/pipeline/settlement.js +137 -0
  192. package/packages/core/workflow/pipeline/wake.js +38 -0
  193. package/packages/core/workflow/pipeline/web-status.js +108 -0
  194. package/scripts/build-release-discussion-reader.mjs +11 -7
  195. package/scripts/check-pipeline-publication-topology.mjs +36 -0
  196. package/scripts/check-release-topology.mjs +6 -1
  197. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  198. package/scripts/verify-golden-path.mjs +5 -1
  199. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  200. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  201. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  202. package/templates/minimal-consumer/binary/package.json +6 -0
  203. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  204. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  205. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  206. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  207. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  208. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  209. package/templates/minimal-consumer/npm/package.json +6 -0
  210. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  211. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  212. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  213. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  214. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  215. package/templates/minimal-consumer/paper/package.json +6 -0
  216. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  217. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,178 @@
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 pullRequestBinding(pr) {
14
+ return {
15
+ number: pr.number,
16
+ state: pr.state,
17
+ merged: pr.merged,
18
+ mergeCommit: pr.merge_commit_sha,
19
+ head: pr.head,
20
+ base: pr.base,
21
+ draft: pr.draft,
22
+ labels: pr.labels,
23
+ updatedAt: pr.updated_at,
24
+ };
25
+ }
26
+
27
+ export function githubPipelineSource(request, repository) {
28
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
29
+ throw new Error("Invalid pipeline repository");
30
+ const base = `/repos/${repository}`;
31
+ async function source(commitInput, configPath) {
32
+ const commit = exactSha(commitInput);
33
+ const git = await request(`${base}/git/commits/${commit}`);
34
+ const config = await request(
35
+ `${base}/contents/${configPath}?ref=${commit}`,
36
+ );
37
+ if (
38
+ config.type !== "file" ||
39
+ config.encoding !== "base64" ||
40
+ !Number.isSafeInteger(config.size) ||
41
+ config.size < 1 ||
42
+ config.size > 256_000 ||
43
+ typeof config.content !== "string" ||
44
+ config.content.length > 512_000
45
+ )
46
+ throw new Error("Pipeline TOML is missing or exceeds its byte bound");
47
+ const bytes = Buffer.from(config.content, "base64");
48
+ if (bytes.length !== config.size)
49
+ throw new Error("Pipeline TOML size drift");
50
+ return bindConsumerSource(
51
+ {
52
+ repository,
53
+ commit,
54
+ tree: exactSha(git.tree?.sha),
55
+ configPath,
56
+ configBlob: config.sha,
57
+ },
58
+ bytes,
59
+ );
60
+ }
61
+ async function branchHead(branch) {
62
+ if (
63
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
64
+ branch || "",
65
+ ) ||
66
+ branch.includes("..")
67
+ )
68
+ throw new Error("Invalid protected branch");
69
+ const pointer = await request(`${base}/git/ref/heads/${branch}`);
70
+ if (pointer.object?.type !== "commit")
71
+ throw new Error("Protected ref is not a commit");
72
+ return exactSha(pointer.object.sha);
73
+ }
74
+ async function observe(pullRequest, inputs = {}) {
75
+ if (!Number.isSafeInteger(pullRequest) || pullRequest < 1)
76
+ throw new Error("Pipeline requires an exact PR number");
77
+ const { configPath } = normalInputs(inputs);
78
+ const pr = await request(`${base}/pulls/${pullRequest}`);
79
+ const repositoryInfo = await request(base);
80
+ if (pr.number !== pullRequest || !repositoryInfo.node_id)
81
+ throw new Error("Pipeline PR provider identity drift");
82
+ // Read delivery policy from the protected base. A PR may change its own
83
+ // product commands, but cannot lower review or grant itself a channel route.
84
+ const baseCommit = await branchHead(pr.base?.ref);
85
+ const protectedSource = await source(baseCommit, configPath);
86
+ const route = channelRoute(protectedSource.plan, pr, repository);
87
+ if (!route) {
88
+ if (baseCommit !== (await branchHead(pr.base.ref)))
89
+ throw new Error("Protected policy changed during route observation");
90
+ return { eligible: false, reason: "channel-not-enabled" };
91
+ }
92
+ const admitted = await source(pr.head.sha, configPath);
93
+ const again = await request(`${base}/pulls/${pullRequest}`);
94
+ if (
95
+ recordDigest(pullRequestBinding(pr)) !==
96
+ recordDigest(pullRequestBinding(again)) ||
97
+ baseCommit !== (await branchHead(pr.base.ref))
98
+ )
99
+ throw new Error("Pipeline source changed during observation; reobserve");
100
+ return {
101
+ eligible: true,
102
+ repositoryId: repositoryInfo.node_id,
103
+ route,
104
+ plan: admitted.plan,
105
+ protectedPlan: protectedSource.plan,
106
+ protectedSource: protectedSource.identity,
107
+ live: {
108
+ pullRequest,
109
+ observedHead: pr.head.sha,
110
+ targetBranch: pr.base.ref,
111
+ routeEnabled: true,
112
+ source: admitted.identity,
113
+ baseCommit,
114
+ state: pr.state,
115
+ merged: pr.merged === true,
116
+ mergeCommit: pr.merge_commit_sha,
117
+ draft: pr.draft === true,
118
+ ready: pr.labels.some((label) => label.name === "ready"),
119
+ },
120
+ };
121
+ }
122
+ async function observeIntent(intent, generation, inputs = {}) {
123
+ validateGeneration(generation, intent);
124
+ if (intent.repository !== repository)
125
+ throw new Error("Recorded intent repository drift");
126
+ const number = intent.source.pullRequest;
127
+ const pr = await request(`${base}/pulls/${number}`);
128
+ const repositoryInfo = await request(base);
129
+ if (
130
+ repositoryInfo.node_id !== intent.source.repositoryId ||
131
+ pr.number !== number ||
132
+ pr.base?.repo?.full_name !== repository
133
+ )
134
+ throw new Error("Recorded pipeline intent provider identity drift");
135
+ const sameHead =
136
+ pr.head?.repo?.full_name === repository &&
137
+ pr.head.sha === generation.source.commit;
138
+ const live = {
139
+ pullRequest: number,
140
+ observedHead: exactSha(pr.head?.sha),
141
+ targetBranch: pr.base.ref,
142
+ state: pr.state,
143
+ merged: pr.merged === true,
144
+ mergeCommit: pr.merge_commit_sha,
145
+ source: sameHead ? generation.source : null,
146
+ baseCommit: null,
147
+ ready: pr.labels.some((label) => label.name === "ready"),
148
+ draft: pr.draft === true,
149
+ };
150
+ // Closing, updating or retargeting an admitted PR must remain recoverable
151
+ // even when its new TOML is invalid, its old route disappeared or it is now a fork.
152
+ if (
153
+ pr.state !== "open" ||
154
+ !sameHead ||
155
+ pr.base.ref !== intent.source.targetBranch
156
+ ) {
157
+ const again = await request(`${base}/pulls/${number}`);
158
+ if (
159
+ recordDigest(pullRequestBinding(pr)) !==
160
+ recordDigest(pullRequestBinding(again))
161
+ )
162
+ throw new Error("Recorded intent changed during terminal observation");
163
+ return { eligible: true, cleanupOnly: true, live };
164
+ }
165
+ const observed = await observe(number, inputs);
166
+ if (observed.eligible) return observed;
167
+ return {
168
+ eligible: true,
169
+ cleanupOnly: true,
170
+ live: {
171
+ ...live,
172
+ baseCommit: await branchHead(intent.source.targetBranch),
173
+ routeEnabled: false,
174
+ },
175
+ };
176
+ }
177
+ return { observe, observeIntent, source, branchHead };
178
+ }
@@ -0,0 +1,201 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import {
4
+ materializePipelineVersion,
5
+ readPipelineVersion,
6
+ } from "../../publication/pipeline/version.js";
7
+ import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
8
+
9
+ function blobBytes(value, sha) {
10
+ if (
11
+ value.encoding !== "base64" ||
12
+ value.size > 4 * 1024 * 1024 ||
13
+ typeof value.content !== "string"
14
+ )
15
+ throw new Error("Version material requires a bounded Git blob");
16
+ const bytes = Buffer.from(value.content, "base64");
17
+ const digest = createHash("sha1")
18
+ .update(`blob ${bytes.length}\0`)
19
+ .update(bytes)
20
+ .digest("hex");
21
+ if (bytes.length !== value.size || digest !== sha || value.sha !== sha)
22
+ throw new Error("Version material blob identity drift");
23
+ return bytes.toString("utf8");
24
+ }
25
+
26
+ export function githubPipelineVersion(request, repository) {
27
+ const base = `/repos/${repository}`;
28
+ const post = (url, body) => request(url, { method: "POST", body });
29
+ async function inspect(source, policy) {
30
+ if (source.repository !== repository)
31
+ throw new Error("Version material repository mismatch");
32
+ const commit = await request(`${base}/git/commits/${source.commit}`);
33
+ if (commit.sha !== source.commit || commit.tree?.sha !== source.tree)
34
+ throw new Error("Version material source tree drift");
35
+ const tree = await request(`${base}/git/trees/${source.tree}?recursive=1`);
36
+ if (tree.truncated || tree.sha !== source.tree || !Array.isArray(tree.tree))
37
+ throw new Error("Version material requires a complete source tree");
38
+ const files = {},
39
+ entries = {};
40
+ for (const file of [
41
+ ...policy.files,
42
+ ...(policy.derived_files || []).map((path) => ({ path })),
43
+ ]) {
44
+ if (Object.hasOwn(files, file.path)) continue;
45
+ const found = tree.tree.filter((entry) => entry.path === file.path);
46
+ if (
47
+ found.length !== 1 ||
48
+ found[0].type !== "blob" ||
49
+ !["100644", "100755"].includes(found[0].mode)
50
+ )
51
+ throw new Error("Version material must be a tracked regular file");
52
+ const entry = found[0];
53
+ entries[file.path] = entry;
54
+ files[file.path] = blobBytes(
55
+ await request(`${base}/git/blobs/${entry.sha}`),
56
+ entry.sha,
57
+ );
58
+ }
59
+ return {
60
+ commit,
61
+ files,
62
+ entries,
63
+ version: readPipelineVersion(policy, files),
64
+ sourceTimestamp: commit.committer.date,
65
+ };
66
+ }
67
+ async function materialize(plan) {
68
+ verifyPipelinePublicationPlan(plan);
69
+ const input = await inspect(plan.source, plan.versionPolicy);
70
+ if (input.version !== plan.candidateVersion)
71
+ throw new Error("Version material changed after planning");
72
+ const material = materializePipelineVersion(
73
+ plan.versionPolicy,
74
+ input.files,
75
+ plan.version,
76
+ );
77
+ let source = plan.source;
78
+ if (material.changes.length)
79
+ source = await createMaterializedSource(plan, input, material);
80
+ const body = {
81
+ schema: "buildchain.pipeline-version-materialization/v1",
82
+ planRoot: plan.root,
83
+ protectedSource: plan.source,
84
+ source,
85
+ material,
86
+ };
87
+ return { ...body, root: recordDigest(body) };
88
+ }
89
+ async function materializeDevelopment(plan) {
90
+ const input = await inspect(plan.source, plan.versionPolicy);
91
+ const material = materializePipelineVersion(
92
+ plan.versionPolicy,
93
+ input.files,
94
+ plan.version,
95
+ );
96
+ const branch = `feature/buildchain-next/${plan.root.slice(7)}`;
97
+ const source = material.changes.length
98
+ ? await createMaterializedSource(plan, input, material, `heads/${branch}`)
99
+ : plan.source;
100
+ const body = {
101
+ schema: "buildchain.pipeline-development-materialization/v1",
102
+ transitionRoot: plan.root,
103
+ protectedSource: plan.source,
104
+ source,
105
+ branch,
106
+ material,
107
+ };
108
+ return { ...body, root: recordDigest(body) };
109
+ }
110
+ async function createMaterializedSource(plan, input, material, targetRef) {
111
+ const changes = material.changes.map(({ path, content }) => ({
112
+ path,
113
+ content,
114
+ mode: input.entries[path].mode,
115
+ type: "blob",
116
+ }));
117
+ const tree = await post(`${base}/git/trees`, {
118
+ base_tree: plan.source.tree,
119
+ tree: changes,
120
+ });
121
+ const identity = {
122
+ name: "Buildchain automation",
123
+ email: "noreply@github.com",
124
+ date: plan.sourceTimestamp,
125
+ };
126
+ const parents = [plan.source.commit];
127
+ // Preserve the prior published version overlay as real Git ancestry. The
128
+ // resulting tree still changes only the current protected version fields;
129
+ // no provider force-update is needed for the next stable floating channel.
130
+ if (
131
+ plan.previousChannelCommit &&
132
+ plan.previousChannelCommit !== plan.source.commit
133
+ ) {
134
+ if (!/^[0-9a-f]{40}$/u.test(plan.previousChannelCommit))
135
+ throw new Error(
136
+ "Version overlay parent must be an exact prior channel commit",
137
+ );
138
+ const previous = await request(
139
+ `${base}/git/commits/${plan.previousChannelCommit}`,
140
+ );
141
+ if (previous.sha !== plan.previousChannelCommit)
142
+ throw new Error("Prior version overlay commit changed");
143
+ parents.push(previous.sha);
144
+ }
145
+ const created = await post(`${base}/git/commits`, {
146
+ message: `chore(release): materialize ${plan.version}\n\nBuildchain-Plan: ${plan.root}\nSigned-off-by: Buildchain automation <noreply@github.com>`,
147
+ tree: tree.sha,
148
+ parents,
149
+ author: identity,
150
+ committer: identity,
151
+ });
152
+ const commit = await request(`${base}/git/commits/${created.sha}`);
153
+ if (
154
+ commit.tree?.sha !== tree.sha ||
155
+ recordDigest(commit.parents?.map(({ sha }) => sha)) !==
156
+ recordDigest(parents)
157
+ )
158
+ throw new Error("Materialized version commit readback mismatch");
159
+ const source = { ...plan.source, commit: commit.sha, tree: tree.sha };
160
+ const verified = await inspect(source, plan.versionPolicy);
161
+ if (verified.version !== plan.version)
162
+ throw new Error("Version material did not retain the planned version");
163
+ const expectedFiles = {
164
+ ...input.files,
165
+ ...Object.fromEntries(
166
+ material.changes.map(({ path, content }) => [path, content]),
167
+ ),
168
+ };
169
+ if (recordDigest(verified.files) !== recordDigest(expectedFiles))
170
+ throw new Error(
171
+ "Version material changed document bytes beyond the planned fields",
172
+ );
173
+ const comparison = await request(
174
+ `${base}/compare/${plan.source.commit}...${commit.sha}`,
175
+ );
176
+ const paths = comparison.files?.map((file) => file.filename).sort();
177
+ if (
178
+ comparison.status !== "ahead" ||
179
+ (parents.length === 1 && comparison.total_commits !== 1) ||
180
+ recordDigest(paths) !==
181
+ recordDigest(material.changes.map(({ path }) => path).sort()) ||
182
+ comparison.files.some((file) => file.status !== "modified")
183
+ )
184
+ throw new Error(
185
+ "Version material changed paths outside the declared version fields",
186
+ );
187
+ const ref =
188
+ targetRef || `heads/buildchain/publication-source/${plan.root.slice(7)}`;
189
+ try {
190
+ await post(`${base}/git/refs`, { ref: `refs/${ref}`, sha: commit.sha });
191
+ } catch (error) {
192
+ const observed = await request(`${base}/git/ref/${ref}`);
193
+ if (observed.object?.sha !== commit.sha) throw error;
194
+ }
195
+ const observed = await request(`${base}/git/ref/${ref}`);
196
+ if (observed.object?.sha !== commit.sha)
197
+ throw new Error("Version material ref readback mismatch");
198
+ return source;
199
+ }
200
+ return { inspect, materialize, materializeDevelopment };
201
+ }
@@ -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
+ }
@@ -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,87 @@
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
+ async function credential(name) {
16
+ if (environment.NODE_AUTH_TOKEN) return environment.NODE_AUTH_TOKEN;
17
+ if (
18
+ !environment.ACTIONS_ID_TOKEN_REQUEST_URL ||
19
+ !environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN
20
+ )
21
+ throw new Error(
22
+ "npm channel publication requires its hosted package-scoped OIDC or configured publisher credential",
23
+ );
24
+ const url = new URL(environment.ACTIONS_ID_TOKEN_REQUEST_URL);
25
+ if (url.protocol !== "https:")
26
+ throw new Error("OIDC request requires the provider HTTPS endpoint");
27
+ url.searchParams.set("audience", "npm:registry.npmjs.org");
28
+ const identity = await json(url.href, {
29
+ headers: {
30
+ authorization: `Bearer ${environment.ACTIONS_ID_TOKEN_REQUEST_TOKEN}`,
31
+ accept: "application/json",
32
+ },
33
+ });
34
+ if (typeof identity.value !== "string" || !identity.value)
35
+ throw new Error("Hosted OIDC provider omitted its identity token");
36
+ const exchange = await json(
37
+ `${REGISTRY}/-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(name)}`,
38
+ {
39
+ method: "POST",
40
+ headers: {
41
+ authorization: `Bearer ${identity.value}`,
42
+ accept: "application/json",
43
+ },
44
+ },
45
+ );
46
+ if (typeof exchange.token !== "string" || !exchange.token)
47
+ throw new Error(
48
+ "npm OIDC exchange omitted its package-scoped credential",
49
+ );
50
+ return exchange.token;
51
+ }
52
+ return {
53
+ async observe(effect) {
54
+ if (effect.access === "restricted" && !environment.NODE_AUTH_TOKEN)
55
+ throw new Error(
56
+ "Restricted npm readback requires a configured package read credential",
57
+ );
58
+ const headers = environment.NODE_AUTH_TOKEN
59
+ ? { authorization: `Bearer ${environment.NODE_AUTH_TOKEN}` }
60
+ : {};
61
+ const tags = await json(
62
+ `${REGISTRY}/-/package/${encodeURIComponent(effect.name)}/dist-tags`,
63
+ { headers },
64
+ );
65
+ const version = tags[effect.tag] || null;
66
+ if (version !== null && typeof version !== "string")
67
+ throw new Error("npm channel has no exact version");
68
+ return version
69
+ ? { state: "present", version }
70
+ : { state: "absent", version: null };
71
+ },
72
+ async apply(effect) {
73
+ const token = await credential(effect.name);
74
+ await json(
75
+ `${REGISTRY}/-/package/${encodeURIComponent(effect.name)}/dist-tags/${encodeURIComponent(effect.tag)}`,
76
+ {
77
+ method: "PUT",
78
+ headers: {
79
+ authorization: `Bearer ${token}`,
80
+ "content-type": "application/json",
81
+ },
82
+ body: JSON.stringify(effect.version),
83
+ },
84
+ );
85
+ },
86
+ };
87
+ }