@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,46 @@
1
+ // Closed data validation shared by TOML, event identities and managed locks.
2
+ export function object(value, required, optional = [], location = "config") {
3
+ if (!value || Object.getPrototypeOf(value) !== Object.prototype)
4
+ throw new Error(`${location}: expected a table`);
5
+ for (const key of Object.keys(value))
6
+ if (![...required, ...optional].includes(key))
7
+ throw new Error(`${location}.${key}: unknown field`);
8
+ for (const key of required)
9
+ if (!Object.hasOwn(value, key))
10
+ throw new Error(`${location}.${key}: required field`);
11
+ return value;
12
+ }
13
+
14
+ export function text(value, location, pattern = /\S/u) {
15
+ if (typeof value !== "string" || !pattern.test(value) || value.includes("\0"))
16
+ throw new Error(`${location}: invalid string`);
17
+ return value;
18
+ }
19
+
20
+ export function choice(value, choices, location) {
21
+ if (!choices.includes(value))
22
+ throw new Error(`${location}: expected ${choices.join(" or ")}`);
23
+ return value;
24
+ }
25
+
26
+ export function list(value, location, validate) {
27
+ if (!Array.isArray(value) || !value.length)
28
+ throw new Error(`${location}: expected a nonempty list`);
29
+ return value.map((item, index) => validate(item, `${location}[${index}]`));
30
+ }
31
+
32
+ export function unique(values, location) {
33
+ if (new Set(values).size !== values.length)
34
+ throw new Error(`${location}: duplicate identity`);
35
+ }
36
+
37
+ export function relativePath(value, location) {
38
+ text(value, location, /^[A-Za-z0-9_.][A-Za-z0-9_./*-]*$/u);
39
+ if (value.split("/").some((part) => !part || part === ".." || part === "."))
40
+ throw new Error(`${location}: expected a repository-relative path`);
41
+ return value;
42
+ }
43
+
44
+ export function slug(value, location) {
45
+ return text(value, location, /^[a-z][a-z0-9-]*$/u);
46
+ }
@@ -1,10 +1,7 @@
1
1
  import path from "node:path";
2
2
  import fs from "node:fs";
3
3
  import crypto from "node:crypto";
4
- import {
5
- sourceCoordinates,
6
- validateNativeContract,
7
- } from "./coordinates.js";
4
+ import { sourceCoordinates, validateNativeContract } from "./coordinates.js";
8
5
  import {
9
6
  verifyProjectCutReplayProof,
10
7
  createSourceQualificationProof,
@@ -16,6 +13,7 @@ import { assertBranchUnlocked } from "../../providers/dev-delivery/protection.js
16
13
  import { deriveSourcePaths } from "./source-paths.js";
17
14
  import { admissionPolicyRequest } from "../admission/request.js";
18
15
  import { runAdmissionTransaction } from "../admission/transaction.js";
16
+ import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
19
17
  export function admitDeliveryRequest(input, defaultBranch) {
20
18
  const target = sourceCoordinates({
21
19
  branch: input["target-branch"],
@@ -46,6 +44,7 @@ export async function qualifyDeliverySource(
46
44
  },
47
45
  dependencies = {},
48
46
  ) {
47
+ const pipeline = await guardPipelineAdmission(request, { repository, token });
49
48
  const outputs = {
50
49
  "runtime-sha": runtimeSha,
51
50
  "qualify-outcome": "skipped",
@@ -146,7 +145,12 @@ export async function qualifyDeliverySource(
146
145
  if (!affectedPaths.length)
147
146
  affectedPaths = await (dependencies.derivePaths || deriveSourcePaths)(
148
147
  source,
149
- { token, apiUrl, environment },
148
+ {
149
+ token,
150
+ apiUrl,
151
+ environment,
152
+ qualifiedBase: pipeline?.history.at(-1).generation.baseCommit,
153
+ },
150
154
  );
151
155
  const receiptRoot = qualification.result.receiptRoot;
152
156
  if (!/^sha256:[0-9a-f]{64}$/u.test(receiptRoot || ""))
@@ -10,6 +10,8 @@ import { dispatchDeliveryHandoff } from "../warrant/handoff.js";
10
10
  import { resolveCandidateAffectedPaths } from "./evidence.js";
11
11
  import { deliverySubmissionRequest } from "./submission.js";
12
12
  import { jsonList } from "../warrant/values.js";
13
+ import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
14
+ import { wakePipelineCandidate } from "../../workflow/pipeline/wake.js";
13
15
  export async function reserveDeliveryCandidate(
14
16
  {
15
17
  workspace,
@@ -31,6 +33,7 @@ export async function reserveDeliveryCandidate(
31
33
  );
32
34
  if (!predecessorsOk || proofOutcome !== "success")
33
35
  throw new Error("Delivery candidate submission failed");
36
+ await guardPipelineAdmission(input, connection);
34
37
  const service =
35
38
  dependencies.service ||
36
39
  createDeliveryWarrantService({ ...connection, branch });
@@ -77,6 +80,14 @@ export async function reserveDeliveryCandidate(
77
80
  warrant.pullRequestNumber !== Number(input["expected-pr-number"]) ||
78
81
  warrant.sourceHead !== input["expected-head-sha"]
79
82
  ) {
83
+ if (input["pipeline-attempt"]) {
84
+ await wakePipelineCandidate(warrant, { ...connection, branch });
85
+ return {
86
+ "warrant-outcome": "success",
87
+ "warrant-handoff-required": "true",
88
+ "warrant-handoff-dispatched": "true",
89
+ };
90
+ }
80
91
  outputs = await dispatchDeliveryHandoff(
81
92
  {
82
93
  warrant,
@@ -52,7 +52,7 @@ export function pathsAtQualifiedSource(directory, qualifiedBase, source) {
52
52
 
53
53
  export async function deriveSourcePaths(
54
54
  source,
55
- { token, apiUrl, environment = process.env },
55
+ { token, apiUrl, environment = process.env, qualifiedBase },
56
56
  provider = new GitHubTwoPhaseClient({
57
57
  repository: source.repository,
58
58
  token,
@@ -65,10 +65,18 @@ export async function deriveSourcePaths(
65
65
  /^[1-9]\d*$/u.test(String(source.sourceWorkflowRunId || "")),
66
66
  "Exact source run coordinates are required",
67
67
  );
68
- const run = await provider.request(
69
- `/repos/${source.repository}/actions/runs/${source.sourceWorkflowRunId}`,
68
+ if (qualifiedBase === undefined) {
69
+ const run = await provider.request(
70
+ `/repos/${source.repository}/actions/runs/${source.sourceWorkflowRunId}`,
71
+ );
72
+ qualifiedBase = qualifiedRunBase(run, source);
73
+ }
74
+ // The internal pipeline guard already requalifies the source run and immutable
75
+ // generation. Zero-delta recovery still verifies the full source identity below.
76
+ requireValue(
77
+ /^[0-9a-f]{40}$/u.test(qualifiedBase || ""),
78
+ "Exact qualified source base is required",
70
79
  );
71
- const qualifiedBase = qualifiedRunBase(run, source);
72
80
  const directory = fs.mkdtempSync(
73
81
  path.join(os.tmpdir(), "buildchain-source-paths-"),
74
82
  );
@@ -61,19 +61,32 @@ function git(
61
61
  {
62
62
  env = {},
63
63
  input = undefined,
64
+ inputFile = undefined,
65
+ outputFile = undefined,
64
66
  encoding = "utf8",
65
67
  reason = "git-failed",
66
68
  } = {},
67
69
  ) {
68
- const result = spawnSync("git", args, {
69
- cwd,
70
- env: { ...process.env, ...env },
71
- input,
72
- encoding,
73
- maxBuffer: 64 * 1024 * 1024,
74
- });
70
+ let result, inputDescriptor, outputDescriptor;
71
+ try {
72
+ if (inputFile) inputDescriptor = fs.openSync(inputFile, "r");
73
+ if (outputFile) outputDescriptor = fs.openSync(outputFile, "w");
74
+ result = spawnSync("git", args, {
75
+ cwd,
76
+ env: { ...process.env, ...env },
77
+ input,
78
+ encoding,
79
+ stdio: [inputDescriptor ?? "pipe", outputDescriptor ?? "pipe", "pipe"],
80
+ maxBuffer: 64 * 1024 * 1024,
81
+ });
82
+ } finally {
83
+ for (const descriptor of [inputDescriptor, outputDescriptor])
84
+ if (descriptor !== undefined) fs.closeSync(descriptor);
85
+ }
75
86
  if (result.status !== 0) {
76
- const detail = String(result.stderr || result.stdout || "").trim();
87
+ const detail = String(
88
+ result.stderr || result.error?.message || result.stdout || "",
89
+ ).trim();
77
90
  throw new ProjectCutAdmissionError(
78
91
  reason,
79
92
  `git ${args.join(" ")} failed${detail ? `: ${detail}` : ""}`,
@@ -135,7 +148,7 @@ function linearSourceCommits(cwd, fork, head) {
135
148
 
136
149
  function patchId(cwd, patch) {
137
150
  const output = git(cwd, ["patch-id", "--stable"], {
138
- input: patch,
151
+ inputFile: patch,
139
152
  encoding: "utf8",
140
153
  reason: "patch-id-failed",
141
154
  });
@@ -184,30 +197,41 @@ function replayProjectCut(cwd, base, head) {
184
197
  env: indexEnvironment,
185
198
  reason: "base-tree-unreadable",
186
199
  });
187
- const sourcePatch = git(
200
+ // Generated bundles can exceed the metadata-output bound. Git streams the
201
+ // exact patches through private files; patch identity and replay stay intact.
202
+ const sourcePatch = path.join(temporary, "source.patch");
203
+ git(
188
204
  cwd,
189
205
  ["diff", "--binary", "--full-index", "--no-ext-diff", fork, head],
190
206
  {
191
- encoding: null,
207
+ outputFile: sourcePatch,
192
208
  reason: "source-patch-unreadable",
193
209
  },
194
210
  );
195
- const emptySourcePatch = sourcePatch.length === 0;
211
+ const emptySourcePatch = fs.statSync(sourcePatch).size === 0;
196
212
  if (emptySourcePatch && commits.length !== 1)
197
- throw new ProjectCutAdmissionError("empty-source-composition", "an empty source composition must be exactly one linear commit");
198
- if (!emptySourcePatch) git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
199
- env: indexEnvironment, input: sourcePatch, encoding: "utf8", reason: "project-cut-conflict",
213
+ throw new ProjectCutAdmissionError(
214
+ "empty-source-composition",
215
+ "an empty source composition must be exactly one linear commit",
216
+ );
217
+ if (!emptySourcePatch)
218
+ git(cwd, ["apply", "--cached", "--3way", "--whitespace=nowarn", "-"], {
219
+ env: indexEnvironment,
220
+ inputFile: sourcePatch,
221
+ encoding: "utf8",
222
+ reason: "project-cut-conflict",
200
223
  });
201
224
  const candidateTreeOid = git(cwd, ["write-tree"], {
202
225
  env: indexEnvironment,
203
226
  reason: "candidate-tree-unreadable",
204
227
  });
205
- const replayPatch = git(
228
+ const replayPatch = path.join(temporary, "replay.patch");
229
+ git(
206
230
  cwd,
207
231
  ["diff", "--cached", "--binary", "--full-index", "--no-ext-diff", base],
208
232
  {
209
233
  env: indexEnvironment,
210
- encoding: null,
234
+ outputFile: replayPatch,
211
235
  reason: "replay-patch-unreadable",
212
236
  },
213
237
  );
@@ -226,9 +250,12 @@ function replayProjectCut(cwd, base, head) {
226
250
  reason: "replay-paths-unreadable",
227
251
  },
228
252
  );
229
- const compositionDrifted = emptySourcePatch ? replayPatch.length !== 0 ||
230
- sourceNames !== "" || replayNames !== "" : patchId(cwd, sourcePatch) !==
231
- patchId(cwd, replayPatch) || sourceNames !== replayNames;
253
+ const compositionDrifted = emptySourcePatch
254
+ ? fs.statSync(replayPatch).size !== 0 ||
255
+ sourceNames !== "" ||
256
+ replayNames !== ""
257
+ : patchId(cwd, sourcePatch) !== patchId(cwd, replayPatch) ||
258
+ sourceNames !== replayNames;
232
259
  if (compositionDrifted) {
233
260
  throw new ProjectCutAdmissionError(
234
261
  "composition-drift",
@@ -5,6 +5,7 @@ import {
5
5
  qualifyTransferredNative,
6
6
  } from "./transactions.js";
7
7
  import { admitProviderFinalizer } from "./finalizer-admission.js";
8
+ import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
8
9
  function request(core, env) {
9
10
  const candidate = JSON.parse(
10
11
  core.getInput("request-json", { required: true }),
@@ -41,11 +42,16 @@ export async function executeDeliveryNativeAction(core, env) {
41
42
  }
42
43
  export async function qualifyTransferredNativeAction(core, env) {
43
44
  const context = deliveryActionContext(core, env);
45
+ const input = request(core, env);
46
+ await guardPipelineAdmission(input.request, {
47
+ repository: env.GITHUB_REPOSITORY,
48
+ token: core.getInput("token", { required: true }),
49
+ });
44
50
  outputs(
45
51
  core,
46
52
  await qualifyTransferredNative({
47
53
  ...context,
48
- candidate: request(core, env).candidate,
54
+ candidate: input.candidate,
49
55
  sourceProofRoot: core.getInput("source-proof-root", { required: true }),
50
56
  token: core.getInput("token", { required: true }),
51
57
  apiUrl: env.GITHUB_API_URL || "https://api.github.com",
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { deliveryActionContext } from "./action-context.js";
4
4
  import { heartbeatDeliveryAttempt } from "./heartbeat.js";
5
+ import { guardPipelineDelivery } from "../../workflow/pipeline/guard.js";
5
6
  export async function heartbeatDeliveryAttemptAction(core, env) {
6
7
  const { workspace, run } = deliveryActionContext(core, env);
7
8
  const admission = JSON.parse(
@@ -20,6 +21,21 @@ export async function heartbeatDeliveryAttemptAction(core, env) {
20
21
  apiUrl: env.GITHUB_API_URL || "https://api.github.com",
21
22
  leaseSeconds: Number(core.getInput("lease-seconds")),
22
23
  heartbeatSeconds: Number(core.getInput("heartbeat-seconds")),
24
+ beforeHeartbeat: () => {
25
+ const warrant = admission.observation?.activeWarrant || admission.warrant;
26
+ return guardPipelineDelivery(
27
+ {
28
+ "pipeline-attempt": core.getInput("pipeline-attempt"),
29
+ "target-branch": core.getInput("branch", { required: true }),
30
+ "expected-pr-number": warrant?.pullRequestNumber,
31
+ "expected-head-sha": warrant?.sourceHead,
32
+ },
33
+ {
34
+ repository: env.GITHUB_REPOSITORY,
35
+ token: core.getInput("token", { required: true }),
36
+ },
37
+ );
38
+ },
23
39
  });
24
40
  fs.writeFileSync(
25
41
  path.join(workspace, ".buildchain/provider-heartbeat-receipt.json"),
@@ -68,13 +68,14 @@ export async function heartbeatDeliveryAttempt(options) {
68
68
  heartbeatSeconds: options.heartbeatSeconds,
69
69
  },
70
70
  {
71
- heartbeat: ({
71
+ heartbeat: async ({
72
72
  expectedOldStateRoot,
73
73
  fencingToken,
74
74
  leaseGeneration,
75
75
  leaseSeconds,
76
- }) =>
77
- runDevDeliveryCommand({
76
+ }) => {
77
+ await options.beforeHeartbeat?.();
78
+ return runDevDeliveryCommand({
78
79
  command: "heartbeat",
79
80
  repository: options.repository,
80
81
  branch: options.branch,
@@ -85,7 +86,8 @@ export async function heartbeatDeliveryAttempt(options) {
85
86
  execute: true,
86
87
  token: options.token,
87
88
  apiUrl: options.apiUrl,
88
- }),
89
+ });
90
+ },
89
91
  readJobs: () =>
90
92
  githubJson(
91
93
  options.apiUrl,
@@ -3,9 +3,14 @@ import { deliveryActionContext } from "../native/action-context.js";
3
3
  import { admissionPolicyRequest } from "../admission/request.js";
4
4
  import { runAdmissionTransaction } from "../admission/transaction.js";
5
5
  import { enforceLanding } from "./completion.js";
6
+ import { guardPipelineAdmission } from "../../workflow/pipeline/guard.js";
6
7
  export async function admitLandingAction(core, env) {
7
8
  const context = deliveryActionContext(core, env);
8
9
  const request = JSON.parse(core.getInput("request-json", { required: true }));
10
+ await guardPipelineAdmission(request, {
11
+ repository: env.GITHUB_REPOSITORY,
12
+ token: core.getInput("token", { required: true }),
13
+ });
9
14
  const result = await runAdmissionTransaction(
10
15
  {
11
16
  ...admissionPolicyRequest(request, {
@@ -39,6 +39,12 @@ const DESCRIPTORS = Object.freeze([
39
39
  [".github/workflows/public-ops-warrant-cancel.yml", "governance-write"],
40
40
  [".github/workflows/self-ops-merge-queue.yml", "governance-write"],
41
41
  [".github/workflows/public-ops-dev-auto-merge.yml", "governance-write"],
42
+ [".github/workflows/public-ops-pipeline.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
43
+ [".github/workflows/public-ops-recover.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
44
+ [".github/workflows/.ops-pipeline-execute.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed", ".github/workflows/.release-pipeline-products.yml"],
45
+ [".github/workflows/.release-pipeline-products.yml", "product-publication", true, ["npm-publish", "github-release", "channel-ref"], "trusted-publishing", "none", "fixed", "fixed"],
46
+ [".github/workflows/.release-pipeline-version.yml", "governance-write"],
47
+ [".github/workflows/.ops-pipeline-delivery.yml", "governance-write"],
42
48
  [".github/workflows/self-ops-housekeeping-daily.yml", "governance-write"],
43
49
  [".github/workflows/self-ops-housekeeping-monthly.yml", "governance-write"],
44
50
  [".github/workflows/self-ops-housekeeping-weekly.yml", "governance-write"],
@@ -1,3 +1,4 @@
1
+ import { readNpmPackResult } from "../../publication/npm/pack-result.js";
1
2
  import path from "node:path";
2
3
  import fs from "node:fs";
3
4
  import { jsonText, extractUrls, safeParseJson, toPosix } from "./files.js";
@@ -66,6 +67,7 @@ export function executePaperNpmBootstrap(options = {}) {
66
67
  path,
67
68
  resolvePaperRepository,
68
69
  safeParseJson,
70
+ readNpmPackResult,
69
71
  toPosix,
70
72
  validatePaperProvisioningAuthority,
71
73
  writePaperReceipt,
@@ -20,6 +20,7 @@ function executePaperNpmBootstrap(options = {}, runtime) {
20
20
  path,
21
21
  resolvePaperRepository,
22
22
  safeParseJson,
23
+ readNpmPackResult,
23
24
  toPosix,
24
25
  validatePaperProvisioningAuthority,
25
26
  writePaperReceipt,
@@ -208,8 +209,7 @@ function executePaperNpmBootstrap(options = {}, runtime) {
208
209
  pack: {
209
210
  status: pack.ok ? "pass" : "fail",
210
211
  entryCount: (() => {
211
- const parsed = safeParseJson(pack.stdout);
212
- const value = Array.isArray(parsed) ? parsed[0] : parsed;
212
+ const value = pack.ok ? readNpmPackResult(pack.stdout) : null;
213
213
  return Array.isArray(value?.files) ? value.files.length : 0;
214
214
  })(),
215
215
  },
@@ -0,0 +1,60 @@
1
+ // Immutable lookup pointers are derived from admitted journal commits. The
2
+ // canonical intent journal must contain the selected root before a pointer can
3
+ // resolve; an orphan commit or pointer cannot create attempt authority.
4
+ export function githubAttemptIndex(request, journal, repository) {
5
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
6
+ throw new Error("Invalid attempt lookup repository");
7
+ const base = `/repos/${repository}`;
8
+ function ref(attempt) {
9
+ if (!/^attempt-[0-9a-f]{64}$/u.test(attempt || ""))
10
+ throw new Error("Invalid business attempt selector");
11
+ return `buildchain/attempt-index/${attempt}`;
12
+ }
13
+ function root(snapshot, attempt) {
14
+ const roots = snapshot.records.filter(
15
+ (record) => record.attempt === attempt && record.node === "attempt",
16
+ );
17
+ if (roots.length !== 1)
18
+ throw new Error("Lookup does not identify one admitted attempt root");
19
+ return roots[0];
20
+ }
21
+ async function resolve(attempt) {
22
+ const pointer = await request(`${base}/git/ref/heads/${ref(attempt)}`, {
23
+ allow404: true,
24
+ });
25
+ if (!pointer) throw new Error("Business attempt lookup is not retained");
26
+ const indexed = await journal.readCommit(repository, pointer.object?.sha);
27
+ const selected = root(indexed.snapshot, attempt);
28
+ const current = await journal.read(indexed.snapshot.intent);
29
+ if (!current || root(current.snapshot, attempt).id !== selected.id)
30
+ throw new Error(
31
+ "Lookup attempt is absent from the authoritative journal",
32
+ );
33
+ return { ...current, selectedAttempt: attempt };
34
+ }
35
+ async function retain(intent, attempt) {
36
+ if (intent.repository !== repository)
37
+ throw new Error("Attempt lookup repository drift");
38
+ const current = await journal.read(intent);
39
+ if (!current) throw new Error("Cannot index an unadmitted attempt");
40
+ const expected = root(current.snapshot, attempt);
41
+ let failure;
42
+ try {
43
+ await request(`${base}/git/refs`, {
44
+ method: "POST",
45
+ body: { ref: `refs/heads/${ref(attempt)}`, sha: current.commit },
46
+ });
47
+ } catch (error) {
48
+ failure = error;
49
+ }
50
+ try {
51
+ const result = await resolve(attempt);
52
+ if (root(result.snapshot, attempt).id !== expected.id)
53
+ throw new Error("Conflicting business attempt lookup");
54
+ return result;
55
+ } catch (error) {
56
+ throw failure || error;
57
+ }
58
+ }
59
+ return { retain, resolve };
60
+ }
@@ -0,0 +1,154 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ canonicalJson,
4
+ recordDigest,
5
+ } from "../../release/discussion/envelope.js";
6
+ import { validateJournal } from "../../workflow/attempt/journal.js";
7
+
8
+ const PATH = "attempt.json";
9
+ const LIMIT = 34 * 1024 * 1024;
10
+
11
+ function exactSha(value) {
12
+ if (!/^[0-9a-f]{40}$/u.test(value || ""))
13
+ throw new Error("Attempt journal requires an exact Git SHA");
14
+ return value;
15
+ }
16
+
17
+ export function githubAttemptJournal(request) {
18
+ function coordinates(intent) {
19
+ if (
20
+ !/^[\w.-]+\/[\w.-]+$/u.test(intent.repository) ||
21
+ !/^sha256:[0-9a-f]{64}$/u.test(intent.id)
22
+ )
23
+ throw new Error("Invalid attempt journal coordinates");
24
+ return {
25
+ base: `/repos/${intent.repository}`,
26
+ ref: `buildchain/attempts/${intent.id.slice(7)}`,
27
+ };
28
+ }
29
+ async function verifyRepository(intent, base) {
30
+ const repository = await request(base);
31
+ if (repository.node_id !== intent.source.repositoryId)
32
+ throw new Error("Attempt repository identity changed");
33
+ }
34
+ async function read(intent) {
35
+ const { base, ref } = coordinates(intent);
36
+ await verifyRepository(intent, base);
37
+ const pointer = await request(`${base}/git/ref/heads/${ref}`, {
38
+ allow404: true,
39
+ });
40
+ if (!pointer) return null;
41
+ if (pointer.object?.type !== "commit")
42
+ throw new Error("Attempt journal ref is not a commit");
43
+ const result = await readCommit(intent.repository, pointer.object.sha);
44
+ validateJournal(result.snapshot, intent);
45
+ return result;
46
+ }
47
+ async function readCommit(repository, commitSha) {
48
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
49
+ throw new Error("Invalid attempt journal repository");
50
+ const base = `/repos/${repository}`;
51
+ const sha = exactSha(commitSha);
52
+ const commit = await request(`${base}/git/commits/${sha}`);
53
+ const tree = await request(
54
+ `${base}/git/trees/${exactSha(commit.tree?.sha)}`,
55
+ );
56
+ if (
57
+ tree.truncated ||
58
+ tree.tree?.length !== 1 ||
59
+ tree.tree[0].path !== PATH ||
60
+ tree.tree[0].type !== "blob" ||
61
+ tree.tree[0].mode !== "100644"
62
+ )
63
+ throw new Error("Invalid attempt journal tree");
64
+ const blobSha = exactSha(tree.tree[0].sha);
65
+ const blob = await request(`${base}/git/blobs/${blobSha}`);
66
+ if (
67
+ blob.encoding !== "base64" ||
68
+ !Number.isSafeInteger(blob.size) ||
69
+ blob.size < 0 ||
70
+ blob.size > LIMIT ||
71
+ typeof blob.content !== "string" ||
72
+ blob.content.length > LIMIT * 2
73
+ )
74
+ throw new Error("Invalid or oversized attempt journal blob");
75
+ const bytes = Buffer.from(blob.content, "base64");
76
+ const digest = createHash("sha1")
77
+ .update(`blob ${bytes.length}\0`)
78
+ .update(bytes)
79
+ .digest("hex");
80
+ if (bytes.length !== blob.size || digest !== blobSha)
81
+ throw new Error("Attempt journal blob integrity mismatch");
82
+ const snapshot = JSON.parse(bytes.toString("utf8"));
83
+ if (snapshot.intent?.repository !== repository)
84
+ throw new Error("Attempt journal snapshot belongs to another repository");
85
+ validateJournal(snapshot, snapshot.intent);
86
+ await verifyRepository(snapshot.intent, base);
87
+ return { commit: sha, snapshot };
88
+ }
89
+ async function append({ intent, snapshot, expectedCommit }) {
90
+ validateJournal(snapshot, intent);
91
+ const { base, ref } = coordinates(intent);
92
+ await verifyRepository(intent, base);
93
+ const bytes = `${canonicalJson(snapshot)}\n`;
94
+ if (Buffer.byteLength(bytes) > LIMIT)
95
+ throw new Error("Attempt journal byte bound exceeded");
96
+ const blob = await request(`${base}/git/blobs`, {
97
+ method: "POST",
98
+ body: { content: bytes, encoding: "utf-8" },
99
+ });
100
+ const tree = await request(`${base}/git/trees`, {
101
+ method: "POST",
102
+ body: {
103
+ tree: [
104
+ { path: PATH, mode: "100644", type: "blob", sha: exactSha(blob.sha) },
105
+ ],
106
+ },
107
+ });
108
+ const commit = await request(`${base}/git/commits`, {
109
+ method: "POST",
110
+ body: {
111
+ message: `chore(attempt): append ${snapshot.root}`,
112
+ tree: exactSha(tree.sha),
113
+ parents: expectedCommit ? [exactSha(expectedCommit)] : [],
114
+ },
115
+ });
116
+ const sha = exactSha(commit.sha);
117
+ if (expectedCommit)
118
+ await request(`${base}/git/refs/heads/${ref}`, {
119
+ method: "PATCH",
120
+ body: { sha, force: false },
121
+ });
122
+ else
123
+ await request(`${base}/git/refs`, {
124
+ method: "POST",
125
+ body: { ref: `refs/heads/${ref}`, sha },
126
+ });
127
+ return { commit: sha };
128
+ }
129
+ async function lookup(repository, pullRequest, targetBranch) {
130
+ if (
131
+ !Number.isSafeInteger(pullRequest) ||
132
+ pullRequest < 1 ||
133
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
134
+ targetBranch || "",
135
+ ) ||
136
+ targetBranch.includes("..")
137
+ )
138
+ throw new Error("Invalid pipeline intent lookup");
139
+ const id = recordDigest({
140
+ repository,
141
+ key: `pr-${pullRequest}:${targetBranch}`,
142
+ });
143
+ const { base, ref } = coordinates({ repository, id });
144
+ const pointer = await request(`${base}/git/ref/heads/${ref}`, {
145
+ allow404: true,
146
+ });
147
+ if (!pointer) return null;
148
+ const result = await readCommit(repository, pointer.object?.sha);
149
+ if (result.snapshot.intent.id !== id)
150
+ throw new Error("Pipeline intent lookup drift");
151
+ return result;
152
+ }
153
+ return { read, append, readCommit, lookup };
154
+ }
@@ -6,7 +6,12 @@ export function materialDigest(bytes) {
6
6
 
7
7
  // Draft release assets retain immutable recovery bytes outside the source Git
8
8
  // object database. Only the Discussion record selects a committed manifest.
9
- export function discussionMaterials({ octokit, repository, intentId }) {
9
+ export function discussionMaterials({
10
+ octokit,
11
+ repository,
12
+ intentId,
13
+ authorityDescription = "The associated Discussion owns transaction state.",
14
+ }) {
10
15
  const [owner, repo] = repository.split("/");
11
16
  const tag = `buildchain-records/${intentId.replace("sha256:", "")}`;
12
17
  const repos = octokit.rest.repos;
@@ -40,7 +45,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
40
45
  // Storage archives use the repository default; candidate identity is
41
46
  // retained in the material manifest, never in a storage Git ref.
42
47
  name: `Buildchain transaction materials ${intentId}`,
43
- body: "Immutable recovery material. The associated Discussion owns transaction state.",
48
+ body: `Immutable recovery material. ${authorityDescription}`,
44
49
  draft: true,
45
50
  prerelease: true,
46
51
  })