@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,372 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
4
+ import { compareDevelopmentVersions } from "../../publication/publication-development.js";
5
+ import { githubPipelineSource } from "./pipeline-source.js";
6
+ import { githubPipelineVersion } from "./pipeline-version.js";
7
+ import { normalInputs } from "../../consumer/contract/entries.js";
8
+ import { readPipelineStableBaseline } from "./pipeline-stable-baseline.js";
9
+
10
+ const SHA = /^[0-9a-f]{40}$/u;
11
+ const STABLE = /^v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
12
+
13
+ function publishedRelease(value, repository, tag, prerelease) {
14
+ if (
15
+ !Number.isSafeInteger(value?.id) ||
16
+ value.id < 1 ||
17
+ value.tag_name !== tag ||
18
+ value.draft !== false ||
19
+ value.prerelease !== prerelease ||
20
+ value.html_url !== `https://github.com/${repository}/releases/tag/${tag}` ||
21
+ typeof value.published_at !== "string" ||
22
+ !Number.isFinite(Date.parse(value.published_at))
23
+ )
24
+ throw new Error(
25
+ "Stable eligibility requires the exact public release identity",
26
+ );
27
+ return {
28
+ id: value.id,
29
+ tag,
30
+ publishedAt: new Date(value.published_at).toISOString(),
31
+ };
32
+ }
33
+
34
+ async function exactTag(get, tag) {
35
+ const pointer = await get(`/git/ref/tags/${encodeURIComponent(tag)}`);
36
+ if (
37
+ pointer.ref !== `refs/tags/${tag}` ||
38
+ !SHA.test(pointer.object?.sha || "")
39
+ )
40
+ throw new Error("Stable release tag identity changed");
41
+ let object = pointer.object;
42
+ for (let depth = 0; object.type === "tag" && depth < 4; depth++) {
43
+ const annotated = await get(`/git/tags/${object.sha}`);
44
+ if (annotated.sha !== object.sha || !SHA.test(annotated.object?.sha || ""))
45
+ throw new Error("Stable annotated tag identity changed");
46
+ object = annotated.object;
47
+ }
48
+ if (object.type !== "commit" || !SHA.test(object.sha))
49
+ throw new Error(
50
+ "Stable release tag must resolve to a bounded exact commit",
51
+ );
52
+ return { object: pointer.object.sha, commit: object.sha };
53
+ }
54
+
55
+ async function releases(get) {
56
+ const result = [],
57
+ ids = new Set();
58
+ for (let page = 1; page <= 20; page++) {
59
+ const values = await get(`/releases?per_page=100&page=${page}`);
60
+ if (!Array.isArray(values) || values.length > 100)
61
+ throw new Error("Stable release inventory is incomplete");
62
+ for (const value of values) {
63
+ if (!Number.isSafeInteger(value.id) || value.id < 1 || ids.has(value.id))
64
+ throw new Error(
65
+ "Stable release inventory has duplicate or invalid identities",
66
+ );
67
+ ids.add(value.id);
68
+ result.push(value);
69
+ }
70
+ if (values.length < 100) return result;
71
+ }
72
+ throw new Error("Stable release inventory exceeds its complete-read bound");
73
+ }
74
+
75
+ function predecessors(values, plan) {
76
+ const older = [],
77
+ tags = new Set(),
78
+ major = plan.version.split(".")[0];
79
+ for (const value of values) {
80
+ const match = STABLE.exec(value.tag_name || "");
81
+ if (!match || match[1] !== major || value.draft === true) continue;
82
+ const release = publishedRelease(
83
+ value,
84
+ plan.source.repository,
85
+ value.tag_name,
86
+ false,
87
+ );
88
+ if (tags.has(release.tag))
89
+ throw new Error("Stable release inventory repeats an exact tag");
90
+ tags.add(release.tag);
91
+ const order = compareDevelopmentVersions(
92
+ release.tag.slice(1),
93
+ plan.version,
94
+ );
95
+ if (order > 0)
96
+ throw new Error("A newer stable product has already been published");
97
+ // The current version can already exist during recovery of the same
98
+ // transaction. The effect journal separately verifies its exact bytes.
99
+ if (order < 0) older.push(release);
100
+ }
101
+ older.sort((a, b) =>
102
+ compareDevelopmentVersions(b.tag.slice(1), a.tag.slice(1)),
103
+ );
104
+ const comparison = older[0] || null;
105
+ const latest =
106
+ [...older].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt))[0] ||
107
+ null;
108
+ return { comparison, latest };
109
+ }
110
+
111
+ async function treeFiles(get, commitSha) {
112
+ const commit = await get(`/git/commits/${commitSha}`);
113
+ if (commit.sha !== commitSha || !SHA.test(commit.tree?.sha || ""))
114
+ throw new Error("Stable source commit or tree identity changed");
115
+ const tree = await get(`/git/trees/${commit.tree.sha}?recursive=1`);
116
+ if (
117
+ tree.sha !== commit.tree.sha ||
118
+ tree.truncated !== false ||
119
+ !Array.isArray(tree.tree) ||
120
+ tree.tree.length > 100_000
121
+ )
122
+ throw new Error("Stable product difference requires a complete Git tree");
123
+ const files = new Map(),
124
+ paths = new Set();
125
+ for (const entry of tree.tree) {
126
+ if (
127
+ typeof entry.path !== "string" ||
128
+ !entry.path ||
129
+ entry.path
130
+ .split("/")
131
+ .some((part) => !part || part === "." || part === "..") ||
132
+ paths.has(entry.path) ||
133
+ !SHA.test(entry.sha || "")
134
+ )
135
+ throw new Error("Stable product tree has an ambiguous or invalid path");
136
+ paths.add(entry.path);
137
+ const modes = {
138
+ tree: ["040000"],
139
+ blob: ["100644", "100755", "120000"],
140
+ commit: ["160000"],
141
+ };
142
+ if (!modes[entry.type]?.includes(entry.mode))
143
+ throw new Error(
144
+ "Stable product tree has an unsupported entry type or mode",
145
+ );
146
+ if (entry.type !== "tree")
147
+ files.set(entry.path, {
148
+ sha: entry.sha,
149
+ mode: entry.mode,
150
+ type: entry.type,
151
+ });
152
+ }
153
+ return { tree: commit.tree.sha, files };
154
+ }
155
+
156
+ async function impactDocument(get, files, pathname, version) {
157
+ const entry = files.get(pathname);
158
+ if (entry?.type !== "blob" || !["100644", "100755"].includes(entry.mode))
159
+ throw new Error(
160
+ "Stable impact must be a regular file in the exact Alpha tree",
161
+ );
162
+ const blob = await get(`/git/blobs/${entry.sha}`);
163
+ if (
164
+ blob.sha !== entry.sha ||
165
+ blob.encoding !== "base64" ||
166
+ !Number.isSafeInteger(blob.size) ||
167
+ blob.size < 1 ||
168
+ blob.size > 1024 * 1024 ||
169
+ typeof blob.content !== "string" ||
170
+ blob.content.length > 2 * 1024 * 1024
171
+ )
172
+ throw new Error("Stable impact exceeds its exact Git blob boundary");
173
+ const bytes = Buffer.from(blob.content, "base64");
174
+ const sha = createHash("sha1")
175
+ .update(`blob ${bytes.length}\0`)
176
+ .update(bytes)
177
+ .digest("hex");
178
+ if (bytes.length !== blob.size || sha !== entry.sha)
179
+ throw new Error("Stable impact Git blob bytes changed");
180
+ const value = JSON.parse(
181
+ new TextDecoder("utf-8", { fatal: true }).decode(bytes),
182
+ );
183
+ if (value?.release?.version !== version)
184
+ throw new Error("Stable impact does not belong to the exact Alpha version");
185
+ return {
186
+ path: pathname,
187
+ blob: sha,
188
+ digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
189
+ value,
190
+ };
191
+ }
192
+
193
+ function changedPaths(before, after) {
194
+ return [...new Set([...before.keys(), ...after.keys()])]
195
+ .filter(
196
+ (pathname) =>
197
+ recordDigest(before.get(pathname) || null) !==
198
+ recordDigest(after.get(pathname) || null),
199
+ )
200
+ .sort();
201
+ }
202
+
203
+ function stableSourceReader(plan, host) {
204
+ verifyPipelinePublicationPlan(plan);
205
+ const repository = plan.source.repository;
206
+ if (
207
+ host.repository !== repository ||
208
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository) ||
209
+ plan.channel !== "stable" ||
210
+ !plan.stablePolicy ||
211
+ !/^\d+\.\d+\.\d+-alpha\.\d+$/u.test(plan.candidateVersion) ||
212
+ plan.candidateVersion.split("-alpha.")[0] !== plan.version
213
+ )
214
+ throw new Error(
215
+ "Stable source collection requires its exact repository and Alpha candidate plan",
216
+ );
217
+ const base = `/repos/${repository}`;
218
+ normalInputs({ "config-path": plan.source.configPath });
219
+ const request = (endpoint, options = {}) => {
220
+ if (
221
+ !endpoint.startsWith(`${base}/`) ||
222
+ (options.method && options.method !== "GET")
223
+ )
224
+ throw new Error(
225
+ "Stable source collection cannot mutate or cross repositories",
226
+ );
227
+ return host.request(endpoint, options);
228
+ };
229
+ const get = (pathname) => request(`${base}${pathname}`);
230
+ return { repository, request, get };
231
+ }
232
+
233
+ async function recheckStableSources(
234
+ get,
235
+ { plan, pointer, release, selected, prior, latest },
236
+ ) {
237
+ const repository = plan.source.repository,
238
+ tag = `v${plan.candidateVersion}`;
239
+ const repeatedRelease = publishedRelease(
240
+ await get(`/releases/tags/${tag}`),
241
+ repository,
242
+ tag,
243
+ true,
244
+ );
245
+ if (
246
+ recordDigest(await exactTag(get, tag)) !== recordDigest(pointer) ||
247
+ recordDigest(repeatedRelease) !== recordDigest(release) ||
248
+ recordDigest(predecessors(await releases(get), plan)) !==
249
+ recordDigest(selected) ||
250
+ (prior &&
251
+ recordDigest(await exactTag(get, selected.comparison.tag)) !==
252
+ recordDigest(prior)) ||
253
+ (latest &&
254
+ recordDigest(await exactTag(get, selected.latest.tag)) !==
255
+ recordDigest(latest))
256
+ )
257
+ throw new Error("Stable source facts changed during provider readback");
258
+ }
259
+
260
+ // Read-only provider facts. This result intentionally contains no successful
261
+ // canary or release authorization; those require independent qualification.
262
+ export async function readPipelineStableSource(plan, host) {
263
+ const { repository, request, get } = stableSourceReader(plan, host);
264
+ const tag = `v${plan.candidateVersion}`;
265
+ const pointer = await exactTag(get, tag);
266
+ if (pointer.commit !== plan.intentSource.commit)
267
+ throw new Error("Published Alpha differs from the exact channel PR source");
268
+ const release = publishedRelease(
269
+ await get(`/releases/tags/${tag}`),
270
+ repository,
271
+ tag,
272
+ true,
273
+ );
274
+ const candidate = await githubPipelineSource(request, repository).source(
275
+ pointer.commit,
276
+ plan.source.configPath,
277
+ );
278
+ if (
279
+ recordDigest(candidate.identity) !== recordDigest(plan.intentSource) ||
280
+ recordDigest(candidate.plan) !== plan.contractRoot ||
281
+ recordDigest(candidate.plan.stable) !== recordDigest(plan.stablePolicy) ||
282
+ recordDigest(candidate.plan.version) !== recordDigest(plan.versionPolicy)
283
+ )
284
+ throw new Error(
285
+ "Published Alpha source or product policy differs from the admitted plan",
286
+ );
287
+ const version = await githubPipelineVersion(request, repository).inspect(
288
+ candidate.identity,
289
+ candidate.plan.version,
290
+ );
291
+ if (version.version !== plan.candidateVersion)
292
+ throw new Error(
293
+ "Published Alpha version documents disagree with its exact tag",
294
+ );
295
+ const selected = predecessors(await releases(get), plan);
296
+ const prior = selected.comparison
297
+ ? await exactTag(get, selected.comparison.tag)
298
+ : null;
299
+ const baseline = await readPipelineStableBaseline(
300
+ plan,
301
+ { ...host, request },
302
+ selected.comparison,
303
+ prior,
304
+ );
305
+ const current = await treeFiles(get, pointer.commit);
306
+ if (current.tree !== candidate.identity.tree)
307
+ throw new Error(
308
+ "Alpha tree changed between independent source observations",
309
+ );
310
+ const config = current.files.get(candidate.identity.configPath);
311
+ if (
312
+ config?.type !== "blob" ||
313
+ !["100644", "100755"].includes(config.mode) ||
314
+ config.sha !== candidate.identity.configBlob
315
+ )
316
+ throw new Error(
317
+ "Alpha configuration is not the exact blob in its complete tree",
318
+ );
319
+ const previous = prior
320
+ ? await treeFiles(get, prior.commit)
321
+ : { files: new Map() };
322
+ const impact = await impactDocument(
323
+ get,
324
+ current.files,
325
+ plan.stablePolicy.impact_file,
326
+ plan.candidateVersion,
327
+ );
328
+ const latest =
329
+ selected.latest &&
330
+ (selected.latest.tag === selected.comparison.tag
331
+ ? prior
332
+ : await exactTag(get, selected.latest.tag));
333
+ await recheckStableSources(get, {
334
+ plan,
335
+ pointer,
336
+ release,
337
+ selected,
338
+ prior,
339
+ latest,
340
+ });
341
+ if (
342
+ baseline &&
343
+ recordDigest(
344
+ await readPipelineStableBaseline(
345
+ plan,
346
+ { ...host, request },
347
+ selected.comparison,
348
+ prior,
349
+ ),
350
+ ) !== recordDigest(baseline)
351
+ )
352
+ throw new Error(
353
+ "Stable baseline evidence changed during provider readback",
354
+ );
355
+ const body = {
356
+ schema: "buildchain.pipeline-stable-source/v1",
357
+ planRoot: plan.root,
358
+ source: candidate.identity,
359
+ contractRoot: plan.contractRoot,
360
+ candidate: { ...release, sha: pointer.commit, tree: current.tree },
361
+ previousStable: selected.latest
362
+ ? { ...selected.latest, sha: latest.commit }
363
+ : null,
364
+ comparisonStable: selected.comparison
365
+ ? { ...selected.comparison, sha: prior.commit, tree: previous.tree }
366
+ : null,
367
+ ...(baseline ? { comparisonChannel: baseline } : {}),
368
+ impact,
369
+ changedPaths: changedPaths(previous.files, current.files),
370
+ };
371
+ return { ...body, root: recordDigest(body) };
372
+ }
@@ -0,0 +1,91 @@
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 {
6
+ publicationPath,
7
+ writeImmutablePublicationFile,
8
+ } from "../../publication/pipeline/files.js";
9
+ import { pipelineVersionRegenerationResult } from "../../publication/pipeline/version-regeneration.js";
10
+ import {
11
+ pipelineVersionArtifactName,
12
+ readPipelineVersionBuild,
13
+ } from "./pipeline-version-readback.js";
14
+
15
+ function verifyResult(context, result, platform) {
16
+ const expected = pipelineVersionRegenerationResult(
17
+ context.preparation,
18
+ platform,
19
+ result.files,
20
+ );
21
+ if (recordDigest(result) !== recordDigest(expected))
22
+ throw new Error(
23
+ "Version artifact changed its exact preparation, platform or bytes",
24
+ );
25
+ return result;
26
+ }
27
+
28
+ export function githubPipelineVersionArtifacts(host, client = artifact) {
29
+ async function upload(context, result, directory) {
30
+ verifyResult(context, result, result.platform);
31
+ const file = writeImmutablePublicationFile(
32
+ path.join(directory, "version.json"),
33
+ JSON.stringify(result),
34
+ );
35
+ const uploaded = await client.uploadArtifact(
36
+ pipelineVersionArtifactName(context, result.platform),
37
+ [file],
38
+ directory,
39
+ {
40
+ retentionDays: 30,
41
+ compressionLevel: 0,
42
+ },
43
+ );
44
+ if (!uploaded.id || !uploaded.digest)
45
+ throw new Error("Version upload omitted immutable provider coordinates");
46
+ return uploaded;
47
+ }
48
+ async function download(context, directory) {
49
+ const { build, assets } = await readPipelineVersionBuild(context, host);
50
+ const [repositoryOwner, repositoryName] = host.repository.split("/");
51
+ const results = [];
52
+ for (const [index, asset] of assets.entries()) {
53
+ const target = path.join(directory, String(asset.id));
54
+ fs.mkdirSync(target, { recursive: true });
55
+ const downloaded = await client.downloadArtifact(asset.id, {
56
+ path: target,
57
+ expectedHash: asset.digest,
58
+ findBy: {
59
+ repositoryOwner,
60
+ repositoryName,
61
+ workflowRunId: context.runId,
62
+ token: host.token,
63
+ },
64
+ });
65
+ if (downloaded.digestMismatch)
66
+ throw new Error(
67
+ "Version download digest differs from its provider archive",
68
+ );
69
+ if (
70
+ recordDigest(fs.readdirSync(target)) !== recordDigest(["version.json"])
71
+ )
72
+ throw new Error("Version artifact contains undeclared files");
73
+ const file = publicationPath(target, "version.json");
74
+ // JSON escaping can expand the bounded UTF-8 document inventory sixfold.
75
+ if (fs.statSync(file).size > 384 * 1024 * 1024)
76
+ throw new Error("Version artifact exceeds its encoded material limit");
77
+ const bytes = fs.readFileSync(file);
78
+ if (!Buffer.from(bytes.toString("utf8")).equals(bytes))
79
+ throw new Error("Version artifact is not canonical UTF-8 text");
80
+ results.push(
81
+ verifyResult(
82
+ context,
83
+ JSON.parse(bytes.toString("utf8")),
84
+ context.preparation.platforms[index],
85
+ ),
86
+ );
87
+ }
88
+ return { build, results };
89
+ }
90
+ return { upload, download };
91
+ }
@@ -0,0 +1,117 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { readPipelineCaller } from "./pipeline-run-entry.js";
3
+
4
+ export const pipelineVersionArtifactName = (context, platform) =>
5
+ `buildchain-version-${context.preparation.root.slice(7)}-${context.runId}-${context.runAttempt}-${platform}`;
6
+
7
+ export const pipelineVersionJobName = (preparation, platform) =>
8
+ `Prepare version (${preparation.purpose}, ${platform})`;
9
+
10
+ export async function readPipelineVersionBuild(context, host) {
11
+ const { preparation, runId, runAttempt, definitionSha } = context;
12
+ const { repository, request, runs } = host;
13
+ if (preparation.source.repository !== repository)
14
+ throw new Error("Version preparation crossed its repository boundary");
15
+ const { run, jobs } = await runs.read(runId, runAttempt);
16
+ await readPipelineCaller(
17
+ run,
18
+ preparation.source.configPath,
19
+ request,
20
+ repository,
21
+ );
22
+ const definitions = (run.referenced_workflows || []).filter((entry) =>
23
+ entry.path?.startsWith(
24
+ "kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
25
+ ),
26
+ );
27
+ const versionDefinitions = (run.referenced_workflows || []).filter((entry) =>
28
+ entry.path?.startsWith(
29
+ "kungfu-systems/buildchain/.github/workflows/.release-pipeline-version.yml@",
30
+ ),
31
+ );
32
+ if (
33
+ definitions.length !== 1 ||
34
+ definitions[0].sha !== definitionSha ||
35
+ versionDefinitions.length !== 1 ||
36
+ versionDefinitions[0].sha !== definitionSha ||
37
+ !/^[0-9a-f]{40}$/u.test(definitionSha || "")
38
+ )
39
+ throw new Error("Version preparation changed its exact hosted definition");
40
+ const selected = preparation.platforms.map((platform) => {
41
+ const name = pipelineVersionJobName(preparation, platform);
42
+ const matches = jobs.filter(
43
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
44
+ );
45
+ if (
46
+ matches.length !== 1 ||
47
+ matches[0].run_id !== runId ||
48
+ matches[0].run_attempt !== runAttempt ||
49
+ matches[0].status !== "completed" ||
50
+ matches[0].conclusion !== "success"
51
+ )
52
+ throw new Error(
53
+ "Version preparation requires every exact successful product job",
54
+ );
55
+ return matches[0];
56
+ });
57
+ if (
58
+ !selected.length ||
59
+ new Set(preparation.platforms).size !== selected.length
60
+ )
61
+ throw new Error(
62
+ "Version preparation platform inventory is empty or duplicated",
63
+ );
64
+ const inventory = await request(
65
+ `/repos/${repository}/actions/runs/${runId}/artifacts?per_page=100`,
66
+ );
67
+ if (
68
+ !Array.isArray(inventory.artifacts) ||
69
+ inventory.total_count !== inventory.artifacts.length ||
70
+ inventory.total_count > 100
71
+ )
72
+ throw new Error("Version preparation artifact inventory is incomplete");
73
+ const assets = preparation.platforms.map((platform) => {
74
+ const matches = inventory.artifacts.filter(
75
+ (asset) => asset.name === pipelineVersionArtifactName(context, platform),
76
+ );
77
+ if (
78
+ matches.length !== 1 ||
79
+ !Number.isSafeInteger(matches[0].id) ||
80
+ matches[0].id < 1 ||
81
+ matches[0].expired ||
82
+ matches[0].workflow_run?.id !== runId ||
83
+ matches[0].workflow_run?.head_sha !== run.head_sha ||
84
+ !/^sha256:[0-9a-f]{64}$/u.test(matches[0].digest || "")
85
+ )
86
+ throw new Error(
87
+ "Version preparation has no unique immutable platform artifact",
88
+ );
89
+ return matches[0];
90
+ });
91
+ const again = await runs.read(runId, runAttempt);
92
+ if (
93
+ again.run.head_sha !== run.head_sha ||
94
+ recordDigest(again.run.referenced_workflows) !==
95
+ recordDigest(run.referenced_workflows) ||
96
+ selected.some(
97
+ (job) =>
98
+ !again.jobs.some((value) => recordDigest(value) === recordDigest(job)),
99
+ )
100
+ )
101
+ throw new Error(
102
+ "Version preparation producer changed during independent readback",
103
+ );
104
+ const body = {
105
+ schema: "buildchain.pipeline-version-build-readback/v1",
106
+ preparationRoot: preparation.root,
107
+ source: preparation.source,
108
+ runtime: preparation.runtime,
109
+ runId,
110
+ runAttempt,
111
+ definitionSha,
112
+ providerSource: run.head_sha,
113
+ jobs: selected,
114
+ artifacts: assets.map(({ id, digest }) => ({ id, digest })),
115
+ };
116
+ return { build: { ...body, root: recordDigest(body) }, assets };
117
+ }