@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
@@ -43,6 +43,14 @@
43
43
  * `chant/lifecycle`, as terragucci's wave gate does, and that approval is
44
44
  * spent: the same digest needs a new approval to apply again.
45
45
  *
46
+ * ## Approval rules (#4290)
47
+ *
48
+ * With `approvals: { quorum: [...] }` (read at base), a wave takes each rule
49
+ * whose `targets` glob matches one of its runs' targets and waits until each
50
+ * is met: `count` distinct people, from `roles` when it names them (granted
51
+ * in `.chant/trust.json` at base), one of them not the author with
52
+ * `notAuthor`. See `./gate-quorum.ts`.
53
+ *
46
54
  * ## Wave-level commands and hooks (#3762)
47
55
  *
48
56
  * A product whose tool plans a whole wave in one call (Terragrunt's `run
@@ -66,6 +74,7 @@
66
74
  * the wave (exit 8). See `./op-waves-run.ts`.
67
75
  */
68
76
  import { gatePointProblems } from "./gate-point.js";
77
+ import { quorumProblems } from "./gate-quorum.js";
69
78
  import { notifyConfigProblem } from "../notify.js";
70
79
  import { WAVE_GATE_NAME_PATTERN, waveGateNameFrom, waveShare } from "../wave-layout.js";
71
80
  /**
@@ -102,11 +111,13 @@ export function opWavePointAt(spec, name) {
102
111
  }
103
112
  export const OP_WAVE_HOOK_FAILURES = ["fail", "approve"];
104
113
  /**
105
- * Every moment, in the order a wave reaches them. `after-gate` (#3821) runs
114
+ * Every moment, in the order a wave reaches them. `before-observe` (#4288)
115
+ * runs before anything reads live state: ahead of `before-plan` in every job
116
+ * that plans, the deciding job's and each share's. `after-gate` (#3821) runs
106
117
  * once the gate decided, whatever it decided: approved, let through,
107
118
  * waiting, or refused by a policy denial.
108
119
  */
109
- export const OP_WAVE_HOOK_MOMENTS = ["before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
120
+ export const OP_WAVE_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "after-gate", "before-apply", "after-apply"];
110
121
  export const OP_WAVE_APPLY_HOOKS_RUNNERS = ["chant", "apply"];
111
122
  export const OP_WAVES_DIGEST_OVER = ["all", "changing"];
112
123
  /** The default gate name pattern. */
@@ -171,13 +182,25 @@ export function assertOpWavesSpec(spec) {
171
182
  if (spec.approvals !== undefined) {
172
183
  const a = spec.approvals;
173
184
  if (a === null || typeof a !== "object" || Array.isArray(a))
174
- throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false }.`);
185
+ throw new Error(`${where}: \`approvals\` must be { "spend"?: true | false, "quorum"?: [...] }.`);
175
186
  for (const [key, value] of Object.entries(a)) {
187
+ if (key === "quorum") {
188
+ // #4290: approval rules, read at base like the policy.
189
+ if (!Array.isArray(value))
190
+ throw new Error(`${where}: approvals.quorum must be a list of rules, each { "targets"?, "count", "roles"?, "notAuthor"? }.`);
191
+ const problems = quorumProblems(value, "approvals.quorum", { notAuthor: true });
192
+ if (problems.length > 0)
193
+ throw new Error(`${where}: ${problems.join("; ")}.`);
194
+ continue;
195
+ }
176
196
  if (key !== "spend")
177
- throw new Error(`${where}: approvals.${key} is not a setting; the one there is approvals.spend.`);
197
+ throw new Error(`${where}: approvals.${key} is not a setting; the ones there are approvals.spend and approvals.quorum.`);
178
198
  if (typeof value !== "boolean")
179
199
  throw new Error(`${where}: approvals.spend must be true or false.`);
180
200
  }
201
+ if (a.quorum?.length && (spec.point !== undefined || spec.waves.some((w) => w.point !== undefined))) {
202
+ throw new Error(`${where}: approvals.quorum and a decision point are both set; a gate that asks a point takes its deciders and quorum from the point, so drop one of them.`);
203
+ }
181
204
  }
