@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,79 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import {
6
+ publicationFile,
7
+ publicationPath,
8
+ writeImmutablePublicationFile,
9
+ } from "./files.js";
10
+
11
+ export async function retainPipelineProducts(archive, qualified, bundles) {
12
+ const retained = [];
13
+ for (const artifact of qualified.artifacts) {
14
+ const bundle = bundles.find(
15
+ ({ manifest }) => manifest.root === artifact.manifestRoot,
16
+ );
17
+ if (!bundle)
18
+ throw new Error("Qualified product has no exact source manifest");
19
+ const file = publicationFile(
20
+ publicationPath(bundle.directory, artifact.file),
21
+ );
22
+ if (file.digest !== artifact.digest || file.size !== artifact.size)
23
+ throw new Error("Qualified product changed before durable retention");
24
+ const handle = await archive.put(file.bytes, {
25
+ name: `${artifact.digest.slice(7)}-${path.basename(artifact.file)}`,
26
+ mediaType: "application/octet-stream",
27
+ });
28
+ await verifiedBytes(archive, handle, artifact);
29
+ retained.push({ id: artifact.id, handle });
30
+ }
31
+ const body = {
32
+ schema: "buildchain.pipeline-sealed-products/v1",
33
+ qualificationRoot: qualified.root,
34
+ products: retained,
35
+ };
36
+ return { ...body, root: recordDigest(body) };
37
+ }
38
+
39
+ async function verifiedBytes(archive, handle, artifact) {
40
+ if (handle.digest !== artifact.digest || handle.size !== artifact.size)
41
+ throw new Error("Retained product handle does not bind qualified bytes");
42
+ const bytes = Buffer.from(await archive.read(handle));
43
+ if (
44
+ bytes.length !== artifact.size ||
45
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}` !==
46
+ artifact.digest
47
+ )
48
+ throw new Error("Retained product readback differs from qualified bytes");
49
+ return bytes;
50
+ }
51
+
52
+ export async function restorePipelineProducts(
53
+ archive,
54
+ qualified,
55
+ sealed,
56
+ directory,
57
+ ) {
58
+ const { root, ...body } = sealed;
59
+ if (
60
+ root !== recordDigest(body) ||
61
+ sealed.qualificationRoot !== qualified.root ||
62
+ recordDigest(sealed.products.map(({ id }) => id)) !==
63
+ recordDigest(qualified.artifacts.map(({ id }) => id))
64
+ )
65
+ throw new Error(
66
+ "Retained products must cover the exact qualification inventory",
67
+ );
68
+ fs.mkdirSync(directory, { recursive: true });
69
+ for (const artifact of qualified.artifacts) {
70
+ const { handle } = sealed.products.find(({ id }) => id === artifact.id);
71
+ const bytes = await verifiedBytes(archive, handle, artifact);
72
+ const target = path.resolve(directory, artifact.file);
73
+ if (!target.startsWith(`${path.resolve(directory)}${path.sep}`))
74
+ throw new Error("Retained product path escapes its directory");
75
+ writeImmutablePublicationFile(target, bytes);
76
+ publicationPath(directory, artifact.file);
77
+ }
78
+ return directory;
79
+ }
@@ -0,0 +1,43 @@
1
+ import fs from "node:fs";
2
+ import { publicationContext, uniquePublicationMaterial } from "./context.js";
3
+ import { distributePipelineProducts } from "./distribution.js";
4
+ import { nextPipelineDevelopment } from "./next-development.js";
5
+
6
+ export async function settlePipelinePublication(
7
+ context,
8
+ host,
9
+ directory,
10
+ environment,
11
+ ) {
12
+ const { session, journal } = await publicationContext(context, host);
13
+ if (session.observed.phases.publish?.payload.state !== "success")
14
+ throw new Error(
15
+ "Publication must retain success before distribution and next-development",
16
+ );
17
+ fs.mkdirSync(directory, { recursive: true });
18
+ const retained = await uniquePublicationMaterial(
19
+ journal,
20
+ "publication/qualified/",
21
+ );
22
+ if (session.observed.phases.distribution?.payload.state !== "success") {
23
+ const receipt = await distributePipelineProducts(
24
+ context,
25
+ host,
26
+ journal,
27
+ retained,
28
+ environment,
29
+ directory,
30
+ );
31
+ await journal.record("publication/distribution-complete", receipt, {
32
+ phase: "distribution",
33
+ state: "success",
34
+ });
35
+ }
36
+ const next = await nextPipelineDevelopment(context, host, journal);
37
+ await journal.record("publication/next-development-result", next, {
38
+ phase: "next-development",
39
+ state: next.state,
40
+ });
41
+ await host.project(session);
42
+ return next;
43
+ }
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { execFileSync } from "node:child_process";
5
+ import {
6
+ canonicalJson,
7
+ recordDigest,
8
+ } from "../../release/discussion/envelope.js";
9
+ import { verifyPipelineQualification } from "./documents.js";
10
+ import { writeImmutablePublicationFile } from "./files.js";
11
+
12
+ export const PIPELINE_PRODUCT_PREDICATE =
13
+ "https://buildchain.libkungfu.dev/attestations/pipeline-products/v1";
14
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
15
+
16
+ export function preparePipelineSigning({
17
+ plan,
18
+ materialization,
19
+ qualified,
20
+ directory,
21
+ evaluatedAt,
22
+ }) {
23
+ verifyPipelineQualification({
24
+ plan,
25
+ materialization,
26
+ qualified,
27
+ evaluatedAt,
28
+ });
29
+ const predicate = {
30
+ schema: "buildchain.pipeline-product-attestation/v1",
31
+ planRoot: plan.root,
32
+ materializationRoot: materialization.root,
33
+ qualificationRoot: qualified.root,
34
+ source: qualified.source,
35
+ protectedSource: plan.source,
36
+ publisher: plan.publisher,
37
+ runtime: plan.runtime,
38
+ provider: {
39
+ runId: qualified.build.runId,
40
+ runAttempt: qualified.build.runAttempt,
41
+ source: qualified.build.providerSource,
42
+ },
43
+ };
44
+ const subjectBytes = Buffer.from(`${canonicalJson(qualified)}\n`);
45
+ const subjectPath = writeImmutablePublicationFile(
46
+ path.join(directory, "qualified-products.json"),
47
+ subjectBytes,
48
+ );
49
+ const predicatePath = writeImmutablePublicationFile(
50
+ path.join(directory, "predicate.json"),
51
+ `${canonicalJson(predicate)}\n`,
52
+ );
53
+ return {
54
+ subjectPath,
55
+ predicatePath,
56
+ predicate,
57
+ subjectDigest: digest(subjectBytes),
58
+ predicateType: PIPELINE_PRODUCT_PREDICATE,
59
+ };
60
+ }
61
+
62
+ export function verifyPipelineSigning({
63
+ plan,
64
+ materialization,
65
+ qualified,
66
+ bundlePath,
67
+ directory,
68
+ token,
69
+ execute = execFileSync,
70
+ evaluatedAt,
71
+ }) {
72
+ const prepared = preparePipelineSigning({
73
+ plan,
74
+ materialization,
75
+ qualified,
76
+ directory,
77
+ evaluatedAt,
78
+ });
79
+ const output = execute(
80
+ "gh",
81
+ [
82
+ "attestation",
83
+ "verify",
84
+ prepared.subjectPath,
85
+ "--repo",
86
+ qualified.source.repository,
87
+ "--signer-workflow",
88
+ `${plan.publisher.repository}/${plan.publisher.workflow}`,
89
+ "--signer-digest",
90
+ plan.publisher.workflowSha,
91
+ "--source-digest",
92
+ qualified.build.providerSource,
93
+ "--predicate-type",
94
+ PIPELINE_PRODUCT_PREDICATE,
95
+ "--bundle",
96
+ bundlePath,
97
+ "--deny-self-hosted-runners",
98
+ "--format",
99
+ "json",
100
+ ],
101
+ {
102
+ encoding: "utf8",
103
+ stdio: ["ignore", "pipe", "pipe"],
104
+ maxBuffer: 16 * 1024 * 1024,
105
+ env: { ...process.env, GH_TOKEN: token },
106
+ },
107
+ );
108
+ const results = JSON.parse(output);
109
+ const statements = Array.isArray(results)
110
+ ? results
111
+ .map((result) => result.verificationResult?.statement)
112
+ .filter(Boolean)
113
+ : [];
114
+ if (
115
+ statements.length !== 1 ||
116
+ statements[0].predicateType !== prepared.predicateType ||
117
+ recordDigest(statements[0].predicate) !==
118
+ recordDigest(prepared.predicate) ||
119
+ statements[0].subject?.length !== 1 ||
120
+ statements[0].subject[0].digest?.sha256 !== prepared.subjectDigest
121
+ )
122
+ throw new Error(
123
+ "Cryptographically verified attestation does not bind the exact publication predicate and subject",
124
+ );
125
+ if (digest(fs.readFileSync(prepared.subjectPath)) !== prepared.subjectDigest)
126
+ throw new Error("Signed publication subject changed during verification");
127
+ const bundleBytes = fs.readFileSync(bundlePath);
128
+ return {
129
+ schema: "buildchain.pipeline-product-signing/v1",
130
+ verified: true,
131
+ planRoot: plan.root,
132
+ materializationRoot: materialization.root,
133
+ qualificationRoot: qualified.root,
134
+ subjectDigest: `sha256:${prepared.subjectDigest}`,
135
+ predicateRoot: recordDigest(prepared.predicate),
136
+ bundleDigest: `sha256:${digest(bundleBytes)}`,
137
+ signer: plan.publisher,
138
+ providerSource: qualified.build.providerSource,
139
+ };
140
+ }
@@ -0,0 +1,83 @@
1
+ import { planPipelinePublication } from "./plan.js";
2
+
3
+ export async function createPipelinePublicationPlan(
4
+ session,
5
+ publisherSha,
6
+ host,
7
+ version,
8
+ ) {
9
+ const observed = session.observed;
10
+ const attempt = observed.attempt;
11
+ const integration = await host.integration.observe({
12
+ ...observed.history.at(-1),
13
+ intent: session.intent,
14
+ });
15
+ const source = await host.source.source(
16
+ integration.mergeCommit,
17
+ observed.history.at(-1).generation.source.configPath,
18
+ );
19
+ const route = source.plan.channels.find(
20
+ (entry) =>
21
+ entry.to === session.intent.source.targetBranch &&
22
+ entry.operation !== "develop",
23
+ );
24
+ if (!route)
25
+ throw new Error(
26
+ "Protected source does not declare this publication channel",
27
+ );
28
+ const input = await version.inspect(source.identity, source.plan.version);
29
+ const runtimeCommit = await host.request(
30
+ `/repos/${host.runtime.repository}/git/commits/${host.runtime.sha}`,
31
+ );
32
+ if (
33
+ runtimeCommit.sha !== host.runtime.sha ||
34
+ !/^[0-9a-f]{40}$/u.test(runtimeCommit.tree?.sha || "")
35
+ )
36
+ throw new Error("Publication runtime has no exact source tree");
37
+ const definition = {
38
+ repository: "kungfu-systems/buildchain",
39
+ workflow: ".github/workflows/.release-pipeline-products.yml",
40
+ workflowSha: publisherSha,
41
+ job: "apply",
42
+ };
43
+ const parameters = {
44
+ attempt,
45
+ generation: observed.generation,
46
+ source: source.identity,
47
+ intentSource: observed.history.at(-1).generation.source,
48
+ runtime: {
49
+ repository: host.runtime.repository,
50
+ commit: runtimeCommit.sha,
51
+ tree: runtimeCommit.tree.sha,
52
+ },
53
+ publisher: definition,
54
+ contract: source.plan,
55
+ route,
56
+ version: input.version,
57
+ sourceTimestamp: input.sourceTimestamp,
58
+ };
59
+ let plan = planPipelinePublication(parameters);
60
+ const tag = await host.request(
61
+ `/repos/${host.repository}/git/ref/tags/${encodeURIComponent(plan.tag)}`,
62
+ { allow404: true },
63
+ );
64
+ const channel = `v${plan.version.split(".")[0]}${plan.channel === "alpha" ? "-alpha" : ""}`;
65
+ const previous = await host.request(
66
+ `/repos/${host.repository}/git/ref/tags/${channel}`,
67
+ { allow404: true },
68
+ );
69
+ if (
70
+ previous &&
71
+ (previous.object?.type !== "commit" ||
72
+ !/^[0-9a-f]{40}$/u.test(previous.object.sha))
73
+ )
74
+ throw new Error(
75
+ "Publication channel requires an exact lightweight commit ref",
76
+ );
77
+ plan = planPipelinePublication({
78
+ ...parameters,
79
+ expectedTagSha: tag?.object?.sha || null,
80
+ previousChannelCommit: previous?.object?.sha || null,
81
+ });
82
+ return plan;
83
+ }
@@ -0,0 +1,215 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
3
+ import { verifyRootedPublication } from "./documents.js";
4
+ import { verifyPipelinePublicationPlan } from "./plan.js";
5
+ import {
6
+ pipelineEntryRuns,
7
+ pipelineEntryRun,
8
+ pipelineEntryCheck,
9
+ } from "../../providers/github/pipeline-entry-runs.js";
10
+
11
+ async function nativeBuild(check, run, consumer, host) {
12
+ const loaded = await host.index.resolve(check.attempt);
13
+ const observed = readBusinessAttempt(loaded.snapshot);
14
+ const selected = observed.history.find(
15
+ (item) => item.identity.id === check.attempt,
16
+ );
17
+ if (
18
+ observed.intent.repository !== host.repository ||
19
+ !selected ||
20
+ selected.identity.requestKey.startsWith("recover:") ||
21
+ recordDigest(selected.generation.source) !== recordDigest(consumer.source)
22
+ )
23
+ throw new Error(
24
+ "Published entry check points to another source or a recovery attempt",
25
+ );
26
+ const phase = selected.phases.build;
27
+ if (phase?.payload.state !== "success" || check.outcome !== "success")
28
+ return {
29
+ status: "missing",
30
+ reason: "latest-published-entry-build-failed",
31
+ attempt: check.attempt,
32
+ };
33
+ if (
34
+ phase.payload.writer.runId !== String(run.id) ||
35
+ phase.payload.writer.runAttempt !== String(run.run_attempt) ||
36
+ phase.runtime.repository !== "kungfu-systems/buildchain" ||
37
+ phase.runtime.sha !== consumer.selection.ref
38
+ )
39
+ throw new Error(
40
+ "Published entry native build changed its actual runtime or execution",
41
+ );
42
+ const references = phase.payload.materials.filter((item) =>
43
+ item.id.startsWith("build/provider-"),
44
+ );
45
+ if (references.length !== 1)
46
+ throw new Error(
47
+ "Published entry requires one original normal build receipt",
48
+ );
49
+ const session = {
50
+ intent: loaded.snapshot.intent,
51
+ observed: { ...observed, history: [selected] },
52
+ };
53
+ const readback = await host.materialStore(session).read(references[0]);
54
+ verifyRootedPublication(readback, "buildchain.pipeline-build-readback/v1");
55
+ if (
56
+ readback.runId !== run.id ||
57
+ readback.runAttempt !== run.run_attempt ||
58
+ readback.outcome !== "success" ||
59
+ recordDigest(readback.source) !== recordDigest(consumer.source) ||
60
+ recordDigest(readback.entry) !== recordDigest(consumer.entry) ||
61
+ check.summary !==
62
+ `Exact product jobs: ${readback.root}\nAttempt: ${check.attempt}`
63
+ )
64
+ throw new Error("Published entry check and native build material disagree");
65
+ return {
66
+ readback,
67
+ phase,
68
+ reference: references[0],
69
+ attempt: check.attempt,
70
+ intent: observed.intent.id,
71
+ };
72
+ }
73
+
74
+ function completion(run, jobs, native, source) {
75
+ const recorder = jobs.filter(
76
+ (job) => String(job.id) === native.phase.payload.writer.jobId,
77
+ );
78
+ if (
79
+ recorder.length !== 1 ||
80
+ recorder[0].status !== "completed" ||
81
+ recorder[0].conclusion !== "success" ||
82
+ recorder[0].run_id !== run.id ||
83
+ recorder[0].run_attempt !== run.run_attempt ||
84
+ !(
85
+ recorder[0].name === "Record product build" ||
86
+ recorder[0].name.endsWith(" / Record product build")
87
+ )
88
+ )
89
+ throw new Error(
90
+ "Published entry native receipt lacks its exact completed recorder job",
91
+ );
92
+ const selected = [...native.readback.jobs, recorder[0]];
93
+ const start = Date.parse(source.candidate.publishedAt),
94
+ completions = [];
95
+ for (const job of selected) {
96
+ const started = Date.parse(job.started_at),
97
+ ended = Date.parse(job.completed_at);
98
+ if (
99
+ !Number.isFinite(started) ||
100
+ !Number.isFinite(ended) ||
101
+ started < start ||
102
+ ended < started
103
+ )
104
+ throw new Error(
105
+ "Published entry jobs must execute after the exact Alpha publication",
106
+ );
107
+ completions.push(ended);
108
+ }
109
+ return new Date(Math.max(...completions)).toISOString();
110
+ }
111
+
112
+ async function inspectEntryRun(listed, plan, source, host) {
113
+ const { run, jobs } = await host.runs.read(listed.id, listed.run_attempt);
114
+ const consumer = await pipelineEntryRun(run, plan, source, host);
115
+ if (!consumer) return null;
116
+ if (
117
+ !jobs.some((job) => /(?:^| \/ )Build product \([^)]+\)$/u.test(job.name)) &&
118
+ run.conclusion === "success"
119
+ )
120
+ return null;
121
+ const check = await pipelineEntryCheck(run, host);
122
+ if (!check)
123
+ return {
124
+ status: "missing",
125
+ reason: "latest-published-entry-build-unrecorded",
126
+ runId: run.id,
127
+ };
128
+ const native = await nativeBuild(check, run, consumer, host);
129
+ if (native.status === "missing") return native;
130
+ const platforms = [
131
+ ...new Set(plan.outputs.map(({ platform }) => platform)),
132
+ ].sort();
133
+ const fresh = await host.runs.build(
134
+ run.id,
135
+ run.run_attempt,
136
+ consumer.source,
137
+ platforms,
138
+ );
139
+ if (recordDigest(fresh) !== recordDigest(native.readback))
140
+ throw new Error(
141
+ "Published entry original jobs changed or omit declared platforms",
142
+ );
143
+ const completedAt = completion(run, jobs, native, source);
144
+ const again = await host.runs.read(run.id, run.run_attempt);
145
+ const repeated = await nativeBuild(
146
+ await pipelineEntryCheck(again.run, host),
147
+ again.run,
148
+ consumer,
149
+ host,
150
+ );
151
+ if (
152
+ recordDigest(repeated) !== recordDigest(native) ||
153
+ recordDigest(again) !== recordDigest({ run, jobs })
154
+ )
155
+ throw new Error(
156
+ "Published entry evidence changed during final provider readback",
157
+ );
158
+ const body = {
159
+ schema: "buildchain.pipeline-stable-entry/v1",
160
+ planRoot: plan.root,
161
+ sourceRoot: source.root,
162
+ consumer,
163
+ native,
164
+ runId: run.id,
165
+ runAttempt: run.run_attempt,
166
+ canary: {
167
+ id: "published-entry",
168
+ source: "public-build",
169
+ status: "success",
170
+ repository: host.repository,
171
+ candidateSha: source.candidate.sha,
172
+ completedAt,
173
+ attestor: "github-actions[bot]",
174
+ runtimeRef: consumer.selection.ref,
175
+ evidenceUrl: `https://github.com/${host.repository}/actions/runs/${run.id}/attempts/${run.run_attempt}`,
176
+ },
177
+ };
178
+ return { ...body, root: recordDigest(body) };
179
+ }
180
+
181
+ export async function readPipelineStableEntry(plan, source, host) {
182
+ verifyPipelinePublicationPlan(plan);
183
+ verifyRootedPublication(source, "buildchain.pipeline-stable-source/v1");
184
+ if (
185
+ plan.channel !== "stable" ||
186
+ source.planRoot !== plan.root ||
187
+ host.repository !== plan.source.repository ||
188
+ source.contractRoot !== plan.contractRoot ||
189
+ source.candidate.sha !== plan.intentSource.commit ||
190
+ !Number.isFinite(Date.parse(source.candidate.publishedAt))
191
+ )
192
+ throw new Error(
193
+ "Published entry collection requires its exact admitted Alpha plan",
194
+ );
195
+ const inventory = await pipelineEntryRuns(host, source.candidate.publishedAt);
196
+ for (const run of inventory) {
197
+ const result = await inspectEntryRun(run, plan, source, host);
198
+ if (result) {
199
+ if (
200
+ result.canary &&
201
+ recordDigest(
202
+ await pipelineEntryRuns(host, source.candidate.publishedAt),
203
+ ) !== recordDigest(inventory)
204
+ )
205
+ throw new Error(
206
+ "Published entry run inventory changed during qualification",
207
+ );
208
+ return result;
209
+ }
210
+ }
211
+ return {
212
+ status: "missing",
213
+ reason: "no-normal-published-entry-build-for-exact-runtime",
214
+ };
215
+ }