@intentius/chant 0.124.28 → 0.124.30

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 (449) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +18 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +67 -8
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +91 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +73 -2
  41. package/dist/cli/registry.d.ts +32 -4
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +49 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +38 -4
  67. package/dist/components/pr-run.d.ts +20 -2
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +26 -13
  70. package/dist/components/promote.d.ts +23 -0
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +60 -2
  73. package/dist/components/release-compile.d.ts +9 -0
  74. package/dist/components/release-compile.d.ts.map +1 -1
  75. package/dist/components/release-compile.js +9 -3
  76. package/dist/components/release-gate.d.ts +11 -1
  77. package/dist/components/release-gate.d.ts.map +1 -1
  78. package/dist/components/release-gate.js +10 -4
  79. package/dist/components/wave-plan.d.ts +16 -0
  80. package/dist/components/wave-plan.d.ts.map +1 -1
  81. package/dist/components/wave-plan.js +36 -2
  82. package/dist/config.d.ts +38 -0
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +2 -0
  85. package/dist/flag-params.d.ts +54 -0
  86. package/dist/flag-params.d.ts.map +1 -0
  87. package/dist/flag-params.js +130 -0
  88. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  89. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  90. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  91. package/dist/lifecycle/change-set-store.d.ts +2 -1
  92. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  93. package/dist/lifecycle/change-set-store.js +2 -1
  94. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  95. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  96. package/dist/lifecycle/delivery-metrics.js +145 -0
  97. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  98. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  99. package/dist/lifecycle/deploy-outcome.js +101 -0
  100. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  101. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  102. package/dist/lifecycle/gate-change-set.js +35 -3
  103. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +30 -1
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/git.js +31 -5
  108. package/dist/lifecycle/index.d.ts +4 -0
  109. package/dist/lifecycle/index.d.ts.map +1 -1
  110. package/dist/lifecycle/index.js +4 -0
  111. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  112. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  113. package/dist/lifecycle/mixed-environment.js +154 -0
  114. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  115. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-attachments.js +179 -0
  117. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  118. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-credentials.js +69 -0
  120. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  121. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-declaration.js +83 -0
  123. package/dist/lifecycle/publish-forge.d.ts +53 -0
  124. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-forge.js +298 -0
  126. package/dist/lifecycle/publish-npm.d.ts +61 -0
  127. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-npm.js +242 -0
  129. package/dist/lifecycle/publish-oci.d.ts +64 -0
  130. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-oci.js +383 -0
  132. package/dist/lifecycle/publish-record.d.ts +66 -0
  133. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-record.js +84 -0
  135. package/dist/lifecycle/publish-targets.d.ts +45 -0
  136. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  137. package/dist/lifecycle/publish-targets.js +85 -0
  138. package/dist/lifecycle/release-cut.d.ts +48 -1
  139. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  140. package/dist/lifecycle/release-cut.js +116 -23
  141. package/dist/lifecycle/release-join.d.ts +95 -0
  142. package/dist/lifecycle/release-join.d.ts.map +1 -0
  143. package/dist/lifecycle/release-join.js +139 -0
  144. package/dist/lifecycle/release-ledger.d.ts +8 -3
  145. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  146. package/dist/lifecycle/release-ledger.js +2 -0
  147. package/dist/lifecycle/release-publish.d.ts +312 -0
  148. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  149. package/dist/lifecycle/release-publish.js +335 -0
  150. package/dist/lifecycle/release-revocation.d.ts +80 -0
  151. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  152. package/dist/lifecycle/release-revocation.js +123 -0
  153. package/dist/lifecycle/release-store.d.ts +12 -2
  154. package/dist/lifecycle/release-store.d.ts.map +1 -1
  155. package/dist/lifecycle/release-store.js +4 -2
  156. package/dist/lifecycle/release-verify.d.ts +61 -0
  157. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  158. package/dist/lifecycle/release-verify.js +92 -0
  159. package/dist/lifecycle/release-workstation.d.ts +91 -0
  160. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  161. package/dist/lifecycle/release-workstation.js +180 -0
  162. package/dist/lifecycle/retention-policy.d.ts +45 -0
  163. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  164. package/dist/lifecycle/retention-policy.js +43 -0
  165. package/dist/lifecycle/retention.d.ts +185 -0
  166. package/dist/lifecycle/retention.d.ts.map +1 -0
  167. package/dist/lifecycle/retention.js +400 -0
  168. package/dist/lifecycle/run-ledger.d.ts +1 -1
  169. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/run-ledger.js +9 -0
  171. package/dist/op/activities/activity-contracts.d.ts +1 -0
  172. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  173. package/dist/op/activities/activity-contracts.js +1 -0
  174. package/dist/op/activities/shell.d.ts +6 -0
  175. package/dist/op/activities/shell.d.ts.map +1 -1
  176. package/dist/op/activities/shell.js +1 -1
  177. package/dist/op/local-executor.d.ts +23 -1
  178. package/dist/op/local-executor.d.ts.map +1 -1
  179. package/dist/op/local-executor.js +9 -3
  180. package/dist/op/runtime.d.ts +27 -0
  181. package/dist/op/runtime.d.ts.map +1 -1
  182. package/dist/op/runtime.js +20 -0
  183. package/dist/plan-summary.d.ts.map +1 -1
  184. package/dist/plan-summary.js +8 -2
  185. package/dist/pr-forge.d.ts +23 -0
  186. package/dist/pr-forge.d.ts.map +1 -1
  187. package/dist/pr-forge.js +44 -0
  188. package/dist/pr-loop.d.ts +17 -0
  189. package/dist/pr-loop.d.ts.map +1 -1
  190. package/dist/pr-loop.js +30 -4
  191. package/dist/pr-report-url.d.ts +82 -0
  192. package/dist/pr-report-url.d.ts.map +1 -0
  193. package/dist/pr-report-url.js +174 -0
  194. package/dist/provenance.d.ts +2 -2
  195. package/dist/provenance.d.ts.map +1 -1
  196. package/dist/run-event-store.d.ts +29 -2
  197. package/dist/run-event-store.d.ts.map +1 -1
  198. package/dist/run-event-store.js +50 -3
  199. package/dist/run-event-stream.d.ts +4 -3
  200. package/dist/run-event-stream.d.ts.map +1 -1
  201. package/dist/run-event-stream.js +7 -3
  202. package/dist/run-event.d.ts +34 -1
  203. package/dist/run-event.d.ts.map +1 -1
  204. package/dist/run-event.js +4 -0
  205. package/dist/wave-runtime.mjs +260 -229
  206. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  207. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  208. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  209. package/dist/workspace/change-set.schema.json +5 -1
  210. package/dist/workspace/check.schema.json +4 -2
  211. package/dist/workspace/checks/records.d.ts +9 -1
  212. package/dist/workspace/checks/records.d.ts.map +1 -1
  213. package/dist/workspace/checks/records.js +35 -1
  214. package/dist/workspace/checks.d.ts +1 -0
  215. package/dist/workspace/checks.d.ts.map +1 -1
  216. package/dist/workspace/checks.js +2 -1
  217. package/dist/workspace/ci-green-forge.d.ts +2 -0
  218. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  219. package/dist/workspace/ci-green.d.ts +10 -0
  220. package/dist/workspace/ci-green.d.ts.map +1 -1
  221. package/dist/workspace/ci-green.js +14 -5
  222. package/dist/workspace/ci-run.d.ts +222 -0
  223. package/dist/workspace/ci-run.d.ts.map +1 -0
  224. package/dist/workspace/ci-run.js +464 -0
  225. package/dist/workspace/ci-serve.d.ts +214 -0
  226. package/dist/workspace/ci-serve.d.ts.map +1 -0
  227. package/dist/workspace/ci-serve.js +412 -0
  228. package/dist/workspace/declaration.d.ts +16 -0
  229. package/dist/workspace/declaration.d.ts.map +1 -1
  230. package/dist/workspace/declaration.js +26 -0
  231. package/dist/workspace/declaration.schema.json +64 -0
  232. package/dist/workspace/declared-kinds.d.ts +16 -0
  233. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  234. package/dist/workspace/declared-kinds.js +25 -0
  235. package/dist/workspace/delivery-metrics.d.ts +91 -0
  236. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  237. package/dist/workspace/delivery-metrics.js +217 -0
  238. package/dist/workspace/delivery-metrics.schema.json +519 -0
  239. package/dist/workspace/flags-read.d.ts +37 -0
  240. package/dist/workspace/flags-read.d.ts.map +1 -0
  241. package/dist/workspace/flags-read.js +103 -0
  242. package/dist/workspace/flags.d.ts +100 -0
  243. package/dist/workspace/flags.d.ts.map +1 -0
  244. package/dist/workspace/flags.js +98 -0
  245. package/dist/workspace/graph-cli.js +1 -1
  246. package/dist/workspace/intent-cli.d.ts +11 -2
  247. package/dist/workspace/intent-cli.d.ts.map +1 -1
  248. package/dist/workspace/intent-cli.js +54 -5
  249. package/dist/workspace/intent-trace.d.ts +219 -0
  250. package/dist/workspace/intent-trace.d.ts.map +1 -0
  251. package/dist/workspace/intent-trace.js +314 -0
  252. package/dist/workspace/intent.d.ts +30 -6
  253. package/dist/workspace/intent.d.ts.map +1 -1
  254. package/dist/workspace/intent.js +141 -21
  255. package/dist/workspace/intent.schema.json +702 -6
  256. package/dist/workspace/overlap.d.ts +146 -0
  257. package/dist/workspace/overlap.d.ts.map +1 -0
  258. package/dist/workspace/overlap.js +276 -0
  259. package/dist/workspace/pr-report.schema.json +5 -0
  260. package/dist/workspace/reason-codes.d.ts +2 -0
  261. package/dist/workspace/reason-codes.d.ts.map +1 -1
  262. package/dist/workspace/reason-codes.js +3 -0
  263. package/dist/workspace/records.d.ts +15 -2
  264. package/dist/workspace/records.d.ts.map +1 -1
  265. package/dist/workspace/records.js +32 -0
  266. package/dist/workspace/records.schema.json +21 -2
  267. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  268. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  269. package/dist/workspace/releases.d.ts +9 -0
  270. package/dist/workspace/releases.d.ts.map +1 -1
  271. package/dist/workspace/releases.js +9 -0
  272. package/dist/workspace/releases.schema.json +123 -1
  273. package/dist/workspace/run-event.schema.json +24 -6
  274. package/dist/workspace/status-gates.d.ts +8 -0
  275. package/dist/workspace/status-gates.d.ts.map +1 -1
  276. package/dist/workspace/status-stewards.d.ts +2 -0
  277. package/dist/workspace/status-stewards.d.ts.map +1 -1
  278. package/dist/workspace/status-stewards.js +1 -0
  279. package/dist/workspace/status.d.ts +78 -0
  280. package/dist/workspace/status.d.ts.map +1 -1
  281. package/dist/workspace/status.js +170 -3
  282. package/dist/workspace/status.schema.json +210 -2
  283. package/dist/workspace/wip.d.ts.map +1 -1
  284. package/dist/workspace/wip.js +11 -3
  285. package/package.json +1 -1
  286. package/src/build-params.test.ts +46 -0
  287. package/src/build-params.ts +55 -1
  288. package/src/change-set-graph.ts +10 -2
  289. package/src/change-set.ts +14 -3
  290. package/src/cli/build-params-cli.ts +22 -9
  291. package/src/cli/commands/build.ts +6 -3
  292. package/src/cli/handlers/approve-release.test.ts +5 -1
  293. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  294. package/src/cli/handlers/approve.ts +47 -0
  295. package/src/cli/handlers/build.test.ts +30 -0
  296. package/src/cli/handlers/build.ts +20 -4
  297. package/src/cli/handlers/fan-out.test.ts +262 -1
  298. package/src/cli/handlers/fan-out.ts +72 -8
  299. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  300. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  301. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  302. package/src/cli/handlers/pr.test.ts +148 -1
  303. package/src/cli/handlers/pr.ts +144 -28
  304. package/src/cli/handlers/promote.test.ts +280 -2
  305. package/src/cli/handlers/promote.ts +210 -34
  306. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  307. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  308. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  309. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  310. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  311. package/src/cli/handlers/release.e2e.test.ts +146 -32
  312. package/src/cli/handlers/release.ts +415 -28
  313. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  314. package/src/cli/handlers/run-release.test.ts +55 -0
  315. package/src/cli/handlers/run.ts +69 -5
  316. package/src/cli/main.test.ts +1 -0
  317. package/src/cli/main.ts +91 -10
  318. package/src/cli/parse-args.ts +54 -2
  319. package/src/cli/registry.ts +32 -4
  320. package/src/cli/run-events.ts +5 -2
  321. package/src/components/auto-release.test.ts +9 -0
  322. package/src/components/auto-release.ts +3 -0
  323. package/src/components/capability.ts +9 -0
  324. package/src/components/driver.test.ts +24 -0
  325. package/src/components/driver.ts +36 -0
  326. package/src/components/fan-out-inputs.test.ts +85 -0
  327. package/src/components/fan-out-inputs.ts +192 -0
  328. package/src/components/fan-out-record.test.ts +10 -0
  329. package/src/components/fan-out-record.ts +19 -0
  330. package/src/components/fan-out-run.ts +48 -0
  331. package/src/components/fan-out.ts +4 -0
  332. package/src/components/pr-pipeline.ts +67 -4
  333. package/src/components/pr-run.test.ts +127 -0
  334. package/src/components/pr-run.ts +32 -12
  335. package/src/components/promote.ts +68 -2
  336. package/src/components/release-compile.ts +17 -3
  337. package/src/components/release-gate.ts +18 -4
  338. package/src/components/wave-plan.ts +45 -2
  339. package/src/config.ts +8 -0
  340. package/src/flag-params.test.ts +144 -0
  341. package/src/flag-params.ts +134 -0
  342. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  343. package/src/lifecycle/change-set-store.ts +2 -1
  344. package/src/lifecycle/delivery-metrics.ts +263 -0
  345. package/src/lifecycle/deploy-outcome.ts +147 -0
  346. package/src/lifecycle/gate-change-set.ts +37 -6
  347. package/src/lifecycle/gate-ledger.ts +2 -2
  348. package/src/lifecycle/git.ts +61 -8
  349. package/src/lifecycle/index.ts +4 -0
  350. package/src/lifecycle/mixed-environment.test.ts +97 -0
  351. package/src/lifecycle/mixed-environment.ts +204 -0
  352. package/src/lifecycle/publish-attachments.ts +196 -0
  353. package/src/lifecycle/publish-credentials.ts +84 -0
  354. package/src/lifecycle/publish-declaration.ts +95 -0
  355. package/src/lifecycle/publish-forge.ts +325 -0
  356. package/src/lifecycle/publish-npm.ts +259 -0
  357. package/src/lifecycle/publish-oci.ts +396 -0
  358. package/src/lifecycle/publish-record.ts +113 -0
  359. package/src/lifecycle/publish-resume.test.ts +168 -0
  360. package/src/lifecycle/publish-targets.test.ts +494 -0
  361. package/src/lifecycle/publish-targets.ts +100 -0
  362. package/src/lifecycle/release-cut.test.ts +74 -2
  363. package/src/lifecycle/release-cut.ts +158 -22
  364. package/src/lifecycle/release-flag.test.ts +112 -0
  365. package/src/lifecycle/release-join.test.ts +68 -0
  366. package/src/lifecycle/release-join.ts +182 -0
  367. package/src/lifecycle/release-ledger.ts +9 -3
  368. package/src/lifecycle/release-publish.test.ts +225 -0
  369. package/src/lifecycle/release-publish.ts +568 -0
  370. package/src/lifecycle/release-revocation.ts +147 -0
  371. package/src/lifecycle/release-store.ts +15 -4
  372. package/src/lifecycle/release-verify.test.ts +110 -0
  373. package/src/lifecycle/release-verify.ts +120 -0
  374. package/src/lifecycle/release-workstation.test.ts +204 -0
  375. package/src/lifecycle/release-workstation.ts +216 -0
  376. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  377. package/src/lifecycle/retention-policy.ts +63 -0
  378. package/src/lifecycle/retention.test.ts +363 -0
  379. package/src/lifecycle/retention.ts +508 -0
  380. package/src/lifecycle/run-ledger.ts +7 -1
  381. package/src/op/activities/activity-contracts.ts +1 -0
  382. package/src/op/activities/shell.ts +7 -1
  383. package/src/op/local-executor.ts +27 -4
  384. package/src/op/runtime.ts +42 -0
  385. package/src/plan-summary.test.ts +20 -0
  386. package/src/plan-summary.ts +8 -1
  387. package/src/pr-forge.test.ts +25 -0
  388. package/src/pr-forge.ts +62 -0
  389. package/src/pr-loop.test.ts +13 -1
  390. package/src/pr-loop.ts +41 -4
  391. package/src/pr-report-url.test.ts +131 -0
  392. package/src/pr-report-url.ts +209 -0
  393. package/src/provenance.ts +2 -2
  394. package/src/run-event-store.ts +63 -3
  395. package/src/run-event-stream.ts +8 -4
  396. package/src/run-event.ts +38 -1
  397. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  398. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  399. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  400. package/src/workspace/change-set.schema.json +5 -1
  401. package/src/workspace/check.schema.json +4 -2
  402. package/src/workspace/checks/records.ts +35 -1
  403. package/src/workspace/checks.ts +2 -1
  404. package/src/workspace/ci-green-forge.ts +2 -0
  405. package/src/workspace/ci-green.ts +20 -5
  406. package/src/workspace/ci-run.test.ts +416 -0
  407. package/src/workspace/ci-run.ts +571 -0
  408. package/src/workspace/ci-serve.test.ts +268 -0
  409. package/src/workspace/ci-serve.ts +521 -0
  410. package/src/workspace/declaration.schema.json +64 -0
  411. package/src/workspace/declaration.test.ts +28 -0
  412. package/src/workspace/declaration.ts +38 -0
  413. package/src/workspace/declared-kinds.ts +40 -1
  414. package/src/workspace/delivery-metrics.schema.json +519 -0
  415. package/src/workspace/delivery-metrics.test.ts +236 -0
  416. package/src/workspace/delivery-metrics.ts +286 -0
  417. package/src/workspace/flags-read.ts +121 -0
  418. package/src/workspace/flags.test.ts +141 -0
  419. package/src/workspace/flags.ts +157 -0
  420. package/src/workspace/graph-cli.ts +1 -1
  421. package/src/workspace/intent-cli.ts +46 -5
  422. package/src/workspace/intent-trace.test.ts +173 -0
  423. package/src/workspace/intent-trace.ts +484 -0
  424. package/src/workspace/intent.schema.json +702 -6
  425. package/src/workspace/intent.ts +148 -25
  426. package/src/workspace/older-records.test.ts +412 -0
  427. package/src/workspace/overlap.test.ts +165 -0
  428. package/src/workspace/overlap.ts +340 -0
  429. package/src/workspace/pr-report.schema.json +5 -0
  430. package/src/workspace/read-contract.e2e.test.ts +2 -1
  431. package/src/workspace/reason-codes.test.ts +5 -0
  432. package/src/workspace/reason-codes.ts +3 -0
  433. package/src/workspace/record-assets.test.ts +2 -1
  434. package/src/workspace/records-contract.e2e.test.ts +2 -1
  435. package/src/workspace/records.schema.json +21 -2
  436. package/src/workspace/records.ts +36 -2
  437. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  438. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  439. package/src/workspace/releases.schema.json +123 -1
  440. package/src/workspace/releases.ts +11 -0
  441. package/src/workspace/run-event.schema.json +24 -6
  442. package/src/workspace/status-contract.test.ts +29 -0
  443. package/src/workspace/status-gates.ts +8 -0
  444. package/src/workspace/status-mixed.test.ts +152 -0
  445. package/src/workspace/status-revoked.test.ts +153 -0
  446. package/src/workspace/status-stewards.ts +3 -0
  447. package/src/workspace/status.schema.json +210 -2
  448. package/src/workspace/status.ts +254 -3
  449. package/src/workspace/wip.ts +11 -2
