@intentius/chant 0.124.40 → 0.124.42

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 (346) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/handlers/approve.d.ts +2 -1
  11. package/dist/cli/handlers/approve.d.ts.map +1 -1
  12. package/dist/cli/handlers/approve.js +20 -6
  13. package/dist/cli/handlers/build.d.ts.map +1 -1
  14. package/dist/cli/handlers/build.js +18 -1
  15. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  16. package/dist/cli/handlers/fan-out.js +42 -5
  17. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  18. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  19. package/dist/cli/handlers/gate-resume.js +15 -2
  20. package/dist/cli/handlers/lock.d.ts +8 -0
  21. package/dist/cli/handlers/lock.d.ts.map +1 -0
  22. package/dist/cli/handlers/lock.js +197 -0
  23. package/dist/cli/handlers/pr-env.d.ts +34 -0
  24. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  25. package/dist/cli/handlers/pr-env.js +436 -0
  26. package/dist/cli/handlers/pr.d.ts +76 -0
  27. package/dist/cli/handlers/pr.d.ts.map +1 -1
  28. package/dist/cli/handlers/pr.js +615 -55
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +9 -0
  31. package/dist/cli/handlers/query.d.ts +6 -0
  32. package/dist/cli/handlers/query.d.ts.map +1 -0
  33. package/dist/cli/handlers/query.js +80 -0
  34. package/dist/cli/handlers/run.d.ts.map +1 -1
  35. package/dist/cli/handlers/run.js +11 -0
  36. package/dist/cli/main.d.ts.map +1 -1
  37. package/dist/cli/main.js +49 -4
  38. package/dist/cli/parse-args.d.ts.map +1 -1
  39. package/dist/cli/parse-args.js +46 -2
  40. package/dist/cli/registry.d.ts +31 -2
  41. package/dist/cli/registry.d.ts.map +1 -1
  42. package/dist/components/capability.d.ts +47 -0
  43. package/dist/components/capability.d.ts.map +1 -1
  44. package/dist/components/capability.js +44 -0
  45. package/dist/components/cli-support.d.ts +6 -0
  46. package/dist/components/cli-support.d.ts.map +1 -1
  47. package/dist/components/cli-support.js +8 -1
  48. package/dist/components/driver.d.ts +10 -0
  49. package/dist/components/driver.d.ts.map +1 -1
  50. package/dist/components/driver.js +52 -2
  51. package/dist/components/fan-out-run.d.ts +37 -0
  52. package/dist/components/fan-out-run.d.ts.map +1 -1
  53. package/dist/components/fan-out-run.js +44 -4
  54. package/dist/components/gate-plan.d.ts +2 -0
  55. package/dist/components/gate-plan.d.ts.map +1 -1
  56. package/dist/components/gate-plan.js +1 -0
  57. package/dist/components/pr-environment.d.ts +298 -0
  58. package/dist/components/pr-environment.d.ts.map +1 -0
  59. package/dist/components/pr-environment.js +406 -0
  60. package/dist/components/pr-hooks.d.ts +66 -0
  61. package/dist/components/pr-hooks.d.ts.map +1 -0
  62. package/dist/components/pr-hooks.js +119 -0
  63. package/dist/components/pr-pipeline.d.ts +92 -2
  64. package/dist/components/pr-pipeline.d.ts.map +1 -1
  65. package/dist/components/pr-pipeline.js +150 -21
  66. package/dist/components/pr-policy.d.ts +109 -0
  67. package/dist/components/pr-policy.d.ts.map +1 -0
  68. package/dist/components/pr-policy.js +150 -0
  69. package/dist/components/pr-run.d.ts +6 -0
  70. package/dist/components/pr-run.d.ts.map +1 -1
  71. package/dist/components/pr-run.js +8 -3
  72. package/dist/components/wave-plan.d.ts +14 -1
  73. package/dist/components/wave-plan.d.ts.map +1 -1
  74. package/dist/components/wave-plan.js +34 -2
  75. package/dist/gate-decision.d.ts +15 -2
  76. package/dist/gate-decision.d.ts.map +1 -1
  77. package/dist/gate-decision.js +17 -2
  78. package/dist/gate-policy.d.ts +140 -0
  79. package/dist/gate-policy.d.ts.map +1 -0
  80. package/dist/gate-policy.js +242 -0
  81. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  82. package/dist/lifecycle/gate-checkpoint.js +1 -1
  83. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  84. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  85. package/dist/lifecycle/query.d.ts +68 -0
  86. package/dist/lifecycle/query.d.ts.map +1 -0
  87. package/dist/lifecycle/query.js +246 -0
  88. package/dist/lifecycle/run-ledger.d.ts +13 -0
  89. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  90. package/dist/lifecycle/run-ledger.js +16 -0
  91. package/dist/lifecycle/target-lock.d.ts +97 -0
  92. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  93. package/dist/lifecycle/target-lock.js +286 -0
  94. package/dist/op/activities/activity-contracts.d.ts +30 -0
  95. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  96. package/dist/op/activities/activity-contracts.js +26 -0
  97. package/dist/op/activities/index.d.ts +2 -0
  98. package/dist/op/activities/index.d.ts.map +1 -1
  99. package/dist/op/activities/index.js +4 -0
  100. package/dist/op/activities/pr-refs.d.ts +10 -0
  101. package/dist/op/activities/pr-refs.d.ts.map +1 -0
  102. package/dist/op/activities/pr-refs.js +26 -0
  103. package/dist/op/activities/predict-behaviour.d.ts +2 -4
  104. package/dist/op/activities/predict-behaviour.d.ts.map +1 -1
  105. package/dist/op/activities/predict-behaviour.js +2 -21
  106. package/dist/op/activities/review-agent.d.ts +137 -0
  107. package/dist/op/activities/review-agent.d.ts.map +1 -0
  108. package/dist/op/activities/review-agent.js +266 -0
  109. package/dist/op/builders.d.ts +9 -0
  110. package/dist/op/builders.d.ts.map +1 -1
  111. package/dist/op/builders.js +11 -0
  112. package/dist/op/cron.d.ts +8 -0
  113. package/dist/op/cron.d.ts.map +1 -1
  114. package/dist/op/cron.js +51 -0
  115. package/dist/op/gate-approval.d.ts +20 -13
  116. package/dist/op/gate-approval.d.ts.map +1 -1
  117. package/dist/op/gate-approval.js +13 -18
  118. package/dist/op/gate-quorum.d.ts +131 -0
  119. package/dist/op/gate-quorum.d.ts.map +1 -0
  120. package/dist/op/gate-quorum.js +183 -0
  121. package/dist/op/gate-resume.d.ts +4 -2
  122. package/dist/op/gate-resume.d.ts.map +1 -1
  123. package/dist/op/gate-resume.js +5 -4
  124. package/dist/op/gate-review.js +1 -1
  125. package/dist/op/gate.d.ts +32 -4
  126. package/dist/op/gate.d.ts.map +1 -1
  127. package/dist/op/gate.js +40 -24
  128. package/dist/op/index.d.ts +6 -4
  129. package/dist/op/index.d.ts.map +1 -1
  130. package/dist/op/index.js +4 -3
  131. package/dist/op/local-executor.d.ts.map +1 -1
  132. package/dist/op/local-executor.js +9 -3
  133. package/dist/op/op-waves-run.d.ts +28 -43
  134. package/dist/op/op-waves-run.d.ts.map +1 -1
  135. package/dist/op/op-waves-run.js +170 -120
  136. package/dist/op/op-waves.d.ts +55 -3
  137. package/dist/op/op-waves.d.ts.map +1 -1
  138. package/dist/op/op-waves.js +63 -8
  139. package/dist/op/schedule-overdue.d.ts +57 -0
  140. package/dist/op/schedule-overdue.d.ts.map +1 -0
  141. package/dist/op/schedule-overdue.js +138 -0
  142. package/dist/op/stage-hooks.d.ts +132 -0
  143. package/dist/op/stage-hooks.d.ts.map +1 -0
  144. package/dist/op/stage-hooks.js +142 -0
  145. package/dist/override-gate.d.ts +2 -2
  146. package/dist/override-gate.d.ts.map +1 -1
  147. package/dist/override-gate.js +1 -1
  148. package/dist/pr-forge.d.ts +64 -0
  149. package/dist/pr-forge.d.ts.map +1 -1
  150. package/dist/pr-forge.js +331 -44
  151. package/dist/pr-loop.d.ts +107 -1
  152. package/dist/pr-loop.d.ts.map +1 -1
  153. package/dist/pr-loop.js +84 -3
  154. package/dist/pr-post.d.ts +72 -0
  155. package/dist/pr-post.d.ts.map +1 -0
  156. package/dist/pr-post.js +134 -0
  157. package/dist/pr-requires.d.ts +38 -0
  158. package/dist/pr-requires.d.ts.map +1 -0
  159. package/dist/pr-requires.js +48 -0
  160. package/dist/wave-blocked.d.ts +79 -0
  161. package/dist/wave-blocked.d.ts.map +1 -0
  162. package/dist/wave-blocked.js +245 -0
  163. package/dist/wave-engine.d.ts +6 -0
  164. package/dist/wave-engine.d.ts.map +1 -1
  165. package/dist/wave-engine.js +2 -0
  166. package/dist/wave-runtime.mjs +243 -243
  167. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  168. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  169. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  170. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -1
  171. package/dist/workspace/checks/ci-workflow.js +19 -4
  172. package/dist/workspace/ci-branches.d.ts +51 -0
  173. package/dist/workspace/ci-branches.d.ts.map +1 -0
  174. package/dist/workspace/ci-branches.js +69 -0
  175. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  176. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  177. package/dist/workspace/ci-cloud-identity.js +24 -0
  178. package/dist/workspace/ci-init.d.ts.map +1 -1
  179. package/dist/workspace/ci-init.js +5 -4
  180. package/dist/workspace/ci-locks.d.ts +12 -0
  181. package/dist/workspace/ci-locks.d.ts.map +1 -0
  182. package/dist/workspace/ci-locks.js +6 -0
  183. package/dist/workspace/ci-run.d.ts +17 -1
  184. package/dist/workspace/ci-run.d.ts.map +1 -1
  185. package/dist/workspace/ci-run.js +66 -4
  186. package/dist/workspace/ci-workflow-at.d.ts +14 -0
  187. package/dist/workspace/ci-workflow-at.d.ts.map +1 -0
  188. package/dist/workspace/ci-workflow-at.js +17 -0
  189. package/dist/workspace/ci-workflow.d.ts +11 -3
  190. package/dist/workspace/ci-workflow.d.ts.map +1 -1
  191. package/dist/workspace/ci-workflow.js +10 -2
  192. package/dist/workspace/declaration.d.ts +69 -3
  193. package/dist/workspace/declaration.d.ts.map +1 -1
  194. package/dist/workspace/declaration.js +72 -1
  195. package/dist/workspace/declaration.schema.json +149 -2
  196. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  197. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  198. package/dist/workspace/identity-gate-rule.js +1 -1
  199. package/dist/workspace/identity.d.ts +1 -1
  200. package/dist/workspace/identity.d.ts.map +1 -1
  201. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  202. package/dist/workspace/lineage-upgrade.js +8 -5
  203. package/dist/workspace/member-pipeline.d.ts +6 -0
  204. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  205. package/dist/workspace/member-pipeline.js +23 -0
  206. package/dist/workspace/pr-report.schema.json +94 -3
  207. package/dist/workspace/reason-codes.d.ts +1 -1
  208. package/dist/workspace/reason-codes.d.ts.map +1 -1
  209. package/dist/workspace/reason-codes.js +1 -1
  210. package/dist/workspace/security.d.ts.map +1 -1
  211. package/dist/workspace/security.js +4 -0
  212. package/dist/workspace/status-gates.d.ts +3 -2
  213. package/dist/workspace/status-gates.d.ts.map +1 -1
  214. package/dist/workspace/status-gates.js +8 -7
  215. package/dist/workspace/status.d.ts.map +1 -1
  216. package/dist/workspace/status.js +4 -1
  217. package/dist/workspace/trust/seal.d.ts +2 -2
  218. package/dist/workspace/trust/seal.d.ts.map +1 -1
  219. package/dist/workspace/trust/seal.js +2 -2
  220. package/package.json +1 -1
  221. package/src/__fixtures__/oidc-emulator.ts +118 -0
  222. package/src/ci-handover.test.ts +209 -0
  223. package/src/ci-handover.ts +237 -0
  224. package/src/ci-identity.test.ts +201 -0
  225. package/src/ci-identity.ts +310 -0
  226. package/src/cli/handlers/approve.ts +18 -5
  227. package/src/cli/handlers/build.ts +18 -1
  228. package/src/cli/handlers/fan-out.ts +43 -6
  229. package/src/cli/handlers/gate-resume.ts +16 -2
  230. package/src/cli/handlers/lock.test.ts +98 -0
  231. package/src/cli/handlers/lock.ts +202 -0
  232. package/src/cli/handlers/operator.test.ts +3 -1
  233. package/src/cli/handlers/pr-before-merge.test.ts +276 -0
  234. package/src/cli/handlers/pr-env.ts +462 -0
  235. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  236. package/src/cli/handlers/pr.test.ts +184 -0
  237. package/src/cli/handlers/pr.ts +605 -61
  238. package/src/cli/handlers/promote.ts +9 -0
  239. package/src/cli/handlers/query.ts +78 -0
  240. package/src/cli/handlers/run.ts +10 -0
  241. package/src/cli/main.ts +49 -4
  242. package/src/cli/parse-args.ts +34 -2
  243. package/src/cli/registry.ts +31 -2
  244. package/src/components/capability-targets.test.ts +162 -0
  245. package/src/components/capability.ts +71 -0
  246. package/src/components/cli-support.ts +14 -1
  247. package/src/components/driver.ts +54 -3
  248. package/src/components/fan-out-run.ts +58 -3
  249. package/src/components/fan-out-waves.test.ts +45 -1
  250. package/src/components/gate-plan.ts +3 -0
  251. package/src/components/pr-environment.test.ts +249 -0
  252. package/src/components/pr-environment.ts +538 -0
  253. package/src/components/pr-hooks.test.ts +118 -0
  254. package/src/components/pr-hooks.ts +157 -0
  255. package/src/components/pr-pipeline.ts +194 -21
  256. package/src/components/pr-policy.test.ts +145 -0
  257. package/src/components/pr-policy.ts +211 -0
  258. package/src/components/pr-run.ts +14 -2
  259. package/src/components/wave-plan.ts +38 -3
  260. package/src/gate-decision-bundle.test.ts +4 -3
  261. package/src/gate-decision.ts +39 -2
  262. package/src/gate-policy-bundle.test.ts +28 -0
  263. package/src/gate-policy.test.ts +162 -0
  264. package/src/gate-policy.ts +298 -0
  265. package/src/lifecycle/gate-checkpoint.ts +1 -1
  266. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  267. package/src/lifecycle/query.test.ts +91 -0
  268. package/src/lifecycle/query.ts +315 -0
  269. package/src/lifecycle/run-ledger.ts +18 -0
  270. package/src/lifecycle/target-lock.test.ts +195 -0
  271. package/src/lifecycle/target-lock.ts +347 -0
  272. package/src/op/activities/activity-contracts.ts +31 -0
  273. package/src/op/activities/index.ts +6 -0
  274. package/src/op/activities/pr-refs.ts +28 -0
  275. package/src/op/activities/predict-behaviour.ts +3 -23
  276. package/src/op/activities/review-agent.test.ts +240 -0
  277. package/src/op/activities/review-agent.ts +355 -0
  278. package/src/op/builders.ts +13 -0
  279. package/src/op/cron.ts +44 -0
  280. package/src/op/gate-alias.test.ts +2 -2
  281. package/src/op/gate-approval.test.ts +30 -9
  282. package/src/op/gate-approval.ts +23 -26
  283. package/src/op/gate-quorum.test.ts +139 -0
  284. package/src/op/gate-quorum.ts +248 -0
  285. package/src/op/gate-resume.ts +6 -3
  286. package/src/op/gate-review.ts +1 -1
  287. package/src/op/gate.ts +64 -23
  288. package/src/op/index.ts +13 -1
  289. package/src/op/local-executor.ts +9 -2
  290. package/src/op/op-waves-hook-targets.test.ts +126 -0
  291. package/src/op/op-waves-policy-hold.test.ts +66 -0
  292. package/src/op/op-waves-quorum.test.ts +101 -0
  293. package/src/op/op-waves-run.ts +197 -183
  294. package/src/op/op-waves.ts +83 -10
  295. package/src/op/schedule-overdue.test.ts +220 -0
  296. package/src/op/schedule-overdue.ts +161 -0
  297. package/src/op/stage-hooks.ts +251 -0
  298. package/src/override-gate-bundle.test.ts +1 -1
  299. package/src/override-gate.ts +2 -2
  300. package/src/pr-forge-bundle.test.ts +2 -2
  301. package/src/pr-forge.test.ts +33 -0
  302. package/src/pr-forge.ts +347 -35
  303. package/src/pr-loop-bundle.test.ts +2 -2
  304. package/src/pr-loop.test.ts +58 -0
  305. package/src/pr-loop.ts +170 -4
  306. package/src/pr-post.test.ts +141 -0
  307. package/src/pr-post.ts +159 -0
  308. package/src/pr-requires.test.ts +184 -0
  309. package/src/pr-requires.ts +79 -0
  310. package/src/review-agent-bundle.test.ts +35 -0
  311. package/src/stage-hooks-bundle.test.ts +38 -0
  312. package/src/wave-blocked-bundle.test.ts +25 -0
  313. package/src/wave-blocked.test.ts +146 -0
  314. package/src/wave-blocked.ts +271 -0
  315. package/src/wave-engine.ts +8 -0
  316. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  317. package/src/workspace/checks/ci-workflow.test.ts +9 -0
  318. package/src/workspace/checks/ci-workflow.ts +18 -4
  319. package/src/workspace/ci-branches.test.ts +62 -0
  320. package/src/workspace/ci-branches.ts +79 -0
  321. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  322. package/src/workspace/ci-cloud-identity.ts +23 -0
  323. package/src/workspace/ci-init.ts +5 -4
  324. package/src/workspace/ci-locks.ts +16 -0
  325. package/src/workspace/ci-run-index.test.ts +2 -1
  326. package/src/workspace/ci-run.test.ts +107 -8
  327. package/src/workspace/ci-run.ts +74 -5
  328. package/src/workspace/ci-workflow-at.ts +23 -0
  329. package/src/workspace/ci-workflow.ts +13 -2
  330. package/src/workspace/declaration.schema.json +149 -2
  331. package/src/workspace/declaration.test.ts +76 -0
  332. package/src/workspace/declaration.ts +140 -5
  333. package/src/workspace/identity-gate-rule.ts +1 -1
  334. package/src/workspace/identity.ts +1 -1
  335. package/src/workspace/lineage-upgrade.ts +11 -8
  336. package/src/workspace/member-pipeline.test.ts +29 -0
  337. package/src/workspace/member-pipeline.ts +30 -0
  338. package/src/workspace/pr-report.schema.json +94 -3
  339. package/src/workspace/reason-codes.ts +1 -1
  340. package/src/workspace/security.ts +4 -0
  341. package/src/workspace/status-gates.ts +9 -6
  342. package/src/workspace/status.ts +4 -1
  343. package/src/workspace/trust/gate-seal.test.ts +1 -1
  344. package/src/workspace/trust/seal.ts +2 -2
  345. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  346. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -0,0 +1,247 @@
