@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,75 @@
1
+ import { releaseDiscussionStore } from "../../release/discussion/store.js";
2
+ import { validateAttemptRecord } from "./records.js";
3
+ import { readBusinessAttempt } from "./reader.js";
4
+
5
+ // exclusive.run owns the repository/intent-wide writer scope, not merely this
6
+ // process. assertOwner rechecks the live provider fence before every mutation.
7
+ // No default mutex: an in-memory lock cannot serialize independent workflows.
8
+ export function businessAttemptStore(transport, exclusive, options = {}) {
9
+ if (typeof exclusive?.run !== "function")
10
+ throw new Error("Attempt writes require an exclusive provider writer");
11
+ const journal = releaseDiscussionStore(transport, options);
12
+ async function read(session) {
13
+ const observed = await journal.read(session);
14
+ return { ...observed, ...readBusinessAttempt(observed) };
15
+ }
16
+ async function scoped(intent, operation) {
17
+ return exclusive.run(
18
+ { repository: intent.repository, intent: intent.id },
19
+ async (owner) => {
20
+ if (typeof owner?.assertOwner !== "function")
21
+ throw new Error("Missing live writer fence");
22
+ await owner.assertOwner();
23
+ return operation(owner);
24
+ },
25
+ );
26
+ }
27
+ async function initialize(request) {
28
+ readBusinessAttempt({ intent: request.intent, records: [] });
29
+ return scoped(request.intent, async (owner) => {
30
+ const repository = await transport.repository(request.intent.repository);
31
+ if (repository.id !== request.intent.source.repositoryId)
32
+ throw new Error("Intent repository identity changed");
33
+ const fenced = { ...transport };
34
+ for (const name of ["create", "append"])
35
+ fenced[name] = async (...args) => {
36
+ await owner.assertOwner();
37
+ return transport[name](...args);
38
+ };
39
+ return releaseDiscussionStore(fenced, options).initialize(request);
40
+ });
41
+ }
42
+ async function append(session, record, expectedHead) {
43
+ return scoped(session.intent, async (owner) => {
44
+ validateAttemptRecord(record, session.intent);
45
+ const observed = await read(session);
46
+ const existing = observed.records.find((event) => event.id === record.id);
47
+ if (existing) return observed;
48
+ if ((observed.head || "") !== expectedHead)
49
+ throw new Error("Stale attempt head; reobserve before writing");
50
+ if (record.node === "attempt") {
51
+ if (record.predecessor !== (observed.attempt || ""))
52
+ throw new Error("Recovery must succeed the current attempt");
53
+ } else if (record.attempt !== observed.attempt)
54
+ throw new Error("A superseded attempt cannot append new results");
55
+ readBusinessAttempt({
56
+ intent: observed.intent,
57
+ records: [...observed.records, record],
58
+ });
59
+ await owner.assertOwner();
60
+ const fenced = {
61
+ ...transport,
62
+ append: async (...args) => {
63
+ await owner.assertOwner();
64
+ return transport.append(...args);
65
+ },
66
+ };
67
+ await releaseDiscussionStore(fenced, options).append(session, record);
68
+ const result = await read(session);
69
+ if (result.head !== record.id)
70
+ throw new Error("Attempt append readback has a conflicting writer");
71
+ return result;
72
+ });
73
+ }
74
+ return { initialize, read, append };
75
+ }
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { normalInputs } from "../../consumer/contract/entries.js";
5
+ import { compileConsumerPlan } from "../../consumer/contract/plan.js";
6
+ import { buildPipelineProducts } from "../pipeline/build.js";
7
+
8
+ const args = process.argv.slice(2);
9
+ if (
10
+ args.length !== 4 ||
11
+ args[0] !== "--platform" ||
12
+ args[2] !== "--config-path"
13
+ )
14
+ throw new Error(
15
+ "usage: pipeline-build --platform <platform> --config-path <TOML>",
16
+ );
17
+ const { configPath } = normalInputs({ "config-path": args[3] });
18
+ const cwd = fs.realpathSync(process.cwd());
19
+ const config = fs.realpathSync(path.join(cwd, configPath));
20
+ if (!config.startsWith(`${cwd}${path.sep}`))
21
+ throw new Error("Candidate TOML escapes its checkout");
22
+ const plan = compileConsumerPlan(fs.readFileSync(config, "utf8"));
23
+ await buildPipelineProducts({ cwd, plan, platform: args[1] });
@@ -0,0 +1,11 @@
1
+ export function pipelineActionOutputs(core, result) {
2
+ core.setOutput("operation", result.operation);
3
+ core.setOutput("context", JSON.stringify(result.context || {}));
4
+ core.setOutput(
5
+ "matrix",
6
+ JSON.stringify({ include: result.context?.platforms || [] }),
7
+ );
8
+ core.setOutput("request", JSON.stringify(result.request || {}));
9
+ core.setOutput("attempt", result.attempt || result.context?.attempt || "");
10
+ core.info(result.reason || result.operation);
11
+ }
@@ -0,0 +1,62 @@
1
+ import { pipelineActionOutputs } from "./action-output.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { normalInputs } from "../../consumer/contract/entries.js";
5
+ import { pipelineHost } from "./host.js";
6
+ import { controlPipeline } from "./controller.js";
7
+ import { buildPipelineSource } from "./build.js";
8
+ import { recordPipelineBuild } from "./build-control.js";
9
+ import {
10
+ RECOVERY_BUILD_CONTEXT,
11
+ recordRecoveryBuild,
12
+ } from "./recovery-build-control.js";
13
+
14
+ export async function controlPipelineAction(core, env) {
15
+ const { configPath } = normalInputs({
16
+ "config-path": core.getInput("config-path") || undefined,
17
+ });
18
+ const host = await pipelineHost(core, env, "Buildchain pipeline controller");
19
+ const result = await controlPipeline(
20
+ env.GITHUB_EVENT_NAME,
21
+ JSON.parse(fs.readFileSync(env.GITHUB_EVENT_PATH, "utf8")),
22
+ { "config-path": configPath },
23
+ host,
24
+ );
25
+ pipelineActionOutputs(core, result);
26
+ }
27
+
28
+ export async function buildPipelineAction(core, env) {
29
+ const context = JSON.parse(core.getInput("context", { required: true }));
30
+ const result = await buildPipelineSource({
31
+ cwd: path.join(env.GITHUB_WORKSPACE, ".buildchain/product"),
32
+ source: context.source,
33
+ platform: core.getInput("platform", { required: true }),
34
+ environment: env,
35
+ });
36
+ core.info(
37
+ `Verified ${result.products.length} products on ${result.platform}`,
38
+ );
39
+ }
40
+
41
+ export async function recordPipelineBuildAction(core, env) {
42
+ const host = await pipelineHost(core, env, "Record product build");
43
+ const context = JSON.parse(core.getInput("context", { required: true }));
44
+ if (context.schema === "buildchain.pipeline-group-build-context/v1")
45
+ return host.groupRecord(context);
46
+ const result =
47
+ context.schema === RECOVERY_BUILD_CONTEXT
48
+ ? await recordRecoveryBuild(context, host)
49
+ : await recordPipelineBuild(context, host);
50
+ if (result.outcome !== "success")
51
+ throw new Error("Product build did not succeed");
52
+ core.info(
53
+ result.wakePending
54
+ ? "Build retained; next event will retry delivery wake"
55
+ : "Build retained and attempt woken",
56
+ );
57
+ }
58
+
59
+ export async function wakePipelineAction(core, env) {
60
+ const host = await pipelineHost(core, env, "Record delivery attempt");
61
+ await host.wake(core.getInput("attempt", { required: true }));
62
+ }
@@ -0,0 +1,140 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { object } from "../../consumer/contract/shape.js";
3
+ import { pipelinePlatforms } from "./platforms.js";
4
+ import { resumePipelineSession } from "./session.js";
5
+ import { retainPipelineBuildResult } from "./build-result.js";
6
+
7
+ export async function beginPipelineBuild(session, admission, host) {
8
+ const observed = await session.journal.read();
9
+ const attempt = observed.attempt;
10
+ const previous = observed.phases.build;
11
+ if (
12
+ previous &&
13
+ (previous.payload.writer.runId !== String(host.runId) ||
14
+ previous.payload.writer.runAttempt !== String(host.runAttempt))
15
+ ) {
16
+ const { run } = await host.runs.read(
17
+ Number(previous.payload.writer.runId),
18
+ Number(previous.payload.writer.runAttempt),
19
+ );
20
+ if (run.status !== "completed") return null;
21
+ const material = await host
22
+ .materialStore(session)
23
+ .retain(
24
+ `build/incomplete-${run.id}-${run.run_attempt}`,
25
+ run,
26
+ "provider-readback",
27
+ );
28
+ await session.progress.progress({
29
+ attempt,
30
+ phase: "build",
31
+ state: "failure",
32
+ eventKey: `build-incomplete:${run.id}:${run.run_attempt}`,
33
+ reason: "build-recording-incomplete",
34
+ materials: [material],
35
+ expectedHead: observed.head,
36
+ });
37
+ return null;
38
+ }
39
+ if (!observed.phases.admission) {
40
+ const receipt = {
41
+ schema: "buildchain.pipeline-source-observation/v1",
42
+ repositoryId: admission.repositoryId,
43
+ source: admission.live.source,
44
+ baseCommit: admission.live.baseCommit,
45
+ protectedSource: admission.protectedSource,
46
+ route: admission.route,
47
+ runtime: host.runtime,
48
+ };
49
+ const reference = await host
50
+ .materialStore(session)
51
+ .retain("source/admission", receipt, "provider-readback");
52
+ await session.progress.progress({
53
+ attempt,
54
+ phase: "admission",
55
+ state: "success",
56
+ eventKey: `admission:${observed.generation}`,
57
+ materials: [reference],
58
+ });
59
+ }
60
+ const scheduling = await session.journal.read();
61
+ const scheduled = scheduling.phases.build?.payload.writer;
62
+ if (
63
+ scheduled &&
64
+ (scheduled.runId !== String(host.runId) ||
65
+ scheduled.runAttempt !== String(host.runAttempt))
66
+ )
67
+ return null;
68
+ await session.progress.progress({
69
+ attempt,
70
+ phase: "build",
71
+ state: "running",
72
+ eventKey: `build-start:${host.runId}:${host.runAttempt}`,
73
+ expectedHead: scheduling.head,
74
+ });
75
+ return {
76
+ schema: "buildchain.pipeline-build-context/v1",
77
+ attempt,
78
+ generation: observed.generation,
79
+ source: admission.live.source,
80
+ runId: host.runId,
81
+ runAttempt: host.runAttempt,
82
+ platforms: pipelinePlatforms(admission.plan),
83
+ };
84
+ }
85
+
86
+ export function validatePipelineBuildContext(context) {
87
+ object(context, [
88
+ "schema",
89
+ "attempt",
90
+ "generation",
91
+ "source",
92
+ "runId",
93
+ "runAttempt",
94
+ "platforms",
95
+ ]);
96
+ if (
97
+ context.schema !== "buildchain.pipeline-build-context/v1" ||
98
+ !/^attempt-[0-9a-f]{64}$/u.test(context.attempt) ||
99
+ !/^sha256:[0-9a-f]{64}$/u.test(context.generation) ||
100
+ ![context.runId, context.runAttempt].every(
101
+ (value) => Number.isSafeInteger(value) && value > 0,
102
+ )
103
+ )
104
+ throw new Error("Invalid internal pipeline build context");
105
+ return context;
106
+ }
107
+
108
+ export async function recordPipelineBuild(context, host) {
109
+ validatePipelineBuildContext(context);
110
+ if (context.runId !== host.runId || context.runAttempt !== host.runAttempt)
111
+ throw new Error("Build callback belongs to another provider execution");
112
+ const session = await resumePipelineSession(
113
+ { ...host, attempt: context.attempt },
114
+ host,
115
+ );
116
+ const current = session.observed.history.at(-1);
117
+ if (
118
+ context.generation !== current.generation.id ||
119
+ recordDigest(context.source) !== recordDigest(current.generation.source)
120
+ )
121
+ throw new Error("Build callback changed its admitted source generation");
122
+ const admitted = await host.source.source(
123
+ context.source.commit,
124
+ context.source.configPath,
125
+ );
126
+ if (
127
+ recordDigest(context.platforms) !==
128
+ recordDigest(pipelinePlatforms(admitted.plan))
129
+ )
130
+ throw new Error(
131
+ "Build callback omitted or changed a declared product platform",
132
+ );
133
+ const readback = await host.runs.build(
134
+ context.runId,
135
+ context.runAttempt,
136
+ context.source,
137
+ context.platforms.map((platform) => platform.platform),
138
+ );
139
+ return retainPipelineBuildResult(context, readback, session, host);
140
+ }
@@ -0,0 +1,92 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import {
3
+ PIPELINE_BUILD_QUALIFICATION,
4
+ verifyPipelineBuildQualification,
5
+ } from "./build-qualification.js";
6
+ import { pipelinePlatforms } from "./platforms.js";
7
+
8
+ // The archive reference is already validated against the current attempt. A
9
+ // build result is still re-read from the provider before it can admit delivery.
10
+ export async function pipelineBuildEvidence(session, host) {
11
+ const observed = await session.journal.read();
12
+ const current = observed.history.at(-1);
13
+ const phase = current.phases.build;
14
+ if (phase?.payload.state !== "success") return null;
15
+ const references = phase.payload.materials.filter(
16
+ (material) =>
17
+ material.id.startsWith("build/provider-") ||
18
+ material.id.startsWith("build/qualified-"),
19
+ );
20
+ if (references.length !== 1)
21
+ throw new Error("Pipeline build requires one retained provider receipt");
22
+ const retained = await host.materialStore(session).read(references[0]);
23
+ let { run } = await host.runs.read(retained.runId, retained.runAttempt);
24
+ if (run.status !== "completed" && run.id !== host.runId)
25
+ run = await host.runs.completed(retained.runId, retained.runAttempt);
26
+ if (retained.schema === PIPELINE_BUILD_QUALIFICATION) {
27
+ const source = await host.source.source(
28
+ current.generation.source.commit,
29
+ current.generation.source.configPath,
30
+ );
31
+ if (
32
+ recordDigest(source.identity) !== recordDigest(current.generation.source)
33
+ )
34
+ throw new Error("Recovered build source changed during qualification");
35
+ await verifyPipelineBuildQualification(
36
+ retained,
37
+ source.identity,
38
+ pipelinePlatforms(source.plan).map(({ platform }) => platform),
39
+ host.runs,
40
+ );
41
+ } else {
42
+ const verified = await host.runs.build(
43
+ retained.runId,
44
+ retained.runAttempt,
45
+ current.generation.source,
46
+ retained.jobs.map(
47
+ (job) => job.name.match(/Build product \(([^)]+)\)$/u)?.[1],
48
+ ),
49
+ );
50
+ if (recordDigest(verified) !== recordDigest(retained))
51
+ throw new Error("Retained product build provider result changed");
52
+ }
53
+ return {
54
+ run,
55
+ readback: retained,
56
+ sourceHead: current.generation.source.commit,
57
+ outcome: retained.outcome,
58
+ };
59
+ }
60
+
61
+ export async function publishPipelineBuildCheck(
62
+ context,
63
+ readback,
64
+ request,
65
+ repository,
66
+ ) {
67
+ if (
68
+ context.source.repository !== repository ||
69
+ recordDigest(context.source) !== recordDigest(readback.source) ||
70
+ context.runId !== readback.runId ||
71
+ context.runAttempt !== readback.runAttempt
72
+ )
73
+ throw new Error(
74
+ "Product check source differs from independently verified build",
75
+ );
76
+ const externalId = `buildchain:${context.attempt}:${context.runId}:${context.runAttempt}`;
77
+ return request(`/repos/${repository}/check-runs`, {
78
+ method: "POST",
79
+ body: {
80
+ name: "check",
81
+ head_sha: context.source.commit,
82
+ external_id: externalId,
83
+ status: "completed",
84
+ conclusion: readback.outcome === "success" ? "success" : "failure",
85
+ output: {
86
+ title: "Buildchain product verification",
87
+ summary: `Exact product jobs: ${readback.root}\nAttempt: ${context.attempt}`,
88
+ },
89
+ details_url: `https://github.com/${repository}/actions/runs/${context.runId}/attempts/${context.runAttempt}`,
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,145 @@
1
+ import {
2
+ recordDigest,
3
+ validateRuntime,
4
+ } from "../../release/discussion/envelope.js";
5
+ import { object } from "../../consumer/contract/shape.js";
6
+
7
+ export const PIPELINE_BUILD_QUALIFICATION =
8
+ "buildchain.pipeline-build-qualification/v1";
9
+
10
+ export function buildReadbackPlatforms(readback) {
11
+ const { root, ...body } = readback;
12
+ if (
13
+ body.schema !== "buildchain.pipeline-build-readback/v1" ||
14
+ root !== recordDigest(body) ||
15
+ !Array.isArray(body.jobs) ||
16
+ !body.jobs.length
17
+ )
18
+ throw new Error(
19
+ "Build qualification requires immutable provider job readback",
20
+ );
21
+ const platforms = body.jobs.map(
22
+ (job) => job.name?.match(/Build product \(([^)]+)\)$/u)?.[1],
23
+ );
24
+ if (
25
+ platforms.some((platform) => !platform) ||
26
+ new Set(platforms).size !== platforms.length
27
+ )
28
+ throw new Error(
29
+ "Provider build has ambiguous or unsupported platform jobs",
30
+ );
31
+ return platforms;
32
+ }
33
+
34
+ function validateBuildInventory(platforms, segments, runId, runAttempt) {
35
+ if (
36
+ !Array.isArray(platforms) ||
37
+ !platforms.length ||
38
+ platforms.length > 5 ||
39
+ new Set(platforms).size !== platforms.length ||
40
+ !Array.isArray(segments) ||
41
+ !segments.length ||
42
+ segments.length > 5 ||
43
+ ![runId, runAttempt].every(
44
+ (value) => Number.isSafeInteger(value) && value > 0,
45
+ )
46
+ )
47
+ throw new Error(
48
+ "Build qualification requires bounded exact platform and execution coordinates",
49
+ );
50
+ }
51
+
52
+ export function qualifyPipelineBuild({
53
+ source,
54
+ platforms,
55
+ segments,
56
+ runId,
57
+ runAttempt,
58
+ }) {
59
+ validateBuildInventory(platforms, segments, runId, runAttempt);
60
+ const selected = [];
61
+ let successful = true;
62
+ for (const segment of segments) {
63
+ object(segment, ["readback", "platforms", "runtime"]);
64
+ validateRuntime(segment.runtime);
65
+ if (
66
+ segment.runtime.repository !== "kungfu-systems/buildchain" ||
67
+ !Array.isArray(segment.platforms) ||
68
+ !segment.platforms.length ||
69
+ recordDigest(segment.readback.source) !== recordDigest(source)
70
+ )
71
+ throw new Error(
72
+ "Build segment changed its exact source or runtime repository",
73
+ );
74
+ const available = buildReadbackPlatforms(segment.readback);
75
+ for (const platform of segment.platforms) {
76
+ const index = available.indexOf(platform),
77
+ job = segment.readback.jobs[index];
78
+ if (
79
+ index < 0 ||
80
+ !platforms.includes(platform) ||
81
+ selected.includes(platform) ||
82
+ job.status !== "completed" ||
83
+ !job.conclusion ||
84
+ job.run_id !== segment.readback.runId ||
85
+ job.run_attempt !== segment.readback.runAttempt ||
86
+ !Number.isSafeInteger(job.id) ||
87
+ job.id < 1
88
+ )
89
+ throw new Error(
90
+ "Build segments omitted, duplicated or changed a real platform execution",
91
+ );
92
+ selected.push(platform);
93
+ successful &&= job.conclusion === "success";
94
+ }
95
+ }
96
+ if (selected.length !== platforms.length)
97
+ throw new Error("Build qualification is missing declared platforms");
98
+ const body = {
99
+ schema: PIPELINE_BUILD_QUALIFICATION,
100
+ source,
101
+ platforms: [...platforms].sort(),
102
+ runId,
103
+ runAttempt,
104
+ segments: structuredClone(segments),
105
+ outcome: successful ? "success" : "failure",
106
+ };
107
+ return { ...body, root: recordDigest(body) };
108
+ }
109
+
110
+ export async function verifyPipelineBuildQualification(
111
+ value,
112
+ source,
113
+ platforms,
114
+ runs,
115
+ ) {
116
+ object(value, [
117
+ "schema",
118
+ "source",
119
+ "platforms",
120
+ "runId",
121
+ "runAttempt",
122
+ "segments",
123
+ "outcome",
124
+ "root",
125
+ ]);
126
+ const expected = qualifyPipelineBuild({ ...value, source, platforms });
127
+ if (recordDigest(value) !== recordDigest(expected))
128
+ throw new Error(
129
+ "Retained build qualification changed its source or exact platform inventory",
130
+ );
131
+ for (const segment of value.segments) {
132
+ const old = segment.readback;
133
+ const fresh = await runs.build(
134
+ old.runId,
135
+ old.runAttempt,
136
+ source,
137
+ buildReadbackPlatforms(old),
138
+ );
139
+ if (recordDigest(fresh) !== recordDigest(old))
140
+ throw new Error(
141
+ "A recovered platform result changed during provider requalification",
142
+ );
143
+ }
144
+ return value;
145
+ }
@@ -0,0 +1,69 @@
1
+ import { publishPipelineBuildCheck } from "./build-evidence.js";
2
+ import { PIPELINE_BUILD_QUALIFICATION } from "./build-qualification.js";
3
+ import { recordDigest } from "../../release/discussion/envelope.js";
4
+ import { replayRecoveryIntegration } from "./recovery-integration.js";
5
+
6
+ async function resultReference(context, readback, session, host) {
7
+ const observed = await session.journal.read();
8
+ if (observed.attempt !== context.attempt)
9
+ throw new Error("Late build result belongs to a superseded attempt");
10
+ const prior = observed.history
11
+ .at(-1)
12
+ .events.find(
13
+ (event) =>
14
+ event.payload.eventKey ===
15
+ `build-result:${context.runId}:${context.runAttempt}`,
16
+ );
17
+ const store = host.materialStore(session);
18
+ if (prior) {
19
+ const references = prior.payload.materials;
20
+ if (
21
+ references.length !== 1 ||
22
+ recordDigest(await store.read(references[0])) !== recordDigest(readback)
23
+ )
24
+ throw new Error("Repeated build callback changed its immutable result");
25
+ return references[0];
26
+ }
27
+ return store.retain(
28
+ `build/${readback.schema === PIPELINE_BUILD_QUALIFICATION ? "qualified" : "provider"}-${context.runId}-${context.runAttempt}`,
29
+ readback,
30
+ "provider-readback",
31
+ );
32
+ }
33
+
34
+ export async function retainPipelineBuildResult(
35
+ context,
36
+ readback,
37
+ session,
38
+ host,
39
+ ) {
40
+ const reference = await resultReference(context, readback, session, host);
41
+ await session.progress.progress({
42
+ attempt: context.attempt,
43
+ phase: "build",
44
+ state: readback.outcome === "success" ? "success" : "failure",
45
+ eventKey: `build-result:${context.runId}:${context.runAttempt}`,
46
+ reason: readback.outcome === "success" ? "" : "product-build-failed",
47
+ materials: [reference],
48
+ });
49
+ await host.project(session);
50
+ await publishPipelineBuildCheck(
51
+ context,
52
+ readback,
53
+ host.request,
54
+ host.repository,
55
+ );
56
+ if (
57
+ readback.schema === PIPELINE_BUILD_QUALIFICATION &&
58
+ readback.outcome === "success"
59
+ )
60
+ await replayRecoveryIntegration(session, host);
61
+ try {
62
+ await host.wake(context.attempt);
63
+ return { outcome: readback.outcome, wakePending: false };
64
+ } catch {
65
+ // The source build remains qualified if only notification failed. Its
66
+ // durable result is reobserved on the next PR event or exact attempt wake.
67
+ return { outcome: readback.outcome, wakePending: true };
68
+ }
69
+ }