@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.3

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 (228) hide show
  1. package/architecture/action-taxonomy.json +24 -0
  2. package/architecture/agent-change-map.md +256 -2
  3. package/architecture/ci-lane-change-budget.json +374 -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/decisions/0009-pipeline-version-preparation.md +186 -0
  9. package/architecture/internal-capabilities.json +361 -2
  10. package/architecture/maintainability-debt.json +6 -4
  11. package/architecture/maintainability-policy.json +8 -8
  12. package/architecture/minimal-consumer-migration.json +7567 -0
  13. package/architecture/product-upstreams.json +20 -0
  14. package/architecture/release-topology.json +282 -11
  15. package/architecture/universal-workflow-bootstrap.json +12 -0
  16. package/architecture/universal-workflow-capability-policy.json +1 -1
  17. package/architecture/workflow-taxonomy.json +66 -0
  18. package/dist/readers/business-attempt.cjs +809 -0
  19. package/dist/site/buildchain-contract.json +7 -5
  20. package/dist/site/buildchain-site.json +18 -13
  21. package/dist/site/capability-registry.json +3 -3
  22. package/dist/site/kfd-claims.json +385 -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 +21 -21
  26. package/dist/site/page-registry.json +13 -8
  27. package/dist/site/public-surface-audit.json +258 -7
  28. package/dist/site/publication-authority-registry.json +141 -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 +522 -7
  32. package/docs/node-api-reference.md +39 -39
  33. package/docs/runtime-entry.md +45 -5
  34. package/docs/workflow-catalog.md +6 -0
  35. package/package.json +4 -3
  36. package/packages/core/build/standalone/build.js +7 -1
  37. package/packages/core/consumer/buildchain-config.js +2 -21
  38. package/packages/core/consumer/contract/entries.js +85 -0
  39. package/packages/core/consumer/contract/examples.js +96 -0
  40. package/packages/core/consumer/contract/identity.js +65 -0
  41. package/packages/core/consumer/contract/inspection.js +45 -0
  42. package/packages/core/consumer/contract/plan.js +146 -0
  43. package/packages/core/consumer/contract/products.js +119 -0
  44. package/packages/core/consumer/contract/reader.js +47 -0
  45. package/packages/core/consumer/contract/shape.js +46 -0
  46. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  47. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  48. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  49. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  50. package/packages/core/dev-delivery/native/actions.js +7 -1
  51. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  52. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  53. package/packages/core/dev-delivery/queue/landing-action.js +5 -0
  54. package/packages/core/governance/buildchain-publication-authority.js +6 -0
  55. package/packages/core/paper/operations/bootstrap.js +2 -0
  56. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  57. package/packages/core/providers/github/attempt-index.js +60 -0
  58. package/packages/core/providers/github/attempt-journal.js +154 -0
  59. package/packages/core/providers/github/discussions/materials.js +7 -2
  60. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  61. package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
  62. package/packages/core/providers/github/pipeline-events.js +52 -0
  63. package/packages/core/providers/github/pipeline-integration.js +95 -0
  64. package/packages/core/providers/github/pipeline-policy.js +213 -0
  65. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  66. package/packages/core/providers/github/pipeline-product-release.js +191 -0
  67. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  68. package/packages/core/providers/github/pipeline-release-evidence.js +111 -0
  69. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  70. package/packages/core/providers/github/pipeline-runs.js +132 -0
  71. package/packages/core/providers/github/pipeline-source.js +178 -0
  72. package/packages/core/providers/github/pipeline-stable-source.js +354 -0
  73. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  74. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  75. package/packages/core/providers/github/pipeline-version.js +199 -0
  76. package/packages/core/providers/github/pipeline-worker.js +108 -0
  77. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  78. package/packages/core/publication/npm/pack-preview.js +2 -2
  79. package/packages/core/publication/npm/pack-result.js +31 -0
  80. package/packages/core/publication/npm/package.js +3 -6
  81. package/packages/core/publication/npm/pipeline-channel.js +87 -0
  82. package/packages/core/publication/npm/pipeline-provider.js +109 -0
  83. package/packages/core/publication/npm/registry.js +12 -9
  84. package/packages/core/publication/pipeline/actions.js +116 -0
  85. package/packages/core/publication/pipeline/apply.js +188 -0
  86. package/packages/core/publication/pipeline/build-segments.js +107 -0
  87. package/packages/core/publication/pipeline/capsules.js +89 -0
  88. package/packages/core/publication/pipeline/context.js +39 -0
  89. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  90. package/packages/core/publication/pipeline/development-current.js +63 -0
  91. package/packages/core/publication/pipeline/development-pr.js +112 -0
  92. package/packages/core/publication/pipeline/development-proof.js +76 -0
  93. package/packages/core/publication/pipeline/development-transition.js +112 -0
  94. package/packages/core/publication/pipeline/distribution.js +124 -0
  95. package/packages/core/publication/pipeline/documents.js +192 -0
  96. package/packages/core/publication/pipeline/effects.js +162 -0
  97. package/packages/core/publication/pipeline/files.js +66 -0
  98. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  99. package/packages/core/publication/pipeline/journal.js +110 -0
  100. package/packages/core/publication/pipeline/next-development.js +117 -0
  101. package/packages/core/publication/pipeline/pack.js +157 -0
  102. package/packages/core/publication/pipeline/package-policy.js +22 -0
  103. package/packages/core/publication/pipeline/plan.js +136 -0
  104. package/packages/core/publication/pipeline/prepare.js +139 -0
  105. package/packages/core/publication/pipeline/qualification.js +175 -0
  106. package/packages/core/publication/pipeline/qualify.js +104 -0
  107. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  108. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  109. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  110. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  111. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  112. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  113. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  114. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  115. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  116. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  117. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  118. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  119. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  120. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  121. package/packages/core/publication/pipeline/settle.js +43 -0
  122. package/packages/core/publication/pipeline/signing.js +140 -0
  123. package/packages/core/publication/pipeline/source-plan.js +83 -0
  124. package/packages/core/publication/pipeline/stable-entry.js +215 -0
  125. package/packages/core/publication/pipeline/stable-products.js +317 -0
  126. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  127. package/packages/core/publication/pipeline/stable.js +82 -0
  128. package/packages/core/publication/pipeline/version-actions.js +93 -0
  129. package/packages/core/publication/pipeline/version-build.js +130 -0
  130. package/packages/core/publication/pipeline/version-context.js +143 -0
  131. package/packages/core/publication/pipeline/version-material.js +36 -0
  132. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  133. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  134. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  135. package/packages/core/publication/pipeline/version.js +81 -0
  136. package/packages/core/publication/pipeline/worker.js +72 -0
  137. package/packages/core/publication/publication-reproducibility.js +2 -2
  138. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  139. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  140. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  141. package/packages/core/release/stable-release-gate.js +11 -0
  142. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  143. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  144. package/packages/core/runtime/entry/actions.js +27 -25
  145. package/packages/core/runtime/entry/attempt.js +25 -0
  146. package/packages/core/runtime/entry/selection.js +14 -0
  147. package/packages/core/runtime/entry/source.js +62 -0
  148. package/packages/core/workflow/attempt/identity.js +170 -0
  149. package/packages/core/workflow/attempt/journal.js +87 -0
  150. package/packages/core/workflow/attempt/materials.js +72 -0
  151. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  152. package/packages/core/workflow/attempt/reader.js +149 -0
  153. package/packages/core/workflow/attempt/records.js +153 -0
  154. package/packages/core/workflow/attempt/store.js +75 -0
  155. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  156. package/packages/core/workflow/pipeline/action-output.js +11 -0
  157. package/packages/core/workflow/pipeline/actions.js +75 -0
  158. package/packages/core/workflow/pipeline/build-control.js +140 -0
  159. package/packages/core/workflow/pipeline/build-evidence.js +92 -0
  160. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  161. package/packages/core/workflow/pipeline/build-result.js +69 -0
  162. package/packages/core/workflow/pipeline/build.js +142 -0
  163. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  164. package/packages/core/workflow/pipeline/channel-control.js +127 -0
  165. package/packages/core/workflow/pipeline/controller.js +133 -0
  166. package/packages/core/workflow/pipeline/delivery-control.js +204 -0
  167. package/packages/core/workflow/pipeline/delivery-observation.js +118 -0
  168. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  169. package/packages/core/workflow/pipeline/events.js +84 -0
  170. package/packages/core/workflow/pipeline/fence.js +42 -0
  171. package/packages/core/workflow/pipeline/group-control.js +56 -0
  172. package/packages/core/workflow/pipeline/guard-build.js +37 -0
  173. package/packages/core/workflow/pipeline/guard.js +156 -0
  174. package/packages/core/workflow/pipeline/host.js +114 -0
  175. package/packages/core/workflow/pipeline/materials.js +58 -0
  176. package/packages/core/workflow/pipeline/notifications.js +36 -0
  177. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  178. package/packages/core/workflow/pipeline/platforms.js +21 -0
  179. package/packages/core/workflow/pipeline/progress.js +108 -0
  180. package/packages/core/workflow/pipeline/projection.js +20 -0
  181. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  182. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  183. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  184. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  185. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  186. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  187. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  188. package/packages/core/workflow/pipeline/recovery-integration.js +66 -0
  189. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  190. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  191. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  192. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  193. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  194. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  195. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  196. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  197. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  198. package/packages/core/workflow/pipeline/selection.js +106 -0
  199. package/packages/core/workflow/pipeline/session.js +72 -0
  200. package/packages/core/workflow/pipeline/settlement.js +137 -0
  201. package/packages/core/workflow/pipeline/wake.js +38 -0
  202. package/packages/core/workflow/pipeline/web-status.js +108 -0
  203. package/scripts/build-release-discussion-reader.mjs +11 -7
  204. package/scripts/check-pipeline-publication-topology.mjs +48 -0
  205. package/scripts/check-release-topology.mjs +7 -1
  206. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  207. package/scripts/generate-site-bundle.mjs +1 -1
  208. package/scripts/verify-golden-path.mjs +5 -1
  209. package/scripts/verify-standalone-product.mjs +42 -0
  210. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  211. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  212. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  213. package/templates/minimal-consumer/binary/package.json +6 -0
  214. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  215. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  216. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  217. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  218. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  219. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  220. package/templates/minimal-consumer/npm/package.json +6 -0
  221. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  222. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  223. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  224. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  225. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  226. package/templates/minimal-consumer/paper/package.json +6 -0
  227. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  228. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -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