1
+ /**
2
+ * `chant components pr-apply --wave-gate` (#4291): a failed member blocks
3
+ * only the members that depend on it. `db` fails in wave 1; `app` reads
4
+ * `net` and still applies in wave 2, and `web` reads `db` and is blocked.
5
+ * The note lists the blocked set. Git, the change signal, the derivation and
6
+ * the gate ledger are fakes, as in `pr-waves.test.ts`; the planning, the
7
+ * coverage check and the apply are the real ones.
8
+ */
9
+
10
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
11
+ import { tmpdir } from "node:os";
12
+ import { existsSync } from "node:fs";
13
+ import { join } from "node:path";
14
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
15
+ import type { DriverComponent } from "../../components/driver";
16
+ import type { DeployContext } from "../../components/capability";
17
+ import type { ParsedArgs } from "../registry";
18
+ import type { PrReport } from "../../pr-loop";
19
+
20
+ const HEAD = "a".repeat(40);
21
+ const BASE = "b".repeat(40);
22
+
23
+ vi.mock("../../components/changed-files", () => ({
24
+ resolveCommit: async () => HEAD,
25
+ resolveMergeBase: async () => BASE,
26
+ }));
27
+
28
+ vi.mock("../../config", async () => {
29
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
30
+ return { ...actual, loadChantConfig: async () => ({ config: {} }) };
31
+ });
32
+
33
+ vi.mock("./fan-out", () => ({
34
+ resolveChangedUnits: async () => ({ units: { changed: ["net", "db"], indeterminate: [] } }),
35
+ }));
36
+
37
+ // pr-apply stores the change set a gate binds on chant/lifecycle (#4022); never in the real repository.
38
+ vi.mock("../../lifecycle/gate-change-set", () => ({
39
+ storeGateChangeSet: async () => ({ written: false }),
40
+ readGraphIds: async () => ({ ids: [] }),
41
+ setGraphIdReader: () => {},
42
+ gateGraphReader: () => async () => ({ ids: [] }),
43
+ }));
44
+ vi.mock("../../lifecycle/git", async () => {
45
+ const actual = await vi.importActual<typeof import("../../lifecycle/git")>("../../lifecycle/git");
46
+ return { ...actual, pushLifecycleReport: async () => { throw new Error("pr-waves.test must not push chant/lifecycle"); } };
47
+ });
48
+
49
+ vi.mock("./run", () => ({
50
+ GATED_EXIT_CODE: 3,
51
+ autoReleaseRun: (id: string) => ({ runId: id, runOrigin: { forge: "local" } }),
52
+ recordAutoReleasesForRun: async () => [],
53
+ }));
54
+
55
+ let gateLedger: ReturnType<typeof import("../../op/gate").memoryGateLedgerPort>;
56
+ vi.mock("../../op/gate", async () => {
57
+ const actual = await vi.importActual<typeof import("../../op/gate")>("../../op/gate");
58
+ return { ...actual, gitGateLedgerPort: () => gateLedger, gateIsSealed: async () => false };
59
+ });
60
+
61
+ /** The outputs each root exposes on the estate: what `outputs` reads and `run` writes. */
62
+ const deployed: Record<string, Record<string, unknown>> = {};
63
+ /** The roots that applied, in order. */
64
+ let ran: string[] = [];
65
+ /**
66
+ * Whether a root's plan says the `cidr` it will expose (#3999), as
67
+ * terraform's `planned_values` does. Off, every output of a root the change
68
+ * plans is known only after apply.
69
+ */
70
+ let predicts = true;
71
+ /** Roots whose apply fails. */
72
+ const failing = new Set<string>();
73
+ vi.mock("../../components/fan-out-support", async () => {
74
+ const { CapabilityRegistry } = await import("../../components/capability");
75
+ const { planFanOut } = await import("../../components/fan-out");
76
+ const { computePlanDigest } = await import("../../lifecycle/plan-digest");
77
+ const registry = new CapabilityRegistry();
78
+ /** A root whose one resource writes its `cidr` input: the plan says so, so a moved input moves the entry. */
79
+ registry.register({
80
+ kind: "fake-root",
81
+ async plan(_ctx: DeployContext, input: { root: string; cidr?: string }) {
82
+ const cidr = input.cidr ?? null;
83
+ const planDigest = computePlanDigest("fake-root", { root: input.root, cidr });
84
+ return {
85
+ member: input.root,
86
+ planDigest,
87
+ artifact: { planDigest, cidr },
88
+ ...(predicts ? { outputs: { known: { cidr }, unknown: [] } } : {}),
89
+ changeSet: {
90
+ member: { member: input.root, lexicon: "terraform", planner: "tofu", status: "planned", planDigest, holes: [] },
91
+ entries: [
92
+ {
93
+ member: input.root,
94
+ lexicon: "terraform",
95
+ planner: "tofu",
96
+ address: `${input.root}.main`,
97
+ type: "fake_resource",
98
+ action: "update",
99
+ attributes: [{ path: "cidr", before: deployed[input.root]?.cidr ?? null, after: cidr }],
100
+ },
101
+ ],
102
+ },
103
+ };
104
+ },
105
+ async outputs(_ctx: DeployContext, input: { root: string }) {
106
+ return deployed[input.root] ?? {};
107
+ },
108
+ // Applies the plan it is handed, as a planned root does, else what its input says now.
109
+ async run(ctx: DeployContext, input: { root: string; cidr?: string }) {
110
+ if (failing.has(input.root)) throw new Error(`${input.root} failed`);
111
+ ran.push(input.root);
112
+ const planned = (ctx as { plans?: Record<string, { cidr: string | null }> }).plans?.[input.root];
113
+ deployed[input.root] = { cidr: planned ? planned.cidr : (input.cidr ?? null) };
114
+ return { outputs: deployed[input.root] };
115
+ },
116
+ } as never);
117
+ return {
118
+ fanOutRegistry: async () => registry,
119
+ deriveFanOut: async () => ({
120
+ success: true,
121
+ components: ESTATE,
122
+ plan: planFanOut({ components: ESTATE, changed: ["net", "db"] }),
123
+ signal: { changed: ["net", "db"], indeterminate: [], unclaimed: [] },
124
+ }),
125
+ };
126
+ });
127
+
128
+ /** Wave 1: `net` and `db`. Wave 2: `app` reads `net`, `web` reads `db`. */
129
+ const fixed = (name: string, dependsOn: string[] = []): DriverComponent => ({
130
+ name,
131
+ dependsOn,
132
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-root", root: name, cidr: "10.0.0.0/16" }] }],
133
+ });
134
+ const ESTATE: DriverComponent[] = [
135
+ fixed("net"),
136
+ fixed("db"),
137
+ { name: "app", dependsOn: ["net"], deploy: [{ phase: "Apply", steps: [{ kind: "fake-root", root: "app", cidr: { stackOutput: { stack: "net", name: "cidr" } } }] }] },
138
+ fixed("web", ["db"]),
139
+ ];
140
+ const netStep = ESTATE[0]!.deploy[0]!.steps[0] as Record<string, unknown>;
141
+
142
+ const { memoryGateLedgerPort } = await import("../../op/gate");
143
+ const { runComponentsPrApply, runComponentsPrPlan } = await import("./pr");
144
+ const { checkRunEventLines } = await import("../../workspace/__fixtures__/run-event-check");
145
+
146
+ let dir: string;
147
+ let record: string;
148
+ let errors: string[];
149
+ let out: string[];
150
+
151
+ const ctx = (a: Partial<ParsedArgs>) =>
152
+ ({ args: { command: "components", path: "pr-apply", format: "", fix: false, watch: false, verbose: false, help: false, live: false, base: "main", env: "prod", output: dir, ...a } as ParsedArgs, plugins: [] }) as never;
153
+ /** One wave's job, as the generated pipeline runs it. */
154
+ const wave = (k?: number, a: Partial<ParsedArgs> = {}) => ctx({ pr: 12, waveGate: "release", resume: record, ...(k !== undefined ? { wave: k } : {}), ...a });
155
+
156
+ const resolution = (gate: string, planDigest: string, by = "github:alice") => ({
157
+ version: 1 as const,
158
+ kind: "resolution" as const,
159
+ op: "pr-12",
160
+ gate,
161
+ resolvedBy: by,
162
+ timestamp: new Date().toISOString(),
163
+ planDigest,
164
+ });
165
+
166
+ const report = async (): Promise<PrReport> => JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8")) as PrReport;
167
+ const note = async (): Promise<string> => readFile(join(dir, "pr-note.md"), "utf8");
168
+
169
+ /** Plan pull request 12, as its plan job does, and approve its digest as alice. */
170
+ async function planAndApprove(): Promise<string> {
171
+ expect(await runComponentsPrPlan(ctx({ pr: 12 }))).toBe(0);
172
+ const digest = (JSON.parse(await readFile(join(dir, "pr-plan.json"), "utf8")) as PrReport).digest;
173
+ gateLedger = memoryGateLedgerPort({ resolutions: [resolution("pr-apply", digest)] });
174
+ errors = [];
175
+ return digest;
176
+ }
177
+
178
+ beforeEach(async () => {
179
+ dir = await mkdtemp(join(tmpdir(), "pr-waves-"));
180
+ record = join(dir, "resume", "pr-apply.json");
181
+ errors = [];
182
+ ran = [];
183
+ predicts = true;
184
+ for (const k of Object.keys(deployed)) delete deployed[k];
185
+ deployed.net = { cidr: "10.0.0.0/16" };
186
+ deployed.app = { cidr: "10.0.0.0/16" };
187
+ failing.clear();
188
+ netStep.cidr = "10.0.0.0/16";
189
+ gateLedger = memoryGateLedgerPort();
190
+ out = [];
191
+ vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
192
+ vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
193
+ vi.spyOn(console, "log").mockImplementation(() => {});
194
+ });
195
+
196
+ afterEach(async () => {
197
+ vi.restoreAllMocks();
198
+ await rm(dir, { recursive: true, force: true });
199
+ });
200
+
201
+ describe("pr-apply --wave-gate: a failed member blocks only its dependents (#4291)", () => {
202
+ test("wave 1 fails db and applies net; wave 2's job still runs, applies app and blocks web by db", async () => {
203
+ await planAndApprove();
204
+ failing.add("db");
205
+
206
+ expect(await runComponentsPrApply(wave(1))).toBe(1);
207
+ expect(ran).toEqual(["net"]);
208
+
209
+ expect(await runComponentsPrApply(wave(2))).toBe(1);
210
+ expect(ran).toEqual(["net", "app"]);
211
+ const r = await report();
212
+ expect(r.status).toBe("failed");
213
+ expect(Object.fromEntries(r.members.map((m) => [m.member, [m.status, m.blockedBy]]))).toEqual({
214
+ net: ["applied", undefined],
215
+ db: ["failed", undefined],
216
+ app: ["applied", undefined],
217
+ web: ["blocked", "db"],
218
+ });
219
+ const text = await note();
220
+ expect(text).toContain("| `web` | web | blocked by `db` |");
221
+ expect(text).toContain("Blocked, so not applied: `web` (blocked by `db`).");
222
+ expect(errors.some((e) => /wave 2: web \(blocked by db\), so not planned or applied/.test(e))).toBe(true);
223
+ });
224
+
225
+ test("re-run once db is fixed: wave 1 applies db, and wave 2 applies web", async () => {
226
+ await planAndApprove();
227
+ failing.add("db");
228
+ expect(await runComponentsPrApply(wave(1))).toBe(1);
229
+ expect(await runComponentsPrApply(wave(2))).toBe(1);
230
+
231
+ failing.clear();
232
+ expect(await runComponentsPrApply(wave(1))).toBe(0);
233
+ expect(await runComponentsPrApply(wave(2))).toBe(0);
234
+ expect(ran).toEqual(["net", "app", "db", "web"]);
235
+ expect((await report()).status).toBe("applied");
236
+ });
237
+
238
+ test("without --wave the waves run in one job: app applies after db failed, web is blocked, exit 1", async () => {
239
+ await planAndApprove();
240
+ failing.add("db");
241
+ expect(await runComponentsPrApply(wave())).toBe(1);
242
+ expect(ran).toEqual(["net", "app"]);
243
+ const r = await report();
244
+ expect(r.members.find((m) => m.member === "web")).toMatchObject({ status: "blocked", blockedBy: "db" });
245
+ expect(r.message).toContain("Blocked: web (blocked by db).");
246
+ });
247
+ });
@@ -26,6 +26,33 @@ vi.mock("../../workspace/security", async (original) => ({
26
26
  securityGroupsAt: () => new Set(groups),
27
27
  }));
