@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,151 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFileSync } from "node:child_process";
4
+ import { packNpmArtifact, sealedPackResult } from "../npm/package.js";
5
+ import { readNpmPackageJsonFromTarball } from "../../release/candidate/payloads.js";
6
+ import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
7
+ import { recordDigest } from "../../release/discussion/envelope.js";
8
+ import { verifyPipelinePublicationPlan } from "./plan.js";
9
+ import { assertPipelinePackagePolicy } from "./package-policy.js";
10
+ import {
11
+ publicationPath,
12
+ publicationFile,
13
+ writeImmutablePublicationFile,
14
+ } from "./files.js";
15
+
16
+ function packageArtifact(directory, output, expected, version, environment) {
17
+ const product = publicationPath(directory, expected.path, "directory");
18
+ const pack = packNpmArtifact({
19
+ cwd: product,
20
+ env: createNativeChildEnvironment(environment),
21
+ outputDirectory: output,
22
+ ignoreScripts: true,
23
+ registry: "https://registry.npmjs.org/",
24
+ });
25
+ const sealed = sealedPackResult({
26
+ tarballPath: pack.tarballPath,
27
+ integrity: pack.integrity,
28
+ });
29
+ const pkg = readNpmPackageJsonFromTarball(pack.tarballPath);
30
+ assertPipelinePackagePolicy(pkg);
31
+ if (
32
+ pkg.name !== pack.name ||
33
+ pkg.version !== version ||
34
+ pack.version !== version ||
35
+ pkg.private === true
36
+ )
37
+ throw new Error(
38
+ "Sealed npm package does not match the planned public package version",
39
+ );
40
+ return {
41
+ file: pack.tarballPath,
42
+ suffix: ".tgz",
43
+ package: { name: pkg.name, version, integrity: sealed.integrity },
44
+ };
45
+ }
46
+
47
+ export function inspectPipelineFileArtifact(directory, expected) {
48
+ const file = publicationPath(directory, expected.path);
49
+ if (expected.kind === "pdf") {
50
+ const bytes = fs.readFileSync(file);
51
+ if (
52
+ !bytes.subarray(0, 8).toString().startsWith("%PDF-") ||
53
+ !bytes.subarray(-1024).toString().includes("%%EOF")
54
+ )
55
+ throw new Error("Declared Paper output is not a complete PDF document");
56
+ return { file, suffix: ".pdf" };
57
+ }
58
+ const suffix = [".tar.gz", ".tar.xz", ".tgz", ".zip", ".tar"].find((value) =>
59
+ file.endsWith(value),
60
+ );
61
+ if (!suffix)
62
+ throw new Error("Declared binary output must be a standard archive");
63
+ const names = execFileSync("tar", ["-tf", path.basename(file)], {
64
+ cwd: path.dirname(file),
65
+ encoding: "utf8",
66
+ maxBuffer: 16 * 1024 * 1024,
67
+ })
68
+ .trim()
69
+ .split(/\r?\n/u);
70
+ if (
71
+ !names[0] ||
72
+ names.some(
73
+ (name) =>
74
+ name.startsWith("/") ||
75
+ /^[A-Za-z]:/u.test(name) ||
76
+ name.replaceAll("\\", "/").split("/").includes(".."),
77
+ )
78
+ )
79
+ throw new Error("Binary archive contains unsafe or empty paths");
80
+ return { file, suffix };
81
+ }
82
+
83
+ export function packPipelineProducts({
84
+ cwd,
85
+ output,
86
+ plan,
87
+ platform,
88
+ source,
89
+ environment = process.env,
90
+ }) {
91
+ verifyPipelinePublicationPlan(plan);
92
+ const expected = plan.outputs.filter((item) => item.platform === platform);
93
+ if (!expected.length)
94
+ throw new Error("Publication platform was not declared");
95
+ fs.mkdirSync(output, { recursive: true });
96
+ const artifacts = expected.map((item) => {
97
+ const directory = publicationPath(cwd, item.directory, "directory");
98
+ const staging = path.join(output, "npm-pack", item.product, item.artifact);
99
+ const packed =
100
+ item.kind === "npm-package"
101
+ ? packageArtifact(directory, staging, item, plan.version, environment)
102
+ : inspectPipelineFileArtifact(directory, item);
103
+ const observed = publicationFile(packed.file);
104
+ const name = `${item.product}-${platform}-${item.artifact}${packed.suffix}`;
105
+ writeImmutablePublicationFile(
106
+ path.join(output, "payloads", name),
107
+ observed.bytes,
108
+ );
109
+ return {
110
+ ...item,
111
+ file: `payloads/${name}`,
112
+ size: observed.size,
113
+ digest: observed.digest,
114
+ ...(packed.package ? { package: packed.package } : {}),
115
+ };
116
+ });
117
+ const body = {
118
+ schema: "buildchain.pipeline-publication-products/v1",
119
+ planRoot: plan.root,
120
+ source,
121
+ platform,
122
+ artifacts,
123
+ };
124
+ const manifest = { ...body, root: recordDigest(body) };
125
+ writeImmutablePublicationFile(
126
+ path.join(output, "manifest.json"),
127
+ `${JSON.stringify(manifest, null, 2)}\n`,
128
+ );
129
+ return manifest;
130
+ }
131
+
132
+ export function verifyPipelineProductFiles(directory, manifest) {
133
+ const { root, ...body } = manifest;
134
+ if (
135
+ body.schema !== "buildchain.pipeline-publication-products/v1" ||
136
+ root !== recordDigest(body)
137
+ )
138
+ throw new Error("Publication manifest root does not match retained bytes");
139
+ for (const artifact of manifest.artifacts) {
140
+ const observed = publicationFile(publicationPath(directory, artifact.file));
141
+ if (
142
+ observed.size !== artifact.size ||
143
+ observed.digest !== artifact.digest ||
144
+ (artifact.package && observed.integrity !== artifact.package.integrity)
145
+ )
146
+ throw new Error(
147
+ "Publication artifact changed after its manifest was sealed",
148
+ );
149
+ }
150
+ return manifest;
151
+ }
@@ -0,0 +1,22 @@
1
+ export function assertPipelinePackagePolicy(pkg) {
2
+ const config = pkg.publishConfig;
3
+ if (config === undefined) return;
4
+ if (
5
+ !config ||
6
+ Array.isArray(config) ||
7
+ typeof config !== "object" ||
8
+ Object.keys(config).some(
9
+ (key) => !["access", "registry", "tag", "provenance"].includes(key),
10
+ )
11
+ )
12
+ throw new Error(
13
+ "npm publishConfig cannot add provider, credential or execution configuration to the product contract",
14
+ );
15
+ if (
16
+ config.registry !== undefined &&
17
+ !["https://registry.npmjs.org", "https://registry.npmjs.org/"].includes(
18
+ config.registry,
19
+ )
20
+ )
21
+ throw new Error("npm publication requires the declared npmjs provider");
22
+ }
@@ -0,0 +1,114 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { selectProductPublicationIntent } from "../../release/product-publication.js";
3
+
4
+ export function pipelineExpectedProducts(plan) {
5
+ const outputs = plan.products
6
+ .flatMap((product) =>
7
+ product.platforms.flatMap((platform) =>
8
+ product.artifacts.map((artifact) => ({
9
+ id: `${product.id}/${platform}/${artifact.id}`,
10
+ product: product.id,
11
+ platform,
12
+ artifact: artifact.id,
13
+ kind: artifact.kind,
14
+ directory: product.directory || ".",
15
+ path: artifact.path,
16
+ targets: product.targets
17
+ .filter((target) => target.artifacts.includes(artifact.id))
18
+ .map(({ provider, access }) => ({
19
+ provider,
20
+ ...(access ? { access } : {}),
21
+ })),
22
+ })),
23
+ ),
24
+ )
25
+ .sort((left, right) => left.id.localeCompare(right.id));
26
+ if (
27
+ !outputs.length ||
28
+ outputs.length > 256 ||
29
+ new Set(outputs.map(({ id }) => id)).size !== outputs.length
30
+ )
31
+ throw new Error(
32
+ "Publication requires a bounded unique product/platform/artifact inventory",
33
+ );
34
+ return outputs;
35
+ }
36
+
37
+ export function planPipelinePublication({
38
+ attempt,
39
+ generation,
40
+ source,
41
+ intentSource = source,
42
+ runtime,
43
+ publisher,
44
+ contract,
45
+ route,
46
+ version,
47
+ sourceTimestamp,
48
+ expectedTagSha = null,
49
+ previousChannelCommit = null,
50
+ }) {
51
+ if (route.operation === "develop")
52
+ throw new Error("Development delivery cannot publish products");
53
+ const developmentRoutes = contract.channels.filter(
54
+ (entry) =>
55
+ entry.operation === "alpha" &&
56
+ entry.to === (route.operation === "alpha" ? route.to : route.from),
57
+ );
58
+ if (developmentRoutes.length !== 1)
59
+ throw new Error(
60
+ "Publication requires one declared protected development channel",
61
+ );
62
+ const outputs = pipelineExpectedProducts(contract);
63
+ // The Rust-owned version/route decision operates on a declaration here.
64
+ // Actual sealed artifact roots are admitted independently after the build.
65
+ const selection = selectProductPublicationIntent({
66
+ channel: route.operation === "alpha" ? "alpha" : "stable",
67
+ targetRef: route.to,
68
+ sourceSha: source.commit,
69
+ sourceTimestamp,
70
+ repository: source.repository,
71
+ artifactKind: "custom",
72
+ requiredArtifactsRoot: recordDigest(outputs),
73
+ candidateVersion: version,
74
+ observedVersions: [],
75
+ });
76
+ if (contract.version.strategy === "anchored" && selection.version !== version)
77
+ throw new Error(
78
+ "Anchored publication requires an explicitly materialized version; inference is forbidden",
79
+ );
80
+ const body = {
81
+ schema: "buildchain.pipeline-publication-plan/v1",
82
+ attempt,
83
+ generation,
84
+ source,
85
+ intentSource,
86
+ runtime,
87
+ publisher,
88
+ contractRoot: recordDigest(contract),
89
+ version: selection.version,
90
+ candidateVersion: version,
91
+ tag: selection.exactTag,
92
+ channel: selection.channel,
93
+ route,
94
+ sourceTimestamp,
95
+ versionPolicy: contract.version,
96
+ developmentBranch: developmentRoutes[0].from,
97
+ expectedTagSha,
98
+ previousChannelCommit,
99
+ outputs,
100
+ versionSelection: selection,
101
+ phases: ["QUALIFY", "APPLY", "SETTLE"],
102
+ };
103
+ return { ...body, root: recordDigest(body) };
104
+ }
105
+
106
+ export function verifyPipelinePublicationPlan(plan) {
107
+ const { root, ...body } = plan || {};
108
+ if (
109
+ body.schema !== "buildchain.pipeline-publication-plan/v1" ||
110
+ root !== recordDigest(body)
111
+ )
112
+ throw new Error("Publication plan bytes do not match their retained root");
113
+ return plan;
114
+ }
@@ -0,0 +1,151 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { resumePipelineSession } from "../../workflow/pipeline/session.js";
3
+ import { pipelinePlatforms } from "../../workflow/pipeline/platforms.js";
4
+ import { githubPipelineVersion } from "../../providers/github/pipeline-version.js";
5
+ import { createPipelinePublicationPlan } from "./source-plan.js";
6
+ import { pipelinePublicationJournal } from "./journal.js";
7
+ import { prepareRecoveredPublication } from "./recovery-prepare.js";
8
+
9
+ function publicationNextOperation(phase, existing, recovery) {
10
+ if (phase !== "publish") return "settle";
11
+ if (existing.length) return "apply";
12
+ if (recovery?.mode === "prepared" || recovery?.build?.scheduled.length === 0)
13
+ return "qualify";
14
+ return "build";
15
+ }
16
+
17
+ export async function preparePipelinePublication(attempt, publisherSha, host) {
18
+ const { run: execution } = await host.runs.read(host.runId, host.runAttempt);
19
+ const definitions = (execution.referenced_workflows || []).filter((entry) =>
20
+ entry.path?.startsWith(
21
+ "kungfu-systems/buildchain/.github/workflows/.release-pipeline-products.yml@",
22
+ ),
23
+ );
24
+ if (
25
+ !/^[0-9a-f]{40}$/u.test(publisherSha || "") ||
26
+ definitions.length !== 1 ||
27
+ definitions[0].sha !== publisherSha
28
+ )
29
+ throw new Error(
30
+ "Publisher definition is not the exact hosted reusable workflow",
31
+ );
32
+ const session = await resumePipelineSession({ ...host, attempt }, host);
33
+ const observed = session.observed;
34
+ if (observed.phases.merge?.payload.state !== "success")
35
+ throw new Error(
36
+ "Product publication requires completed protected integration",
37
+ );
38
+ if (observed.status === "complete") return { operation: "wait" };
39
+ const phase = observed.missing[0];
40
+ if (!["publish", "distribution", "next-development"].includes(phase))
41
+ throw new Error("Attempt is not at product publication");
42
+ const prior = observed.history
43
+ .at(-1)
44
+ .events.filter((event) =>
45
+ event.payload.materials.some((material) =>
46
+ material.id.startsWith("publication/worker/"),
47
+ ),
48
+ )
49
+ .at(-1)?.payload.writer;
50
+ if (
51
+ prior &&
52
+ (prior.runId !== String(host.runId) ||
53
+ prior.runAttempt !== String(host.runAttempt))
54
+ ) {
55
+ const { run } = await host.runs.read(
56
+ Number(prior.runId),
57
+ Number(prior.runAttempt),
58
+ );
59
+ if (run.status !== "completed") return { operation: "wait" };
60
+ }
61
+ const journal = pipelinePublicationJournal(session, host);
62
+ const claim = {
63
+ schema: "buildchain.pipeline-publication-worker/v1",
64
+ attempt,
65
+ runId: host.runId,
66
+ runAttempt: host.runAttempt,
67
+ };
68
+ await journal.record("publication/worker", claim, {
69
+ phase,
70
+ expectedHead: observed.head,
71
+ });
72
+ await journal.fence();
73
+ const recovery = await prepareRecoveredPublication(
74
+ session,
75
+ publisherSha,
76
+ host,
77
+ journal,
78
+ phase,
79
+ );
80
+ const plans = await journal.materials("publication/plan/");
81
+ if (plans.length > 1)
82
+ throw new Error("Publication has conflicting retained plans");
83
+ const version = githubPipelineVersion(host.request, host.repository);
84
+ let plan = plans[0];
85
+ if (!plan) {
86
+ plan = await createPipelinePublicationPlan(
87
+ session,
88
+ publisherSha,
89
+ host,
90
+ version,
91
+ );
92
+ await journal.record("publication/plan", plan);
93
+ }
94
+ if (
95
+ !recovery &&
96
+ (plan.publisher.workflowSha !== publisherSha ||
97
+ plan.runtime.commit !== host.runtime.sha)
98
+ )
99
+ throw new Error(
100
+ "Publication resume requires its retained publisher and runtime; use typed recovery for upgrades",
101
+ );
102
+ const materializations = await journal.materials(
103
+ "publication/materialization/",
104
+ );
105
+ if (materializations.length > 1)
106
+ throw new Error("Publication materialized source is ambiguous");
107
+ let materialization = materializations[0];
108
+ if (!materialization) {
109
+ await journal.fence();
110
+ materialization = await version.materialize(plan);
111
+ const verified = await host.source.source(
112
+ materialization.source.commit,
113
+ plan.source.configPath,
114
+ );
115
+ if (recordDigest(verified.plan) !== plan.contractRoot)
116
+ throw new Error("Version materialization altered the product contract");
117
+ const { root, ...body } = materialization;
118
+ materialization = { ...body, source: verified.identity };
119
+ materialization.root = recordDigest(materialization);
120
+ await journal.record("publication/materialization", materialization);
121
+ }
122
+ const admitted = await host.source.source(
123
+ materialization.source.commit,
124
+ plan.source.configPath,
125
+ );
126
+ if (
127
+ recordDigest(admitted.identity) !== recordDigest(materialization.source) ||
128
+ recordDigest(admitted.plan) !== plan.contractRoot
129
+ )
130
+ throw new Error("Retained publication source or contract drifted");
131
+ const existing = await journal.materials("publication/qualified/");
132
+ const context = {
133
+ schema: "buildchain.pipeline-publication-context/v1",
134
+ attempt,
135
+ generation: observed.generation,
136
+ plan,
137
+ materialization,
138
+ platforms: pipelinePlatforms(admitted.plan).filter(
139
+ ({ platform }) =>
140
+ !recovery?.build || recovery.build.scheduled.includes(platform),
141
+ ),
142
+ runId: host.runId,
143
+ runAttempt: host.runAttempt,
144
+ ...(recovery ? { recovery } : {}),
145
+ };
146
+ await journal.record("publication/context", context, { phase });
147
+ return {
148
+ operation: publicationNextOperation(phase, existing, recovery),
149
+ context,
150
+ };
151
+ }
@@ -0,0 +1,170 @@
1
+ import {
2
+ PUBLICATION_BUILD_AGGREGATE,
3
+ publicationArtifactProducer,
4
+ verifyPublicationBuildAggregate,
5
+ } from "./build-segments.js";
6
+ import { recordDigest } from "../../release/discussion/envelope.js";
7
+ import { createDomainPublicationQualificationReceipt } from "../publication-qualification.js";
8
+ import { verifyPipelinePublicationPlan } from "./plan.js";
9
+ import {
10
+ inspectPipelineFileArtifact,
11
+ verifyPipelineProductFiles,
12
+ } from "./pack.js";
13
+ import { publicationPath } from "./files.js";
14
+ import { assertPipelinePackagePolicy } from "./package-policy.js";
15
+ import { readNpmPackageJsonFromTarball } from "../../release/candidate/payloads.js";
16
+
17
+ function productDescriptor(artifact) {
18
+ const { file, size, digest, package: pkg, ...descriptor } = artifact;
19
+ return descriptor;
20
+ }
21
+
22
+ export function inspectPipelinePublicationArtifacts(directory, manifest, plan) {
23
+ verifyPipelineProductFiles(directory, manifest);
24
+ for (const artifact of manifest.artifacts) {
25
+ if (artifact.kind === "npm-package") {
26
+ const pkg = readNpmPackageJsonFromTarball(
27
+ publicationPath(directory, artifact.file),
28
+ );
29
+ assertPipelinePackagePolicy(pkg);
30
+ if (
31
+ !artifact.package ||
32
+ pkg.name !== artifact.package.name ||
33
+ pkg.version !== plan.version ||
34
+ pkg.version !== artifact.package.version ||
35
+ pkg.private === true
36
+ )
37
+ throw new Error(
38
+ "Independent npm qualification rejected package identity or version",
39
+ );
40
+ } else
41
+ inspectPipelineFileArtifact(directory, {
42
+ path: artifact.file,
43
+ kind: artifact.kind,
44
+ });
45
+ }
46
+ }
47
+
48
+ export function qualifyPipelineProducts({
49
+ plan,
50
+ source,
51
+ bundles,
52
+ build,
53
+ policyRoot,
54
+ now = new Date(),
55
+ }) {
56
+ verifyPipelinePublicationPlan(plan);
57
+ if (build.schema === PUBLICATION_BUILD_AGGREGATE)
58
+ verifyPublicationBuildAggregate(build, {
59
+ plan,
60
+ materialization: { source },
61
+ runId: build.runId,
62
+ runAttempt: build.runAttempt,
63
+ });
64
+ const { root: buildRoot, ...buildBody } = build;
65
+ if (
66
+ buildRoot !== recordDigest(buildBody) ||
67
+ !Number.isSafeInteger(build.runId) ||
68
+ build.runId < 1 ||
69
+ ![
70
+ "buildchain.pipeline-publication-build-readback/v1",
71
+ PUBLICATION_BUILD_AGGREGATE,
72
+ ].includes(build.schema) ||
73
+ build.outcome !== "success" ||
74
+ build.planRoot !== plan.root ||
75
+ recordDigest(build.source) !== recordDigest(source)
76
+ )
77
+ throw new Error(
78
+ "Publication requires exact independent completed build readback",
79
+ );
80
+ const platforms = [
81
+ ...new Set(plan.outputs.map(({ platform }) => platform)),
82
+ ].sort();
83
+ const observed = bundles.map(({ manifest }) => manifest.platform).sort();
84
+ if (
85
+ recordDigest(platforms) !== recordDigest(observed) ||
86
+ recordDigest(platforms) !== recordDigest(build.platforms)
87
+ )
88
+ throw new Error(
89
+ "Publication build must cover exactly every declared platform",
90
+ );
91
+ const artifactIds = bundles
92
+ .map(({ providerArtifact }) => providerArtifact.id)
93
+ .sort((left, right) => left - right);
94
+ if (
95
+ new Set(artifactIds).size !== platforms.length ||
96
+ recordDigest(artifactIds) !== recordDigest(build.artifactIds)
97
+ )
98
+ throw new Error(
99
+ "Publication readback must cover exactly the retained provider artifacts",
100
+ );
101
+ const descriptors = [],
102
+ artifacts = [];
103
+ for (const { directory, manifest, providerArtifact } of bundles) {
104
+ const producer = publicationArtifactProducer(build, providerArtifact.id);
105
+ if (
106
+ manifest.planRoot !== producer.build.planRoot ||
107
+ recordDigest(manifest.source) !== recordDigest(source) ||
108
+ !build.artifactIds.includes(providerArtifact.id) ||
109
+ providerArtifact.expired ||
110
+ providerArtifact.workflow_run?.id !== producer.build.runId
111
+ )
112
+ throw new Error(
113
+ "Publication artifact is not bound to the exact admitted source run",
114
+ );
115
+ inspectPipelinePublicationArtifacts(directory, manifest, plan);
116
+ descriptors.push(...manifest.artifacts.map(productDescriptor));
117
+ artifacts.push(
118
+ ...manifest.artifacts.map((artifact) => ({
119
+ ...artifact,
120
+ manifestRoot: manifest.root,
121
+ providerArtifactId: providerArtifact.id,
122
+ })),
123
+ );
124
+ }
125
+ descriptors.sort((left, right) => left.id.localeCompare(right.id));
126
+ artifacts.sort((left, right) => left.id.localeCompare(right.id));
127
+ if (recordDigest(descriptors) !== recordDigest(plan.outputs))
128
+ throw new Error(
129
+ "Publication artifacts do not exactly match declared outputs and targets",
130
+ );
131
+ if (new Set(artifacts.map(({ file }) => file)).size !== artifacts.length)
132
+ throw new Error("Publication asset filenames must be unique");
133
+ const packages = artifacts.filter((artifact) => artifact.package);
134
+ if (
135
+ new Set(packages.map(({ package: pkg }) => pkg.name)).size !==
136
+ packages.length
137
+ )
138
+ throw new Error(
139
+ "Multi-platform npm publication requires distinct declared package names",
140
+ );
141
+ const candidateRoot = recordDigest({
142
+ planRoot: plan.root,
143
+ source,
144
+ artifacts,
145
+ });
146
+ const qualification = createDomainPublicationQualificationReceipt({
147
+ repository: source.repository,
148
+ candidateRoot,
149
+ sourceSha: source.commit,
150
+ sourceRoot: recordDigest(source),
151
+ policyDigest: policyRoot,
152
+ artifacts: artifacts.map((artifact) => ({
153
+ role: `${artifact.product}/${artifact.artifact}`,
154
+ platform: artifact.platform,
155
+ artifactRoot: artifact.digest,
156
+ manifestRoot: artifact.manifestRoot,
157
+ })),
158
+ issuedAt: now.toISOString(),
159
+ expiresAt: new Date(now.getTime() + 60 * 60 * 1000).toISOString(),
160
+ });
161
+ const body = {
162
+ schema: "buildchain.pipeline-publication-qualification/v1",
163
+ planRoot: plan.root,
164
+ source,
165
+ build,
166
+ artifacts,
167
+ qualification,
168
+ };
169
+ return { ...body, root: recordDigest(body) };
170
+ }