@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,156 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { releaseTailRoot } from "../../release/release-tail-provider-plane.js";
3
+ import {
4
+ RELEASE_INVOCATION_CONTRACT,
5
+ RELEASE_PROVIDER_CONTRACT,
6
+ createReleaseInvocation,
7
+ createDomainReleaseTransaction,
8
+ } from "../../release/release-invocation.js";
9
+ import {
10
+ validatePublicationQualificationReceipt,
11
+ domainPublicationQualificationRoot,
12
+ } from "../publication-qualification.js";
13
+ import { verifyPipelinePublicationPlan } from "./plan.js";
14
+
15
+ export function verifyRootedPublication(value, schema) {
16
+ const { root, ...body } = value || {};
17
+ if (body.schema !== schema || root !== recordDigest(body))
18
+ throw new Error(`Invalid retained ${schema} bytes`);
19
+ return value;
20
+ }
21
+
22
+ export function verifyPipelineQualification({
23
+ plan,
24
+ materialization,
25
+ qualified,
26
+ evaluatedAt,
27
+ }) {
28
+ verifyPipelinePublicationPlan(plan);
29
+ verifyRootedPublication(
30
+ materialization,
31
+ "buildchain.pipeline-version-materialization/v1",
32
+ );
33
+ verifyRootedPublication(
34
+ qualified,
35
+ "buildchain.pipeline-publication-qualification/v1",
36
+ );
37
+ if (
38
+ qualified.planRoot !== plan.root ||
39
+ materialization.planRoot !== plan.root ||
40
+ recordDigest(materialization.protectedSource) !==
41
+ recordDigest(plan.source) ||
42
+ recordDigest(qualified.source) !== recordDigest(materialization.source) ||
43
+ materialization.material.version !== plan.version
44
+ )
45
+ throw new Error(
46
+ "Publication plan, materialized source and qualification differ",
47
+ );
48
+ const artifacts = qualified.artifacts
49
+ .map((artifact) => ({
50
+ role: `${artifact.product}/${artifact.artifact}`,
51
+ platform: artifact.platform,
52
+ artifactRoot: artifact.digest,
53
+ manifestRoot: artifact.manifestRoot,
54
+ }))
55
+ .sort((a, b) =>
56
+ `${a.role}\0${a.platform}`.localeCompare(`${b.role}\0${b.platform}`),
57
+ );
58
+ validatePublicationQualificationReceipt(qualified.qualification, {
59
+ repository: plan.source.repository,
60
+ sourceSha: qualified.source.commit,
61
+ sourceRoot: recordDigest(qualified.source),
62
+ candidateRoot: recordDigest({
63
+ planRoot: plan.root,
64
+ source: qualified.source,
65
+ artifacts: qualified.artifacts,
66
+ }),
67
+ artifactRoot: domainPublicationQualificationRoot(artifacts),
68
+ policyDigest: plan.contractRoot,
69
+ ...(evaluatedAt ? { evaluatedAt } : {}),
70
+ });
71
+ return qualified;
72
+ }
73
+
74
+ export function pipelineReleaseDocuments({
75
+ plan,
76
+ materialization,
77
+ qualified,
78
+ capsules,
79
+ signing,
80
+ evaluatedAt,
81
+ }) {
82
+ verifyPipelineQualification({
83
+ plan,
84
+ materialization,
85
+ qualified,
86
+ evaluatedAt,
87
+ });
88
+ verifyRootedPublication(capsules, "buildchain.pipeline-product-capsules/v1");
89
+ if (
90
+ capsules.qualificationRoot !== qualified.root ||
91
+ signing.planRoot !== plan.root ||
92
+ signing.qualificationRoot !== qualified.root ||
93
+ signing.materializationRoot !== materialization.root ||
94
+ signing.verified !== true
95
+ )
96
+ throw new Error(
97
+ "Release requires independently verified signing and capsules for this qualification",
98
+ );
99
+ // The Rust domain still owns publisher admission and QUALIFY/APPLY/SETTLE.
100
+ const invocation = createReleaseInvocation({
101
+ schema: RELEASE_INVOCATION_CONTRACT,
102
+ publisher: plan.publisher,
103
+ runtime: plan.runtime,
104
+ candidate: {
105
+ repository: qualified.source.repository,
106
+ commit: qualified.source.commit,
107
+ tree: qualified.source.tree,
108
+ version: plan.version,
109
+ },
110
+ target: {
111
+ channel: plan.channel,
112
+ tag: plan.tag,
113
+ expectedOldSha: plan.expectedTagSha,
114
+ },
115
+ authority: {
116
+ policyRoot: plan.contractRoot,
117
+ qualificationRoot: qualified.qualification.receiptRoot,
118
+ warrantRoot: qualified.qualification.receiptRoot,
119
+ },
120
+ provider: {
121
+ adapter: "built-in-provider-plane",
122
+ contract: RELEASE_PROVIDER_CONTRACT,
123
+ repository: qualified.source.repository,
124
+ },
125
+ parent: { invocationRoot: null, transactionRoot: null, receiptRoot: null },
126
+ });
127
+ const transaction = createDomainReleaseTransaction({
128
+ invocationRoot: invocation.roots.invocationRoot,
129
+ publisherRoot: invocation.roots.publisherRoot,
130
+ runtimeRoot: invocation.roots.runtimeRoot,
131
+ providerRoot: invocation.roots.providerRoot,
132
+ parentRoot: invocation.roots.parentRoot,
133
+ });
134
+ const body = {
135
+ schema: "kungfu.buildchain.release-passport/v4",
136
+ repository: qualified.source.repository,
137
+ source: qualified.source,
138
+ // This binding explicitly retains the declared version overlay. It never
139
+ // claims tree equivalence with the protected commit.
140
+ versionMaterialization: materialization,
141
+ release: { version: plan.version, tag: plan.tag, channel: plan.channel },
142
+ candidateRoot: qualified.qualification.candidateRoot,
143
+ policyDigest: qualified.qualification.policyDigest,
144
+ artifactRoot: qualified.qualification.artifactRoot,
145
+ publicationQualificationRoot: qualified.qualification.receiptRoot,
146
+ stageCapsuleAggregateRoot: capsules.root,
147
+ stageCapsuleRoots: capsules.capsules
148
+ .map(({ capsule }) => capsule.capsuleRoot)
149
+ .sort(),
150
+ signingRoot: recordDigest(signing),
151
+ invocationRoot: invocation.roots.invocationRoot,
152
+ transactionRoot: transaction.transactionRoot,
153
+ };
154
+ const passport = { ...body, passportRoot: releaseTailRoot(body) };
155
+ return { invocation, transaction, passport };
156
+ }
@@ -0,0 +1,162 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ const KINDS = new Set([
4
+ "exact-tag",
5
+ "npm-package",
6
+ "release-asset",
7
+ "release-visibility",
8
+ "npm-channel",
9
+ "git-channel",
10
+ ]);
11
+
12
+ export function pipelinePublicationEffects({
13
+ plan,
14
+ qualified,
15
+ documents,
16
+ evidence = [],
17
+ }) {
18
+ const effects = [
19
+ {
20
+ id: "exact-tag",
21
+ kind: "exact-tag",
22
+ tag: plan.tag,
23
+ commit: qualified.source.commit,
24
+ },
25
+ ];
26
+ for (const artifact of qualified.artifacts) {
27
+ for (const target of artifact.targets) {
28
+ if (target.provider === "npm")
29
+ effects.push({
30
+ id: `npm:${artifact.id}`,
31
+ kind: "npm-package",
32
+ product: artifact.id,
33
+ name: artifact.package.name,
34
+ version: artifact.package.version,
35
+ integrity: artifact.package.integrity,
36
+ access: target.access || "public",
37
+ tag: `buildchain-${plan.root.slice(7, 23)}`,
38
+ });
39
+ if (target.provider === "github-release")
40
+ effects.push({
41
+ id: `asset:${artifact.id}`,
42
+ kind: "release-asset",
43
+ product: artifact.id,
44
+ tag: plan.tag,
45
+ name: artifact.file.split("/").at(-1),
46
+ digest: artifact.digest,
47
+ size: artifact.size,
48
+ });
49
+ }
50
+ }
51
+ for (const asset of evidence)
52
+ effects.push({
53
+ id: `evidence:${asset.id}`,
54
+ kind: "release-asset",
55
+ product: `evidence:${asset.id}`,
56
+ tag: plan.tag,
57
+ name: asset.name,
58
+ digest: asset.digest,
59
+ size: asset.size,
60
+ });
61
+ effects.push({
62
+ id: "release-passport",
63
+ kind: "release-asset",
64
+ product: "release-passport",
65
+ tag: plan.tag,
66
+ name: "buildchain.release.json",
67
+ documentRoot: documents.passport.passportRoot,
68
+ });
69
+ effects.push({
70
+ id: "release-visibility",
71
+ kind: "release-visibility",
72
+ tag: plan.tag,
73
+ prerelease: plan.channel === "alpha",
74
+ });
75
+ return effects.map((effect) => ({ ...effect, root: recordDigest(effect) }));
76
+ }
77
+
78
+ // This executor owns only bounded provider operations. Journal admission/fencing
79
+ // and provider implementations are required dependencies, never consumer hooks.
80
+ // Each irreversible success is retained before a later operation is attempted.
81
+ export async function applyPipelineEffects({
82
+ effects,
83
+ transactionRoot,
84
+ receipts,
85
+ provider,
86
+ fence,
87
+ retain,
88
+ }) {
89
+ const results = [];
90
+ for (const effect of effects) {
91
+ const { root, ...body } = effect;
92
+ if (!KINDS.has(effect.kind) || root !== recordDigest(body))
93
+ throw new Error("Unrecognized or modified publication effect");
94
+ await fence();
95
+ const prior = receipts.filter((receipt) => receipt.effectId === effect.id);
96
+ if (
97
+ prior.some(
98
+ (receipt) =>
99
+ receipt.effectRoot !== root ||
100
+ receipt.transactionRoot !== transactionRoot,
101
+ )
102
+ )
103
+ throw new Error(
104
+ "Publication effect conflicts with immutable prior intent",
105
+ );
106
+ const completed = prior.find((receipt) => receipt.state === "success");
107
+ let observed = await provider.observe(effect);
108
+ if (completed) {
109
+ if (!provider.matches(effect, observed))
110
+ throw new Error(
111
+ "A completed publication no longer matches provider bytes",
112
+ );
113
+ results.push(completed);
114
+ continue;
115
+ }
116
+ if (
117
+ observed.state !== "absent" &&
118
+ !provider.matches(effect, observed) &&
119
+ !provider.canApply?.(effect, observed)
120
+ )
121
+ throw new Error(
122
+ "Publication destination already contains conflicting bytes",
123
+ );
124
+ if (!prior.length)
125
+ await retain(rootedReceipt(effect, transactionRoot, "pending", observed));
126
+ if (!provider.matches(effect, observed)) {
127
+ await fence();
128
+ // Preserve unknown outcomes. A lost response is reconciled from provider
129
+ // state before any retry; absence still propagates the original failure.
130
+ let failure;
131
+ try {
132
+ await provider.apply(effect);
133
+ } catch (error) {
134
+ failure = error;
135
+ }
136
+ observed = await provider.observe(effect);
137
+ if (!provider.matches(effect, observed)) {
138
+ if (failure) throw failure;
139
+ throw new Error(
140
+ "Publication write lacks exact successful provider readback",
141
+ );
142
+ }
143
+ }
144
+ await fence();
145
+ const receipt = rootedReceipt(effect, transactionRoot, "success", observed);
146
+ await retain(receipt);
147
+ results.push(receipt);
148
+ }
149
+ return results;
150
+ }
151
+
152
+ function rootedReceipt(effect, transactionRoot, state, observed) {
153
+ const body = {
154
+ schema: "buildchain.pipeline-publication-effect/v1",
155
+ effectId: effect.id,
156
+ effectRoot: effect.root,
157
+ transactionRoot,
158
+ state,
159
+ observed,
160
+ };
161
+ return { ...body, root: recordDigest(body) };
162
+ }
@@ -0,0 +1,66 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+
5
+ export function publicationPath(root, relative, kind = "file") {
6
+ const base = fs.realpathSync(root);
7
+ const target = path.resolve(base, relative);
8
+ if (target !== base && !target.startsWith(`${base}${path.sep}`))
9
+ throw new Error("Publication path escapes its owned directory");
10
+ let current = base;
11
+ for (const part of path
12
+ .relative(base, target)
13
+ .split(path.sep)
14
+ .filter(Boolean)) {
15
+ current = path.join(current, part);
16
+ if (fs.lstatSync(current).isSymbolicLink())
17
+ throw new Error("Publication paths cannot traverse symbolic links");
18
+ }
19
+ const stat = fs.statSync(target);
20
+ if (
21
+ (kind === "file" && !stat.isFile()) ||
22
+ (kind === "directory" && !stat.isDirectory())
23
+ )
24
+ throw new Error(`Publication expected a ${kind}`);
25
+ return target;
26
+ }
27
+
28
+ export function publicationFile(file) {
29
+ const size = fs.statSync(file).size;
30
+ if (!size || size > 256 * 1024 * 1024)
31
+ throw new Error("Publication artifact must contain 1 byte to 256 MiB");
32
+ const bytes = fs.readFileSync(file);
33
+ if (!bytes.length || bytes.length > 256 * 1024 * 1024)
34
+ throw new Error("Publication artifact must contain 1 byte to 256 MiB");
35
+ return {
36
+ bytes,
37
+ size: bytes.length,
38
+ digest: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
39
+ integrity: `sha512-${createHash("sha512").update(bytes).digest("base64")}`,
40
+ };
41
+ }
42
+
43
+ export function writeImmutablePublicationFile(file, bytes) {
44
+ // Refuse parent symlinks before mkdir/write, including during recovery into
45
+ // an existing output directory.
46
+ let parent = path.dirname(path.resolve(file));
47
+ while (parent !== path.dirname(parent)) {
48
+ if (fs.existsSync(parent) && fs.lstatSync(parent).isSymbolicLink())
49
+ throw new Error("Publication output cannot traverse symbolic links");
50
+ parent = path.dirname(parent);
51
+ }
52
+ fs.mkdirSync(path.dirname(file), { recursive: true });
53
+ try {
54
+ fs.writeFileSync(file, bytes, { flag: "wx" });
55
+ } catch (error) {
56
+ if (
57
+ error.code !== "EEXIST" ||
58
+ fs.lstatSync(file).isSymbolicLink() ||
59
+ !fs.readFileSync(file).equals(Buffer.from(bytes))
60
+ )
61
+ throw new Error(
62
+ `Publication refuses conflicting retained bytes: ${path.basename(file)}`,
63
+ );
64
+ }
65
+ return file;
66
+ }
@@ -0,0 +1,32 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ export const PUBLICATION_IMPORT = "buildchain.pipeline-publication-import/v1";
4
+
5
+ export function publicationImportedValues(value) {
6
+ const { root, ...body } = value;
7
+ if (
8
+ body.schema !== PUBLICATION_IMPORT ||
9
+ root !== recordDigest(body) ||
10
+ !Array.isArray(body.values) ||
11
+ !body.values.length ||
12
+ body.values.length > 1000
13
+ )
14
+ throw new Error(
15
+ "Publication import is not one bounded immutable material bundle",
16
+ );
17
+ const ids = new Set();
18
+ for (const item of body.values) {
19
+ if (
20
+ typeof item.id !== "string" ||
21
+ !item.id.startsWith("publication/") ||
22
+ /^publication\/(?:worker|context|recovery-import)\//u.test(item.id) ||
23
+ ids.has(item.id) ||
24
+ !item.id.endsWith(`/${recordDigest(item.value).slice(7)}`)
25
+ )
26
+ throw new Error(
27
+ "Publication import contains duplicate, recursive or unbound materials",
28
+ );
29
+ ids.add(item.id);
30
+ }
31
+ return body.values;
32
+ }
@@ -0,0 +1,110 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { assertPipelineExecution } from "../../workflow/pipeline/fence.js";
3
+ import {
4
+ PUBLICATION_IMPORT,
5
+ publicationImportedValues,
6
+ } from "./imported-materials.js";
7
+
8
+ export function pipelinePublicationJournal(session, host) {
9
+ const initial = session.observed;
10
+ const expected = {
11
+ attempt: initial.attempt,
12
+ generation: initial.generation,
13
+ intent: session.intent.id,
14
+ sourceRoot: recordDigest(initial.history.at(-1).generation.source),
15
+ };
16
+ const store = host.materialStore(session);
17
+ async function fence() {
18
+ const observed = await session.journal.read();
19
+ assertPipelineExecution(observed, expected);
20
+ const writer = observed.history
21
+ .at(-1)
22
+ .events.filter((event) =>
23
+ event.payload.materials.some((material) =>
24
+ material.id.startsWith("publication/worker/"),
25
+ ),
26
+ )
27
+ .at(-1)?.payload.writer;
28
+ if (
29
+ writer &&
30
+ (writer.runId !== String(host.runId) ||
31
+ writer.runAttempt !== String(host.runAttempt))
32
+ )
33
+ throw new Error(
34
+ "Publication worker lost its exact provider execution reservation",
35
+ );
36
+ return observed;
37
+ }
38
+ async function materials(prefix) {
39
+ const observed = await session.journal.read();
40
+ if (observed.attempt !== initial.attempt)
41
+ throw new Error("Publication journal is no longer the current attempt");
42
+ const references = observed.history
43
+ .at(-1)
44
+ .events.flatMap((event) => event.payload.materials)
45
+ .filter(
46
+ (reference) =>
47
+ reference.id.startsWith(prefix) ||
48
+ reference.id.startsWith("publication/recovery-import/"),
49
+ );
50
+ const unique = [
51
+ ...new Map(
52
+ references.map((reference) => [reference.digest, reference]),
53
+ ).values(),
54
+ ];
55
+ const values = [];
56
+ for (const reference of unique) {
57
+ const value = await store.read(reference);
58
+ if (value.schema === PUBLICATION_IMPORT)
59
+ values.push(
60
+ ...publicationImportedValues(value)
61
+ .filter((item) => item.id.startsWith(prefix))
62
+ .map((item) => item.value),
63
+ );
64
+ else if (reference.id.startsWith(prefix)) values.push(value);
65
+ }
66
+ return [
67
+ ...new Map(values.map((value) => [recordDigest(value), value])).values(),
68
+ ];
69
+ }
70
+ async function record(
71
+ id,
72
+ value,
73
+ { phase = "publish", state = "running", expectedHead } = {},
74
+ ) {
75
+ const root = recordDigest(value);
76
+ const observed = await session.journal.read();
77
+ const eventKey = `publication:${id}:${root}`;
78
+ const prior = observed.history
79
+ .at(-1)
80
+ .events.find((event) => event.payload.eventKey === eventKey);
81
+ if (prior) {
82
+ if (
83
+ observed.attempt !== initial.attempt ||
84
+ prior.node !== phase ||
85
+ prior.payload.state !== state ||
86
+ prior.payload.materials.length !== 1 ||
87
+ recordDigest(await store.read(prior.payload.materials[0])) !== root
88
+ )
89
+ throw new Error(
90
+ "Publication retry changed its immutable recorded result",
91
+ );
92
+ return value;
93
+ }
94
+ const reference = await store.retain(
95
+ `${id}/${root.slice(7)}`,
96
+ value,
97
+ "receipt",
98
+ );
99
+ await session.progress.progress({
100
+ attempt: initial.attempt,
101
+ phase,
102
+ state,
103
+ eventKey,
104
+ materials: [reference],
105
+ ...(expectedHead !== undefined ? { expectedHead } : {}),
106
+ });
107
+ return value;
108
+ }
109
+ return { fence, materials, record };
110
+ }
@@ -0,0 +1,116 @@
1
+ import { observeAdvancedPipelineDevelopment } from "./development-current.js";
2
+ import { createPipelineDevelopmentTransition } from "./development-transition.js";
3
+ import { githubPipelineVersion } from "../../providers/github/pipeline-version.js";
4
+ import { uniquePublicationMaterial } from "./context.js";
5
+ import { observePipelineVersionAnchor } from "./development-anchor.js";
6
+ import { developmentTransitionReadback } from "./development-proof.js";
7
+ import { prepareNextDevelopmentPullRequest } from "./development-pr.js";
8
+
9
+ export async function nextPipelineDevelopment(context, host, journal) {
10
+ const { plan } = context;
11
+ const publication = await uniquePublicationMaterial(
12
+ journal,
13
+ "publication/complete/",
14
+ );
15
+ const transitions = await journal.materials(
16
+ "publication/next-development-plan/",
17
+ );
18
+ if (transitions.length > 1)
19
+ throw new Error("Next-development has conflicting retained declarations");
20
+ let transition = transitions[0];
21
+ if (!transition) {
22
+ transition = createPipelineDevelopmentTransition(context, publication);
23
+ await journal.record("publication/next-development-plan", transition, {
24
+ phase: "next-development",
25
+ });
26
+ }
27
+ if (transition.model.strategy === "anchored")
28
+ return observePipelineVersionAnchor(context, host, transition);
29
+ const retained = await journal.materials("publication/next-development-pr/");
30
+ if (retained.length > 1)
31
+ throw new Error("Next-development has conflicting retained PRs");
32
+ let selected = retained[0];
33
+ const adapter = githubPipelineVersion(host.request, host.repository);
34
+ if (!selected) {
35
+ const advanced = await observeAdvancedPipelineDevelopment(
36
+ context,
37
+ host,
38
+ transition,
39
+ adapter,
40
+ );
41
+ if (advanced) return advanced;
42
+ selected = await prepareNextDevelopmentPullRequest(
43
+ context,
44
+ host,
45
+ journal,
46
+ transition,
47
+ adapter,
48
+ );
49
+ }
50
+ const pr = await host.request(
51
+ `/repos/${host.repository}/pulls/${selected.number}`,
52
+ );
53
+ if (
54
+ pr.head?.sha !== selected.source.commit ||
55
+ pr.head?.ref !== selected.branch ||
56
+ pr.base?.ref !== selected.base ||
57
+ pr.head?.repo?.full_name !== host.repository
58
+ )
59
+ throw new Error(
60
+ "Next-development PR changed its retained source or target",
61
+ );
62
+ const before = await adapter.inspect(
63
+ selected.materialization.protectedSource,
64
+ plan.versionPolicy,
65
+ );
66
+ const after = await adapter.inspect(selected.source, plan.versionPolicy);
67
+ const pending = developmentTransitionReadback(transition, {
68
+ before,
69
+ after,
70
+ evidence: selected,
71
+ createdAt: pr.created_at,
72
+ });
73
+ if (!pr.merged)
74
+ return {
75
+ state: "waiting",
76
+ reason:
77
+ pr.state === "open"
78
+ ? "next-development-protected-pr-pending"
79
+ : "next-development-pr-closed",
80
+ pullRequest: selected.number,
81
+ source: selected.source,
82
+ transition: pending,
83
+ };
84
+ const integration = await host.integration.observe({
85
+ intent: {
86
+ source: { pullRequest: selected.number, targetBranch: selected.base },
87
+ },
88
+ generation: {
89
+ source: { ...selected.source, configPath: plan.source.configPath },
90
+ },
91
+ });
92
+ const source = await host.source.source(
93
+ integration.mergeCommit,
94
+ plan.source.configPath,
95
+ );
96
+ const readback = await adapter.inspect(source.identity, plan.versionPolicy);
97
+ if (readback.version !== transition.target.version)
98
+ throw new Error(
99
+ "Protected next-development version readback differs from its declaration",
100
+ );
101
+ return {
102
+ state: "success",
103
+ reason: "protected-next-development-verified",
104
+ transitionRoot: transition.idempotencyKey,
105
+ integration,
106
+ version: readback.version,
107
+ publicationRoot: publication.release.receiptRoot,
108
+ transition: developmentTransitionReadback(transition, {
109
+ before,
110
+ after: readback,
111
+ evidence: integration,
112
+ createdAt: pr.created_at,
113
+ mergedAt: pr.merged_at,
114
+ }),
115
+ };
116
+ }