@@ -0,0 +1,521 @@
1
+ /**
2
+ * `chant ci serve` (#4095, epic #4101): the chant runner's trigger loop on one
3
+ * host. It polls the forge for the open pull requests into the target branch
4
+ * and for the branch's head, and runs the pull-request jobs with
5
+ * {@link ciRun} (./ci-run.ts): `plan` for each new pull request head, and
6
+ * `apply` for each new commit on the target branch, measured from the head
7
+ * the previous poll saw (the push's `before`).
8
+ *
9
+ * - Dedupe. A state file in the state directory keeps every job it started,
10
+ * keyed by job and commit (`plan:<pr>:<sha>`, `apply:<sha>`), with how it
11
+ * ended, and the target head the last apply was measured to. A job already
12
+ * in it is not started again, unless it failed and `rerunFailed` asks for
13
+ * it: the forge's "re-run failed jobs".
14
+ * - One apply per group. An apply takes the git-ref lease (../lifecycle/lease.ts,
15
+ * the one `chant operator` holds) named for `prApplyGroup`, renews it while
16
+ * it runs and releases it after. An apply that finds it held waits for a
17
+ * later poll, as a forge's concurrency group queues it.
18
+ * - Resume records. Each apply gets `<state>/resume/<prApplyRecordKey>.json`
19
+ * as its `--resume` record, so a re-run of a failed apply at the same
20
+ * commit finishes under the approval it has. This replaces the forge's CI
21
+ * cache.
22
+ * - Capacity. At most `maxJobs` jobs run at once. A job over the limit is not
23
+ * recorded, so a later poll starts it.
24
+ *
25
+ * Host mode only: each job runs in a fresh worktree on this host as this
26
+ * user, with the clean environment `ciRun` gives it. A pull request from a
27
+ * fork is never planned, and with `authors` only those people's pull requests
28
+ * are. Container isolation is #4015.
29
+ *
30
+ * A webhook is not here yet. {@link ciServeSchedule} takes triggers, and
31
+ * {@link ciServeObserve} is the poll that makes them; a webhook handler would
32
+ * turn a checked payload into the same triggers and schedule them.
33
+ */
34
+
35
+ import { execFileSync } from "node:child_process";
36
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
37
+ import { isAbsolute, join, resolve } from "node:path";
38
+ import { prApplyGroup, prApplyRecordKey } from "../components/pr-pipeline";
39
+ import { forgeFromEnv, type OpenPullRequest } from "../pr-forge";
40
+ import type { CommandContext } from "../cli/registry";
41
+ import { formatError, formatWarning } from "../cli/format";
42
+ import { acquireLease, currentHolderId, releaseLease, DEFAULT_LEASE_TTL_MS, type LeaseRecord } from "../lifecycle/lease";
43
+ import { parseDuration } from "../op/duration";
44
+ import { CI_JOB_OPS, ciJobPlace, ciRun, CiRunError, type CiJobName, type CiRunRequest, type CiRunResult } from "./ci-run";
45
+ import { RUNNER_KEY_ENV } from "./ci-green";
46
+ import { gitTop } from "./tree";
47
+
48
+ /** The forges `ci serve` polls. GitLab is later. */
49
+ export type CiServeForgeKind = "github" | "forgejo";
50
+
51
+ /** What `ci serve` reads from the forge: the two calls GitHub's and Forgejo's clients have (../pr-forge.ts). */
52
+ export interface CiServeForge {
53
+ openPullRequests(base: string): Promise<OpenPullRequest[]>;
54
+ branchHead(branch: string): Promise<string | undefined>;
55
+ }
56
+
57
+ /** A job the forge's state calls for: the plan of a pull request's head, or the apply of a target-branch commit. */
58
+ export type CiServeTrigger =
59
+ | { job: "plan"; commit: string; base: string; pr: number; rerun?: boolean }
60
+ | { job: "apply"; commit: string; base: string; rerun?: boolean };
61
+
62
+ /** One job in the state file. */
63
+ export interface CiServeJobState {
64
+ job: CiJobName;
65
+ commit: string;
66
+ base: string;
67
+ pr?: number;
68
+ /** `running` while it runs; a `running` entry no live process holds is taken as failed. */
69
+ status: "running" | "ok" | "fail";
70
+ runId: string;
71
+ attempts: number;
72
+ startedAt: string;
73
+ settledAt?: string;
74
+ /** Why it failed, in a line. */
75
+ failure?: string;
76
+ /** Where its report was copied. */
77
+ report?: string;
78
+ }
79
+
80
+ /** The state file, `<state>/<prApplyGroup>.json`. */
81
+ export interface CiServeState {
82
+ schema: 1;
83
+ forge: CiServeForgeKind;
84
+ repo: string;
85
+ env: string;
86
+ branch: string;
87
+ member?: string;
88
+ /** The target branch's head the last apply was measured to, or the head the first poll found. */
89
+ target?: string;
90
+ jobs: Record<string, CiServeJobState>;
91
+ }
92
+
93
+ /** What a pass did, in order, for the log. */
94
+ export type CiServeEvent =
95
+ | { type: "baseline"; commit: string }
96
+ | { type: "no-branch"; branch: string }
97
+ | { type: "skipped"; pr: number; reason: "fork" | "author"; author?: string }
98
+ | { type: "held"; job: CiJobName; commit: string; pr?: number; reason: "capacity" | "lease"; heldBy?: string }
99
+ | { type: "started"; job: CiJobName; commit: string; pr?: number; base: string; runId: string; attempt: number; resume?: string }
100
+ | { type: "settled"; job: CiJobName; commit: string; pr?: number; status: "ok" | "fail"; failure?: string; report?: string }
101
+ | { type: "error"; message: string };
102
+
103
+ /** The `ciRun` request fields `ci serve` passes through to every job: the generator's options and the runner's. */
104
+ export type CiServeJobOptions = Omit<CiRunRequest, "cwd" | "job" | "env" | "forge" | "base" | "pr" | "commit" | "output" | "resumeRecord" | "runId">;
105
+
106
+ export interface CiServeOptions {
107
+ /** The host's clone, or a workspace member's directory in it. */
108
+ cwd: string;
109
+ forge: CiServeForgeKind;
110
+ /** `owner/name`. */
111
+ repo: string;
112
+ env: string;
113
+ client: CiServeForge;
114
+ /** Default `<git common dir>/chant/ci-serve`. */
115
+ stateDir?: string;
116
+ /** Default 1. */
117
+ maxJobs?: number;
118
+ /** Plan only pull requests these logins opened, without case. Default anyone with a branch in the repository. */
119
+ authors?: readonly string[];
120
+ /** Run again the failed apply at the target's head, and the failed plans of open heads. */
121
+ rerunFailed?: boolean;
122
+ /** Passed to each job. `job.hostEnv` gets `GITHUB_REPOSITORY` set to `repo`. */
123
+ job?: CiServeJobOptions;
124
+ /** This process's lease identity. Default `currentHolderId()`. */
125
+ holder?: string;
126
+ leaseTtlMs?: number;
127
+ log?: (event: CiServeEvent) => void;
128
+ now?: () => Date;
129
+ /** Runs a job. Default {@link ciRun}. */
130
+ run?: (req: CiRunRequest) => Promise<CiRunResult>;
131
+ }
132
+
133
+ /** The jobs this process runs now, by state key, shared by every pass of one loop. */
134
+ export interface CiServeRuntime {
135
+ inflight: Map<string, Promise<CiServeJobState>>;
136
+ }
137
+
138
+ export const newCiServeRuntime = (): CiServeRuntime => ({ inflight: new Map() });
139
+
140
+ /** The state key of a job. */
141
+ export function ciServeKey(t: { job: CiJobName; commit: string; pr?: number }): string {
142
+ return t.job === "plan" ? `plan:${t.pr}:${t.commit}` : `apply:${t.commit}`;
143
+ }
144
+
145
+ const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
146
+
147
+ function tryGit(cwd: string, args: string[]): string | undefined {
148
+ try {
149
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
150
+ } catch {
151
+ return undefined;
152
+ }
153
+ }
154
+
155
+ /** Where a serve keeps its state when `--state` names nowhere: beside the run events, in the repository's common git directory. */
156
+ export function defaultCiServeStateDir(cwd: string): string {
157
+ const common = tryGit(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
158
+ if (!common) throw new CiRunError("ci serve runs in a clone of the repository it serves, and this directory is not in a git repository");
159
+ return join(common, "chant", "ci-serve");
160
+ }
161
+
162
+ /** What a serve works out once, before its first pass. */
163
+ interface ServePlace {
164
+ top: string;
165
+ member?: string;
166
+ branch: string;
167
+ group: string;
168
+ stateDir: string;
169
+ stateFile: string;
170
+ }
171
+
172
+ function servePlace(opts: CiServeOptions): ServePlace {
173
+ const cwd = resolve(opts.cwd);
174
+ const top = gitTop(cwd);
175
+ if (!top) throw new CiRunError("ci serve runs in a clone of the repository it serves, and this directory is not in a git repository");
176
+ const { member } = ciJobPlace(cwd, top);
177
+ const group = prApplyGroup(opts.env, member);
178
+ const stateDir = opts.stateDir ? resolve(cwd, opts.stateDir) : defaultCiServeStateDir(cwd);
179
+ return { top, ...(member ? { member } : {}), branch: opts.job?.loop?.branch ?? "main", group, stateDir, stateFile: join(stateDir, `${group}.json`) };
180
+ }
181
+
182
+ /** The state file, or a new one. */
183
+ export function readCiServeState(file: string, fresh: Omit<CiServeState, "schema" | "jobs">): CiServeState {
184
+ if (existsSync(file)) {
185
+ const s = JSON.parse(readFileSync(file, "utf-8")) as CiServeState;
186
+ if (s.schema !== 1 || typeof s.jobs !== "object" || s.jobs === null) throw new CiRunError(`${file} is not a ci serve state file this chant reads`);
187
+ return s;
188
+ }
189
+ return { schema: 1, ...fresh, jobs: {} };
190
+ }
191
+
192
+ function writeState(file: string, state: CiServeState): void {
193
+ mkdirSync(resolve(file, ".."), { recursive: true });
194
+ const tmp = `${file}.${process.pid}.tmp`;
195
+ writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
196
+ renameSync(tmp, file);
197
+ }
198
+
199
+ /** A job entry that can run again with `rerunFailed`: it failed, or it says `running` and no job of this process runs it. */
200
+ const failedEntry = (entry: CiServeJobState | undefined, key: string, rt: CiServeRuntime): entry is CiServeJobState =>
201
+ !!entry && (entry.status === "fail" || (entry.status === "running" && !rt.inflight.has(key)));
202
+
203
+ /**
204
+ * One poll of the forge, as triggers: an apply when the target's head moved
205
+ * since the last apply (none on the first poll, which only notes the head),
206
+ * then a plan for each open pull request head not yet planned, oldest pull
207
+ * request first. A job already in the state file is left out unless it
208
+ * failed and `rerunFailed` is set. The first poll's head is written to the
209
+ * state file.
210
+ */
211
+ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime()): Promise<{ triggers: CiServeTrigger[]; events: CiServeEvent[] }> {
212
+ const place = servePlace(opts);
213
+ const state = readCiServeState(place.stateFile, { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) });
214
+ const triggers: CiServeTrigger[] = [];
215
+ const events: CiServeEvent[] = [];
216
+ const [head, prs] = await Promise.all([opts.client.branchHead(place.branch), opts.client.openPullRequests(place.branch)]);
217
+
218
+ if (head === undefined) {
219
+ events.push({ type: "no-branch", branch: place.branch });
220
+ } else if (state.target === undefined) {
221
+ // What the branch held before this runner watched it was applied by whatever ran then.
222
+ state.target = head;
223
+ writeState(place.stateFile, state);
224
+ events.push({ type: "baseline", commit: head });
225
+ } else if (head !== state.target) {
226
+ const entry = state.jobs[ciServeKey({ job: "apply", commit: head })];
227
+ if (!entry) triggers.push({ job: "apply", commit: head, base: state.target });
228
+ else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job: "apply", commit: head, base: entry.base, rerun: true });
229
+ } else {
230
+ const key = ciServeKey({ job: "apply", commit: head });
231
+ const entry = state.jobs[key];
232
+ if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: "apply", commit: head, base: entry.base, rerun: true });
233
+ }
234
+
235
+ const authors = opts.authors?.length ? new Set(opts.authors.map((a) => a.toLowerCase())) : undefined;
236
+ for (const pr of prs) {
237
+ if (pr.fork) {
238
+ events.push({ type: "skipped", pr: pr.number, reason: "fork" });
239
+ continue;
240
+ }
241
+ if (authors && !authors.has((pr.author ?? "").toLowerCase())) {
242
+ events.push({ type: "skipped", pr: pr.number, reason: "author", ...(pr.author ? { author: pr.author } : {}) });
243
+ continue;
244
+ }
245
+ const key = ciServeKey({ job: "plan", commit: pr.headSha, pr: pr.number });
246
+ const entry = state.jobs[key];
247
+ const base = pr.baseSha ?? head;
248
+ if (!base) continue;
249
+ if (!entry) triggers.push({ job: "plan", commit: pr.headSha, base, pr: pr.number });
250
+ else if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: "plan", commit: pr.headSha, base: entry.base, pr: pr.number, rerun: true });
251
+ }
252
+ return { triggers, events };
253
+ }
254
+
255
+ /** What {@link ciServeSchedule} did with the triggers. */
256
+ export interface CiServeScheduled {
257
+ /** Each job started, settling to its state entry. */
258
+ started: Promise<CiServeJobState>[];
259
+ events: CiServeEvent[];
260
+ }
261
+
262
+ /**
263
+ * Start the triggers' jobs, at most `maxJobs` at once with the jobs this
264
+ * process already runs. An apply first takes its group's lease. A trigger
265
+ * that can't start (over capacity, lease held, or already running here) is
266
+ * reported and not recorded, so the next poll finds it again.
267
+ */
268
+ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly CiServeTrigger[], rt: CiServeRuntime = newCiServeRuntime()): Promise<CiServeScheduled> {
269
+ const place = servePlace(opts);
270
+ const max = opts.maxJobs ?? 1;
271
+ const holder = opts.holder ?? currentHolderId();
272
+ const ttlMs = opts.leaseTtlMs ?? DEFAULT_LEASE_TTL_MS;
273
+ const now = opts.now ?? (() => new Date());
274
+ const log = opts.log ?? (() => {});
275
+ const events: CiServeEvent[] = [];
276
+ const emit = (e: CiServeEvent): void => {
277
+ events.push(e);
278
+ log(e);
279
+ };
280
+ const started: Promise<CiServeJobState>[] = [];
281
+ const fresh = { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) };
282
+ const update = (fn: (s: CiServeState) => void): void => {
283
+ const s = readCiServeState(place.stateFile, fresh);
284
+ fn(s);
285
+ writeState(place.stateFile, s);
286
+ };
287
+
288
+ for (const t of triggers) {
289
+ const key = ciServeKey(t);
290
+ const pr = t.job === "plan" ? { pr: t.pr } : {};
291
+ if (rt.inflight.has(key)) continue;
292
+ if (rt.inflight.size >= max) {
293
+ emit({ type: "held", job: t.job, commit: t.commit, ...pr, reason: "capacity" });
294
+ continue;
295
+ }
296
+ let lease: LeaseRecord | undefined;
297
+ if (t.job === "apply") {
298
+ let got: Awaited<ReturnType<typeof acquireLease>>;
299
+ try {
300
+ got = await acquireLease(place.group, holder, { cwd: opts.cwd, ttlMs, mode: "claim", requirePush: true, now });
301
+ } catch (err) {
302
+ emit({ type: "error", message: `the ${place.group} lease could not be taken: ${message(err)}` });
303
+ continue;
304
+ }
305
+ if (!got.acquired || !got.lease) {
306
+ emit({ type: "held", job: "apply", commit: t.commit, reason: "lease", ...(got.heldBy ? { heldBy: got.heldBy.holder } : {}) });
307
+ continue;
308
+ }
309
+ lease = got.lease;
310
+ }
311
+
312
+ const previous = readCiServeState(place.stateFile, fresh).jobs[key];
313
+ const attempt = (previous?.attempts ?? 0) + 1;
314
+ const runId = `chant-serve-${now().getTime()}-${t.job}-${t.commit.slice(0, 8)}`;
315
+ const resume = t.job === "apply" ? join(place.stateDir, "resume", `${prApplyRecordKey(opts.env, t.commit, place.member)}.json`) : undefined;
316
+ const report = join(place.stateDir, "reports", place.group, `${CI_JOB_OPS[t.job]}${t.job === "plan" ? `-${t.pr}` : ""}-${t.commit.slice(0, 12)}`);
317
+ const entry: CiServeJobState = { job: t.job, commit: t.commit, base: t.base, ...pr, status: "running", runId, attempts: attempt, startedAt: now().toISOString() };
318
+ update((s) => {
319
+ s.jobs[key] = entry;
320
+ if (t.job === "apply" && !t.rerun) s.target = t.commit;
321
+ });
322
+ emit({ type: "started", job: t.job, commit: t.commit, ...pr, base: t.base, runId, attempt, ...(resume && existsSync(resume) ? { resume } : {}) });
323
+
324
+ const job = runJob(opts, place, t, { runId, report, ...(resume ? { resume } : {}) }, lease ? { lease, holder, ttlMs, now } : undefined).then((done) => {
325
+ rt.inflight.delete(key);
326
+ const settled: CiServeJobState = { ...entry, ...done, settledAt: now().toISOString() };
327
+ try {
328
+ update((s) => {
329
+ s.jobs[key] = settled;
330
+ });
331
+ } catch (err) {
332
+ emit({ type: "error", message: `${key} settled, and the state file could not be written: ${message(err)}` });
333
+ }
334
+ emit({ type: "settled", job: t.job, commit: t.commit, ...pr, status: settled.status === "ok" ? "ok" : "fail", ...(settled.failure ? { failure: settled.failure } : {}), ...(settled.report ? { report: settled.report } : {}) });
335
+ return settled;
336
+ });
337
+ rt.inflight.set(key, job);
338
+ started.push(job);
339
+ }
340
+ return { started, events };
341
+ }
342
+
343
+ /** Run one job: fetch its commit, run it with `ciRun`, keep the lease renewed while it runs, and release it after. Never throws. */
344
+ async function runJob(
345
+ opts: CiServeOptions,
346
+ place: ServePlace,
347
+ t: CiServeTrigger,
348
+ paths: { runId: string; report: string; resume?: string },
349
+ held: { lease: LeaseRecord; holder: string; ttlMs: number; now: () => Date } | undefined,
350
+ ): Promise<Pick<CiServeJobState, "status" | "failure" | "report">> {
351
+ const renew = held
352
+ ? setInterval(() => {
353
+ void acquireLease(place.group, held.holder, { cwd: opts.cwd, ttlMs: held.ttlMs, mode: "renew", token: held.lease.token, now: held.now }).catch(() => undefined);
354
+ }, Math.max(1000, Math.floor(held.ttlMs / 3)))
355
+ : undefined;
356
+ renew?.unref?.();
357
+ try {
358
+ // The pull request's head is on the forge's refs/pull/<n>/head, the target's on the branch.
359
+ tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", t.job === "plan" ? `refs/pull/${t.pr}/head` : place.branch]);
360
+ const hostEnv = { ...(opts.job?.hostEnv ?? process.env), GITHUB_REPOSITORY: opts.repo };
361
+ const run = opts.run ?? ciRun;
362
+ const r = await run({
363
+ ...opts.job,
364
+ hostEnv,
365
+ cwd: opts.cwd,
366
+ job: t.job,
367
+ env: opts.env,
368
+ forge: opts.forge,
369
+ base: t.base,
370
+ ...(t.job === "plan" ? { pr: t.pr } : {}),
371
+ commit: t.commit,
372
+ output: paths.report,
373
+ runId: paths.runId,
374
+ ...(paths.resume ? { resumeRecord: paths.resume } : {}),
375
+ });
376
+ const why = r.record.failure ? `${r.record.failure.code}: ${r.record.failure.reason}` : r.failure ? `${r.failure.phase}: ${r.failure.error.split("\n")[0]}` : undefined;
377
+ return { status: r.status === "ok" ? "ok" : "fail", ...(why && r.status !== "ok" ? { failure: why } : {}), ...(r.report ? { report: r.report } : {}) };
378
+ } catch (err) {
379
+ return { status: "fail", failure: message(err) };
380
+ } finally {
381
+ if (renew) clearInterval(renew);
382
+ if (held) await releaseLease(place.group, held.holder, held.lease.token, { cwd: opts.cwd }).catch(() => false);
383
+ }
384
+ }
385
+
386
+ /** One pass: observe, schedule, and with `wait` wait for the jobs it started. */
387
+ export async function ciServePass(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime(), wait = true): Promise<{ events: CiServeEvent[]; jobs: CiServeJobState[] }> {
388
+ const observed = await ciServeObserve(opts, rt);
389
+ for (const e of observed.events) opts.log?.(e);
390
+ const scheduled = await ciServeSchedule(opts, observed.triggers, rt);
391
+ const jobs = wait ? await Promise.all(scheduled.started) : [];
392
+ return { events: [...observed.events, ...scheduled.events], jobs };
393
+ }
394
+
395
+ /**
396
+ * The loop: a pass every `intervalMs` until `signal` aborts, jobs running
397
+ * across passes up to `maxJobs`. A pass the forge fails is logged and the
398
+ * loop goes on. On abort it waits for the jobs that run.
399
+ */
400
+ export async function ciServeLoop(opts: CiServeOptions & { intervalMs: number; signal?: AbortSignal }): Promise<void> {
401
+ const rt = newCiServeRuntime();
402
+ while (!opts.signal?.aborted) {
403
+ try {
404
+ await ciServePass(opts, rt, false);
405
+ } catch (err) {
406
+ opts.log?.({ type: "error", message: message(err) });
407
+ }
408
+ await new Promise<void>((done) => {
409
+ const timer = setTimeout(done, opts.intervalMs);
410
+ opts.signal?.addEventListener("abort", () => {
411
+ clearTimeout(timer);
412
+ done();
413
+ }, { once: true });
414
+ });
415
+ }
416
+ await Promise.allSettled([...rt.inflight.values()]);
417
+ }
418
+
419
+ /** One line for an event, as `ci serve` prints it. */
420
+ export function formatCiServeEvent(e: CiServeEvent): string {
421
+ const at = (job: CiJobName, commit: string, pr?: number): string => `${CI_JOB_OPS[job]} ${commit.slice(0, 12)}${pr !== undefined ? ` (pull request ${pr})` : ""}`;
422
+ switch (e.type) {
423
+ case "baseline":
424
+ return `target at ${e.commit.slice(0, 12)}; the next commit there is applied`;
425
+ case "no-branch":
426
+ return `the forge has no branch ${e.branch}`;
427
+ case "skipped":
428
+ return e.reason === "fork" ? `pull request ${e.pr} is from a fork, and host mode plans none` : `pull request ${e.pr} is by ${e.author ?? "an unknown author"}, who is not an --author`;
429
+ case "held":
430
+ return `${at(e.job, e.commit, e.pr)} waits: ${e.reason === "capacity" ? "--max-jobs jobs run" : `the apply lease is held${e.heldBy ? ` by ${e.heldBy}` : ""}`}`;
431
+ case "started":
432
+ return `${at(e.job, e.commit, e.pr)} started, run ${e.runId}, attempt ${e.attempt}${e.resume ? ", resuming" : ""}`;
433
+ case "settled":
434
+ return `${at(e.job, e.commit, e.pr)} ${e.status === "ok" ? "passed" : `failed${e.failure ? `: ${e.failure}` : ""}`}${e.report ? `; report ${e.report}` : ""}`;
435
+ case "error":
436
+ return `error: ${e.message}`;
437
+ }
438
+ }
439
+
440
+ // ── The command ──────────────────────────────────────────────────────────────
441
+
442
+ const USAGE =
443
+ "chant ci serve --forge forgejo|github --repo <owner/repo> --env <env> [--interval <s>] [--max-jobs <n>] [--state <dir>] " +
444
+ "[--author <login>]... [--once] [--rerun-failed] [--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] " +
445
+ "[--image <image>] [--before-script <line>]... [--pr-build] [--pr-build-command <line>] [--chant <command>] [--pass-env <NAME>]... [--key <runner key.pem>]";
446
+
447
+ const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
448
+
449
+ /** `--interval`: seconds, or a duration such as `30s` or `2m`. Default 30 seconds. */
450
+ export function ciServeInterval(value: string | undefined): number {
451
+ if (value === undefined) return 30_000;
452
+ const ms = /^\d+$/.test(value) ? Number(value) * 1000 : parseDuration(value);
453
+ if (!(ms >= 1000)) throw new CiRunError(`--interval takes seconds or a duration of a second or more, not ${JSON.stringify(value)}`);
454
+ return ms;
455
+ }
456
+
457
+ /** `chant ci serve` (#4095). */
458
+ export async function runCiServe(ctx: CommandContext): Promise<number> {
459
+ const { args } = ctx;
460
+ try {
461
+ const forge = args.forge;
462
+ if (forge !== "forgejo" && forge !== "github") throw new CiRunError(`--forge names the forge ci serve polls: --forge forgejo|github${forge ? `, not ${JSON.stringify(forge)}` : ""}`);
463
+ if (!args.ciRepo) throw new CiRunError("--repo names the repository whose pull requests ci serve polls: --repo <owner/repo>");
464
+ if (!args.env) throw new CiRunError("--env names the environment the pipeline is for: --env <env>");
465
+ for (const name of args.passEnv ?? []) if (!ENV_NAME.test(name)) throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
466
+ const intervalMs = ciServeInterval(args.interval);
467
+ const fromFlag = args.key !== undefined;
468
+ const runnerKey = fromFlag ? readFileSync(resolve(args.key!), "utf-8") : process.env[RUNNER_KEY_ENV] || undefined;
469
+ const hostEnv = { ...process.env, GITHUB_REPOSITORY: args.ciRepo };
470
+ const client = forgeFromEnv(forge, hostEnv);
471
+ if (!client.openPullRequests || !client.branchHead) throw new CiRunError(`the ${forge} client cannot list pull requests`);
472
+ const opts: CiServeOptions = {
473
+ cwd: process.cwd(),
474
+ forge,
475
+ repo: args.ciRepo,
476
+ env: args.env,
477
+ client: { openPullRequests: client.openPullRequests.bind(client), branchHead: client.branchHead.bind(client) },
478
+ ...(args.statePath ? { stateDir: isAbsolute(args.statePath) ? args.statePath : resolve(args.statePath) } : {}),
479
+ ...(args.maxJobs ? { maxJobs: args.maxJobs } : {}),
480
+ ...(args.ciAuthors?.length ? { authors: args.ciAuthors } : {}),
481
+ ...(args.rerunFailed ? { rerunFailed: true } : {}),
482
+ job: {
483
+ loop: {
484
+ ...(args.gate ? { gate: args.gate } : {}),
485
+ ...(args.branch ? { branch: args.branch } : {}),
486
+ ...(args.eventsJson ? { events: true } : {}),
487
+ ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
488
+ ...(args.prBuild ? { build: true } : {}),
489
+ ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
490
+ },
491
+ ...(args.image ? { image: args.image } : {}),
492
+ ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
493
+ ...(args.ciChant ? { chant: args.ciChant } : {}),
494
+ ...(args.passEnv?.length ? { passEnv: args.passEnv } : {}),
495
+ ...(runnerKey !== undefined ? { runnerKey, runnerKeyFromEnv: !fromFlag } : {}),
496
+ ...(args.eventsJson ? { runEvents: (line: string) => void process.stdout.write(line) } : {}),
497
+ hostEnv,
498
+ stepOutput: (line) => void process.stderr.write(`${line}\n`),
499
+ },
500
+ log: (e) => console.error(e.type === "error" ? formatWarning({ message: formatCiServeEvent(e) }) : `ci serve: ${formatCiServeEvent(e)}`),
501
+ };
502
+ if (args.once) {
503
+ const pass = await ciServePass(opts);
504
+ return pass.jobs.some((j) => j.status !== "ok") ? 1 : 0;
505
+ }
506
+ const stop = new AbortController();
507
+ const onSignal = (): void => stop.abort();
508
+ process.once("SIGINT", onSignal);
509
+ process.once("SIGTERM", onSignal);
510
+ try {
511
+ await ciServeLoop({ ...opts, intervalMs, signal: stop.signal });
512
+ } finally {
513
+ process.off("SIGINT", onSignal);
514
+ process.off("SIGTERM", onSignal);
515
+ }
516
+ return 0;
517
+ } catch (err) {
518
+ console.error(formatError({ message: `chant ci serve: ${message(err)}`, hint: USAGE }));
519
+ return 1;
520
+ }
521
+ }
@@ -179,6 +179,58 @@
179
179
  "$ref": "#/$defs/ciGreen"
