@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,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson } from "../../release/discussion/envelope.js";
3
+ import {
4
+ publicationFile,
5
+ publicationPath,
6
+ } from "../../publication/pipeline/files.js";
7
+ const sha256 = (bytes) =>
8
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
9
+
10
+ export function pipelineProductPayload({
11
+ qualified,
12
+ documents,
13
+ directory,
14
+ evidence,
15
+ }) {
16
+ const payload = (effect) => {
17
+ if (effect.product.startsWith("evidence:")) {
18
+ const asset = evidence.find(
19
+ (entry) => `evidence:${entry.id}` === effect.product,
20
+ );
21
+ if (
22
+ !asset ||
23
+ sha256(asset.bytes) !== effect.digest ||
24
+ asset.bytes.length !== effect.size
25
+ )
26
+ throw new Error(
27
+ "Release evidence differs from admitted immutable bytes",
28
+ );
29
+ return asset.bytes;
30
+ }
31
+ if (effect.product === "release-passport")
32
+ return Buffer.from(`${canonicalJson(documents.passport)}\n`);
33
+ const artifact = qualified.artifacts.find(
34
+ (entry) => entry.id === effect.product,
35
+ );
36
+ if (
37
+ !artifact ||
38
+ artifact.digest !== effect.digest ||
39
+ artifact.size !== effect.size
40
+ )
41
+ throw new Error(
42
+ "GitHub Release payload does not match qualified product",
43
+ );
44
+ const file = publicationFile(publicationPath(directory, artifact.file));
45
+ if (file.digest !== artifact.digest || file.size !== artifact.size)
46
+ throw new Error("GitHub Release payload bytes changed");
47
+ return file.bytes;
48
+ };
49
+ return payload;
50
+ }
@@ -0,0 +1,191 @@
1
+ import { createHash } from "node:crypto";
2
+ import { pipelineProductPayload } from "./pipeline-product-payload.js";
3
+
4
+ const sha256 = (bytes) =>
5
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
6
+
7
+ export function githubPipelineProductRelease({
8
+ github,
9
+ request,
10
+ repository,
11
+ plan,
12
+ qualified,
13
+ documents,
14
+ directory,
15
+ evidence = [],
16
+ }) {
17
+ const [owner, repo] = repository.split("/");
18
+ const prefix = `/repos/${repository}`;
19
+ const marker = `Buildchain release transaction ${documents.transaction.transactionRoot}`;
20
+ const payload = pipelineProductPayload({
21
+ qualified,
22
+ documents,
23
+ directory,
24
+ evidence,
25
+ });
26
+ async function tag() {
27
+ const value = await request(
28
+ `${prefix}/git/ref/tags/${encodeURIComponent(plan.tag)}`,
29
+ { allow404: true },
30
+ );
31
+ if (!value) return { state: "absent" };
32
+ let object = value.object;
33
+ for (let depth = 0; object.type === "tag" && depth < 4; depth++)
34
+ object = (await request(`${prefix}/git/tags/${object.sha}`)).object;
35
+ if (object.type !== "commit")
36
+ throw new Error("Exact release tag does not resolve to a commit");
37
+ return {
38
+ state: "present",
39
+ commit: object.sha,
40
+ ref: value.ref,
41
+ objectSha: value.object.sha,
42
+ };
43
+ }
44
+ async function release(create = false) {
45
+ const observedTag = await tag();
46
+ if (observedTag.state === "absent" && !create) return null;
47
+ if (observedTag.commit !== qualified.source.commit)
48
+ throw new Error("Release requires the exact immutable publication tag");
49
+ let value = await findRelease();
50
+ if (!value && create) {
51
+ try {
52
+ await github.rest.repos.createRelease({
53
+ owner,
54
+ repo,
55
+ tag_name: plan.tag,
56
+ target_commitish: qualified.source.commit,
57
+ name: plan.tag,
58
+ body: marker,
59
+ draft: true,
60
+ prerelease: plan.channel === "alpha",
61
+ make_latest: "false",
62
+ });
63
+ } catch (error) {
64
+ value = await findRelease();
65
+ if (!value) throw error;
66
+ }
67
+ value = await findRelease();
68
+ if (!value)
69
+ throw new Error("Release creation lacks exact provider readback");
70
+ }
71
+ if (
72
+ value &&
73
+ (value.tag_name !== plan.tag ||
74
+ value.body !== marker ||
75
+ value.prerelease !== (plan.channel === "alpha"))
76
+ )
77
+ throw new Error(
78
+ "Existing GitHub Release belongs to a different immutable transaction",
79
+ );
80
+ return value;
81
+ }
82
+ const findRelease = () => findPipelineRelease(github, owner, repo, plan.tag);
83
+ async function asset(effect) {
84
+ const published = await release();
85
+ if (!published) return { state: "absent" };
86
+ const assets = await github.paginate(github.rest.repos.listReleaseAssets, {
87
+ owner,
88
+ repo,
89
+ release_id: published.id,
90
+ per_page: 100,
91
+ });
92
+ const matches = assets.filter((entry) => entry.name === effect.name);
93
+ if (!matches.length) return { state: "absent" };
94
+ if (matches.length !== 1 || matches[0].size > 256 * 1024 * 1024)
95
+ throw new Error(
96
+ "Release asset identity is ambiguous or exceeds its bound",
97
+ );
98
+ const bytes = Buffer.from(
99
+ (
100
+ await github.rest.repos.getReleaseAsset({
101
+ owner,
102
+ repo,
103
+ asset_id: matches[0].id,
104
+ headers: { accept: "application/octet-stream" },
105
+ })
106
+ ).data,
107
+ );
108
+ return {
109
+ state: "present",
110
+ id: matches[0].id,
111
+ releaseId: published.id,
112
+ size: bytes.length,
113
+ digest: sha256(bytes),
114
+ };
115
+ }
116
+ return {
117
+ async observe(effect) {
118
+ if (effect.kind === "exact-tag") return tag();
119
+ if (effect.kind === "release-asset") return asset(effect);
120
+ if (effect.kind === "release-visibility") {
121
+ const value = await release();
122
+ return value && !value.draft
123
+ ? {
124
+ state: "present",
125
+ id: value.id,
126
+ tag: value.tag_name,
127
+ prerelease: value.prerelease,
128
+ }
129
+ : { state: "absent" };
130
+ }
131
+ throw new Error("Unsupported GitHub product publication operation");
132
+ },
133
+ matches(effect, observed) {
134
+ if (observed.state !== "present") return false;
135
+ if (effect.kind === "exact-tag") return observed.commit === effect.commit;
136
+ if (effect.kind === "release-asset") {
137
+ const bytes = payload(effect);
138
+ return (
139
+ observed.digest === sha256(bytes) && observed.size === bytes.length
140
+ );
141
+ }
142
+ return (
143
+ effect.kind === "release-visibility" &&
144
+ observed.tag === effect.tag &&
145
+ observed.prerelease === effect.prerelease
146
+ );
147
+ },
148
+ async apply(effect) {
149
+ if (effect.kind === "exact-tag")
150
+ return request(`${prefix}/git/refs`, {
151
+ method: "POST",
152
+ body: { ref: `refs/tags/${effect.tag}`, sha: effect.commit },
153
+ });
154
+ const value = await release(true);
155
+ if (effect.kind === "release-asset")
156
+ return github.rest.repos.uploadReleaseAsset({
157
+ owner,
158
+ repo,
159
+ release_id: value.id,
160
+ name: effect.name,
161
+ data: payload(effect),
162
+ headers: { "content-type": "application/octet-stream" },
163
+ });
164
+ if (effect.kind === "release-visibility")
165
+ return github.rest.repos.updateRelease({
166
+ owner,
167
+ repo,
168
+ release_id: value.id,
169
+ draft: false,
170
+ make_latest: "false",
171
+ });
172
+ throw new Error("Unsupported GitHub product publication operation");
173
+ },
174
+ };
175
+ }
176
+
177
+ async function findPipelineRelease(github, owner, repo, tag) {
178
+ let pages = 0;
179
+ const values = await github.paginate(
180
+ github.rest.repos.listReleases,
181
+ { owner, repo, per_page: 100 },
182
+ (response) => {
183
+ if (++pages > 100)
184
+ throw new Error("Release inventory exceeds its bounded readback");
185
+ return response.data.filter((entry) => entry.tag_name === tag);
186
+ },
187
+ );
188
+ if (values.length > 1)
189
+ throw new Error("Exact release tag has multiple provider releases");
190
+ return values[0];
191
+ }
@@ -0,0 +1,153 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import artifact from "@actions/artifact";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import { readPipelineCaller } from "./pipeline-run-entry.js";
6
+ import { publicationPath } from "../../publication/pipeline/files.js";
7
+
8
+ export const pipelinePublicationArtifactName = (plan, platform) =>
9
+ `buildchain-products-${plan.root.slice(7)}-${platform}`;
10
+
11
+ export function githubPipelinePublicationArtifacts({
12
+ request,
13
+ runs,
14
+ repository,
15
+ token,
16
+ client = artifact,
17
+ }) {
18
+ const prefix = `/repos/${repository}/actions`;
19
+ async function upload(plan, manifest, directory) {
20
+ const paths = [
21
+ publicationPath(directory, "manifest.json"),
22
+ ...manifest.artifacts.map(({ file }) => publicationPath(directory, file)),
23
+ ];
24
+ const result = await client.uploadArtifact(
25
+ pipelinePublicationArtifactName(plan, manifest.platform),
26
+ paths,
27
+ directory,
28
+ { retentionDays: 30, compressionLevel: 0 },
29
+ );
30
+ if (!result.id || !result.digest)
31
+ throw new Error(
32
+ "Product upload omitted its immutable provider coordinates",
33
+ );
34
+ return result;
35
+ }
36
+ async function buildReadback(context, selectedPlatforms) {
37
+ const { plan, materialization, runId, runAttempt } = context;
38
+ const { run, jobs } = await runs.read(runId, runAttempt);
39
+ await readPipelineCaller(
40
+ run,
41
+ materialization.source.configPath,
42
+ request,
43
+ repository,
44
+ );
45
+ const declared = [
46
+ ...new Set(plan.outputs.map(({ platform }) => platform)),
47
+ ].sort();
48
+ const platforms = selectedPlatforms || declared;
49
+ if (
50
+ !platforms.length ||
51
+ new Set(platforms).size !== platforms.length ||
52
+ platforms.some((platform) => !declared.includes(platform))
53
+ )
54
+ throw new Error("Publication subset must name unique declared platforms");
55
+ const selected = platforms.map((platform) => {
56
+ const name = `Build publication (${platform})`;
57
+ const matches = jobs.filter(
58
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
59
+ );
60
+ if (
61
+ matches.length !== 1 ||
62
+ matches[0].run_id !== runId ||
63
+ matches[0].run_attempt !== runAttempt ||
64
+ matches[0].status !== "completed" ||
65
+ matches[0].conclusion !== "success"
66
+ )
67
+ throw new Error(
68
+ "Publication needs every exact credentialless build job to succeed",
69
+ );
70
+ return matches[0];
71
+ });
72
+ const response = await request(
73
+ `${prefix}/runs/${runId}/artifacts?per_page=100`,
74
+ );
75
+ if (
76
+ !Array.isArray(response.artifacts) ||
77
+ response.total_count !== response.artifacts.length ||
78
+ response.total_count > 100
79
+ )
80
+ throw new Error("Publication artifact inventory is incomplete");
81
+ const assets = platforms.map((platform) => {
82
+ const matches = response.artifacts.filter(
83
+ (asset) =>
84
+ asset.name === pipelinePublicationArtifactName(plan, platform),
85
+ );
86
+ if (
87
+ matches.length !== 1 ||
88
+ !Number.isSafeInteger(matches[0].id) ||
89
+ matches[0].id < 1 ||
90
+ matches[0].expired ||
91
+ matches[0].workflow_run?.id !== runId ||
92
+ matches[0].workflow_run?.head_sha !== run.head_sha ||
93
+ !/^sha256:[0-9a-f]{64}$/u.test(matches[0].digest || "")
94
+ )
95
+ throw new Error(
96
+ "Publication requires one retained artifact per exact platform",
97
+ );
98
+ return matches[0];
99
+ });
100
+ const again = await runs.read(runId, runAttempt);
101
+ for (const job of selected)
102
+ if (
103
+ !again.jobs.some((entry) => recordDigest(entry) === recordDigest(job))
104
+ )
105
+ throw new Error(
106
+ "Publication build changed during independent readback",
107
+ );
108
+ if (again.run.head_sha !== run.head_sha)
109
+ throw new Error("Publication provider source changed during readback");
110
+ const body = {
111
+ schema: "buildchain.pipeline-publication-build-readback/v1",
112
+ outcome: "success",
113
+ planRoot: plan.root,
114
+ source: materialization.source,
115
+ runId,
116
+ runAttempt,
117
+ providerSource: run.head_sha,
118
+ platforms,
119
+ jobs: selected,
120
+ artifactIds: assets.map(({ id }) => id).sort((a, b) => a - b),
121
+ };
122
+ return { build: { ...body, root: recordDigest(body) }, assets };
123
+ }
124
+ async function download(context, workspace, selectedPlatforms) {
125
+ const { build, assets } = await buildReadback(context, selectedPlatforms);
126
+ const [repositoryOwner, repositoryName] = repository.split("/");
127
+ const bundles = [];
128
+ for (const asset of assets) {
129
+ const directory = path.join(workspace, String(asset.id));
130
+ fs.mkdirSync(directory, { recursive: true });
131
+ const result = await client.downloadArtifact(asset.id, {
132
+ path: directory,
133
+ expectedHash: asset.digest,
134
+ findBy: {
135
+ repositoryOwner,
136
+ repositoryName,
137
+ workflowRunId: context.runId,
138
+ token,
139
+ },
140
+ });
141
+ if (result.digestMismatch)
142
+ throw new Error(
143
+ "Product download digest does not match the provider archive",
144
+ );
145
+ const manifest = JSON.parse(
146
+ fs.readFileSync(publicationPath(directory, "manifest.json"), "utf8"),
147
+ );
148
+ bundles.push({ directory, manifest, providerArtifact: asset });
149
+ }
150
+ return { build, bundles };
151
+ }
152
+ return { upload, download, buildReadback };
153
+ }
@@ -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
+ }