182
205
  if (spec.gateName !== undefined) {
183
206
  if (typeof spec.gateName !== "string" || !spec.gateName.includes("{k}") || /\s/.test(spec.gateName)) {
@@ -394,6 +417,15 @@ export function opWaveRunMatches(pattern, target) {
394
417
  }
395
418
  return new RegExp(`^${re}$`).test(target);
396
419
  }
420
+ /**
421
+ * The runs of `runs` hook `hook` is for (#4288): those its `targets` match,
422
+ * or every run when it names none.
423
+ */
424
+ export function opWaveHookRuns(hook, runs) {
425
+ if (!hook.targets)
426
+ return [...runs];
427
+ return runs.filter((run) => hook.targets.some((pattern) => opWaveRunMatches(pattern, run.target)));
428
+ }
397
429
  /** The runs of `runs` that job group `group` holds (#3914). */
398
430
  export function opWaveGroupRuns(group, runs) {
399
431
  return runs.filter((run) => group.runs.some((pattern) => opWaveRunMatches(pattern, run.target)));
@@ -422,7 +454,7 @@ export function assertOpWaveHooks(where, hooks) {
422
454
  if (hooks === undefined)
423
455
  return;
424
456
  if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) {
425
- throw new Error(`${where}: \`hooks\` must be { "before"?: { "plan"?, "apply"? }, "after"?: { "plan"?, "apply"? } }.`);
457
+ throw new Error(`${where}: \`hooks\` must be { "before"?: { "observe"?, "plan"?, "apply"? }, "after"?: { "plan"?, "gate"?, "apply"? } }.`);
426
458
  }
427
459
  for (const [side, stages] of Object.entries(hooks)) {
428
460
  if (side === "apply") {
@@ -443,8 +475,10 @@ export function assertOpWaveHooks(where, hooks) {
443
475
  const at = `hooks.${side}.${stage}`;
444
476
  if (stage === "gate" && side === "before")
445
477
  throw new Error(`${where}: ${at} is not a moment; a hook runs after the gate (hooks.after.gate), or before the plan or the apply.`);
446
- if (stage !== "plan" && stage !== "apply" && stage !== "gate")
447
- throw new Error(`${where}: ${at} is not a stage; a hook runs before or after plan or apply, or after the gate.`);
478
+ if (stage === "observe" && side === "after")
479
+ throw new Error(`${where}: ${at} is not a moment; a hook runs before the observe (hooks.before.observe), or after the plan, which reads what the observe saw.`);
480
+ if (stage !== "observe" && stage !== "plan" && stage !== "apply" && stage !== "gate")
481
+ throw new Error(`${where}: ${at} is not a stage; a hook runs before the observe, before or after plan or apply, or after the gate.`);
448
482
  if (!Array.isArray(list))
449
483
  throw new Error(`${where}: ${at} must be a list of hooks.`);
450
484
  list.forEach((hook, i) => {
@@ -463,18 +497,39 @@ export function assertOpWaveHooks(where, hooks) {
463
497
  }
464
498
  if (hook.perRun !== undefined && typeof hook.perRun !== "boolean")
465
499
  throw new Error(`${where}: ${which}.perRun must be true or false.`);
500
+ if (hook.targets !== undefined && (!Array.isArray(hook.targets) || hook.targets.length === 0 || !hook.targets.every((t) => typeof t === "string" && t !== ""))) {
501
+ throw new Error(`${where}: ${which}.targets must be a non-empty list of targets or patterns, such as ["prod/*"].`);
502
+ }
466
503
  if (!hook.perRun) {
467
504
  const named = hook.command.find((part) => part.includes("{target}") || part.includes("{plan}"));
468
505
  if (named)
469
506
  throw new Error(`${where}: ${which} runs once for the wave, so it has no {target} or {plan} ("${named}"). Set "perRun": true, or read the runs from {runs}.`);
470
507
  }
471
- else if (side === "before" && stage === "plan" && hook.command.some((part) => part.includes("{plan}"))) {
508
+ else if (side === "before" && (stage === "plan" || stage === "observe") && hook.command.some((part) => part.includes("{plan}"))) {
472
509
  throw new Error(`${where}: ${which} runs before the plan, so it has no {plan} yet.`);
473
510
  }
474
511
  });
475
512
  }
476
513
  }
477
514
  }
515
+ /**
516
+ * The moments a pull-request job runs hooks at (#4288): `pr-plan` and
517
+ * `pr-record` run the observe and plan moments, and `pr-apply` the apply
518
+ * moments too. The pull request's gate has no `after-gate` moment.
519
+ */
520
+ export const PR_LOOP_HOOK_MOMENTS = ["before-observe", "before-plan", "after-plan", "before-apply", "after-apply"];
521
+ /**
522
+ * Refuse `ci.hooks` a pull-request job could not run (#4288): what
523
+ * {@link assertOpWaveHooks} refuses, plus `after.gate` and `apply.runBy`,
524
+ * since chant itself runs a pull request's apply.
525
+ */
526
+ export function assertPrLoopHooks(where, hooks) {
527
+ assertOpWaveHooks(where, hooks);
528
+ if (hooks?.after?.gate !== undefined)
529
+ throw new Error(`${where}: hooks.after.gate runs in an Op waves job only; a pull-request job runs hooks before the observe, before and after the plan, and before and after the apply.`);
530
+ if (hooks?.apply !== undefined)
531
+ throw new Error(`${where}: hooks.apply.runBy is for an Op waves spec; chant runs a pull request's apply, so it runs the apply-stage hooks itself.`);
532
+ }
478
533
  /** The hooks of each moment for wave `name`: the spec's, then the wave's own. */
479
534
  export function opWaveHooksAt(spec, name) {
480
535
  const wave = spec.waves.find((w) => w.name === name);
@@ -0,0 +1,57 @@
1
+ import type { OpConfig } from "./types.js";
2
+ import type { ForgeKind, PrForge, ScheduledRuns } from "../pr-forge.js";
3
+ /** One scheduled watch or reconcile Op the plan job checks. */
4
+ export interface ScheduledWatch {
5
+ op: string;
6
+ cron: string;
7
+ }
8
+ /** An Op the plan note warns about. */
9
+ export interface OverdueOp {
10
+ op: string;
11
+ cron: string;
12
+ /** The time the count starts from: the job's last run, or the pipeline file's first commit (ISO 8601). */
13
+ since: string;
14
+ /** True when no run is on record and `since` is the pipeline file's first commit. */
15
+ neverRan?: boolean;
16
+ /** The line for the job log and the plan note. */
17
+ message: string;
18
+ }
19
+ /**
20
+ * The discovered Ops that are scheduled watch or reconcile Ops: a
21
+ * `schedule`, and the `Watch` label `WatchOp` sets (./composites/watch-op.ts)
22
+ * or the `Reconcile` label `ReconcileOp` sets (./composites/reconcile-op.ts).
23
+ * Sorted by name.
24
+ */
25
+ export declare function scheduledWatches(ops: Iterable<OpConfig>): ScheduledWatch[];
26
+ /**
27
+ * The file `chant run --generate <forge>` writes for an Op, relative to the
28
+ * repository root: `<dir>/<op>.yml` on GitHub and Forgejo, `<member>-<op>.yml`
29
+ * in a workspace member. GitLab puts every Op in one file, so the root's
30
+ * `*.gitlab-ci.yml` files are read for the one whose rules select the Op.
31
+ * Undefined when the checkout has no such file, so an Op that some other
32
+ * scheduler runs is never checked.
33
+ */
34
+ export declare function opPipelineFile(repoRoot: string, forge: ForgeKind, op: string, member?: string): string | undefined;
35
+ /** When `path` was first committed (ISO 8601), if the checkout's history has it. */
36
+ export declare function fileAdded(repoRoot: string, path: string): string | undefined;
37
+ /**
38
+ * Whether one Op is overdue: its cron has fired at least twice since its
39
+ * last run, or since `added` when no run is on record. Undefined when it is
40
+ * on time, or when nothing says when it should have run.
41
+ */
42
+ export declare function overdueVerdict(watch: ScheduledWatch, forge: ForgeKind, runs: ScheduledRuns, added: string | undefined, now: Date): OverdueOp | undefined;
43
+ /**
44
+ * The plan job's check over every scheduled watch or reconcile Op that has a
45
+ * generated pipeline file. Each Op the forge will not answer for is a line
46
+ * in `log` and no verdict. A forge client with no `scheduledRuns` checks
47
+ * nothing.
48
+ */
49
+ export declare function checkOverdueOps(input: {
50
+ watches: readonly ScheduledWatch[];
51
+ forge: PrForge;
52
+ repoRoot: string;
53
+ member?: string;
54
+ now: Date;
55
+ log: (line: string) => void;
56
+ }): Promise<OverdueOp[]>;
57
+ //# sourceMappingURL=schedule-overdue.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-overdue.d.ts","sourceRoot":"","sources":["../../src/op/schedule-overdue.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAExC,OAAO,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAErE,+DAA+D;AAC/D,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uCAAuC;AACvC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,0GAA0G;IAC1G,KAAK,EAAE,MAAM,CAAC;IACd,qFAAqF;IACrF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,kDAAkD;IAClD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,cAAc,EAAE,CAQ1E;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAqBlH;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI5E;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,IAAI,GAAG,SAAS,GAAG,SAAS,CAkBxJ;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE;IAC3C,OAAO,EAAE,SAAS,cAAc,EAAE,CAAC;IACnC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,IAAI,CAAC;IACV,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAqBvB"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Scheduled watch and reconcile Ops that stopped running (#4289).
3
+ *
4
+ * A schedule can stop with no error anywhere. GitHub turns a scheduled
5
+ * workflow off after 60 days with no activity in the repository and drops
6
+ * scheduled runs under load. A GitLab schedule exists only once someone
7
+ * creates it. A watch Op cannot report its own absence, so the pull-request
8
+ * plan job does. For each discovered Op with a `schedule` and the `Watch` or
9
+ * `Reconcile` label, whose generated pipeline file is in the checkout, it
10
+ * asks the forge when the Op's job last ran. When the cron has fired at
11
+ * least twice since then, or since the pipeline file was added for a job
12
+ * that never ran, the job log and the plan note say the Op is overdue.
13
+ *
14
+ * One late or dropped run is not enough: two fires are. A forge that will
15
+ * not answer is a line in the log and no verdict.
16
+ */
17
+ import { spawnSync } from "node:child_process";
18
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { cronFiresUtc } from "./cron.js";
21
+ /**
22
+ * The discovered Ops that are scheduled watch or reconcile Ops: a
23
+ * `schedule`, and the `Watch` label `WatchOp` sets (./composites/watch-op.ts)
24
+ * or the `Reconcile` label `ReconcileOp` sets (./composites/reconcile-op.ts).
25
+ * Sorted by name.
26
+ */
27
+ export function scheduledWatches(ops) {
28
+ const out = [];
29
+ for (const op of ops) {
30
+ if (!op.schedule?.cron)
31
+ continue;
32
+ if (op.labels?.Watch !== "true" && op.labels?.Reconcile !== "true")
33
+ continue;
34
+ out.push({ op: op.name, cron: op.schedule.cron });
35
+ }
36
+ return out.sort((a, b) => a.op.localeCompare(b.op));
37
+ }
38
+ /**
39
+ * The file `chant run --generate <forge>` writes for an Op, relative to the
40
+ * repository root: `<dir>/<op>.yml` on GitHub and Forgejo, `<member>-<op>.yml`
41
+ * in a workspace member. GitLab puts every Op in one file, so the root's
42
+ * `*.gitlab-ci.yml` files are read for the one whose rules select the Op.
43
+ * Undefined when the checkout has no such file, so an Op that some other
44
+ * scheduler runs is never checked.
45
+ */
46
+ export function opPipelineFile(repoRoot, forge, op, member) {
47
+ const base = member ? `${member}-${op}` : op;
48
+ if (forge === "github" || forge === "forgejo") {
49
+ const path = `.${forge}/workflows/${base}.yml`;
50
+ return existsSync(join(repoRoot, path)) ? path : undefined;
51
+ }
52
+ // The gitlab lexicon's cron rule: `$CHANT_SCHEDULED_OP == "<op>"`.
53
+ const selector = `$CHANT_SCHEDULED_OP == "${op}"`;
54
+ let names;
55
+ try {
56
+ names = readdirSync(repoRoot).filter((n) => n.endsWith(".gitlab-ci.yml")).sort();
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ return names.find((n) => {
62
+ try {
63
+ return readFileSync(join(repoRoot, n), "utf8").includes(selector);
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ });
69
+ }
70
+ /** When `path` was first committed (ISO 8601), if the checkout's history has it. */
71
+ export function fileAdded(repoRoot, path) {
72
+ const r = spawnSync("git", ["-C", repoRoot, "log", "--format=%cI", "--diff-filter=A", "--", path], { encoding: "utf-8" });
73
+ if (r.status !== 0)
74
+ return undefined;
75
+ return r.stdout.trim().split("\n").filter(Boolean).pop();
76
+ }
77
+ const HINT = {
78
+ github: "GitHub turns a scheduled workflow off after 60 days with no activity in the repository. Turn the workflow on again under Actions, or run it by hand",
79
+ gitlab: "A GitLab project runs a scheduled Op only from a pipeline schedule. Check its schedule under CI/CD > Schedules",
80
+ forgejo: "Check that Actions is on for the repository and a runner is up, or run the workflow by hand",
81
+ };
82
+ /**
83
+ * Whether one Op is overdue: its cron has fired at least twice since its
84
+ * last run, or since `added` when no run is on record. Undefined when it is
85
+ * on time, or when nothing says when it should have run.
86
+ */
87
+ export function overdueVerdict(watch, forge, runs, added, now) {
88
+ const since = runs.last ?? added;
89
+ if (!since)
90
+ return undefined;
91
+ if (cronFiresUtc(watch.cron, new Date(since), now, 2).length < 2)
92
+ return undefined;
93
+ const day = since.slice(0, 10);
94
+ const what = runs.last ? `its last run was on ${day}` : `no run is on record since its pipeline was added on ${day}`;
95
+ const why = runs.disabled
96
+ ? `GitHub has turned the workflow off (${runs.disabled}). Turn it on again under Actions`
97
+ : forge === "gitlab" && runs.noSchedule
98
+ ? `No active pipeline schedule sets CHANT_SCHEDULED_OP to ${watch.op}. Add one with the cron \`${watch.cron}\` under CI/CD > Schedules`
99
+ : HINT[forge];
100
+ return {
101
+ op: watch.op,
102
+ cron: watch.cron,
103
+ since,
104
+ ...(runs.last ? {} : { neverRan: true }),
105
+ message: `Op \`${watch.op}\` is overdue: its schedule \`${watch.cron}\` has fired at least twice and ${what}. ${why}.`,
106
+ };
107
+ }
108
+ /**
109
+ * The plan job's check over every scheduled watch or reconcile Op that has a
110
+ * generated pipeline file. Each Op the forge will not answer for is a line
111
+ * in `log` and no verdict. A forge client with no `scheduledRuns` checks
112
+ * nothing.
113
+ */
114
+ export async function checkOverdueOps(input) {
115
+ const { forge } = input;
116
+ if (!forge.scheduledRuns)
117
+ return [];
118
+ const out = [];
119
+ for (const watch of input.watches) {
120
+ const file = opPipelineFile(input.repoRoot, forge.kind, watch.op, input.member);
121
+ if (!file)
122
+ continue;
123
+ let runs;
124
+ try {
125
+ runs = await forge.scheduledRuns({ op: watch.op, workflow: file.split("/").pop() });
126
+ }
127
+ catch (err) {
128
+ input.log(`schedule check: ${forge.kind} did not say when Op ${watch.op} last ran, so it is not checked: ${(err instanceof Error ? err.message : String(err)).split("\n")[0]}`);
129
+ continue;
130
+ }
131
+ const found = overdueVerdict(watch, forge.kind, runs, runs.last ? undefined : fileAdded(input.repoRoot, file), input.now);
132
+ if (found) {
133
+ input.log(found.message);
134
+ out.push(found);
135
+ }
136
+ }
137
+ return out;
138
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * The stage-hook runner (#4288): one moment's hooks, with the `targets`,
3
+ * `perRun`, `onFailure` and argv filling a wave job uses. Its own module so
4
+ * a product that runs hooks around its own stages (terragucci) bundles it
5
+ * without the wave loop, the gate or the ledger. `op-waves-run.ts` runs
6
+ * the same hooks inside a wave job.
7
+ */
8
+ import { type OpWaveHook, type OpWaveHookMoment } from "./op-waves.js";
9
+ /** What one moment's hooks came to, before any hold on another gate is settled. */
10
+ export interface HookListOutcome {
11
+ /** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
12
+ failed?: string;
13
+ /** The `onFailure: approve` hooks that failed. */
14
+ holds: string[];
15
+ outcomes: OpWaveHookOutcome[];
16
+ /** The hook calls that held the wave on another gate. */
17
+ otherGates: OtherGateHold[];
18
+ }
19
+ /** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
20
+ export declare const OP_WAVE_PENDING_RESERVED: readonly ["version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume", "environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy"];
21
+ /** What a hook asks for when it holds a wave on another gate (#3821): it writes this to `CHANT_HOLD` and exits non-zero. */
22
+ export interface OpWaveHoldRequest {
23
+ /** The op the gate is recorded under. Default: the spec's name. */
24
+ op?: string;
25
+ gate: string;
26
+ /** The digest an approval binds. */
27
+ digest: string;
28
+ description?: string;
29
+ /** Fields for the pending fact, as a plans file's `pending`. */
30
+ pending?: Record<string, unknown>;
31
+ }
32
+ /** One hook's run (#3762). */
33
+ export interface OpWaveHookOutcome {
34
+ moment: OpWaveHookMoment;
35
+ name: string;
36
+ /** The run, for a `perRun` hook. */
37
+ target?: string;
38
+ exit: number;
39
+ outcome: "passed" | "failed" | "held" | "skipped";
40
+ /** Why the moment was skipped, for `skipped` (#3811). */
41
+ reason?: string;
42
+ }
43
+ /** One run as a hook sees it (#3804). */
44
+ export interface OpWaveHookRun {
45
+ target: string;
46
+ op: string;
47
+ /** The run's plan file, once planned. */
48
+ plan?: string;
49
+ }
50
+ /** Runs one argv; returns its exit code. `env` is added to the job's environment. Injected so tests run no processes. */
51
+ export type OpWaveExec = (argv: string[], cwd: string, env?: Record<string, string>) => number;
52
+ export declare const spawnExec: OpWaveExec;
53
+ /** Fill one argv template. A placeholder whose value is not given is left as it is. */
54
+ export declare function opWaveArgv(template: readonly string[], values: {
55
+ op: string;
56
+ wave: string;
57
+ target?: string;
58
+ plan?: string;
59
+ runs?: string;
60
+ plans?: string;
61
+ }): string[];
62
+ /** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
63
+ export declare function pendingFields(value: unknown, where: string): Record<string, unknown> | undefined;
64
+ /** A hook call that held the wave on another gate (#3821), kept so it can run again once that gate is approved. */
65
+ export interface OtherGateHold {
66
+ label: string;
67
+ name: string;
68
+ moment: OpWaveHookMoment;
69
+ target?: string;
70
+ argv: string[];
71
+ env: Record<string, string>;
72
+ request: OpWaveHoldRequest;
73
+ }
74
+ /** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
75
+ export declare function readHoldRequest(path: string, log: (line: string) => void, label: string): OpWaveHoldRequest | undefined;
76
+ /** Where hooks run: the wave they belong to and the job's tools. */
77
+ export interface HookPlace {
78
+ /** The spec's name and `op`. */
79
+ name: string;
80
+ op: string;
81
+ waveName: string;
82
+ k: number;
83
+ cwd: string;
84
+ exec: OpWaveExec;
85
+ log: (line: string) => void;
86
+ /** Where a hook writes a hold on another gate (`CHANT_HOLD`). Default: under the wave's directory. */
87
+ holdPath?: string;
88
+ }
89
+ /**
90
+ * Run one moment's hooks, in order: each once for the wave, or once per run
91
+ * for a `perRun` hook. A hook that fails stops the rest, unless its
92
+ * `onFailure` is `approve`: then it holds the wave and the hooks after it run.
93
+ */
94
+ export declare function runHookList(at: HookPlace, moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, runs: readonly OpWaveHookRun[], runsPath: string, extraEnv?: Record<string, string>): HookListOutcome;
95
+ /** The options of {@link runStageHooks} (#4288). */
96
+ export interface RunStageHooksOptions {
97
+ /** What the hooks belong to: `CHANT_OP_WAVES`, and the `{op}` of a hook that runs once. */
98
+ name: string;
99
+ op: string;
100
+ /** `{wave}` and `CHANT_WAVE`. */
101
+ wave: string;
102
+ /** `CHANT_WAVE_NUMBER`. Default 0. */
103
+ k?: number;
104
+ /** The runs a `perRun` hook runs for and `targets` match against. */
105
+ runs: readonly OpWaveHookRun[];
106
+ /** `{runs}` and `CHANT_RUNS`, relative to `cwd`; the caller writes it. */
107
+ runsFile: string;
108
+ /** `CHANT_HOLD`, relative to `cwd`. */
109
+ holdFile: string;
110
+ /** Added to every hook's environment. */
111
+ env?: Record<string, string>;
112
+ cwd?: string;
113
+ exec?: OpWaveExec;
114
+ log?: (line: string) => void;
115
+ }
116
+ /** What {@link runStageHooks} gave. */
117
+ export interface StageHooksRun {
118
+ outcomes: OpWaveHookOutcome[];
119
+ /** Set when a hook with `onFailure: fail` failed; the hooks after it did not run. */
120
+ failed?: string;
121
+ /** The `onFailure: approve` hooks that failed. A hold on another gate counts as one here. */
122
+ holds: string[];
123
+ }
124
+ /**
125
+ * Run one moment's hooks outside an Op waves job (#4288): the pull-request
126
+ * jobs, or a product's own stage, with the same `targets`, `perRun`,
127
+ * `onFailure` and argv filling a wave job uses. A hook that holds on another
128
+ * gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
129
+ * caller's.
130
+ */
131
+ export declare function runStageHooks(moment: OpWaveHookMoment, hooks: readonly OpWaveHook[] | undefined, options: RunStageHooksOptions): StageHooksRun;
132
+ //# sourceMappingURL=stage-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-hooks.d.ts","sourceRoot":"","sources":["../../src/op/stage-hooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The stage-hook runner (#4288): one moment's hooks, with the `targets`,
3
+ * `perRun`, `onFailure` and argv filling a wave job uses. Its own module so
4
+ * a product that runs hooks around its own stages (terragucci) bundles it
5
+ * without the wave loop, the gate or the ledger. `op-waves-run.ts` runs
6
+ * the same hooks inside a wave job.
7
+ */
8
+ import { spawnSync } from "node:child_process";
9
+ import { existsSync, readFileSync, rmSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { opWaveHookRuns } from "./op-waves.js";
12
+ /** Pending-fact keys chant writes itself, which a plans file's `pending` may not set (#3821). */
13
+ export const OP_WAVE_PENDING_RESERVED = [
14
+ "version", "kind", "op", "gate", "timestamp", "expiresAt", "planDigest", "runId", "url", "resume",
15
+ "environment", "origin", "neverOverMcp", "approval", "approvalMode", "answer", "wave", "heldBy",
16
+ ];
17
+ export const spawnExec = (argv, cwd, env) => {
18
+ const result = spawnSync(argv[0], argv.slice(1), { cwd, stdio: "inherit", ...(env ? { env: { ...process.env, ...env } } : {}) });
19
+ if (result.error)
20
+ throw result.error;
21
+ return result.status ?? 1;
22
+ };
23
+ /** Fill one argv template. A placeholder whose value is not given is left as it is. */
24
+ export function opWaveArgv(template, values) {
25
+ const pairs = [
26
+ ["{op}", values.op],
27
+ ["{target}", values.target],
28
+ ["{wave}", values.wave],
29
+ ["{plans}", values.plans],
30
+ ["{plan}", values.plan],
31
+ ["{runs}", values.runs],
32
+ ];
33
+ return template.map((part) => pairs.reduce((out, [key, value]) => (value === undefined ? out : out.replaceAll(key, value)), part));
34
+ }
35
+ /** A plans file's `pending` (#3821): an object whose keys chant does not write itself, `description` aside. Undefined when absent or empty. */
36
+ export function pendingFields(value, where) {
37
+ if (value === undefined)
38
+ return undefined;
39
+ if (value === null || typeof value !== "object" || Array.isArray(value))
40
+ throw new Error(`${where}: \`pending\` must be an object of fields for the pending fact`);
41
+ const reserved = Object.keys(value).filter((key) => OP_WAVE_PENDING_RESERVED.includes(key));
42
+ if (reserved.length > 0)
43
+ throw new Error(`${where}: \`pending\` sets ${reserved.join(", ")}, which chant writes itself`);
44
+ if ("description" in value && typeof value.description !== "string")
45
+ throw new Error(`${where}: pending.description must be a string`);
46
+ return Object.keys(value).length > 0 ? value : undefined;
47
+ }
48
+ /** Read the hold file a hook wrote (#3821), or undefined when it wrote none or one that names no gate and digest. */
49
+ export function readHoldRequest(path, log, label) {
50
+ if (!existsSync(path))
51
+ return undefined;
52
+ try {
53
+ const r = JSON.parse(readFileSync(path, "utf-8"));
54
+ if (typeof r.gate !== "string" || r.gate === "" || typeof r.digest !== "string" || r.digest === "")
55
+ throw new Error("it names no gate and digest");
56
+ if (r.op !== undefined && (typeof r.op !== "string" || r.op === ""))
57
+ throw new Error("its op is not a name");
58
+ return { gate: r.gate, digest: r.digest, ...(r.op ? { op: r.op } : {}), ...(typeof r.description === "string" ? { description: r.description } : {}), ...(r.pending ? { pending: pendingFields(r.pending, `the hold ${label} wrote`) ?? {} } : {}) };
59
+ }
60
+ catch (err) {
61
+ log(`${label} wrote a hold file that cannot be read (${err instanceof Error ? err.message : String(err)}), so it holds the wave on its set digest`);
62
+ return undefined;
63
+ }
64
+ }
65
+ /**
66
+ * Run one moment's hooks, in order: each once for the wave, or once per run
67
+ * for a `perRun` hook. A hook that fails stops the rest, unless its
68
+ * `onFailure` is `approve`: then it holds the wave and the hooks after it run.
69
+ */
70
+ export function runHookList(at, moment, hooks, runs, runsPath, extraEnv = {}) {
71
+ const out = { holds: [], outcomes: [], otherGates: [] };
72
+ if (!hooks || hooks.length === 0)
73
+ return out;
74
+ const { name: specName, op, waveName, k, cwd, exec, log } = at;
75
+ // #3821: a hook that holds the wave on another gate names it in this file.
76
+ const holdPath = at.holdPath ?? `.chant/op-waves/${specName}/wave-${k}/hold.json`;
77
+ const env = { CHANT_OP_WAVES: specName, CHANT_WAVE: waveName, CHANT_WAVE_NUMBER: String(k), CHANT_HOOK: moment, CHANT_RUNS: runsPath, CHANT_HOLD: holdPath, ...extraEnv };
78
+ for (const hook of hooks) {
79
+ const name = hook.name ?? (hook.command.join(" ").length > 60 ? `${hook.command.join(" ").slice(0, 57)}...` : hook.command.join(" "));
80
+ // #4288: a hook with `targets` is for the runs they match. Once for the wave, it runs when one matches or no run is known yet.
81
+ const mine = opWaveHookRuns(hook, runs);
82
+ if (hook.targets && (hook.perRun ? mine.length === 0 : mine.length === 0 && runs.length > 0)) {
83
+ log(`${moment} hook ${name}: no run matches its targets (${hook.targets.join(", ")}), so it did not run`);
84
+ continue;
85
+ }
86
+ const calls = hook.perRun
87
+ ? mine.map((run) => ({
88
+ target: run.target,
89
+ argv: opWaveArgv(hook.command, { op: run.op, wave: waveName, target: run.target, runs: runsPath, ...(run.plan ? { plan: run.plan } : {}) }),
90
+ env: { ...env, CHANT_TARGET: run.target, ...(run.plan ? { CHANT_PLAN: run.plan } : {}) },
91
+ }))
92
+ : [{ target: undefined, argv: opWaveArgv(hook.command, { op, wave: waveName, runs: runsPath }), env: env }];
93
+ for (const call of calls) {
94
+ const label = `${moment} hook ${name}${call.target ? ` (${call.target})` : ""}`;
95
+ let code;
96
+ rmSync(join(cwd, holdPath), { force: true });
97
+ try {
98
+ code = exec(call.argv, cwd, call.env);
99
+ }
100
+ catch (err) {
101
+ log(`${label} did not run: ${err instanceof Error ? err.message : String(err)}`);
102
+ code = 127;
103
+ }
104
+ const base = { moment, name, ...(call.target ? { target: call.target } : {}), exit: code };
105
+ if (code === 0) {
106
+ out.outcomes.push({ ...base, outcome: "passed" });
107
+ continue;
108
+ }
109
+ if (hook.onFailure === "approve") {
110
+ out.outcomes.push({ ...base, outcome: "held" });
111
+ const request = readHoldRequest(join(cwd, holdPath), log, label);
112
+ if (request) {
113
+ out.otherGates.push({ label, name, moment, ...(call.target ? { target: call.target } : {}), argv: call.argv, env: call.env, request });
114
+ log(`${label} exited ${code} and holds wave ${k} (${waveName}) on gate ${request.op ?? specName} / ${request.gate}, digest ${request.digest}`);
115
+ continue;
116
+ }
117
+ out.holds.push(label);
118
+ log(`${label} exited ${code}, so wave ${k} (${waveName}) waits for an approval of its set digest`);
119
+ continue;
120
+ }
121
+ out.outcomes.push({ ...base, outcome: "failed" });
122
+ out.failed = `${label} exited ${code}`;
123
+ log(out.failed);
124
+ return out;
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ /**
130
+ * Run one moment's hooks outside an Op waves job (#4288): the pull-request
131
+ * jobs, or a product's own stage, with the same `targets`, `perRun`,
132
+ * `onFailure` and argv filling a wave job uses. A hook that holds on another
133
+ * gate (`CHANT_HOLD`) is counted as a plain hold; settling that gate is the
134
+ * caller's.
135
+ */
136
+ export function runStageHooks(moment, hooks, options) {
137
+ const cwd = options.cwd ?? process.cwd();
138
+ const log = options.log ?? ((line) => console.error(line));
139
+ const place = { name: options.name, op: options.op, waveName: options.wave, k: options.k ?? 0, cwd, exec: options.exec ?? spawnExec, log, holdPath: options.holdFile };
140
+ const out = runHookList(place, moment, hooks, options.runs, options.runsFile, options.env);
141
+ return { outcomes: out.outcomes, ...(out.failed ? { failed: out.failed } : {}), holds: [...out.holds, ...out.otherGates.map((g) => `${g.label} (on ${g.request.op ?? options.name} / ${g.request.gate})`)] };
142
+ }
@@ -32,12 +32,12 @@
32
32
  * nothing; `override-gate-bundle.test.ts` holds its import graph to the
33
33
  * same bundle. chant's own writers are in `./lifecycle/override-ledger.ts`.
34
34
  */
35
- import { type GateSigners } from "./workspace/trust/gate-seal.js";
35
+ import { type GateSigners } from "./lifecycle/gate-seal.js";
36
36
  import type { GateResolutionRecord, PendingGateRecord } from "./lifecycle/gate-ledger.js";
37
37
  import type { GateApprovalSource } from "./op/op-waves.js";
38
38
  import type { GateAuditApply } from "./lifecycle/gate-audit.js";
39
39
  export type { GateResolutionRecord, PendingGateRecord } from "./lifecycle/gate-ledger.js";
40
- export type { GateSigners } from "./workspace/trust/gate-seal.js";
40
+ export type { GateSigners } from "./lifecycle/gate-seal.js";
41
41
  /** The op every override and denial is recorded under. `./lifecycle/gate-audit.ts` reads this op as overrides by default. */
42
42
  export declare const OVERRIDE_OP = "policy-override";
43
43
  /** The ledger file on `chant/lifecycle` that holds denials and overrides. */