180
180
  }
181
181
  }
182
+ },
183
+ "retention": {
184
+ "type": "object",
185
+ "additionalProperties": false,
186
+ "patternProperties": {
187
+ "^x-": true
188
+ },
189
+ "description": "How long chant lifecycle gc keeps each kind of stored record (#4036). A kind or field left out keeps its default: changeSets 14 days; releases 14 days and the newest 10 of each member; runEvents 14 days and the newest 100. Whatever the policy, gc never prunes a release an environment runs, what a pending gate binds, or a change set a gate names with a kept release. Read from the declaration at base. Added in schema 1 by chant 0.124.29, so a declaration that uses it sets minReader to 0.124.29 or newer.",
190
+ "properties": {
191
+ "changeSets": {
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "description": "Stored change sets, _change-sets/<digest>.json on chant/lifecycle.",
195
+ "properties": {
196
+ "days": { "$ref": "#/$defs/retentionDays" }
197
+ }
198
+ },
199
+ "releases": {
200
+ "type": "object",
201
+ "additionalProperties": false,
202
+ "description": "Releases, _releases/<digest>.json on chant/lifecycle, with the template bytes only they name. A release stays promotable for days after its newest cut, and the newest keep releases of each member stay promotable at any age, unless their commit is revoked.",
203
+ "properties": {
204
+ "days": { "$ref": "#/$defs/retentionDays" },
205
+ "keep": { "$ref": "#/$defs/retentionKeep" }
206
+ }
207
+ },
208
+ "runEvents": {
209
+ "type": "object",
210
+ "additionalProperties": false,
211
+ "description": "Run event files, <git-common-dir>/chant/run-events/<run>.jsonl on each host. A file is kept for days after its last write, and the newest keep files at any age.",
212
+ "properties": {
213
+ "days": { "$ref": "#/$defs/retentionDays" },
214
+ "keep": { "$ref": "#/$defs/retentionKeep" }
215
+ }
216
+ }
217
+ }
218
+ },
219
+ "metrics": {
220
+ "type": "object",
221
+ "additionalProperties": false,
222
+ "patternProperties": {
223
+ "^x-": true
224
+ },
225
+ "description": "How chant workspace metrics counts the DORA metrics (#4039). Added in schema 1 by chant 0.124.29, so a declaration that uses it sets minReader to 0.124.29 or newer.",
226
+ "properties": {
227
+ "failureWindowDays": {
228
+ "type": "integer",
229
+ "minimum": 1,
230
+ "maximum": 365,
231
+ "description": "A deploy counts as failed when, within this many days after it, the commit it deployed is revoked or the environment is rolled back from it. The default is 7."
232
+ }
233
+ }
182
234
  }
