@kungfu-tech/buildchain 4.1.3-alpha.1 → 4.1.3-alpha.10

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 (263) hide show
  1. package/AGENTS.md +41 -75
  2. package/CONTRIBUTING.md +19 -26
  3. package/architecture/action-taxonomy.json +24 -0
  4. package/architecture/agent-change-map.md +266 -10
  5. package/architecture/capability-state-machine-manifest.json +1 -4
  6. package/architecture/ci-lane-change-budget.json +190 -807
  7. package/architecture/core-mechanisms.json +3 -6
  8. package/architecture/decisions/0005-minimal-consumer-contract.md +122 -0
  9. package/architecture/decisions/0006-business-attempt-journal.md +89 -0
  10. package/architecture/decisions/0007-hosted-pipeline-controller.md +106 -0
  11. package/architecture/decisions/0008-pipeline-product-publication.md +120 -0
  12. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  13. package/architecture/internal-capabilities.json +362 -7
  14. package/architecture/maintainability-debt.json +50 -34
  15. package/architecture/maintainability-policy.json +8 -8
  16. package/architecture/minimal-consumer-migration.json +5670 -0
  17. package/architecture/platform-stage-checkpoints.json +31 -7
  18. package/architecture/product-upstreams.json +20 -0
  19. package/architecture/release-topology.json +328 -74
  20. package/architecture/stage-capsule-qualification.json +3 -1
  21. package/architecture/universal-workflow-bootstrap.json +15 -1
  22. package/architecture/universal-workflow-capability-policy.json +4 -4
  23. package/architecture/universal-workflow-fault-campaign.json +3 -1
  24. package/architecture/workflow-taxonomy.json +77 -328
  25. package/dist/readers/business-attempt.cjs +809 -0
  26. package/dist/site/buildchain-contract.json +16 -68
  27. package/dist/site/buildchain-site.json +40 -25
  28. package/dist/site/capability-registry.json +4 -4
  29. package/dist/site/controller-registry.json +1 -47
  30. package/dist/site/kfd-claims.json +499 -494
  31. package/dist/site/kfd-upstream-aggregate.json +1 -1
  32. package/dist/site/manual-registry.json +3 -3
  33. package/dist/site/node-api-registry.json +54 -54
  34. package/dist/site/page-registry.json +33 -18
  35. package/dist/site/public-surface-audit.json +286 -349
  36. package/dist/site/publication-authority-registry.json +203 -523
  37. package/dist/site/publication-registry.json +4 -4
  38. package/dist/site/site-manifest.json +7 -7
  39. package/dist/site/workflow-registry.json +590 -940
  40. package/docs/code-organization.md +9 -6
  41. package/docs/node-api-reference.md +102 -102
  42. package/docs/release-governance.md +10 -12
  43. package/docs/runtime-entry.md +73 -5
  44. package/docs/runtime-train-validation.md +39 -11
  45. package/docs/stable-candidate-patrol.md +6 -5
  46. package/docs/workflow-catalog.md +9 -32
  47. package/package.json +4 -3
  48. package/packages/core/adoption/buildchain-kfd-claims.js +7 -7
  49. package/packages/core/build/standalone/build.js +7 -1
  50. package/packages/core/consumer/buildchain-config.js +2 -21
  51. package/packages/core/consumer/contract/entries.js +85 -0
  52. package/packages/core/consumer/contract/examples.js +96 -0
  53. package/packages/core/consumer/contract/identity.js +65 -0
  54. package/packages/core/consumer/contract/inspection.js +45 -0
  55. package/packages/core/consumer/contract/plan.js +149 -0
  56. package/packages/core/consumer/contract/products.js +119 -0
  57. package/packages/core/consumer/contract/reader.js +47 -0
  58. package/packages/core/consumer/contract/shape.js +46 -0
  59. package/packages/core/dev-delivery/candidate/admission.js +9 -5
  60. package/packages/core/dev-delivery/candidate/reservation.js +11 -0
  61. package/packages/core/dev-delivery/candidate/source-paths.js +12 -4
  62. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  63. package/packages/core/dev-delivery/dev-delivery-provider-heartbeat.js +18 -15
  64. package/packages/core/dev-delivery/native/actions.js +9 -1
  65. package/packages/core/dev-delivery/native/heartbeat-action.js +16 -0
  66. package/packages/core/dev-delivery/native/heartbeat.js +6 -4
  67. package/packages/core/dev-delivery/native/process-entry.js +19 -0
  68. package/packages/core/dev-delivery/queue/landing-action.js +53 -0
  69. package/packages/core/dev-delivery/warrant/failure-settlement.js +8 -6
  70. package/packages/core/dev-delivery/warrant/service.js +7 -0
  71. package/packages/core/governance/buildchain-publication-authority.js +8 -23
  72. package/packages/core/governance/github-governance-authority.js +1 -0
  73. package/packages/core/observability/controller-evidence.js +0 -8
  74. package/packages/core/paper/operations/bootstrap.js +2 -0
  75. package/packages/core/paper/paper-npm-bootstrap.js +2 -2
  76. package/packages/core/providers/github/attempt-index.js +60 -0
  77. package/packages/core/providers/github/attempt-journal.js +154 -0
  78. package/packages/core/providers/github/discussions/materials.js +19 -6
  79. package/packages/core/providers/github/pipeline-checkout.js +49 -0
  80. package/packages/core/providers/github/pipeline-entry-runs.js +210 -0
  81. package/packages/core/providers/github/pipeline-events.js +131 -0
  82. package/packages/core/providers/github/pipeline-integration.js +95 -0
  83. package/packages/core/providers/github/pipeline-policy.js +240 -0
  84. package/packages/core/providers/github/pipeline-product-payload.js +50 -0
  85. package/packages/core/providers/github/pipeline-product-release.js +198 -0
  86. package/packages/core/providers/github/pipeline-publication-artifacts.js +153 -0
  87. package/packages/core/providers/github/pipeline-release-evidence.js +30 -0
  88. package/packages/core/providers/github/pipeline-run-entry.js +86 -0
  89. package/packages/core/providers/github/pipeline-runs.js +132 -0
  90. package/packages/core/providers/github/pipeline-source.js +195 -0
  91. package/packages/core/providers/github/pipeline-stable-baseline.js +86 -0
  92. package/packages/core/providers/github/pipeline-stable-source.js +372 -0
  93. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  94. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  95. package/packages/core/providers/github/pipeline-version.js +199 -0
  96. package/packages/core/providers/github/pipeline-worker.js +108 -0
  97. package/packages/core/providers/github/release-evidence-assets.js +97 -0
  98. package/packages/core/publication/candidate/registry-hydration.js +2 -2
  99. package/packages/core/publication/npm/pack-preview.js +2 -2
  100. package/packages/core/publication/npm/pack-result.js +31 -0
  101. package/packages/core/publication/npm/package.js +3 -6
  102. package/packages/core/publication/npm/pipeline-channel.js +36 -0
  103. package/packages/core/publication/npm/pipeline-provider.js +137 -0
  104. package/packages/core/publication/npm/registry.js +12 -9
  105. package/packages/core/publication/pipeline/actions.js +116 -0
  106. package/packages/core/publication/pipeline/apply.js +189 -0
  107. package/packages/core/publication/pipeline/build-segments.js +107 -0
  108. package/packages/core/publication/pipeline/capsules.js +92 -0
  109. package/packages/core/publication/pipeline/context.js +39 -0
  110. package/packages/core/publication/pipeline/development-anchor.js +91 -0
  111. package/packages/core/publication/pipeline/development-current.js +63 -0
  112. package/packages/core/publication/pipeline/development-pr.js +112 -0
  113. package/packages/core/publication/pipeline/development-proof.js +76 -0
  114. package/packages/core/publication/pipeline/development-transition.js +112 -0
  115. package/packages/core/publication/pipeline/distribution.js +130 -0
  116. package/packages/core/publication/pipeline/documents.js +192 -0
  117. package/packages/core/publication/pipeline/effects.js +192 -0
  118. package/packages/core/publication/pipeline/files.js +66 -0
  119. package/packages/core/publication/pipeline/imported-materials.js +32 -0
  120. package/packages/core/publication/pipeline/journal.js +122 -0
  121. package/packages/core/publication/pipeline/next-development.js +117 -0
  122. package/packages/core/publication/pipeline/pack.js +180 -0
  123. package/packages/core/publication/pipeline/package-policy.js +22 -0
  124. package/packages/core/publication/pipeline/plan.js +136 -0
  125. package/packages/core/publication/pipeline/prepare.js +139 -0
  126. package/packages/core/publication/pipeline/qualification.js +175 -0
  127. package/packages/core/publication/pipeline/qualify.js +104 -0
  128. package/packages/core/publication/pipeline/recovery-admission.js +122 -0
  129. package/packages/core/publication/pipeline/recovery-build-download.js +80 -0
  130. package/packages/core/publication/pipeline/recovery-build-plan.js +122 -0
  131. package/packages/core/publication/pipeline/recovery-build-readback.js +68 -0
  132. package/packages/core/publication/pipeline/recovery-capsules.js +54 -0
  133. package/packages/core/publication/pipeline/recovery-import.js +80 -0
  134. package/packages/core/publication/pipeline/recovery-inspection.js +218 -0
  135. package/packages/core/publication/pipeline/recovery-materials.js +104 -0
  136. package/packages/core/publication/pipeline/recovery-plan.js +46 -0
  137. package/packages/core/publication/pipeline/recovery-prepare.js +73 -0
  138. package/packages/core/publication/pipeline/recovery-qualification.js +128 -0
  139. package/packages/core/publication/pipeline/recovery-readback.js +56 -0
  140. package/packages/core/publication/pipeline/recovery-signing.js +58 -0
  141. package/packages/core/publication/pipeline/sealed-products.js +79 -0
  142. package/packages/core/publication/pipeline/settle.js +42 -0
  143. package/packages/core/publication/pipeline/signing.js +140 -0
  144. package/packages/core/publication/pipeline/source-plan.js +83 -0
  145. package/packages/core/publication/pipeline/stable-entry.js +219 -0
  146. package/packages/core/publication/pipeline/stable-products.js +317 -0
  147. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  148. package/packages/core/publication/pipeline/stable.js +82 -0
  149. package/packages/core/publication/pipeline/version-actions.js +93 -0
  150. package/packages/core/publication/pipeline/version-build.js +130 -0
  151. package/packages/core/publication/pipeline/version-context.js +143 -0
  152. package/packages/core/publication/pipeline/version-material.js +36 -0
  153. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  154. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  155. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  156. package/packages/core/publication/pipeline/version.js +81 -0
  157. package/packages/core/publication/pipeline/worker.js +72 -0
  158. package/packages/core/publication/publication-reproducibility.js +2 -2
  159. package/packages/core/release/next-development-transition.js +15 -15
  160. package/packages/core/release/promote-candidate/product-provider-adapters.js +4 -3
  161. package/packages/core/release/promote-ref/internal/npm-existing-evidence.js +2 -1
  162. package/packages/core/release/promote-ref/internal/publish-command.js +16 -5
  163. package/packages/core/release/stable-release-gate.js +11 -0
  164. package/packages/core/runtime/buildchain-domain.wasm +0 -0
  165. package/packages/core/runtime/domain-wasm-artifact.js +2 -2
  166. package/packages/core/runtime/entry/actions.js +27 -25
  167. package/packages/core/runtime/entry/attempt.js +25 -0
  168. package/packages/core/runtime/entry/selection.js +14 -0
  169. package/packages/core/runtime/entry/source.js +62 -0
  170. package/packages/core/workflow/attempt/identity.js +170 -0
  171. package/packages/core/workflow/attempt/journal.js +89 -0
  172. package/packages/core/workflow/attempt/materials.js +72 -0
  173. package/packages/core/workflow/attempt/reader-entry.js +12 -0
  174. package/packages/core/workflow/attempt/reader.js +149 -0
  175. package/packages/core/workflow/attempt/records.js +153 -0
  176. package/packages/core/workflow/attempt/store.js +75 -0
  177. package/packages/core/workflow/commands/pipeline-build.mjs +23 -0
  178. package/packages/core/workflow/pipeline/action-output.js +11 -0
  179. package/packages/core/workflow/pipeline/actions.js +52 -0
  180. package/packages/core/workflow/pipeline/build-control.js +140 -0
  181. package/packages/core/workflow/pipeline/build-evidence.js +210 -0
  182. package/packages/core/workflow/pipeline/build-qualification.js +145 -0
  183. package/packages/core/workflow/pipeline/build-result.js +78 -0
  184. package/packages/core/workflow/pipeline/build.js +142 -0
  185. package/packages/core/workflow/pipeline/cancellation.js +169 -0
  186. package/packages/core/workflow/pipeline/channel-control.js +157 -0
  187. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  188. package/packages/core/workflow/pipeline/controller.js +141 -0
  189. package/packages/core/workflow/pipeline/delivery-control.js +249 -0
  190. package/packages/core/workflow/pipeline/delivery-observation.js +124 -0
  191. package/packages/core/workflow/pipeline/delivery-request.js +198 -0
  192. package/packages/core/workflow/pipeline/events.js +84 -0
  193. package/packages/core/workflow/pipeline/fence.js +42 -0
  194. package/packages/core/workflow/pipeline/group-control.js +59 -0
  195. package/packages/core/workflow/pipeline/guard-build.js +74 -0
  196. package/packages/core/workflow/pipeline/guard.js +158 -0
  197. package/packages/core/workflow/pipeline/host.js +144 -0
  198. package/packages/core/workflow/pipeline/integration-status.js +113 -0
  199. package/packages/core/workflow/pipeline/materials.js +58 -0
  200. package/packages/core/workflow/pipeline/notifications.js +36 -0
  201. package/packages/core/workflow/pipeline/parent-notification.js +27 -0
  202. package/packages/core/workflow/pipeline/platforms.js +21 -0
  203. package/packages/core/workflow/pipeline/progress.js +108 -0
  204. package/packages/core/workflow/pipeline/projection.js +20 -0
  205. package/packages/core/workflow/pipeline/reconcile.js +194 -0
  206. package/packages/core/workflow/pipeline/record-action.js +27 -0
  207. package/packages/core/workflow/pipeline/recorder-status.js +69 -0
  208. package/packages/core/workflow/pipeline/recovery-action.js +30 -0
  209. package/packages/core/workflow/pipeline/recovery-admission.js +122 -0
  210. package/packages/core/workflow/pipeline/recovery-build-control.js +95 -0
  211. package/packages/core/workflow/pipeline/recovery-build-evidence.js +71 -0
  212. package/packages/core/workflow/pipeline/recovery-build.js +121 -0
  213. package/packages/core/workflow/pipeline/recovery-controller.js +91 -0
  214. package/packages/core/workflow/pipeline/recovery-integration.js +79 -0
  215. package/packages/core/workflow/pipeline/recovery-merge-proof.js +90 -0
  216. package/packages/core/workflow/pipeline/recovery-ownership-settlement.js +150 -0
  217. package/packages/core/workflow/pipeline/recovery-ownership.js +91 -0
  218. package/packages/core/workflow/pipeline/recovery-plan.js +126 -0
  219. package/packages/core/workflow/pipeline/recovery-runtime.js +68 -0
  220. package/packages/core/workflow/pipeline/recovery-session.js +129 -0
  221. package/packages/core/workflow/pipeline/recovery-transition.js +135 -0
  222. package/packages/core/workflow/pipeline/recovery-unrecorded-build.js +74 -0
  223. package/packages/core/workflow/pipeline/runtime-source.js +111 -0
  224. package/packages/core/workflow/pipeline/selection.js +111 -0
  225. package/packages/core/workflow/pipeline/session.js +72 -0
  226. package/packages/core/workflow/pipeline/settlement.js +137 -0
  227. package/packages/core/workflow/pipeline/wake.js +38 -0
  228. package/packages/core/workflow/pipeline/web-status.js +108 -0
  229. package/packages/core/workflow/workflow-taxonomy.mjs +67 -47
  230. package/scripts/build-release-discussion-reader.mjs +11 -7
  231. package/scripts/check-inventory.mjs +7 -70
  232. package/scripts/check-pipeline-publication-topology.mjs +50 -0
  233. package/scripts/check-public-dogfood-contract.mjs +38 -51
  234. package/scripts/check-release-topology.mjs +15 -8
  235. package/scripts/check-self-consumer-contract.mjs +133 -0
  236. package/scripts/check-universal-workflow-bootstrap.mjs +4 -6
  237. package/scripts/generate-minimal-consumer-contract.mjs +155 -0
  238. package/scripts/generate-site-bundle.mjs +1 -1
  239. package/scripts/inventory/binary.mjs +9 -19
  240. package/scripts/inventory/promotion.mjs +2 -13
  241. package/scripts/prepare-product-toolchain.mjs +10 -0
  242. package/scripts/verify-golden-path.mjs +5 -1
  243. package/scripts/verify-product-platform.mjs +65 -0
  244. package/scripts/verify-standalone-product.mjs +42 -0
  245. package/templates/minimal-consumer/binary/.buildchain/buildchain.toml +45 -0
  246. package/templates/minimal-consumer/binary/.github/workflows/buildchain-recover.yml +30 -0
  247. package/templates/minimal-consumer/binary/.github/workflows/buildchain.yml +28 -0
  248. package/templates/minimal-consumer/binary/package.json +6 -0
  249. package/templates/minimal-consumer/binary/src/build.mjs +5 -0
  250. package/templates/minimal-consumer/binary/src/hello.c +2 -0
  251. package/templates/minimal-consumer/binary/src/verify.mjs +3 -0
  252. package/templates/minimal-consumer/npm/.buildchain/buildchain.toml +46 -0
  253. package/templates/minimal-consumer/npm/.github/workflows/buildchain-recover.yml +30 -0
  254. package/templates/minimal-consumer/npm/.github/workflows/buildchain.yml +28 -0
  255. package/templates/minimal-consumer/npm/package.json +6 -0
  256. package/templates/minimal-consumer/npm/src/build.mjs +5 -0
  257. package/templates/minimal-consumer/npm/src/verify.mjs +3 -0
  258. package/templates/minimal-consumer/paper/.buildchain/buildchain.toml +45 -0
  259. package/templates/minimal-consumer/paper/.github/workflows/buildchain-recover.yml +30 -0
  260. package/templates/minimal-consumer/paper/.github/workflows/buildchain.yml +28 -0
  261. package/templates/minimal-consumer/paper/package.json +6 -0
  262. package/templates/minimal-consumer/paper/src/build.mjs +12 -0
  263. package/templates/minimal-consumer/paper/src/verify.mjs +3 -0
