@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,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
  })
@@ -72,11 +77,13 @@ export function discussionMaterials({ octokit, repository, intentId }) {
72
77
  );
73
78
  return bytes;
74
79
  }
75
- async function put(bytes) {
80
+ async function put(bytes, descriptor = {}) {
81
+ const extension =
82
+ descriptor.name?.match(/\.([a-zA-Z0-9]{1,8})$/u)?.[1] || "";
76
83
  if (bytes.length > 256 * 1024 * 1024)
77
84
  throw new Error("Transaction material exceeds the supported file bound");
78
85
  const digest = materialDigest(bytes),
79
- name = digest.replace(":", "-");
86
+ name = digest.replace(":", "-") + (extension ? `.${extension}` : "");
80
87
  const release = await archive();
81
88
  const find = async () => {
82
89
  const assets = await octokit.paginate(repos.listReleaseAssets, {
@@ -101,7 +108,8 @@ export function discussionMaterials({ octokit, repository, intentId }) {
101
108
  name,
102
109
  data: bytes,
103
110
  headers: {
104
- "content-type": "application/octet-stream",
111
+ "content-type":
112
+ descriptor.mediaType || "application/octet-stream",
105
113
  },
106
114
  })
107
115
  ).data;
@@ -113,7 +121,15 @@ export function discussionMaterials({ octokit, repository, intentId }) {
113
121
  });
114
122
  }
115
123
  }
116
- const handle = { id: asset.id, digest, size: bytes.length };
124
+ const handle = {
125
+ id: asset.id,
126
+ digest,
127
+ size: bytes.length,
128
+ ...(asset.browser_download_url
129
+ ? { downloadUrl: asset.browser_download_url }
130
+ : {}),
131
+ ...(release.html_url ? { archiveUrl: release.html_url } : {}),
132
+ };
117
133
  await read(handle);
118
134
  return handle;
119
135
  }
@@ -140,7 +156,7 @@ export function discussionMaterials({ octokit, repository, intentId }) {
140
156
  }
141
157
  }
142
158
  return {
143
- put: (bytes) => diagnose("put", () => put(bytes)),
159
+ put: (bytes, descriptor) => diagnose("put", () => put(bytes, descriptor)),
144
160
  read: (handle) => diagnose("read", () => read(handle)),
145
161
  };
146
162
  }
@@ -1,7 +1,7 @@
1
1
  const fields =
2
2
  "id number url body author { __typename login ... on Node { id } } lastEditedAt repository { nameWithOwner }";
3
3
  const commentFields =
4
- "id url body author { __typename login ... on Node { id } } lastEditedAt";
4
+ "id url body author { __typename login ... on Node { id } } lastEditedAt replyTo { id }";
5
5
 