+ }
@@ -0,0 +1,199 @@
1
+ import { createHash } from "node:crypto";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+ import { readPipelineVersion } from "../../publication/pipeline/version.js";
4
+ import { preparedPipelineVersionMaterial } from "../../publication/pipeline/version-material.js";
5
+ import { verifyPipelinePublicationPlan } from "../../publication/pipeline/plan.js";
6
+
7
+ function blobBytes(value, sha) {
8
+ if (
9
+ value.encoding !== "base64" ||
10
+ value.size > 4 * 1024 * 1024 ||
11
+ typeof value.content !== "string"
12
+ )
13
+ throw new Error("Version material requires a bounded Git blob");
14
+ const bytes = Buffer.from(value.content, "base64");
15
+ const digest = createHash("sha1")
16
+ .update(`blob ${bytes.length}\0`)
17
+ .update(bytes)
18
+ .digest("hex");
19
+ if (bytes.length !== value.size || digest !== sha || value.sha !== sha)
20
+ throw new Error("Version material blob identity drift");
21
+ return bytes.toString("utf8");
22
+ }
23
+
24
+ export function githubPipelineVersion(request, repository) {
25
+ const base = `/repos/${repository}`;
26
+ const post = (url, body) => request(url, { method: "POST", body });
27
+ async function inspect(source, policy) {
28
+ if (source.repository !== repository)
29
+ throw new Error("Version material repository mismatch");
30
+ const commit = await request(`${base}/git/commits/${source.commit}`);
31
+ if (commit.sha !== source.commit || commit.tree?.sha !== source.tree)
32
+ throw new Error("Version material source tree drift");
33
+ const tree = await request(`${base}/git/trees/${source.tree}?recursive=1`);
34
+ if (tree.truncated || tree.sha !== source.tree || !Array.isArray(tree.tree))
35
+ throw new Error("Version material requires a complete source tree");
36
+ const files = {},
37
+ entries = {};
38
+ for (const file of [
39
+ ...policy.files,
40
+ ...(policy.derived_files || []).map((path) => ({ path })),
41
+ ]) {
42
+ if (Object.hasOwn(files, file.path)) continue;
43
+ const found = tree.tree.filter((entry) => entry.path === file.path);
44
+ if (
45
+ found.length !== 1 ||
46
+ found[0].type !== "blob" ||
47
+ !["100644", "100755"].includes(found[0].mode)
48
+ )
49
+ throw new Error("Version material must be a tracked regular file");
50
+ const entry = found[0];
51
+ entries[file.path] = entry;
52
+ files[file.path] = blobBytes(
53
+ await request(`${base}/git/blobs/${entry.sha}`),
54
+ entry.sha,
55
+ );
56
+ }
57
+ return {
58
+ commit,
59
+ files,
60
+ entries,
61
+ version: readPipelineVersion(policy, files),
62
+ sourceTimestamp: commit.committer.date,
63
+ };
64
+ }
65
+ async function materialize(plan, regeneration) {
66
+ verifyPipelinePublicationPlan(plan);
67
+ const input = await inspect(plan.source, plan.versionPolicy);
68
+ if (input.version !== plan.candidateVersion)
69
+ throw new Error("Version material changed after planning");
70
+ const material = preparedPipelineVersionMaterial(
71
+ plan,
72
+ input.files,
73
+ regeneration,
74
+ );
75
+ let source = plan.source;
76
+ if (material.changes.length)
77
+ source = await createMaterializedSource(plan, input, material);
78
+ const body = {
79
+ schema: "buildchain.pipeline-version-materialization/v1",
80
+ planRoot: plan.root,
81
+ protectedSource: plan.source,
82
+ source,
83
+ material,
84
+ };
85
+ return { ...body, root: recordDigest(body) };
86
+ }
87
+ async function materializeDevelopment(plan, regeneration) {
88
+ const input = await inspect(plan.source, plan.versionPolicy);
89
+ const material = preparedPipelineVersionMaterial(
90
+ plan,
91
+ input.files,
92
+ regeneration,
93
+ );
94
+ const branch = `feature/buildchain-next/${plan.root.slice(7)}`;
95
+ const source = material.changes.length
96
+ ? await createMaterializedSource(plan, input, material, `heads/${branch}`)
97
+ : plan.source;
98
+ const body = {
99
+ schema: "buildchain.pipeline-development-materialization/v1",
100
+ transitionRoot: plan.root,
101
+ protectedSource: plan.source,
102
+ source,
103
+ branch,
104
+ material,
105
+ };
106
+ return { ...body, root: recordDigest(body) };
107
+ }
108
+ async function createMaterializedSource(plan, input, material, targetRef) {
109
+ const changes = material.changes.map(({ path, content }) => ({
110
+ path,
111
+ content,
112
+ mode: input.entries[path].mode,
113
+ type: "blob",
114
+ }));
115
+ const tree = await post(`${base}/git/trees`, {
116
+ base_tree: plan.source.tree,
117
+ tree: changes,
118
+ });
119
+ const identity = {
120
+ name: "Buildchain automation",
121
+ email: "noreply@github.com",
122
+ date: plan.sourceTimestamp,
123
+ };
124
+ const parents = [plan.source.commit];
125
+ // Preserve the prior published version overlay as real Git ancestry. The
126
+ // resulting tree still changes only the current protected version fields;
127
+ // no provider force-update is needed for the next stable floating channel.
128
+ if (
129
+ plan.previousChannelCommit &&
130
+ plan.previousChannelCommit !== plan.source.commit
131
+ ) {
132
+ if (!/^[0-9a-f]{40}$/u.test(plan.previousChannelCommit))
133
+ throw new Error(
134
+ "Version overlay parent must be an exact prior channel commit",
135
+ );
136
+ const previous = await request(
137
+ `${base}/git/commits/${plan.previousChannelCommit}`,
138
+ );
139
+ if (previous.sha !== plan.previousChannelCommit)
140
+ throw new Error("Prior version overlay commit changed");
141
+ parents.push(previous.sha);
142
+ }
143
+ const created = await post(`${base}/git/commits`, {
144
+ message: `chore(release): materialize ${plan.version}\n\nBuildchain-Plan: ${plan.root}\nSigned-off-by: Buildchain automation <noreply@github.com>`,
145
+ tree: tree.sha,
146
+ parents,
147
+ author: identity,
148
+ committer: identity,
149
+ });
150
+ const commit = await request(`${base}/git/commits/${created.sha}`);
151
+ if (
152
+ commit.tree?.sha !== tree.sha ||
153
+ recordDigest(commit.parents?.map(({ sha }) => sha)) !==
154
+ recordDigest(parents)
155
+ )
156
+ throw new Error("Materialized version commit readback mismatch");
157
+ const source = { ...plan.source, commit: commit.sha, tree: tree.sha };
158
+ const verified = await inspect(source, plan.versionPolicy);
159
+ if (verified.version !== plan.version)
160
+ throw new Error("Version material did not retain the planned version");
161
+ const expectedFiles = {
162
+ ...input.files,
163
+ ...Object.fromEntries(
164
+ material.changes.map(({ path, content }) => [path, content]),
165
+ ),
166
+ };
167
+ if (recordDigest(verified.files) !== recordDigest(expectedFiles))
168
+ throw new Error(
169
+ "Version material changed document bytes beyond the planned fields",
170
+ );
171
+ const comparison = await request(
172
+ `${base}/compare/${plan.source.commit}...${commit.sha}`,
173
+ );
174
+ const paths = comparison.files?.map((file) => file.filename).sort();
175
+ if (
176
+ comparison.status !== "ahead" ||
177
+ (parents.length === 1 && comparison.total_commits !== 1) ||
178
+ recordDigest(paths) !==
179
+ recordDigest(material.changes.map(({ path }) => path).sort()) ||
180
+ comparison.files.some((file) => file.status !== "modified")
181
+ )
182
+ throw new Error(
183
+ "Version material changed paths outside the declared version fields",
184
+ );
185
+ const ref =
186
+ targetRef || `heads/buildchain/publication-source/${plan.root.slice(7)}`;
187
+ try {
188
+ await post(`${base}/git/refs`, { ref: `refs/${ref}`, sha: commit.sha });
189
+ } catch (error) {
190
+ const observed = await request(`${base}/git/ref/${ref}`);
191
+ if (observed.object?.sha !== commit.sha) throw error;
192
+ }
193
+ const observed = await request(`${base}/git/ref/${ref}`);
194
+ if (observed.object?.sha !== commit.sha)
195
+ throw new Error("Version material ref readback mismatch");
196
+ return source;
197
+ }
198
+ return { inspect, materialize, materializeDevelopment };
199
+ }
@@ -0,0 +1,108 @@
1
+ import { object } from "../../consumer/contract/shape.js";
2
+ import { recordDigest } from "../../release/discussion/envelope.js";
3
+
4
+ export const PIPELINE_WORKER = "buildchain.pipeline-worker/v1";
5
+
6
+ export function validatePipelineWorker(binding, current, warrant) {
7
+ object(binding, [
8
+ "schema",
9
+ "repository",
10
+ "attempt",
11
+ "generation",
12
+ "sourceHead",
13
+ "candidateId",
14
+ "fencingToken",
15
+ "leaseGeneration",
16
+ "runId",
17
+ "runAttempt",
18
+ "nativeJobId",
19
+ "sealJobId",
20
+ ]);
21
+ if (
22
+ binding.schema !== PIPELINE_WORKER ||
23
+ binding.repository !== current.intent.repository ||
24
+ binding.attempt !== current.identity.id ||
25
+ binding.generation !== current.generation.id ||
26
+ binding.sourceHead !== current.generation.source.commit ||
27
+ binding.candidateId !== warrant.candidateId ||
28
+ binding.fencingToken !== warrant.fencingToken ||
29
+ binding.leaseGeneration !== warrant.generation
30
+ )
31
+ throw new Error("Native worker does not bind the active pipeline Warrant");
32
+ for (const key of [
33
+ "runId",
34
+ "runAttempt",
35
+ "nativeJobId",
36
+ "sealJobId",
37
+ "leaseGeneration",
38
+ ])
39
+ if (!Number.isSafeInteger(binding[key]) || binding[key] < 1)
40
+ throw new Error("Native worker provider identity is invalid");
41
+ if (binding.nativeJobId === binding.sealJobId)
42
+ throw new Error("Native execution and evidence seal must be separate jobs");
43
+ return binding;
44
+ }
45
+
46
+ export function githubPipelineWorker(request) {
47
+ async function jobs(base, binding) {
48
+ const result = [];
49
+ for (let page = 1; page <= 100; page++) {
50
+ const response = await request(
51
+ `${base}/actions/runs/${binding.runId}/attempts/${binding.runAttempt}/jobs?per_page=100&page=${page}`,
52
+ );
53
+ if (!Array.isArray(response.jobs))
54
+ throw new Error("Missing provider jobs readback");
55
+ result.push(...response.jobs);
56
+ if (response.jobs.length < 100) return result;
57
+ }
58
+ throw new Error("Pipeline worker jobs exceed the readback bound");
59
+ }
60
+ async function observe(binding, current, warrant) {
61
+ validatePipelineWorker(binding, current, warrant);
62
+ const base = `/repos/${binding.repository}`;
63
+ const run = await request(`${base}/actions/runs/${binding.runId}`);
64
+ if (
65
+ run.id !== binding.runId ||
66
+ run.run_attempt !== binding.runAttempt ||
67
+ run.repository?.full_name !== binding.repository
68
+ )
69
+ throw new Error(
70
+ "Native worker provider run changed; terminality is unproved",
71
+ );
72
+ const entries = await jobs(base, binding);
73
+ const selected = [binding.nativeJobId, binding.sealJobId].map((id) => {
74
+ const matches = entries.filter((job) => job.id === id);
75
+ if (
76
+ matches.length !== 1 ||
77
+ matches[0].run_id !== binding.runId ||
78
+ matches[0].run_attempt !== binding.runAttempt
79
+ )
80
+ throw new Error("Native worker exact job identity drift");
81
+ return matches[0];
82
+ });
83
+ const again = await request(`${base}/actions/runs/${binding.runId}`);
84
+ if (
85
+ again.id !== run.id ||
86
+ again.run_attempt !== run.run_attempt ||
87
+ again.status !== run.status ||
88
+ again.conclusion !== run.conclusion
89
+ )
90
+ throw new Error("Native worker changed during terminal readback");
91
+ const terminal =
92
+ run.status === "completed" &&
93
+ Boolean(run.conclusion) &&
94
+ selected.every(
95
+ (job) =>
96
+ job.status === "completed" && job.conclusion && job.completed_at,
97
+ );
98
+ const body = {
99
+ schema: "buildchain.pipeline-worker-readback/v1",
100
+ binding,
101
+ status: terminal ? "completed" : "in_progress",
102
+ run,
103
+ jobs: selected,
104
+ };
105
+ return { ...body, root: recordDigest(body) };
106
+ }
107
+ return { observe };
108
+ }
@@ -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
+ }