@@ -0,0 +1,240 @@
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){refUpdateRule{requiredApprovingReviewCount requiresCodeOwnerReviews requiredStatusCheckContexts}}mergeQueue(branch:$branch){id}}}`;
5
+
6
+ async function classicPolicy(request, owner, name, branch) {
7
+ // Read viewer-enforced rules and the ordinary Contents:read branch projection.
8
+ // Administrative branchProtectionRule and /protection queries are not needed.
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?.ref)
18
+ throw new Error("Protected branch metadata is unavailable");
19
+ const rule = repository.ref.refUpdateRule;
20
+ const observed = await request(
21
+ `/repos/${owner}/${name}/branches/${encodeURIComponent(branch)}`,
22
+ );
23
+ const status = observed.protection?.required_status_checks;
24
+ const contexts = status?.contexts;
25
+ const checks = status?.checks;
26
+ if (
27
+ observed.name !== branch ||
28
+ !Array.isArray(contexts) ||
29
+ !Array.isArray(checks) ||
30
+ checks.some(
31
+ (check) =>
32
+ typeof check.context !== "string" ||
33
+ !check.context ||
34
+ !Object.hasOwn(check, "app_id") ||
35
+ !(
36
+ check.app_id === null ||
37
+ check.app_id === -1 ||
38
+ (Number.isSafeInteger(check.app_id) && check.app_id > 0)
39
+ ),
40
+ ) ||
41
+ contexts.some(
42
+ (context) => !checks.some((check) => check.context === context),
43
+ ) ||
44
+ checks.some((check) => !contexts.includes(check.context)) ||
45
+ (rule?.requiredStatusCheckContexts || []).some(
46
+ (context) => !contexts.includes(context),
47
+ )
48
+ )
49
+ throw new Error("Protected branch check identity metadata is incomplete");
50
+ return {
51
+ required_merge_queue: Boolean(repository.mergeQueue?.id),
52
+ required_pull_request_reviews:
53
+ rule?.requiredApprovingReviewCount > 0
54
+ ? {
55
+ require_code_owner_reviews: rule.requiresCodeOwnerReviews,
56
+ required_approving_review_count: rule.requiredApprovingReviewCount,
57
+ }
58
+ : null,
59
+ required_status_checks: {
60
+ checks,
61
+ },
62
+ };
63
+ }
64
+
65
+ async function inventory(request, url, key = "") {
66
+ const rows = [];
67
+ for (let page = 1; page <= 100; page++) {
68
+ const response = await request(
69
+ `${url}${url.includes("?") ? "&" : "?"}per_page=100&page=${page}`,
70
+ );
71
+ const entries = key ? response[key] : response;
72
+ if (!Array.isArray(entries))
73
+ throw new Error("Missing protected policy inventory");
74
+ rows.push(...entries);
75
+ if (entries.length < 100) return rows;
76
+ }
77
+ throw new Error("Protected policy inventory exceeds its bound");
78
+ }
79
+
80
+ function protections(rules, classic, policy) {
81
+ const reviews = rules
82
+ .filter((rule) => rule.type === "pull_request")
83
+ .map((rule) => rule.parameters);
84
+ if (classic?.required_pull_request_reviews)
85
+ reviews.push({
86
+ ...classic.required_pull_request_reviews,
87
+ require_code_owner_review:
88
+ classic.required_pull_request_reviews.require_code_owner_reviews,
89
+ });
90
+ if (
91
+ !reviews.some((rule) => rule?.require_code_owner_review === true) ||
92
+ !reviews.some(
93
+ (rule) =>
94
+ rule?.required_approving_review_count >= policy.minimum_approvals,
95
+ ) ||
96
+ !(
97
+ classic?.required_merge_queue ||
98
+ rules.some((rule) => rule.type === "merge_queue")
99
+ )
100
+ )
101
+ throw new Error(
102
+ "Protected branch must enforce declared approvals, code owners and merge queue",
103
+ );
104
+ const contexts = rules
105
+ .filter((rule) => rule.type === "required_status_checks")
106
+ .flatMap((rule) => rule.parameters?.required_status_checks || []);
107
+ for (const check of classic?.required_status_checks?.checks || [])
108
+ contexts.push({ context: check.context, integration_id: check.app_id });
109
+ if (!contexts.length || contexts.some((check) => !check.context))
110
+ throw new Error(
111
+ "Protected pipeline requires provider-enforced status checks",
112
+ );
113
+ return contexts;
114
+ }
115
+
116
+ function approved(pr, reviews, minimum) {
117
+ const latest = new Map();
118
+ for (const review of [...reviews].sort((a, b) => a.id - b.id)) {
119
+ if (
120
+ review.user?.login &&
121
+ ["APPROVED", "CHANGES_REQUESTED", "DISMISSED"].includes(review.state)
122
+ )
123
+ latest.set(review.user.login, review);
124
+ }
125
+ const independent = [...latest.values()].filter(
126
+ (review) => review.user.login !== pr.author?.login,
127
+ );
128
+ return (
129
+ pr.reviewDecision === "APPROVED" &&
130
+ !independent.some((review) => review.state === "CHANGES_REQUESTED") &&
131
+ independent.filter(
132
+ (review) =>
133
+ review.state === "APPROVED" && review.commit_id === pr.headRefOid,
134
+ ).length >= minimum
135
+ );
136
+ }
137
+
138
+ function checksPass(contexts, checks, statuses) {
139
+ return contexts.every(({ context, integration_id: app }) => {
140
+ const candidates = checks.filter(
141
+ (check) =>
142
+ check.name === context && (!app || app === -1 || check.app?.id === app),
143
+ );
144
+ const latest = candidates.sort((a, b) => b.id - a.id)[0];
145
+ if (latest)
146
+ return (
147
+ latest.status === "completed" &&
148
+ ["success", "neutral", "skipped"].includes(latest.conclusion)
149
+ );
150
+ if (app && app !== -1) return false;
151
+ return (
152
+ statuses
153
+ .filter((status) => status.context === context)
154
+ .sort((a, b) => b.id - a.id)[0]?.state === "success"
155
+ );
156
+ });
157
+ }
158
+
159
+ export function githubPipelinePolicy(request, repository) {
160
+ const [owner, name] = repository.split("/");
161
+ const base = `/repos/${repository}`;
162
+ async function pull(number) {
163
+ const response = await request("/graphql", {
164
+ method: "POST",
165
+ body: { query: QUERY, variables: { owner, name, number } },
166
+ });
167
+ const pr = response.data?.repository?.pullRequest;
168
+ if (!pr?.id || pr.number !== number)
169
+ throw new Error("Protected PR policy identity is unavailable");
170
+ return pr;
171
+ }
172
+ async function observe(current, policy, integratedHead) {
173
+ const { source, baseCommit } = current.generation;
174
+ const number = current.intent.source.pullRequest,
175
+ branch = current.intent.source.targetBranch;
176
+ const pr = await pull(number);
177
+ if (pr.headRefOid !== source.commit || pr.baseRefName !== branch)
178
+ throw new Error("Protected PR policy source drift");
179
+ const branchPath = encodeURIComponent(branch);
180
+ const rules = await inventory(
181
+ request,
182
+ `${base}/rules/branches/${branchPath}`,
183
+ );
184
+ const classic = await classicPolicy(request, owner, name, branch);
185
+ const contexts = protections(rules, classic, policy);
186
+ const reviews = await inventory(request, `${base}/pulls/${number}/reviews`);
187
+ const checks = await inventory(
188
+ request,
189
+ `${base}/commits/${source.commit}/check-runs?filter=latest`,
190
+ "check_runs",
191
+ );
192
+ const statuses = await inventory(
193
+ request,
194
+ `${base}/commits/${source.commit}/statuses`,
195
+ );
196
+ const again = await pull(number);
197
+ const ref = await request(`${base}/git/ref/heads/${branch}`);
198
+ if (
199
+ recordDigest(pr) !== recordDigest(again) ||
200
+ ref.object?.sha !== (integratedHead || baseCommit)
201
+ )
202
+ throw new Error("Protected policy changed during readback");
203
+ const body = {
204
+ schema: "buildchain.pipeline-policy-readback/v1",
205
+ source,
206
+ baseCommit,
207
+ pr,
208
+ rules,
209
+ classic: classic || null,
210
+ reviews,
211
+ checks,
212
+ statuses,
213
+ contexts,
214
+ review: !pr.isDraft && approved(pr, reviews, policy.minimum_approvals),
215
+ checksPassing: checksPass(contexts, checks, statuses),
216
+ ...(integratedHead ? { integratedHead } : {}),
217
+ };
218
+ return { ...body, root: recordDigest(body) };
219
+ }
220
+ async function observeMerged(current, policy, integration) {
221
+ const { root, ...body } = integration;
222
+ if (
223
+ root !== recordDigest(body) ||
224
+ body.schema !== "buildchain.pipeline-integration-readback/v1" ||
225
+ body.repository !== repository ||
226
+ body.sourceHead !== current.generation.source.commit ||
227
+ body.branch !== current.intent.source.targetBranch ||
228
+ body.pullRequest !== current.intent.source.pullRequest ||
229
+ !/^[0-9a-f]{40}$/u.test(body.protectedHead || "")
230
+ )
231
+ throw new Error(
232
+ "Merged review qualification requires exact protected integration evidence",
233
+ );
234
+ return observe(current, policy, body.protectedHead);
235
+ }
236
+ return {
237
+ observe: (current, policy) => observe(current, policy),
238
+ observeMerged,
239
+ };
240
+ }
@@ -0,0 +1,50 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalJson } from "../../release/discussion/envelope.js";
3
+ import {
4
+ publicationFile,
5
+ publicationPath,
6
+ } from "../../publication/pipeline/files.js";
7
+ const sha256 = (bytes) =>
8
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
9
+
10
+ export function pipelineProductPayload({
11
+ qualified,
12
+ documents,
13
+ directory,
14
+ evidence,
15
+ }) {
16
+ const payload = (effect) => {
17
+ if (effect.product.startsWith("evidence:")) {
18
+ const asset = evidence.find(
19
+ (entry) => `evidence:${entry.id}` === effect.product,
20
+ );
21
+ if (
22
+ !asset ||
23
+ sha256(asset.bytes) !== effect.digest ||
24
+ asset.bytes.length !== effect.size
25
+ )
26
+ throw new Error(
27
+ "Release evidence differs from admitted immutable bytes",
28
+ );
29
+ return asset.bytes;
30
+ }
31
+ if (effect.product === "release-passport")
32
+ return Buffer.from(`${canonicalJson(documents.passport)}\n`);
33
+ const artifact = qualified.artifacts.find(
34
+ (entry) => entry.id === effect.product,
35
+ );
36
+ if (
37
+ !artifact ||
38
+ artifact.digest !== effect.digest ||
39
+ artifact.size !== effect.size
40
+ )
41
+ throw new Error(
42
+ "GitHub Release payload does not match qualified product",
43
+ );
44
+ const file = publicationFile(publicationPath(directory, artifact.file));
45
+ if (file.digest !== artifact.digest || file.size !== artifact.size)
46
+ throw new Error("GitHub Release payload bytes changed");
47
+ return file.bytes;
48
+ };
49
+ return payload;
50
+ }
@@ -0,0 +1,198 @@
1
+ import { createHash } from "node:crypto";
2
+ import { setTimeout } from "node:timers/promises";
3
+ import { pipelineProductPayload } from "./pipeline-product-payload.js";
4
+
5
+ const sha256 = (bytes) =>
6
+ `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
7
+
8
+ export function githubPipelineProductRelease({
9
+ github,
10
+ request,
11
+ repository,
12
+ plan,
13
+ qualified,
14
+ documents,
15
+ directory,
16
+ evidence = [],
17
+ }) {
18
+ const [owner, repo] = repository.split("/");
19
+ const prefix = `/repos/${repository}`;
20
+ const marker = `Buildchain release transaction ${documents.transaction.transactionRoot}`;
21
+ const payload = pipelineProductPayload({
22
+ qualified,
23
+ documents,
24
+ directory,
25
+ evidence,
26
+ });
27
+ async function tag() {
28
+ const value = await request(
29
+ `${prefix}/git/ref/tags/${encodeURIComponent(plan.tag)}`,
30
+ { allow404: true },
31
+ );
32
+ if (!value) return { state: "absent" };
33
+ let object = value.object;
34
+ for (let depth = 0; object.type === "tag" && depth < 4; depth++)
35
+ object = (await request(`${prefix}/git/tags/${object.sha}`)).object;
36
+ if (object.type !== "commit")
37
+ throw new Error("Exact release tag does not resolve to a commit");
38
+ return {
39
+ state: "present",
40
+ commit: object.sha,
41
+ ref: value.ref,
42
+ objectSha: value.object.sha,
43
+ };
44
+ }
45
+ async function release(create = false) {
46
+ const observedTag = await tag();
47
+ if (observedTag.state === "absent" && !create) return null;
48
+ if (observedTag.commit !== qualified.source.commit)
49
+ throw new Error("Release requires the exact immutable publication tag");
50
+ let value = await findRelease();
51
+ if (!value && create) {
52
+ let failure;
53
+ try {
54
+ await github.rest.repos.createRelease({
55
+ owner,
56
+ repo,
57
+ tag_name: plan.tag,
58
+ target_commitish: qualified.source.commit,
59
+ name: plan.tag,
60
+ body: marker,
61
+ draft: true,
62
+ prerelease: plan.channel === "alpha",
63
+ make_latest: "false",
64
+ });
65
+ } catch (error) {
66
+ failure = error;
67
+ }
68
+ for (let attempt = 0; attempt < 3; attempt++) {
69
+ if (attempt) await setTimeout(1000);
70
+ value = await findRelease();
71
+ if (value) break;
72
+ }
73
+ if (!value)
74
+ throw (
75
+ failure || new Error("Release creation lacks exact provider readback")
76
+ );
77
+ }
78
+ if (
79
+ value &&
80
+ (value.tag_name !== plan.tag ||
81
+ value.body !== marker ||
82
+ value.prerelease !== (plan.channel === "alpha"))
83
+ )
84
+ throw new Error(
85
+ "Existing GitHub Release belongs to a different immutable transaction",
86
+ );
87
+ return value;
88
+ }
89
+ const findRelease = () => findPipelineRelease(github, owner, repo, plan.tag);
90
+ async function asset(effect) {
91
+ const published = await release();
92
+ if (!published) return { state: "absent" };
93
+ const assets = await github.paginate(github.rest.repos.listReleaseAssets, {
94
+ owner,
95
+ repo,
96
+ release_id: published.id,
97
+ per_page: 100,
98
+ });
99
+ const matches = assets.filter((entry) => entry.name === effect.name);
100
+ if (!matches.length) return { state: "absent" };
101
+ if (matches.length !== 1 || matches[0].size > 256 * 1024 * 1024)
102
+ throw new Error(
103
+ "Release asset identity is ambiguous or exceeds its bound",
104
+ );
105
+ const bytes = Buffer.from(
106
+ (
107
+ await github.rest.repos.getReleaseAsset({
108
+ owner,
109
+ repo,
110
+ asset_id: matches[0].id,
111
+ headers: { accept: "application/octet-stream" },
112
+ })
113
+ ).data,
114
+ );
115
+ return {
116
+ state: "present",
117
+ id: matches[0].id,
118
+ releaseId: published.id,
119
+ size: bytes.length,
120
+ digest: sha256(bytes),
121
+ };
122
+ }
123
+ return {
124
+ async observe(effect) {
125
+ if (effect.kind === "exact-tag") return tag();
126
+ if (effect.kind === "release-asset") return asset(effect);
127
+ if (effect.kind === "release-visibility") {
128
+ const value = await release();
129
+ return value && !value.draft
130
+ ? {
131
+ state: "present",
132
+ id: value.id,
133
+ tag: value.tag_name,
134
+ prerelease: value.prerelease,
135
+ }
136
+ : { state: "absent" };
137
+ }
138
+ throw new Error("Unsupported GitHub product publication operation");
139
+ },
140
+ matches(effect, observed) {
141
+ if (observed.state !== "present") return false;
142
+ if (effect.kind === "exact-tag") return observed.commit === effect.commit;
143
+ if (effect.kind === "release-asset") {
144
+ const bytes = payload(effect);
145
+ return (
146
+ observed.digest === sha256(bytes) && observed.size === bytes.length
147
+ );
148
+ }
149
+ return (
150
+ effect.kind === "release-visibility" &&
151
+ observed.tag === effect.tag &&
152
+ observed.prerelease === effect.prerelease
153
+ );
154
+ },
155
+ async apply(effect) {
156
+ if (effect.kind === "exact-tag")
157
+ return request(`${prefix}/git/refs`, {
158
+ method: "POST",
159
+ body: { ref: `refs/tags/${effect.tag}`, sha: effect.commit },
160
+ });
161
+ const value = await release(true);
162
+ if (effect.kind === "release-asset")
163
+ return github.rest.repos.uploadReleaseAsset({
164
+ owner,
165
+ repo,
166
+ release_id: value.id,
167
+ name: effect.name,
168
+ data: payload(effect),
169
+ headers: { "content-type": "application/octet-stream" },
170
+ });
171
+ if (effect.kind === "release-visibility")
172
+ return github.rest.repos.updateRelease({
173
+ owner,
174
+ repo,
175
+ release_id: value.id,
176
+ draft: false,
177
+ make_latest: "false",
178
+ });
179
+ throw new Error("Unsupported GitHub product publication operation");
180
+ },
181
+ };
182
+ }
183
+
184
+ async function findPipelineRelease(github, owner, repo, tag) {
185
+ let pages = 0;
186
+ const values = await github.paginate(
187
+ github.rest.repos.listReleases,
188
+ { owner, repo, per_page: 100 },
189
+ (response) => {
190
+ if (++pages > 100)
191
+ throw new Error("Release inventory exceeds its bounded readback");
192
+ return response.data.filter((entry) => entry.tag_name === tag);
193
+ },
194
+ );
195
+ if (values.length > 1)
196
+ throw new Error("Exact release tag has multiple provider releases");
197
+ return values[0];
198
+ }
@@ -0,0 +1,153 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import artifact from "@actions/artifact";
4
+ import { recordDigest } from "../../release/discussion/envelope.js";
5
+ import { readPipelineCaller } from "./pipeline-run-entry.js";
6
+ import { publicationPath } from "../../publication/pipeline/files.js";
7
+
8
+ export const pipelinePublicationArtifactName = (plan, platform) =>
9
+ `buildchain-products-${plan.root.slice(7)}-${platform}`;
10
+
11
+ export function githubPipelinePublicationArtifacts({
12
+ request,
13
+ runs,
14
+ repository,
15
+ token,
16
+ client = artifact,
17
+ }) {
18
+ const prefix = `/repos/${repository}/actions`;
19
+ async function upload(plan, manifest, directory) {
20
+ const paths = [
21
+ publicationPath(directory, "manifest.json"),
22
+ ...manifest.artifacts.map(({ file }) => publicationPath(directory, file)),
23
+ ];
24
+ const result = await client.uploadArtifact(
25
+ pipelinePublicationArtifactName(plan, manifest.platform),
26
+ paths,
27
+ directory,
28
+ { retentionDays: 30, compressionLevel: 0 },
29
+ );
30
+ if (!result.id || !result.digest)
31
+ throw new Error(
32
+ "Product upload omitted its immutable provider coordinates",
33
+ );
34
+ return result;
35
+ }
36
+ async function buildReadback(context, selectedPlatforms) {
37
+ const { plan, materialization, runId, runAttempt } = context;
38
+ const { run, jobs } = await runs.read(runId, runAttempt);
39
+ await readPipelineCaller(
40
+ run,
41
+ materialization.source.configPath,
42
+ request,
43
+ repository,
44
+ );
45
+ const declared = [
46
+ ...new Set(plan.outputs.map(({ platform }) => platform)),
47
+ ].sort();
48
+ const platforms = selectedPlatforms || declared;
49
+ if (
50
+ !platforms.length ||
51
+ new Set(platforms).size !== platforms.length ||
52
+ platforms.some((platform) => !declared.includes(platform))
53
+ )
54
+ throw new Error("Publication subset must name unique declared platforms");
55
+ const selected = platforms.map((platform) => {
56
+ const name = `Build publication (${platform})`;
57
+ const matches = jobs.filter(
58
+ (job) => job.name === name || job.name.endsWith(` / ${name}`),
59
+ );
60
+ if (
61
+ matches.length !== 1 ||
62
+ matches[0].run_id !== runId ||
63
+ matches[0].run_attempt !== runAttempt ||
64
+ matches[0].status !== "completed" ||
65
+ matches[0].conclusion !== "success"
66
+ )
67
+ throw new Error(
68
+ "Publication needs every exact credentialless build job to succeed",
69
+ );
70
+ return matches[0];
71
+ });
72
+ const response = await request(
73
+ `${prefix}/runs/${runId}/artifacts?per_page=100`,
74
+ );
75
+ if (
76
+ !Array.isArray(response.artifacts) ||
77
+ response.total_count !== response.artifacts.length ||
78
+ response.total_count > 100
79
+ )
80
+ throw new Error("Publication artifact inventory is incomplete");
81
+ const assets = platforms.map((platform) => {
82
+ const matches = response.artifacts.filter(
83
+ (asset) =>
84
+ asset.name === pipelinePublicationArtifactName(plan, platform),
85
+ );
86
+ if (
87
+ matches.length !== 1 ||
88
+ !Number.isSafeInteger(matches[0].id) ||
89
+ matches[0].id < 1 ||
90
+ matches[0].expired ||
91
+ matches[0].workflow_run?.id !== runId ||
92
+ matches[0].workflow_run?.head_sha !== run.head_sha ||
93
+ !/^sha256:[0-9a-f]{64}$/u.test(matches[0].digest || "")
94
+ )
95
+ throw new Error(
96
+ "Publication requires one retained artifact per exact platform",
97
+ );
98
+ return matches[0];
99
+ });
100
+ const again = await runs.read(runId, runAttempt);
101
+ for (const job of selected)
102
+ if (
103
+ !again.jobs.some((entry) => recordDigest(entry) === recordDigest(job))
104
+ )
105
+ throw new Error(
106
+ "Publication build changed during independent readback",
107
+ );
108
+ if (again.run.head_sha !== run.head_sha)
109
+ throw new Error("Publication provider source changed during readback");
110
+ const body = {
111
+ schema: "buildchain.pipeline-publication-build-readback/v1",
112
+ outcome: "success",
113
+ planRoot: plan.root,
114
+ source: materialization.source,
115
+ runId,
116
+ runAttempt,
117
+ providerSource: run.head_sha,
118
+ platforms,
119
+ jobs: selected,
120
+ artifactIds: assets.map(({ id }) => id).sort((a, b) => a - b),
121
+ };
122
+ return { build: { ...body, root: recordDigest(body) }, assets };
123
+ }
124
+ async function download(context, workspace, selectedPlatforms) {
125
+ const { build, assets } = await buildReadback(context, selectedPlatforms);
126
+ const [repositoryOwner, repositoryName] = repository.split("/");
127
+ const bundles = [];
128
+ for (const asset of assets) {
129
+ const directory = path.join(workspace, String(asset.id));
130
+ fs.mkdirSync(directory, { recursive: true });
131
+ const result = await client.downloadArtifact(asset.id, {
132
+ path: directory,
133
+ expectedHash: asset.digest,
134
+ findBy: {
135
+ repositoryOwner,
136
+ repositoryName,
137
+ workflowRunId: context.runId,
138
+ token,
139
+ },
140
+ });
141
+ if (result.digestMismatch)
142
+ throw new Error(
143
+ "Product download digest does not match the provider archive",
144
+ );
145
+ const manifest = JSON.parse(
146
+ fs.readFileSync(publicationPath(directory, "manifest.json"), "utf8"),
147
+ );
148
+ bundles.push({ directory, manifest, providerArtifact: asset });
149
+ }
150
+ return { build, bundles };
151
+ }
152
+ return { upload, download, buildReadback };
153
+ }
@@ -0,0 +1,30 @@
1
+ import { readReleaseEvidenceAssets } from "./release-evidence-assets.js";
2
+
3
+ const NAMES = [
4
+ "plan",
5
+ "qualification",
6
+ "capsules",
7
+ "invocation",
8
+ "attestation",
9
+ "release",
10
+ ];
11
+ export async function readPipelineReleaseEvidence(host, candidate) {
12
+ const result = await readReleaseEvidenceAssets(
13
+ host,
14
+ candidate,
15
+ NAMES.map((id) => `buildchain.${id}.json`),
16
+ );
17
+ if (result.status !== "present") return result;
18
+ const values = {};
19
+ for (const id of NAMES.filter((name) => name !== "attestation"))
20
+ values[id] = JSON.parse(
21
+ new TextDecoder("utf-8", { fatal: true }).decode(
22
+ result.values[`buildchain.${id}.json`],
23
+ ),
24
+ );
25
+ return {
26
+ ...result,
27
+ values,
28
+ bundle: result.values["buildchain.attestation.json"],
29
+ };
30
+ }