183
235
  },
184
236
  "patternProperties": {
@@ -186,6 +238,18 @@
186
238
  },
187
239
  "additionalProperties": false,
188
240
  "$defs": {
241
+ "retentionDays": {
242
+ "type": "integer",
243
+ "minimum": 0,
244
+ "maximum": 36500,
245
+ "description": "Days a record is kept past its last write or use. 0 lets every record go that no never-prune rule keeps."
246
+ },
247
+ "retentionKeep": {
248
+ "type": "integer",
249
+ "minimum": 0,
250
+ "maximum": 100000,
251
+ "description": "How many of the newest are kept at any age."
252
+ },
189
253
  "name": {
190
254
  "type": "string",
191
255
  "pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
@@ -421,3 +421,31 @@ describe("diagram artifacts (#2764)", () => {
421
421
  expect(failure(base([member([diagram({ render: "../out.svg" })])])).code).toBe("declaration-invalid");
422
422
  });
423
423
  });
424
+
425
+ describe("retention (#4036)", () => {
426
+ test("the block is read as written, without x- keys, and is null when absent", () => {
427
+ expect(parse(base([])).retention).toBeNull();
428
+ const d = parse(base([], { minReader: "0.80.0", retention: { changeSets: { days: 30 }, releases: { keep: 3 }, runEvents: { days: 2, keep: 5 }, "x-why": "audits" } }));
429
+ expect(d.retention).toEqual({ changeSets: { days: 30 }, releases: { keep: 3 }, runEvents: { days: 2, keep: 5 } });
430
+ });
431
+
432
+ test("an unknown kind or field, a negative or fractional number, and keep on changeSets are refused", () => {
433
+ for (const retention of [{ plans: { days: 1 } }, { releases: { days: -1 } }, { releases: { days: 1.5 } }, { changeSets: { keep: 2 } }, { runEvents: { hours: 2 } }]) {
434
+ expect(failure(base([], { retention })).code).toBe("declaration-invalid");
435
+ }
436
+ });
437
+ });
438
+
439
+ describe("metrics (#4039)", () => {
440
+ test("failureWindowDays is read as written, without x- keys, and the block is null when absent", () => {
441
+ expect(parse(base([])).metrics).toBeNull();
442
+ expect(parse(base([], { minReader: "0.80.0", metrics: { failureWindowDays: 14, "x-why": "weekly" } })).metrics).toEqual({ failureWindowDays: 14 });
443
+ expect(parse(base([], { metrics: {} })).metrics).toEqual({});
444
+ });
445
+
446
+ test("an unknown field, 0, a fraction and more than a year are refused", () => {
447
+ for (const metrics of [{ window: 7 }, { failureWindowDays: 0 }, { failureWindowDays: 1.5 }, { failureWindowDays: 366 }]) {
448
+ expect(failure(base([], { metrics })).code).toBe("declaration-invalid");
449
+ }
450
+ });
451
+ });