6
6
  export function discussionTransport(graphql) {
7
7
  async function request(query, variables) {
@@ -66,13 +66,22 @@ export function discussionTransport(graphql) {
66
66
  }
67
67
  async function comments(id, after = null) {
68
68
  const result = await request(
69
- `query($id:ID!,$after:String){node(id:$id){... on Discussion{comments(first:100,after:$after){nodes{${commentFields}} pageInfo{hasNextPage endCursor}}}}}`,
69
+ `query($id:ID!,$after:String){node(id:$id){... on Discussion{comments(first:100,after:$after){nodes{${commentFields} replies{totalCount}} pageInfo{hasNextPage endCursor}}}}}`,
70
70
  { id, after },
71
71
  );
72
72
  if (!result.node?.comments)
73
73
  throw new Error("Release Discussion comments are unavailable");
74
74
  return result.node.comments;
75
75
  }
76
+ async function replies(id, after = null) {
77
+ const result = await request(
78
+ `query($id:ID!,$after:String){node(id:$id){... on DiscussionComment{replies(first:100,after:$after){nodes{${commentFields}} pageInfo{hasNextPage endCursor}}}}}`,
79
+ { id, after },
80
+ );
81
+ if (!result.node?.replies)
82
+ throw new Error("Release Discussion replies are unavailable");
83
+ return result.node.replies;
84
+ }
76
85
  async function create({ repositoryId, categoryId, title, body }) {
77
86
  const result = await request(
78
87
  `mutation($input:CreateDiscussionInput!){createDiscussion(input:$input){discussion{${fields}}}}`,
@@ -80,14 +89,16 @@ export function discussionTransport(graphql) {
80
89
  );
81
90
  return result.createDiscussion.discussion;
82
91
  }
83
- async function append(id, body) {
92
+ async function append(id, body, replyToId = null) {
84
93
  const result = await request(
85
94
  `mutation($input:AddDiscussionCommentInput!){addDiscussionComment(input:$input){comment{${commentFields}}}}`,
86
- { input: { discussionId: id, body } },
95
+ {
96
+ input: { discussionId: id, body, ...(replyToId ? { replyToId } : {}) },
97
+ },
87
98
  );
88
99
  return result.addDiscussionComment.comment;
89
100
  }
90
- return { repository, list, get, comments, create, append };
101
+ return { repository, list, get, comments, replies, create, append };
91
102
  }
92
103
 
93
104
  export async function collectDiscussionPages(readPage, limit = 100) {
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { githubAuthEnv } from "../source-checkout/auth.js";
6
+ import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
7
+
8
+ // Read-only Git object access for credentialed source qualification. This is a
9
+ // bare repository: no consumer checkout, hooks or build scripts are executed.
10
+ export async function withPipelineSourceObjects(
11
+ { repository, sourceHead, baseCommit, token },
12
+ operation,
13
+ ) {
14
+ if (
15
+ !/^[\w.-]+\/[\w.-]+$/u.test(repository || "") ||
16
+ ![sourceHead, baseCommit].every((sha) => /^[0-9a-f]{40}$/u.test(sha || ""))
17
+ )
18
+ throw new Error(
19
+ "Pipeline qualification requires exact repository source coordinates",
20
+ );
21
+ const directory = fs.mkdtempSync(
22
+ path.join(os.tmpdir(), "buildchain-pipeline-source-"),
23
+ );
24
+ const environment = {
25
+ ...createNativeChildEnvironment(process.env),
26
+ GIT_TERMINAL_PROMPT: "0",
27
+ ...githubAuthEnv(token),
28
+ };
29
+ const git = (args) =>
30
+ execFileSync("git", ["-C", directory, ...args], {
31
+ env: environment,
32
+ stdio: ["ignore", "pipe", "pipe"],
33
+ maxBuffer: 32 * 1024 * 1024,
34
+ });
35
+ try {
36
+ git(["init", "--bare", "--quiet"]);
37
+ git([
38
+ "fetch",
39
+ "--quiet",
40
+ "--no-tags",
41
+ `https://github.com/${repository}.git`,
42
+ sourceHead,
43
+ baseCommit,
44
+ ]);
45
+ return await operation(directory);
46
+ } finally {
47
+ fs.rmSync(directory, { recursive: true, force: true });
48
+ }
49
+ }
@@ -0,0 +1,52 @@
1
+ import { readBusinessAttempt } from "../../workflow/attempt/reader.js";
2
+
3
+ // Branch/terminal notifications wake existing intents only. They cannot create
4
+ // historical releases or infer an execution request from an untracked old PR.
5
+ export function githubPipelineEvents(request, journal, repository) {
6
+ if (!/^[\w.-]+\/[\w.-]+$/u.test(repository || ""))
7
+ throw new Error("Invalid pipeline event repository");
8
+ const base = `/repos/${repository}`;
9
+ async function list(url) {
10
+ const rows = [];
11
+ for (let page = 1; page <= 100; page++) {
12
+ const entries = await request(
13
+ `${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
14
+ );
15
+ if (!Array.isArray(entries))
16
+ throw new Error("Invalid pipeline PR event readback");
17
+ rows.push(...entries);
18
+ if (entries.length < 100) return rows;
19
+ }
20
+ throw new Error("Pipeline PR event readback exceeds its bound");
21
+ }
22
+ async function branch(branch, commit = "") {
23
+ if (
24
+ !/^(?:dev|alpha|release|publish-gate)\/[A-Za-z0-9/._-]+$/u.test(
25
+ branch || "",
26
+ ) ||
27
+ branch.includes("..")
28
+ )
29
+ throw new Error("Invalid pipeline branch event");
30
+ const pulls = await list(
31
+ `${base}/pulls?state=open&base=${encodeURIComponent(branch)}`,
32
+ );
33
+ if (commit) {
34
+ if (!/^[0-9a-f]{40}$/u.test(commit))
35
+ throw new Error("Branch wake requires exact source commit");
36
+ pulls.push(...(await list(`${base}/commits/${commit}/pulls`)));
37
+ }
38
+ const result = new Set();
39
+ for (const number of [
40
+ ...new Set(
41
+ pulls.filter((pr) => pr.base?.ref === branch).map((pr) => pr.number),
42
+ ),
43
+ ]) {
44
+ const retained = await journal.lookup(repository, number, branch);
45
+ if (!retained) continue;
46
+ const state = readBusinessAttempt(retained.snapshot);
47
+ if (state.attempt) result.add(state.attempt);
48
+ }
49
+ return [...result];
50
+ }
51
+ return { branch };
52
+ }
@@ -0,0 +1,95 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { pipelinePlatforms } from "../../workflow/pipeline/platforms.js";
3
+
4
+ export function githubPipelineIntegration(request, repository, source, runs) {
5
+ const base = `/repos/${repository}`;
6
+ async function observe(current) {
7
+ const number = current.intent.source.pullRequest;
8
+ const branch = current.intent.source.targetBranch;
9
+ const pr = await request(`${base}/pulls/${number}`);
10
+ if (
11
+ !pr.merged ||
12
+ pr.head?.sha !== current.generation.source.commit ||
13
+ pr.base?.ref !== branch ||
14
+ pr.head?.repo?.full_name !== repository ||
15
+ pr.base?.repo?.full_name !== repository ||
16
+ !/^[0-9a-f]{40}$/u.test(pr.merge_commit_sha || "")
17
+ )
18
+ throw new Error(
19
+ "Integration needs the exact source PR merged into its protected target",
20
+ );
21
+ const mergeCommit = pr.merge_commit_sha;
22
+ const protectedHead = await source.branchHead(branch);
23
+ const ancestry = await request(
24
+ `${base}/compare/${mergeCommit}...${protectedHead}`,
25
+ );
26
+ if (
27
+ !["identical", "ahead"].includes(ancestry.status) ||
28
+ ancestry.merge_base_commit?.sha !== mergeCommit
29
+ )
30
+ throw new Error(
31
+ "Exact merge commit is not contained in the protected branch",
32
+ );
33
+ const commits = await source.source(
34
+ mergeCommit,
35
+ current.generation.source.configPath,
36
+ );
37
+ const response = await request(
38
+ `${base}/actions/runs?event=merge_group&head_sha=${mergeCommit}&per_page=100`,
39
+ );
40
+ if (!Array.isArray(response.workflow_runs) || response.total_count > 100)
41
+ throw new Error(
42
+ "Exact merge-group run inventory is unavailable or ambiguous",
43
+ );
44
+ const candidates = response.workflow_runs.filter(
45
+ (run) =>
46
+ run.status === "completed" &&
47
+ run.conclusion === "success" &&
48
+ run.path?.split("@")[0] === ".github/workflows/buildchain.yml",
49
+ );
50
+ if (!candidates.length)
51
+ throw new Error(
52
+ "Exact merged commit has no successful normal pipeline merge-group run",
53
+ );
54
+ const chosen = candidates.sort((a, b) => b.id - a.id)[0];
55
+ const { run } = await runs.read(chosen.id, chosen.run_attempt);
56
+ if (
57
+ run.status !== "completed" ||
58
+ run.conclusion !== "success" ||
59
+ run.event !== "merge_group" ||
60
+ run.head_sha !== mergeCommit
61
+ )
62
+ throw new Error("Integration merge-group execution changed");
63
+ const build = await runs.build(
64
+ run.id,
65
+ run.run_attempt,
66
+ commits.identity,
67
+ pipelinePlatforms(commits.plan).map((item) => item.platform),
68
+ );
69
+ if (build.outcome !== "success")
70
+ throw new Error("Exact merged tree product verification failed");
71
+ const again = await request(`${base}/pulls/${number}`);
72
+ if (
73
+ !again.merged ||
74
+ again.merge_commit_sha !== mergeCommit ||
75
+ again.head.sha !== pr.head.sha ||
76
+ again.base.ref !== branch
77
+ )
78
+ throw new Error("Integration provider source changed during readback");
79
+ const body = {
80
+ schema: "buildchain.pipeline-integration-readback/v1",
81
+ repository,
82
+ pullRequest: number,
83
+ sourceHead: pr.head.sha,
84
+ branch,
85
+ protectedHead,
86
+ mergeCommit,
87
+ mergeTree: commits.identity.tree,
88
+ run,
89
+ build,
90
+ ancestry,
91
+ };
92
+ return { ...body, root: recordDigest(body) };
93
+ }
94
+ return { observe };
95
+ }
@@ -0,0 +1,213 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+
3
+ const QUERY = `query PipelinePolicy($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){id number headRefOid baseRefName isDraft reviewDecision author{login}}}}`;
4
+ const BRANCH_QUERY = `query PipelineBranchPolicy($owner:String!,$name:String!,$branch:String!,$ref:String!){repository(owner:$owner,name:$name){ref(qualifiedName:$ref){branchProtectionRule{requiresApprovingReviews requiredApprovingReviewCount requiresCodeOwnerReviews requiresStatusChecks requiredStatusChecks{context app{databaseId}}}}mergeQueue(branch:$branch){id}}}`;
5
+
6
+ async function classicPolicy(request, owner, name, branch) {
7
+ // REST /branches/:branch/protection requires Administration:read. Ordinary
8
+ // workflow credentials instead read effective rules and GraphQL metadata.
9
+ const result = await request("/graphql", {
10
+ method: "POST",
11
+ body: {
12
+ query: BRANCH_QUERY,
13
+ variables: { owner, name, branch, ref: `refs/heads/${branch}` },
14
+ },
15
+ });
16
+ const repository = result.data?.repository;
17
+ if (!repository) throw new Error("Protected branch metadata is unavailable");
18
+ const rule = repository.ref?.branchProtectionRule;
19
+ return {
20
+ required_merge_queue: Boolean(repository.mergeQueue?.id),
21
+ required_pull_request_reviews: rule?.requiresApprovingReviews
22
+ ? {
23
+ require_code_owner_reviews: rule.requiresCodeOwnerReviews,
24
+ required_approving_review_count: rule.requiredApprovingReviewCount,
25
+ }
26
+ : null,
27
+ required_status_checks: {
28
+ checks: rule?.requiresStatusChecks
29
+ ? (rule.requiredStatusChecks || []).map((check) => ({
30
+ context: check.context,
31
+ app_id: check.app?.databaseId || null,
32
+ }))
33
+ : [],
34
+ },
35
+ };
36
+ }
37
+
38
+ async function inventory(request, url, key = "") {
39
+ const rows = [];
40
+ for (let page = 1; page <= 100; page++) {
41
+ const response = await request(
42
+ `${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
43
+ );
44
+ const entries = key ? response[key] : response;
45
+ if (!Array.isArray(entries))
46
+ throw new Error("Missing protected policy inventory");
47
+ rows.push(...entries);
48
+ if (entries.length < 100) return rows;
49
+ }
50
+ throw new Error("Protected policy inventory exceeds its bound");
51
+ }
52
+
53
+ function protections(rules, classic, policy) {
54
+ const reviews = rules
55
+ .filter((rule) => rule.type === "pull_request")
56
+ .map((rule) => rule.parameters);
57
+ if (classic?.required_pull_request_reviews)
58
+ reviews.push({
59
+ ...classic.required_pull_request_reviews,
60
+ require_code_owner_review:
61
+ classic.required_pull_request_reviews.require_code_owner_reviews,
62
+ });
63
+ if (
64
+ !reviews.some((rule) => rule?.require_code_owner_review === true) ||
65
+ !reviews.some(
66
+ (rule) =>
67
+ rule?.required_approving_review_count >= policy.minimum_approvals,
68
+ ) ||
69
+ !(
70
+ classic?.required_merge_queue ||
71
+ rules.some((rule) => rule.type === "merge_queue")
72
+ )
73
+ )
74
+ throw new Error(
75
+ "Protected branch must enforce declared approvals, code owners and merge queue",
76
+ );
77
+ const contexts = rules
78
+ .filter((rule) => rule.type === "required_status_checks")
79
+ .flatMap((rule) => rule.parameters?.required_status_checks || []);
80
+ for (const check of classic?.required_status_checks?.checks || [])
81
+ contexts.push({ context: check.context, integration_id: check.app_id });
82
+ if (!contexts.length || contexts.some((check) => !check.context))
83
+ throw new Error(
84
+ "Protected pipeline requires provider-enforced status checks",
85
+ );
86
+ return contexts;
87
+ }
88
+
89
+ function approved(pr, reviews, minimum) {
90
+ const latest = new Map();
91
+ for (const review of [...reviews].sort((a, b) => a.id - b.id)) {
92
+ if (
93
+ review.user?.login &&
94
+ ["APPROVED", "CHANGES_REQUESTED", "DISMISSED"].includes(review.state)
95
+ )
96
+ latest.set(review.user.login, review);
97
+ }
98
+ const independent = [...latest.values()].filter(
99
+ (review) => review.user.login !== pr.author?.login,
100
+ );
101
+ return (
102
+ pr.reviewDecision === "APPROVED" &&
103
+ !independent.some((review) => review.state === "CHANGES_REQUESTED") &&
104
+ independent.filter(
105
+ (review) =>
106
+ review.state === "APPROVED" && review.commit_id === pr.headRefOid,
107
+ ).length >= minimum
108
+ );
109
+ }
110
+
111
+ function checksPass(contexts, checks, statuses) {
112
+ return contexts.every(({ context, integration_id: app }) => {
113
+ const candidates = checks.filter(
114
+ (check) =>
115
+ check.name === context && (!app || app === -1 || check.app?.id === app),
116
+ );
117
+ const latest = candidates.sort((a, b) => b.id - a.id)[0];
118
+ if (latest)
119
+ return (
120
+ latest.status === "completed" &&
121
+ ["success", "neutral", "skipped"].includes(latest.conclusion)
122
+ );
123
+ if (app && app !== -1) return false;
124
+ return (
125
+ statuses
126
+ .filter((status) => status.context === context)
127
+ .sort((a, b) => b.id - a.id)[0]?.state === "success"
128
+ );
129
+ });
130
+ }
131
+
132
+ export function githubPipelinePolicy(request, repository) {
133
+ const [owner, name] = repository.split("/");
134
+ const base = `/repos/${repository}`;
135
+ async function pull(number) {
136
+ const response = await request("/graphql", {
137
+ method: "POST",
138
+ body: { query: QUERY, variables: { owner, name, number } },
139
+ });
140
+ const pr = response.data?.repository?.pullRequest;
141
+ if (!pr?.id || pr.number !== number)
142
+ throw new Error("Protected PR policy identity is unavailable");
143
+ return pr;
144
+ }
145
+ async function observe(current, policy, integratedHead) {
146
+ const { source, baseCommit } = current.generation;
147
+ const number = current.intent.source.pullRequest,
148
+ branch = current.intent.source.targetBranch;
149
+ const pr = await pull(number);
150
+ if (pr.headRefOid !== source.commit || pr.baseRefName !== branch)
151
+ throw new Error("Protected PR policy source drift");
152
+ const branchPath = encodeURIComponent(branch);
153
+ const rules = await inventory(
154
+ request,
155
+ `${base}/rules/branches/${branchPath}`,
156
+ );
157
+ const classic = await classicPolicy(request, owner, name, branch);
158
+ const contexts = protections(rules, classic, policy);
159
+ const reviews = await inventory(request, `${base}/pulls/${number}/reviews`);
160
+ const checks = await inventory(
161
+ request,
162
+ `${base}/commits/${source.commit}/check-runs?filter=latest`,
163
+ "check_runs",
164
+ );
165
+ const statuses = await inventory(
166
+ request,
167
+ `${base}/commits/${source.commit}/statuses`,
168
+ );
169
+ const again = await pull(number);
170
+ const ref = await request(`${base}/git/ref/heads/${branch}`);
171
+ if (
172
+ recordDigest(pr) !== recordDigest(again) ||
173
+ ref.object?.sha !== (integratedHead || baseCommit)
174
+ )
175
+ throw new Error("Protected policy changed during readback");
176
+ const body = {
177
+ schema: "buildchain.pipeline-policy-readback/v1",
178
+ source,
179
+ baseCommit,
180
+ pr,
181
+ rules,
182
+ classic: classic || null,
183
+ reviews,
184
+ checks,
185
+ statuses,
186
+ contexts,
187
+ review: !pr.isDraft && approved(pr, reviews, policy.minimum_approvals),
188
+ checksPassing: checksPass(contexts, checks, statuses),
189
+ ...(integratedHead ? { integratedHead } : {}),
190
+ };
191
+ return { ...body, root: recordDigest(body) };
192
+ }
193
+ async function observeMerged(current, policy, integration) {
194
+ const { root, ...body } = integration;
195
+ if (
196
+ root !== recordDigest(body) ||
197
+ body.schema !== "buildchain.pipeline-integration-readback/v1" ||
198
+ body.repository !== repository ||
199
+ body.sourceHead !== current.generation.source.commit ||
200
+ body.branch !== current.intent.source.targetBranch ||
201
+ body.pullRequest !== current.intent.source.pullRequest ||
202
+ !/^[0-9a-f]{40}$/u.test(body.protectedHead || "")
203
+ )
204
+ throw new Error(
205
+ "Merged review qualification requires exact protected integration evidence",
206
+ );
207
+ return observe(current, policy, body.protectedHead);
208
+ }
209
+ return {
210
+ observe: (current, policy) => observe(current, policy),
211
+ observeMerged,
212
+ };
213
+ }