28
28
 
29
+ /** The ci.hooks pr-plan and pr-apply read at base (#4288); none unless a test sets them. */
30
+ let hooksAtBase: Record<string, unknown> = {};
31
+ vi.mock("../../components/pr-hooks", async (original) => ({
32
+ ...(await original<typeof import("../../components/pr-hooks")>()),
33
+ prLoopHooksAt: (_cwd: string, base: string) => ({ hooks: hooksAtBase, base }),
34
+ }));
35
+ /** `ci.locks` pr-apply reads at base (#4293), and the locks other pull requests hold; none unless a test sets them. */
36
+ let locksMode: "apply" | "plan" | null = null;
37
+ let heldLocks: (gate: string, targets: readonly string[], pr: number | null) => Promise<unknown[]> = async () => [];
38
+ vi.mock("../../workspace/ci-locks", () => ({ ciLocksAt: () => locksMode }));
39
+ vi.mock("../../lifecycle/target-lock", async (original) => ({
40
+ ...(await original<typeof import("../../lifecycle/target-lock")>()),
41
+ heldTargetLocks: (gate: string, targets: readonly string[], pr: number | null) => heldLocks(gate, targets, pr),
42
+ }));
43
+
44
+ /** What ci.policy at base says (#4294): no policy unless a test sets a verdict. The engine itself is gate-policy.test.ts's. */
45
+ let policyVerdict: import("../../gate-policy").GatePolicyVerdict | undefined;
46
+ vi.mock("../../components/pr-policy", async (original) => ({
47
+ ...(await original<typeof import("../../components/pr-policy")>()),
48
+ prLoopPolicyAt: (_cwd: string, base: string) => ({ base }),
49
+ runPrPolicy: () => policyVerdict,
50
+ }));
51
+ vi.mock("../../override-gate", async (original) => ({
52
+ ...(await original<typeof import("../../override-gate")>()),
53
+ overridersAt: (_repo: string, base: string) => ({ base, path: ".chant/overriders", active: true, principals: ["github:carol"] }),
54
+ }));
55
+
29
56
  vi.mock("../../components/changed-files", () => ({
30
57
  resolveCommit: async () => HEAD,
31
58
  resolveMergeBase: async () => BASE,
@@ -134,6 +161,10 @@ beforeEach(async () => {
134
161
  refusal.clear();
135
162
  onRun = () => {};
136
163
  groups = [];
164
+ hooksAtBase = {};
165
+ locksMode = null;
166
+ heldLocks = async () => [];
167
+ policyVerdict = undefined;
137
168
  gateLedger = memoryGateLedgerPort();
138
169
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
139
170
  vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
@@ -353,6 +384,39 @@ describe("--events-json beside --json", () => {
353
384
  });
354
385
  });
355
386
 
387
+ describe("pr-apply under ci.locks (#4293)", () => {
388
+ test("a target another open pull request holds refuses the apply as locked, before anything plans or applies", async () => {
389
+ locksMode = "plan";
390
+ const asked: unknown[] = [];
391
+ heldLocks = async (gate, targets, pr) => {
392
+ asked.push({ gate, targets, pr });
393
+ return [{ target: "net", pr: 9, by: "bob", at: "2026-10-22T10:00:00.000Z", stage: "plan" }];
394
+ };
395
+ let ran = false;
396
+ onRun = () => void (ran = true);
397
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(3);
398
+ expect(asked).toEqual([{ gate: "pr-apply", targets: ["net", "app"], pr: 12 }]);
399
+ expect(ran).toBe(false);
400
+ const { readFile } = await import("node:fs/promises");
401
+ const report = JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8"));
402
+ expect(report).toMatchObject({ status: "refused", refusal: "locked" });
403
+ expect(report.message).toContain("net is locked by pull request 9 (planned by bob), so nothing was applied");
404
+ expect(report.message).toContain("chant unlock --pr 9");
405
+ });
406
+
407
+ test("no lock held by another pull request lets the apply go on to its gate; without ci.locks nothing is read", async () => {
408
+ locksMode = "apply";
409
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(3);
410
+ const { readFile } = await import("node:fs/promises");
411
+ expect(JSON.parse(await readFile(join(dir, "pr-apply.json"), "utf8"))).toMatchObject({ status: "refused", refusal: "not-approved" });
412
+ locksMode = null;
413
+ heldLocks = async () => {
414
+ throw new Error("read without ci.locks");
415
+ };
416
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(3);
417
+ });
418
+ });
419
+
356
420
  test("under the review-rules security group pr-apply needs --forge, since it reads the pull request's reviews (#4168)", async () => {
357
421
  groups = ["review-rules"];
358
422
  expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(1);
@@ -495,3 +559,123 @@ describe("pr-apply: a step that refuses its plan because the plan moved (#3997)"
495
559
  expect(logLines()).toEqual(["wave 1/2: net", " net: applied", "wave 2/2: app", " app: failed"]);
496
560
  });
497
561
  });
562
+
563
+ describe("ci.hooks on the pull-request path (#4288)", () => {
564
+ const logged = (): string[] => {
565
+ try {
566
+ return readFileSync(join(dir, "hooks.log"), "utf8").trim().split("\n");
567
+ } catch {
568
+ return [];
569
+ }
570
+ };
571
+ const hook = (name: string, extra: Record<string, unknown> = {}) => ({ name, command: ["sh", "-c", `echo ${name} $CHANT_HOOK \${CHANT_TARGET:-wave} >> ${join(dir, "hooks.log")}`], ...extra });
572
+
573
+ test("pr-plan runs before-observe, before-plan and after-plan around the plan, each for the components its targets match", async () => {
574
+ hooksAtBase = {
575
+ "before-observe": [hook("init", { perRun: true })],
576
+ "before-plan": [hook("lint", { targets: ["app"] })],
577
+ "after-plan": [hook("check", { perRun: true, targets: ["net"] })],
578
+ };
579
+ expect(await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }))).toBe(0);
580
+ expect(logged()).toEqual(["init before-observe net", "init before-observe app", "lint before-plan wave", "check after-plan net"]);
581
+ const report = JSON.parse(readFileSync(join(dir, "pr-plan.json"), "utf8"));
582
+ expect(report.hooks.map((h: { moment: string; name: string; outcome: string }) => `${h.moment} ${h.name} ${h.outcome}`)).toEqual([
583
+ "before-observe init passed",
584
+ "before-observe init passed",
585
+ "before-plan lint passed",
586
+ "after-plan check passed",
587
+ ]);
588
+ expect(readFileSync(join(dir, "pr-note.md"), "utf8")).toContain("Hooks: 4 passed.");
589
+ });
590
+
591
+ test("a failed before-plan hook fails pr-plan before anything plans, and the note says which", async () => {
592
+ hooksAtBase = { "before-plan": [{ name: "lint", command: ["sh", "-c", "exit 4"] }] };
593
+ expect(await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }))).toBe(1);
594
+ const report = JSON.parse(readFileSync(join(dir, "pr-plan.json"), "utf8"));
595
+ expect(report).toMatchObject({ status: "plan-failed", members: [], message: "The before-plan hook lint exited 4, so nothing was planned or applied." });
596
+ expect(readFileSync(join(dir, "pr-note.md"), "utf8")).toContain("A hook failed, so nothing can apply.");
597
+ });
598
+
599
+ test("pr-apply runs before-apply and after-apply around an approved apply; a failed before-apply applies nothing", async () => {
600
+ await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }));
601
+ const digest = await plannedDigest();
602
+ gateLedger = memoryGateLedgerPort({
603
+ resolutions: [{ version: 1, kind: "resolution", op: "pr-12", gate: "pr-apply", resolvedBy: "github:alice", timestamp: new Date().toISOString(), planDigest: digest }],
604
+ });
605
+ hooksAtBase = { "before-apply": [hook("backup")], "after-apply": [hook("smoke", { perRun: true })] };
606
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(0);
607
+ expect(logged()).toEqual(["backup before-apply wave", "smoke after-apply net", "smoke after-apply app"]);
608
+
609
+ const ran: string[] = [];
610
+ onRun = (r) => ran.push(r);
611
+ hooksAtBase = { "before-apply": [{ name: "backup", command: ["sh", "-c", "exit 1"] }] };
612
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(1);
613
+ expect(ran).toEqual([]);
614
+ expect(JSON.parse(readFileSync(join(dir, "pr-apply.json"), "utf8"))).toMatchObject({ status: "failed", message: "The before-apply hook backup exited 1, so nothing was applied." });
615
+ });
616
+ });
617
+
618
+ describe("ci.policy on the pull-request path (#4294)", () => {
619
+ const denied = (): import("../../gate-policy").GatePolicyVerdict => ({
620
+ result: "denied",
621
+ engine: "opa",
622
+ denials: [{ rule: "main.deny_public", message: "bucket logs is public" }],
623
+ rules: ["main.deny_public"],
624
+ holds: [],
625
+ warnings: [],
626
+ });
627
+ const approve = (digest: string) => ({ version: 1 as const, kind: "resolution" as const, op: "pr-12", gate: "pr-apply", resolvedBy: "github:alice", timestamp: new Date(Date.now() - 1000).toISOString(), planDigest: digest });
628
+
629
+ test("pr-plan reports the denial, fails its status and exits 1, and records nothing", async () => {
630
+ policyVerdict = denied();
631
+ expect(await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }))).toBe(1);
632
+ const report = JSON.parse(readFileSync(join(dir, "pr-plan.json"), "utf8"));
633
+ expect(report).toMatchObject({ status: "planned", policy: { result: "denied", rules: ["main.deny_public"], overrideCommand: expect.stringContaining("chant approve policy-override pr-12 --rule main.deny_public") } });
634
+ expect(gateLedger.appended).toEqual([]);
635
+ expect(readFileSync(join(dir, "pr-note.md"), "utf8")).toContain("denied this plan:");
636
+ });
637
+
638
+ test("pr-apply refuses a denied plan though the gate is approved, and records the denial for an override", async () => {
639
+ await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }));
640
+ const digest = await plannedDigest();
641
+ gateLedger = memoryGateLedgerPort({ resolutions: [approve(digest)] });
642
+ policyVerdict = denied();
643
+ const ran: string[] = [];
644
+ onRun = (r) => ran.push(r);
645
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(3);
646
+ const report = JSON.parse(readFileSync(join(dir, "pr-apply.json"), "utf8"));
647
+ expect(report).toMatchObject({ status: "refused", refusal: "policy-denied", policy: { result: "denied" } });
648
+ expect(report.message).toContain("github:carol may override it");
649
+ expect(gateLedger.appended).toEqual([expect.objectContaining({ op: "policy-override", gate: "pr-12", plan: digest, rules: ["main.deny_public"] })]);
650
+ expect(ran).toEqual([]);
651
+ });
652
+
653
+ test("an override by a listed overrider lets pr-apply apply under the gate's approval, and is spent first", async () => {
654
+ await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }));
655
+ const digest = await plannedDigest();
656
+ const { overrideDigest } = await import("../../override-gate");
657
+ const od = overrideDigest("pr-12", digest, ["main.deny_public"]);
658
+ gateLedger = memoryGateLedgerPort({
659
+ resolutions: [approve(digest), { version: 1, kind: "resolution", op: "policy-override", gate: "pr-12", resolvedBy: "github:carol", timestamp: new Date(Date.now() - 500).toISOString(), planDigest: od, note: "the bucket is the docs site" }],
660
+ });
661
+ policyVerdict = denied();
662
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(0);
663
+ const report = JSON.parse(readFileSync(join(dir, "pr-apply.json"), "utf8"));
664
+ expect(report).toMatchObject({ status: "applied", policy: { result: "denied", override: { by: "github:carol", digest: od } } });
665
+ expect(gateLedger.applied).toEqual([expect.objectContaining({ op: "policy-override", gate: "pr-12", planDigest: od })]);
666
+ });
667
+
668
+ test("a policy that could not run applies nothing and exits 1; a hold still needs the gate's approval", async () => {
669
+ await runComponentsPrPlan(ctx({ pr: 12, eventsJson: false }));
670
+ const digest = await plannedDigest();
671
+ gateLedger = memoryGateLedgerPort({ resolutions: [approve(digest)] });
672
+ policyVerdict = { result: "error", engine: "opa", denials: [], rules: [], holds: [], warnings: [], error: "opa did not run" };
673
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(1);
674
+ expect(JSON.parse(readFileSync(join(dir, "pr-apply.json"), "utf8"))).toMatchObject({ status: "refused", refusal: "policy-error" });
675
+
676
+ gateLedger = memoryGateLedgerPort();
677
+ policyVerdict = { result: "held", engine: "opa", denials: [], rules: [], holds: [{ rule: "main.hold_iam", message: "iam" }], warnings: [] };
678
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(3);
679
+ expect(JSON.parse(readFileSync(join(dir, "pr-apply.json"), "utf8"))).toMatchObject({ status: "refused", refusal: "not-approved", policy: { result: "held" } });
680
+ });
681
+ });