@intentius/chant 0.124.26 → 0.124.28

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 (459) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/change-set-graph.d.ts +56 -0
  20. package/dist/change-set-graph.d.ts.map +1 -0
  21. package/dist/change-set-graph.js +128 -0
  22. package/dist/change-set.d.ts +52 -3
  23. package/dist/change-set.d.ts.map +1 -1
  24. package/dist/change-set.js +35 -4
  25. package/dist/chant-version.d.ts +9 -0
  26. package/dist/chant-version.d.ts.map +1 -0
  27. package/dist/chant-version.js +16 -0
  28. package/dist/cli/commands/build.d.ts +8 -0
  29. package/dist/cli/commands/build.d.ts.map +1 -1
  30. package/dist/cli/commands/build.js +9 -8
  31. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  32. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  33. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  34. package/dist/cli/commands/check-lexicon.js +13 -2
  35. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  36. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  37. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  38. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  39. package/dist/cli/commands/init-lexicon.js +17 -8
  40. package/dist/cli/handlers/approve.d.ts.map +1 -1
  41. package/dist/cli/handlers/approve.js +19 -4
  42. package/dist/cli/handlers/build.d.ts.map +1 -1
  43. package/dist/cli/handlers/build.js +28 -0
  44. package/dist/cli/handlers/change-set.d.ts +10 -0
  45. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  46. package/dist/cli/handlers/change-set.js +51 -1
  47. package/dist/cli/handlers/fan-out.d.ts +1 -1
  48. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  49. package/dist/cli/handlers/fan-out.js +14 -3
  50. package/dist/cli/handlers/pr.d.ts +8 -2
  51. package/dist/cli/handlers/pr.d.ts.map +1 -1
  52. package/dist/cli/handlers/pr.js +148 -24
  53. package/dist/cli/handlers/promote.d.ts.map +1 -1
  54. package/dist/cli/handlers/promote.js +256 -21
  55. package/dist/cli/handlers/release.d.ts +34 -0
  56. package/dist/cli/handlers/release.d.ts.map +1 -0
  57. package/dist/cli/handlers/release.js +203 -0
  58. package/dist/cli/handlers/run.d.ts +28 -2
  59. package/dist/cli/handlers/run.d.ts.map +1 -1
  60. package/dist/cli/handlers/run.js +170 -13
  61. package/dist/cli/main.d.ts.map +1 -1
  62. package/dist/cli/main.js +88 -13
  63. package/dist/cli/mcp/workspace-tools.js +1 -1
  64. package/dist/cli/parse-args.d.ts.map +1 -1
  65. package/dist/cli/parse-args.js +49 -3
  66. package/dist/cli/registry.d.ts +29 -3
  67. package/dist/cli/registry.d.ts.map +1 -1
  68. package/dist/cli/run-events.d.ts +29 -0
  69. package/dist/cli/run-events.d.ts.map +1 -0
  70. package/dist/cli/run-events.js +47 -0
  71. package/dist/cli/version.d.ts +3 -5
  72. package/dist/cli/version.d.ts.map +1 -1
  73. package/dist/cli/version.js +3 -13
  74. package/dist/codegen/docs-in-site.d.ts +71 -0
  75. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  76. package/dist/codegen/docs-in-site.js +177 -0
  77. package/dist/codegen/docs-types.d.ts +14 -1
  78. package/dist/codegen/docs-types.d.ts.map +1 -1
  79. package/dist/codegen/docs.d.ts +18 -1
  80. package/dist/codegen/docs.d.ts.map +1 -1
  81. package/dist/codegen/docs.js +40 -7
  82. package/dist/codegen/rehype-base-url.mjs +15 -9
  83. package/dist/components/auto-release.d.ts +4 -1
  84. package/dist/components/auto-release.d.ts.map +1 -1
  85. package/dist/components/auto-release.js +2 -0
  86. package/dist/components/cli-support.d.ts +6 -0
  87. package/dist/components/cli-support.d.ts.map +1 -1
  88. package/dist/components/cli-support.js +10 -9
  89. package/dist/components/driver.d.ts +42 -9
  90. package/dist/components/driver.d.ts.map +1 -1
  91. package/dist/components/driver.js +81 -54
  92. package/dist/components/fan-out-run.d.ts +7 -0
  93. package/dist/components/fan-out-run.d.ts.map +1 -1
  94. package/dist/components/fan-out-run.js +35 -26
  95. package/dist/components/pr-run.d.ts +11 -0
  96. package/dist/components/pr-run.d.ts.map +1 -1
  97. package/dist/components/pr-run.js +20 -1
  98. package/dist/components/promote-order.d.ts +38 -0
  99. package/dist/components/promote-order.d.ts.map +1 -0
  100. package/dist/components/promote-order.js +62 -0
  101. package/dist/components/promote.d.ts +15 -2
  102. package/dist/components/promote.d.ts.map +1 -1
  103. package/dist/components/promote.js +17 -0
  104. package/dist/components/release-compile.d.ts +133 -0
  105. package/dist/components/release-compile.d.ts.map +1 -0
  106. package/dist/components/release-compile.js +376 -0
  107. package/dist/components/release-gate.d.ts +125 -0
  108. package/dist/components/release-gate.d.ts.map +1 -0
  109. package/dist/components/release-gate.js +282 -0
  110. package/dist/components/release-promote.d.ts +96 -0
  111. package/dist/components/release-promote.d.ts.map +1 -0
  112. package/dist/components/release-promote.js +204 -0
  113. package/dist/components/run-progress.d.ts +7 -0
  114. package/dist/components/run-progress.d.ts.map +1 -1
  115. package/dist/components/wave-plan.d.ts +3 -1
  116. package/dist/components/wave-plan.d.ts.map +1 -1
  117. package/dist/components/wave-plan.js +1 -1
  118. package/dist/config.d.ts +7 -0
  119. package/dist/config.d.ts.map +1 -1
  120. package/dist/config.js +1 -0
  121. package/dist/index.d.ts +1 -0
  122. package/dist/index.d.ts.map +1 -1
  123. package/dist/index.js +2 -0
  124. package/dist/lifecycle/change-set-store.d.ts +67 -0
  125. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  126. package/dist/lifecycle/change-set-store.js +96 -0
  127. package/dist/lifecycle/commit-sha.d.ts +13 -0
  128. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  129. package/dist/lifecycle/commit-sha.js +21 -0
  130. package/dist/lifecycle/gate-change-set.d.ts +69 -0
  131. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  132. package/dist/lifecycle/gate-change-set.js +106 -0
  133. package/dist/lifecycle/gate-ledger.d.ts +49 -1
  134. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  135. package/dist/lifecycle/gate-ledger.js +18 -0
  136. package/dist/lifecycle/release-cut.d.ts +107 -0
  137. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  138. package/dist/lifecycle/release-cut.js +273 -0
  139. package/dist/lifecycle/release-ledger.d.ts +54 -0
  140. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  141. package/dist/lifecycle/release-ledger.js +53 -20
  142. package/dist/lifecycle/release-store.d.ts +190 -0
  143. package/dist/lifecycle/release-store.d.ts.map +1 -0
  144. package/dist/lifecycle/release-store.js +318 -0
  145. package/dist/lifecycle/run-ledger.d.ts +4 -0
  146. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  147. package/dist/lifecycle/run-ledger.js +2 -0
  148. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  149. package/dist/op/activities/change-coverage.js +4 -2
  150. package/dist/op/activities/source-release.d.ts.map +1 -1
  151. package/dist/op/activities/source-release.js +7 -3
  152. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  153. package/dist/op/activities/source-rollback.js +7 -3
  154. package/dist/op/activity-registry.d.ts +6 -3
  155. package/dist/op/activity-registry.d.ts.map +1 -1
  156. package/dist/op/activity-registry.js +7 -5
  157. package/dist/op/discover.d.ts.map +1 -1
  158. package/dist/op/discover.js +14 -7
  159. package/dist/op/gate-name.d.ts +11 -2
  160. package/dist/op/gate-name.d.ts.map +1 -1
  161. package/dist/op/gate-name.js +12 -3
  162. package/dist/op/gate-run-locator.d.ts +22 -0
  163. package/dist/op/gate-run-locator.d.ts.map +1 -1
  164. package/dist/op/gate-run-locator.js +31 -0
  165. package/dist/op/gate.d.ts +27 -3
  166. package/dist/op/gate.d.ts.map +1 -1
  167. package/dist/op/gate.js +57 -3
  168. package/dist/op/local-executor.d.ts +36 -1
  169. package/dist/op/local-executor.d.ts.map +1 -1
  170. package/dist/op/local-executor.js +158 -52
  171. package/dist/op/op-waves-run.d.ts.map +1 -1
  172. package/dist/op/op-waves-run.js +4 -0
  173. package/dist/op/run-live.d.ts +2 -0
  174. package/dist/op/run-live.d.ts.map +1 -1
  175. package/dist/op/run-live.js +1 -1
  176. package/dist/op/runtime.d.ts +46 -0
  177. package/dist/op/runtime.d.ts.map +1 -1
  178. package/dist/op/runtimes/local.d.ts.map +1 -1
  179. package/dist/op/runtimes/local.js +12 -2
  180. package/dist/point-decision.d.ts +2 -0
  181. package/dist/point-decision.d.ts.map +1 -1
  182. package/dist/point-decision.js +9 -2
  183. package/dist/run-event-store.d.ts +77 -0
  184. package/dist/run-event-store.d.ts.map +1 -0
  185. package/dist/run-event-store.js +204 -0
  186. package/dist/run-event-stream.d.ts +132 -0
  187. package/dist/run-event-stream.d.ts.map +1 -0
  188. package/dist/run-event-stream.js +238 -0
  189. package/dist/run-event.d.ts +267 -0
  190. package/dist/run-event.d.ts.map +1 -0
  191. package/dist/run-event.js +207 -0
  192. package/dist/wave-runtime.mjs +225 -242
  193. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  194. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  195. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  196. package/dist/workspace/change-set-show.d.ts +56 -0
  197. package/dist/workspace/change-set-show.d.ts.map +1 -0
  198. package/dist/workspace/change-set-show.js +54 -0
  199. package/dist/workspace/change-set-show.schema.json +102 -0
  200. package/dist/workspace/change-set.schema.json +28 -1
  201. package/dist/workspace/changes-cli.d.ts.map +1 -1
  202. package/dist/workspace/changes-cli.js +10 -1
  203. package/dist/workspace/changes.d.ts +37 -0
  204. package/dist/workspace/changes.d.ts.map +1 -1
  205. package/dist/workspace/changes.js +49 -2
  206. package/dist/workspace/changes.schema.json +135 -0
  207. package/dist/workspace/ci-green-cli.d.ts +19 -4
  208. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  209. package/dist/workspace/ci-green-cli.js +75 -12
  210. package/dist/workspace/ci-green-forge.d.ts +20 -3
  211. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  212. package/dist/workspace/ci-green-forge.js +6 -4
  213. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  214. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  215. package/dist/workspace/ci-green-workflow.js +6 -0
  216. package/dist/workspace/ci-green.d.ts +273 -13
  217. package/dist/workspace/ci-green.d.ts.map +1 -1
  218. package/dist/workspace/ci-green.js +449 -38
  219. package/dist/workspace/ci-last-green.schema.json +32 -2
  220. package/dist/workspace/ci-list.schema.json +525 -0
  221. package/dist/workspace/commit-links.d.ts +85 -0
  222. package/dist/workspace/commit-links.d.ts.map +1 -0
  223. package/dist/workspace/commit-links.js +163 -0
  224. package/dist/workspace/declaration.d.ts +19 -0
  225. package/dist/workspace/declaration.d.ts.map +1 -1
  226. package/dist/workspace/declaration.js +1 -1
  227. package/dist/workspace/declaration.schema.json +12 -0
  228. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  229. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  230. package/dist/workspace/identity-gate-rule.js +72 -7
  231. package/dist/workspace/identity.d.ts +12 -0
  232. package/dist/workspace/identity.d.ts.map +1 -1
  233. package/dist/workspace/identity.js +13 -1
  234. package/dist/workspace/intent.d.ts +6 -1
  235. package/dist/workspace/intent.d.ts.map +1 -1
  236. package/dist/workspace/intent.js +51 -1
  237. package/dist/workspace/ls.d.ts +4 -0
  238. package/dist/workspace/ls.d.ts.map +1 -1
  239. package/dist/workspace/ls.js +2 -0
  240. package/dist/workspace/ls.schema.json +3 -1
  241. package/dist/workspace/member-pipeline.d.ts +6 -0
  242. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  243. package/dist/workspace/member-pipeline.js +6 -0
  244. package/dist/workspace/promotion-order.d.ts +41 -0
  245. package/dist/workspace/promotion-order.d.ts.map +1 -0
  246. package/dist/workspace/promotion-order.js +95 -0
  247. package/dist/workspace/reason-codes.d.ts +2 -0
  248. package/dist/workspace/reason-codes.d.ts.map +1 -1
  249. package/dist/workspace/reason-codes.js +4 -1
  250. package/dist/workspace/releases.d.ts +72 -0
  251. package/dist/workspace/releases.d.ts.map +1 -0
  252. package/dist/workspace/releases.js +94 -0
  253. package/dist/workspace/releases.schema.json +391 -0
  254. package/dist/workspace/run-event.schema.json +288 -0
  255. package/dist/workspace/status-gates.d.ts +26 -1
  256. package/dist/workspace/status-gates.d.ts.map +1 -1
  257. package/dist/workspace/status-gates.js +31 -2
  258. package/dist/workspace/status-stewards.d.ts +4 -0
  259. package/dist/workspace/status-stewards.d.ts.map +1 -1
  260. package/dist/workspace/status-stewards.js +2 -0
  261. package/dist/workspace/status.d.ts +39 -0
  262. package/dist/workspace/status.d.ts.map +1 -1
  263. package/dist/workspace/status.js +48 -1
  264. package/dist/workspace/status.schema.json +70 -1
  265. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  266. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  267. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  268. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  269. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  270. package/dist/workspace/trust/op-run-statement.js +143 -0
  271. package/dist/workspace/trust/rotation.d.ts +18 -2
  272. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  273. package/dist/workspace/trust/rotation.js +65 -16
  274. package/dist/workspace/trust/seal.d.ts +11 -0
  275. package/dist/workspace/trust/seal.d.ts.map +1 -1
  276. package/dist/workspace/trust/seal.js +9 -0
  277. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  278. package/dist/workspace/trust/ssh-commit.js +10 -10
  279. package/package.json +1 -1
  280. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  281. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  282. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  283. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  284. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  285. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  286. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  287. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  288. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  289. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  290. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  291. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  292. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  293. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  294. package/src/__golden__/wave-loop/op-spend.json +3 -0
  295. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  296. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  297. package/src/change-set-graph.test.ts +72 -0
  298. package/src/change-set-graph.ts +133 -0
  299. package/src/change-set.ts +76 -6
  300. package/src/chant-version.ts +16 -0
  301. package/src/cli/commands/build.ts +19 -8
  302. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  303. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  304. package/src/cli/commands/check-lexicon.ts +14 -2
  305. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  306. package/src/cli/commands/init-lexicon.test.ts +25 -0
  307. package/src/cli/commands/init-lexicon.ts +18 -8
  308. package/src/cli/compiled-cli.e2e.test.ts +31 -6
  309. package/src/cli/handlers/approve-release.test.ts +80 -0
  310. package/src/cli/handlers/approve.ts +18 -2
  311. package/src/cli/handlers/build.test.ts +52 -0
  312. package/src/cli/handlers/build.ts +29 -0
  313. package/src/cli/handlers/change-set.ts +49 -1
  314. package/src/cli/handlers/fan-out.test.ts +48 -0
  315. package/src/cli/handlers/fan-out.ts +15 -3
  316. package/src/cli/handlers/operator.test.ts +37 -0
  317. package/src/cli/handlers/pr.test.ts +194 -0
  318. package/src/cli/handlers/pr.ts +168 -34
  319. package/src/cli/handlers/promote.test.ts +389 -5
  320. package/src/cli/handlers/promote.ts +279 -20
  321. package/src/cli/handlers/release-implicit.e2e.test.ts +153 -0
  322. package/src/cli/handlers/release.e2e.test.ts +85 -0
  323. package/src/cli/handlers/release.ts +206 -0
  324. package/src/cli/handlers/run-auto-release.test.ts +71 -0
  325. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  326. package/src/cli/handlers/run.test.ts +30 -0
  327. package/src/cli/handlers/run.ts +176 -15
  328. package/src/cli/main.test.ts +7 -0
  329. package/src/cli/main.ts +88 -13
  330. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  331. package/src/cli/mcp/workspace-tools.ts +1 -1
  332. package/src/cli/parse-args.ts +37 -3
  333. package/src/cli/registry.ts +29 -3
  334. package/src/cli/run-events.ts +54 -0
  335. package/src/cli/version.ts +3 -13
  336. package/src/codegen/docs-in-site.test.ts +166 -0
  337. package/src/codegen/docs-in-site.ts +182 -0
  338. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  339. package/src/codegen/docs-types.ts +14 -1
  340. package/src/codegen/docs.ts +60 -7
  341. package/src/codegen/rehype-base-url.d.mts +3 -1
  342. package/src/codegen/rehype-base-url.mjs +15 -9
  343. package/src/codegen/rehype-base-url.test.ts +34 -0
  344. package/src/components/auto-release.test.ts +21 -0
  345. package/src/components/auto-release.ts +6 -1
  346. package/src/components/cli-support.test.ts +82 -0
  347. package/src/components/cli-support.ts +16 -9
  348. package/src/components/driver.ts +129 -56
  349. package/src/components/fan-out-run.ts +56 -35
  350. package/src/components/pr-run.ts +30 -1
  351. package/src/components/promote-order.test.ts +90 -0
  352. package/src/components/promote-order.ts +75 -0
  353. package/src/components/promote.test.ts +10 -1
  354. package/src/components/promote.ts +26 -1
  355. package/src/components/release-compile.test.ts +147 -0
  356. package/src/components/release-compile.ts +445 -0
  357. package/src/components/release-gate.test.ts +215 -0
  358. package/src/components/release-gate.ts +341 -0
  359. package/src/components/release-promote.test.ts +159 -0
  360. package/src/components/release-promote.ts +232 -0
  361. package/src/components/run-progress.ts +7 -0
  362. package/src/components/wave-plan.ts +1 -1
  363. package/src/config.ts +7 -1
  364. package/src/index.ts +2 -0
  365. package/src/lifecycle/change-set-store.test.ts +217 -0
  366. package/src/lifecycle/change-set-store.ts +112 -0
  367. package/src/lifecycle/commit-sha.ts +23 -0
  368. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  369. package/src/lifecycle/gate-change-set.ts +133 -0
  370. package/src/lifecycle/gate-ledger.test.ts +33 -0
  371. package/src/lifecycle/gate-ledger.ts +66 -1
  372. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  373. package/src/lifecycle/release-cut.test.ts +411 -0
  374. package/src/lifecycle/release-cut.ts +367 -0
  375. package/src/lifecycle/release-ledger.test.ts +14 -0
  376. package/src/lifecycle/release-ledger.ts +85 -17
  377. package/src/lifecycle/release-store.ts +423 -0
  378. package/src/lifecycle/run-ledger.test.ts +10 -0
  379. package/src/lifecycle/run-ledger.ts +6 -0
  380. package/src/op/activities/change-coverage.ts +4 -1
  381. package/src/op/activities/source-release.ts +8 -3
  382. package/src/op/activities/source-rollback.ts +8 -4
  383. package/src/op/activity-registry.ts +7 -5
  384. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  385. package/src/op/discover-root.test.ts +39 -0
  386. package/src/op/discover.ts +15 -8
  387. package/src/op/gate-name.ts +13 -3
  388. package/src/op/gate-release.test.ts +126 -0
  389. package/src/op/gate-run-locator.test.ts +110 -0
  390. package/src/op/gate-run-locator.ts +35 -0
  391. package/src/op/gate.ts +88 -6
  392. package/src/op/local-executor.ts +207 -52
  393. package/src/op/op-waves-run.ts +4 -0
  394. package/src/op/run-events.test.ts +166 -0
  395. package/src/op/run-live.ts +1 -1
  396. package/src/op/runtime.ts +39 -0
  397. package/src/op/runtimes/local.test.ts +16 -0
  398. package/src/op/runtimes/local.ts +13 -2
  399. package/src/point-decision.ts +12 -2
  400. package/src/run-event-store.test.ts +144 -0
  401. package/src/run-event-store.ts +213 -0
  402. package/src/run-event-stream.test.ts +175 -0
  403. package/src/run-event-stream.ts +306 -0
  404. package/src/run-event.test.ts +277 -0
  405. package/src/run-event.ts +451 -0
  406. package/src/wave-loop-golden.test.ts +9 -0
  407. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  408. package/src/workspace/ancestry-index.test.ts +93 -0
  409. package/src/workspace/change-set-show.schema.json +102 -0
  410. package/src/workspace/change-set-show.ts +72 -0
  411. package/src/workspace/change-set.schema.json +28 -1
  412. package/src/workspace/changes-cli.ts +10 -1
  413. package/src/workspace/changes.schema.json +135 -0
  414. package/src/workspace/changes.test.ts +1 -1
  415. package/src/workspace/changes.ts +84 -2
  416. package/src/workspace/ci-green-cli.ts +75 -13
  417. package/src/workspace/ci-green-forge.ts +24 -4
  418. package/src/workspace/ci-green-runs.test.ts +242 -0
  419. package/src/workspace/ci-green-signed.test.ts +302 -0
  420. package/src/workspace/ci-green-workflow.test.ts +16 -0
  421. package/src/workspace/ci-green-workflow.ts +12 -0
  422. package/src/workspace/ci-green.test.ts +131 -2
  423. package/src/workspace/ci-green.ts +624 -42
  424. package/src/workspace/ci-last-green.schema.json +32 -2
  425. package/src/workspace/ci-list.schema.json +525 -0
  426. package/src/workspace/commit-links.test.ts +279 -0
  427. package/src/workspace/commit-links.ts +183 -0
  428. package/src/workspace/declaration.schema.json +12 -0
  429. package/src/workspace/declaration.ts +22 -2
  430. package/src/workspace/identity-gate-rule.ts +91 -7
  431. package/src/workspace/identity.ts +14 -1
  432. package/src/workspace/intent.ts +48 -1
  433. package/src/workspace/ls.schema.json +3 -1
  434. package/src/workspace/ls.ts +6 -0
  435. package/src/workspace/member-pipeline.test.ts +17 -0
  436. package/src/workspace/member-pipeline.ts +9 -0
  437. package/src/workspace/promotion-order.ts +97 -0
  438. package/src/workspace/read-contract.e2e.test.ts +4 -1
  439. package/src/workspace/reason-codes.test.ts +2 -1
  440. package/src/workspace/reason-codes.ts +4 -1
  441. package/src/workspace/releases.schema.json +391 -0
  442. package/src/workspace/releases.test.ts +108 -0
  443. package/src/workspace/releases.ts +122 -0
  444. package/src/workspace/run-event.schema.json +288 -0
  445. package/src/workspace/status-contract.test.ts +52 -1
  446. package/src/workspace/status-gates.ts +56 -1
  447. package/src/workspace/status-stewards.ts +6 -0
  448. package/src/workspace/status.schema.json +70 -1
  449. package/src/workspace/status.ts +80 -1
  450. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  451. package/src/workspace/trust/op-run-statement.ts +148 -0
  452. package/src/workspace/trust/rotation.ts +61 -14
  453. package/src/workspace/trust/seal.ts +10 -0
  454. package/src/workspace/trust/ssh-commit.ts +10 -9
  455. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  456. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  457. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  458. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  459. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,117 @@
1
+ /**
2
+ * A gated deploy leaves four records that name each other (#4020): the run
3
+ * record, the pending gate fact, the resolution and the release record. From
4
+ * any one of them, the other three are one lookup away, by the commit sha, the
5
+ * run id or the gate.
6
+ */
7
+ import { describe, test, expect, afterEach, vi } from "vitest";
8
+ import type { ActivityFn, ActivityProfile } from "./activity-registry";
9
+ import type { OpConfig } from "./types";
10
+ import { runOpLocally } from "./local-executor";
11
+ import { memoryGateLedgerPort } from "./gate";
12
+ import { resolveGateRunLocator, runOriginOfLocator } from "./gate-run-locator";
13
+ import { admissionOf, resolveRunId, type ReleaseRecord } from "../lifecycle/release-ledger";
14
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
15
+
16
+ const SHA = "c0ffee".repeat(6) + "c0ff";
17
+ const PROFILES: Record<string, ActivityProfile> = { single: { timeout: "5m", retry: { maximumAttempts: 1 } } };
18
+ const NOW = "2026-10-16T10:00:00.000Z";
19
+ const CI = {
20
+ GITHUB_ACTIONS: "true",
21
+ GITHUB_REPOSITORY: "acme/app",
22
+ GITHUB_RUN_ID: "9001",
23
+ GITHUB_SERVER_URL: "https://github.example",
24
+ GITHUB_API_URL: "https://github.example/api/v3",
25
+ };
26
+
27
+ afterEach(() => vi.unstubAllEnvs());
28
+
29
+ const config: OpConfig = {
30
+ name: "ship",
31
+ overview: "",
32
+ labels: { Env: "prod" },
33
+ phases: [
34
+ { name: "Build", steps: [{ kind: "activity", fn: "build" }] },
35
+ { name: "Release", steps: [{ kind: "gate", gate: "approve-release" }, { kind: "activity", fn: "release" }] },
36
+ ],
37
+ };
38
+ const activities = new Map<string, ActivityFn>([
39
+ ["build", async () => undefined],
40
+ ["release", async () => undefined],
41
+ ]);
42
+
43
+ describe("a gated deploy: commit, run, gate and deploy record join (#4020)", () => {
44
+ test("each record names the commit, and the run, gate and deploy name one another", async () => {
45
+ for (const [k, v] of Object.entries(CI)) vi.stubEnv(k, v);
46
+ const port = memoryGateLedgerPort();
47
+
48
+ // 1. The run reaches the gate and stops. Its record and the pending fact carry the commit.
49
+ const first = await runOpLocally(config, activities, PROFILES, undefined, { gates: port, now: NOW, commit: SHA });
50
+ expect(first.status).toBe("gated");
51
+ const run = first.record;
52
+ const pending = port.appended[0]!;
53
+ expect(run.gitSha).toBe(SHA);
54
+ expect(run.runOrigin).toEqual({ forge: "github", repo: "acme/app", url: "https://github.example/acme/app/actions/runs/9001" });
55
+ expect(pending.gitSha).toBe(SHA);
56
+ expect(pending.runId).toBe(run.id);
57
+
58
+ // 2. Someone approves. `chant approve` copies the commit and run off the pending fact it answers.
59
+ const resolution: GateResolutionRecord = {
60
+ version: 1, op: pending.op, gate: pending.gate, resolvedBy: "alice", timestamp: "2026-10-16T10:05:00.000Z",
61
+ gitSha: pending.gitSha, runId: pending.runId,
62
+ };
63
+ const second = await runOpLocally(config, activities, PROFILES, undefined, {
64
+ gates: memoryGateLedgerPort({ pending: [pending], resolutions: [resolution] }),
65
+ now: "2026-10-16T10:06:00.000Z",
66
+ commit: SHA,
67
+ });
68
+ expect(second.status).toBe("ok");
69
+
70
+ // 3. The release activity records the deploy, naming the gate that admitted it.
71
+ const admitted = admissionOf([resolution], { op: "ship", gate: "approve-release" })!;
72
+ const ci = resolveRunId();
73
+ const release: ReleaseRecord = {
74
+ version: 1, component: "site", env: "prod", digest: `sha256:${"d".repeat(64)}`, gitSha: SHA,
75
+ runId: ci.runId, runOrigin: ci.runOrigin, timestamp: "2026-10-16T10:07:00.000Z", actor: "ci",
76
+ approver: admitted.approver, admission: admitted.admission,
77
+ };
78
+
79
+ // From the commit: the run, the gate and the deploy.
80
+ const byCommit = {
81
+ runs: [run, second.record].filter((r) => r.gitSha === SHA),
82
+ pending: port.appended.filter((p) => p.gitSha === SHA),
83
+ resolutions: [resolution].filter((r) => r.gitSha === SHA),
84
+ releases: [release].filter((r) => r.gitSha === SHA),
85
+ };
86
+ expect(byCommit.runs.map((r) => r.id)).toContain(run.id);
87
+ expect(byCommit.pending).toEqual([pending]);
88
+ expect(byCommit.resolutions).toEqual([resolution]);
89
+ expect(byCommit.releases).toEqual([release]);
90
+
91
+ // From the run: its gate, by run id, and the CI run it ran in.
92
+ expect(run.gate).toMatchObject({ name: pending.gate });
93
+ expect(resolution.runId).toBe(run.id);
94
+ expect(pending.runId).toBe(run.id);
95
+
96
+ // From the deploy: the gate resolution that admitted it, the approver, and the CI run the run was in.
97
+ expect(release.admission).toEqual({ op: pending.op, gate: pending.gate, resolvedAt: resolution.timestamp });
98
+ expect(release.approver).toBe(resolution.resolvedBy);
99
+ expect(release.runOrigin).toEqual(run.runOrigin);
100
+ expect(release.runId).toBe(runOriginOfLocator(resolveGateRunLocator()!).runId);
101
+ expect(release.gitSha).toBe(run.gitSha);
102
+ });
103
+
104
+ test("a run outside CI records its commit and no run origin", async () => {
105
+ for (const k of ["GITHUB_RUN_ID", "CI_PIPELINE_ID"]) vi.stubEnv(k, "");
106
+ const result = await runOpLocally(config, activities, PROFILES, undefined, { gates: memoryGateLedgerPort(), now: NOW, commit: SHA });
107
+ expect(result.record.gitSha).toBe(SHA);
108
+ expect(result.record).not.toHaveProperty("runOrigin");
109
+ });
110
+
111
+ test("admissionOf names the newest of several approvals and joins their approvers", () => {
112
+ const at = (resolvedBy: string, timestamp: string): GateResolutionRecord => ({ version: 1, op: "ship", gate: "g", resolvedBy, timestamp, planDigest: "p" });
113
+ expect(admissionOf([at("a", "2026-01-01T00:00:00Z"), at("b", "2026-01-02T00:00:00Z"), at("a", "2026-01-03T00:00:00Z")], { op: "ship", gate: "g", planDigest: "p" }))
114
+ .toEqual({ approver: "a, b", admission: { op: "ship", gate: "g", resolvedAt: "2026-01-03T00:00:00Z" } });
115
+ expect(admissionOf([at("a", "2026-01-01T00:00:00Z")], { op: "ship", gate: "g", planDigest: "other" })).toBeUndefined();
116
+ });
117
+ });
@@ -170,3 +170,42 @@ describe("discoverOps: an Op may be exported by name (#2171)", () => {
170
170
  expect(errors[0]).toContain(`Duplicate Op name "twice" declared twice in`);
171
171
  });
172
172
  });
173
+
174
+ /**
175
+ * #3976. Reading the project's `exclude`/`include` evaluates its config,
176
+ * which registers tsx, so discovery reads them only when an Op file could be
177
+ * found. An `exclude` that is not a list is an error whenever the config is
178
+ * read, which is how these tests tell whether it was.
179
+ */
180
+ describe("discoverOps reads the config's globs only when there is an Op file (#3976)", () => {
181
+ let checkout: string;
182
+ beforeEach(() => {
183
+ checkout = realpathSync(mkdtempSync(join(tmpdir(), "chant-op-globs-")));
184
+ fakeGitRoot = checkout;
185
+ mkdirSync(join(checkout, "src"), { recursive: true });
186
+ writeFileSync(join(checkout, "src", "infra.ts"), "export const x = 1;\n");
187
+ });
188
+ afterEach(() => rmSync(checkout, { recursive: true, force: true }));
189
+
190
+ test("a project with no Op file has its config left unread", async () => {
191
+ writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], exclude: 42 }));
192
+ const { ops, errors } = await discoverOps({ cwd: checkout });
193
+ expect(errors).toEqual([]);
194
+ expect([...ops.keys()]).toEqual([]);
195
+ });
196
+
197
+ test("with an Op file the config is read", async () => {
198
+ writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], exclude: 42 }));
199
+ writeFileSync(join(checkout, "src", "run.op.ts"), OP_FILE("run"));
200
+ await expect(discoverOps({ cwd: checkout })).rejects.toThrow(/exclude/);
201
+ });
202
+
203
+ test("an Op file only `include` re-admits is still found", async () => {
204
+ writeFileSync(join(checkout, "chant.config.json"), JSON.stringify({ lexicons: ["aws"], include: [".ops"] }));
205
+ mkdirSync(join(checkout, ".ops"));
206
+ writeFileSync(join(checkout, ".ops", "hidden.op.ts"), OP_FILE("hidden"));
207
+ const { ops, errors } = await discoverOps({ cwd: checkout });
208
+ expect(errors).toEqual([]);
209
+ expect([...ops.keys()]).toEqual(["hidden"]);
210
+ });
211
+ });
@@ -57,22 +57,29 @@ async function findDiscoveryRoot(cwd?: string): Promise<string> {
57
57
  return gitRoot;
58
58
  }
59
59
 
60
+ const isOpFile = (name: string, full: string): boolean =>
61
+ name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full);
62
+
60
63
  /**
61
64
  * Every `*.op.ts` file below the Op root, through the one discovery walk
62
65
  * (`../discovery/walk.ts`, #2527). The walk down stops at child projects
63
66
  * whatever the root is, so a configless layout whose Op root is the git root
64
67
  * no longer collects its sibling projects' Ops. The project's
65
68
  * `exclude`/`include` globs and the skip marker apply.
69
+ *
70
+ * The globs come from evaluating the project config, which registers tsx
71
+ * (#3976), so they are resolved only when an Op file could be found. A walk
72
+ * that includes `**` first reaches every path any `include` could re-admit,
73
+ * and stops at the first Op file. When it finds none, no globs could find
74
+ * one either.
66
75
  */
67
76
  async function collectOpFiles(root: string): Promise<string[]> {
68
- return walkDiscovery({
69
- walker: "ops",
70
- root,
71
- globs: await resolveDiscoveryGlobs(root),
72
- excludeDirs: await workspaceMemberDirs(root),
73
- accept: (name, full) =>
74
- name.endsWith(".op.ts") && !name.endsWith(".test.ts") && !name.endsWith(".spec.ts") && !hasDiscoveryMarkerSync(full),
75
- });
77
+ const excludeDirs = await workspaceMemberDirs(root);
78
+ // A limit of 0 ends the walk at the first file it accepts.
79
+ const any = { max: 0, truncated: false };
80
+ walkDiscovery({ walker: "ops", root, globs: { root, exclude: [], include: ["**"] }, excludeDirs, limit: any, accept: isOpFile });
81
+ if (!any.truncated) return [];
82
+ return walkDiscovery({ walker: "ops", root, globs: await resolveDiscoveryGlobs(root), excludeDirs, accept: isOpFile });
76
83
  }
77
84
 
78
85
  /** The `OpConfig` behind an exported value, or `undefined` when the value is not an Op. An Op entity carries its config on `.props` (`./resource.ts`); `name` and `phases` are what every consumer of a discovered Op reads. */
@@ -44,6 +44,16 @@ export const WORKSPACE_UPGRADE_GATE_OP = "workspace-upgrade";
44
44
  */
45
45
  export const OVERRIDE_GATE_OP = "policy-override";
46
46
 
47
+ /**
48
+ * The op name every release gate is recorded under (#4045): the gates
49
+ * `chant components promote --release` reaches, `_gates/release.jsonl`, each
50
+ * named as its composition names it and bound to a release, an environment
51
+ * and the release's change set. Like a fan-out, it has no `*.op.ts`, and like
52
+ * the pull-request gate it approves a change to an environment, so it is
53
+ * never resolved over MCP or ACP.
54
+ */
55
+ export const RELEASE_GATE_OP = "release";
56
+
47
57
  /**
48
58
  * The pull-request loop records its gate under `pr-<number>`, or
49
59
  * `pr-<number>-<member>` in a workspace member (`prOp` in `pr-loop.ts`), and
@@ -54,12 +64,12 @@ const PR_GATE_OP = /^pr-\d+(?:-[a-z0-9][a-z0-9-]*)?$/;
54
64
  /**
55
65
  * Whether gates recorded under this op name are never resolved over MCP or ACP
56
66
  * (chant#3485, chant#3513). These are the gate-only names MCP cannot discover:
57
- * `fan-out`, `workspace-upgrade`, `policy-override` (#3760) and the
58
- * pull-request gate `pr-<number>`. With
67
+ * `fan-out`, `workspace-upgrade`, `policy-override` (#3760), `release`
68
+ * (#4045) and the pull-request gate `pr-<number>`. With
59
69
  * no Op config to read `neverOverMcp` from, the pending record carries it.
60
70
  */
61
71
  export function isNeverOverMcpGateOp(op: string): boolean {
62
- return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || PR_GATE_OP.test(op);
72
+ return op === FAN_OUT_GATE_OP || op === WORKSPACE_UPGRADE_GATE_OP || op === OVERRIDE_GATE_OP || op === RELEASE_GATE_OP || PR_GATE_OP.test(op);
63
73
  }
64
74
 
65
75
  /** Either spelling of a gate step's name. `gate` since #2202; `signalName` through 0.59.0. */
@@ -0,0 +1,126 @@
1
+ /**
2
+ * A release gate (#4045, ws-104 question 2) binds the release, the
3
+ * environment and the change-set document digest; an approval counts only
4
+ * when all three match. An `empty` pass (question 6) counts only once the
5
+ * stored document reads back empty. Decided against an in-memory ledger.
6
+ */
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { evaluateGate, memoryGateLedgerPort } from "./gate";
10
+ import type { GateResolutionRecord } from "../lifecycle/gate-ledger";
11
+
12
+ const sha = (c: string) => `jcs1-sha256:${c.repeat(64)}`;
13
+ const RELEASE_A = sha("a");
14
+ const RELEASE_B = sha("b");
15
+ const DOC = sha("d");
16
+ const NOW = "2026-10-16T12:00:00.000Z";
17
+ const LATER = "2026-10-16T13:00:00.000Z";
18
+
19
+ const gateInput = (over: Partial<Parameters<typeof evaluateGate>[1]> = {}) => ({
20
+ op: "release",
21
+ gate: "prod-release",
22
+ environment: "prod",
23
+ planDigest: DOC,
24
+ releaseDigest: RELEASE_A,
25
+ now: NOW,
26
+ ...over,
27
+ });
28
+
29
+ const approval = (over: Partial<GateResolutionRecord> = {}): GateResolutionRecord => ({
30
+ version: 1,
31
+ op: "release",
32
+ gate: "prod-release",
33
+ resolvedBy: "alice",
34
+ timestamp: "2026-10-16T12:30:00.000Z",
35
+ planDigest: DOC,
36
+ environment: "prod",
37
+ releaseDigest: RELEASE_A,
38
+ ...over,
39
+ });
40
+
41
+ describe("a release gate binds the release, the environment and the change set", () => {
42
+ test("the pending fact names the release it waits on", async () => {
43
+ const port = memoryGateLedgerPort();
44
+ const check = await evaluateGate(port, gateInput());
45
+ expect(check.satisfied).toBe(false);
46
+ expect(port.appended[0]).toMatchObject({ op: "release", gate: "prod-release", environment: "prod", planDigest: DOC, releaseDigest: RELEASE_A });
47
+ });
48
+
49
+ test("an approval for the release, environment and change set passes, and names the release it admitted", async () => {
50
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
51
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
52
+ expect(check.satisfied).toBe(true);
53
+ if (check.satisfied) expect(check.resolution.releaseDigest).toBe(RELEASE_A);
54
+ });
55
+
56
+ test("an approval for one release can't be used by another, even for the same change-set digest", async () => {
57
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
58
+ const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: LATER }));
59
+ expect(check.satisfied).toBe(false);
60
+ if (!check.satisfied) {
61
+ expect(check.mismatch).toMatchObject({ resolvedBy: "alice", approved: DOC, planned: DOC, environment: "prod" });
62
+ expect(check.pending.releaseDigest).toBe(RELEASE_B);
63
+ }
64
+ });
65
+
66
+ test("an approval for one environment can't be used for another", async () => {
67
+ const port = memoryGateLedgerPort({ resolutions: [approval({ environment: "staging" })] });
68
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
69
+ expect(check.satisfied).toBe(false);
70
+ if (!check.satisfied) expect(check.pending.environment).toBe("prod");
71
+ });
72
+
73
+ test("an approval for another change set of the same release doesn't pass", async () => {
74
+ const port = memoryGateLedgerPort({ resolutions: [approval({ planDigest: sha("e") })] });
75
+ const check = await evaluateGate(port, gateInput({ now: LATER }));
76
+ expect(check.satisfied).toBe(false);
77
+ if (!check.satisfied) expect(check.mismatch?.approved).toBe(sha("e"));
78
+ });
79
+
80
+ test("a gate that binds no release counts no approval recorded for one", async () => {
81
+ const port = memoryGateLedgerPort({ resolutions: [approval()] });
82
+ const check = await evaluateGate(port, gateInput({ releaseDigest: undefined, now: LATER }));
83
+ expect(check.satisfied).toBe(false);
84
+ });
85
+
86
+ test("a standing fact for another release is recorded again for this one", async () => {
87
+ const port = memoryGateLedgerPort();
88
+ await evaluateGate(port, gateInput());
89
+ const again = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, now: "2026-10-16T12:01:00.000Z" }));
90
+ expect(again.satisfied).toBe(false);
91
+ if (!again.satisfied) expect(again.recorded).toBe(true);
92
+ expect(port.appended.map((p) => p.releaseDigest)).toEqual([RELEASE_A, RELEASE_B]);
93
+ });
94
+ });
95
+
96
+ describe("an empty pass", () => {
97
+ const pass = approval({ resolvedBy: "bob", empty: true, timestamp: "2026-10-16T11:59:00.000Z" });
98
+
99
+ test("passes the gate once the stored change set reads back empty", async () => {
100
+ const asked: string[] = [];
101
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
102
+ const check = await evaluateGate(port, gateInput({ verifyEmpty: async (d) => (asked.push(d), true) }));
103
+ expect(check).toMatchObject({ satisfied: true, via: "empty", resolution: { resolvedBy: "bob", empty: true, releaseDigest: RELEASE_A } });
104
+ expect(asked).toEqual([DOC]);
105
+ expect(port.appended).toEqual([]);
106
+ });
107
+
108
+ test("doesn't pass when the stored change set is not empty", async () => {
109
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
110
+ const check = await evaluateGate(port, gateInput({ verifyEmpty: async () => false }));
111
+ expect(check.satisfied).toBe(false);
112
+ });
113
+
114
+ test("is never an approval: without a verifier, or on a gate without a plan, it counts for nothing", async () => {
115
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
116
+ expect((await evaluateGate(port, gateInput())).satisfied).toBe(false);
117
+ const plain = memoryGateLedgerPort({ resolutions: [{ ...pass, planDigest: undefined, releaseDigest: undefined }] });
118
+ expect((await evaluateGate(plain, { op: "release", gate: "prod-release", now: NOW })).satisfied).toBe(false);
119
+ });
120
+
121
+ test("for another release it doesn't pass", async () => {
122
+ const port = memoryGateLedgerPort({ resolutions: [pass] });
123
+ const check = await evaluateGate(port, gateInput({ releaseDigest: RELEASE_B, verifyEmpty: async () => true }));
124
+ expect(check.satisfied).toBe(false);
125
+ });
126
+ });
@@ -0,0 +1,110 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { ciCommitFromEnv, ciRunIdFromEnv, resolveGateRunLocator, runOriginOfLocator } from "./gate-run-locator";
3
+ import { resolveCiRun, resolveRunId } from "../lifecycle/release-ledger";
4
+
5
+ const GITHUB = {
6
+ GITHUB_ACTIONS: "true",
7
+ GITHUB_REPOSITORY: "acme/app",
8
+ GITHUB_RUN_ID: "9001",
9
+ GITHUB_RUN_ATTEMPT: "2",
10
+ GITHUB_SERVER_URL: "https://github.example",
11
+ GITHUB_API_URL: "https://github.example/api/v3",
12
+ GITHUB_SHA: "a".repeat(40),
13
+ };
14
+ const GITLAB = {
15
+ GITLAB_CI: "true",
16
+ CI_API_V4_URL: "https://gitlab.example/api/v4",
17
+ CI_PROJECT_ID: "77",
18
+ CI_PROJECT_PATH: "acme/app",
19
+ CI_PIPELINE_ID: "5150",
20
+ CI_PIPELINE_URL: "https://gitlab.example/acme/app/-/pipelines/5150",
21
+ CI_JOB_ID: "31",
22
+ CI_JOB_NAME: "deploy",
23
+ CI_JOB_URL: "https://gitlab.example/acme/app/-/jobs/31",
24
+ CI_COMMIT_SHA: "b".repeat(40),
25
+ };
26
+ const FORGEJO = {
27
+ ...GITHUB,
28
+ FORGEJO_ACTIONS: "true",
29
+ GITHUB_WORKFLOW_REF: "acme/app/.forgejo/workflows/deploy.yml@refs/heads/main",
30
+ GITHUB_REF_NAME: "main",
31
+ GITHUB_SERVER_URL: "https://forgejo.example",
32
+ GITHUB_API_URL: "https://forgejo.example/api/v1",
33
+ };
34
+
35
+ describe("runOriginOfLocator (#4020)", () => {
36
+ test("a GitHub locator maps to forge github, with its repo and run page, and the run as the run id", () => {
37
+ const locator = resolveGateRunLocator(GITHUB)!;
38
+ expect(runOriginOfLocator(locator)).toEqual({
39
+ runId: "9001",
40
+ runOrigin: { forge: "github", repo: "acme/app", url: "https://github.example/acme/app/actions/runs/9001" },
41
+ });
42
+ });
43
+
44
+ test("a Forgejo locator maps to forge github, since the url names the host", () => {
45
+ const locator = resolveGateRunLocator(FORGEJO)!;
46
+ expect(locator.forge).toBe("forgejo");
47
+ expect(runOriginOfLocator(locator)).toEqual({
48
+ runId: "9001",
49
+ runOrigin: { forge: "github", repo: "acme/app", url: "https://forgejo.example/acme/app/actions/runs/9001" },
50
+ });
51
+ });
52
+
53
+ test("a GitLab locator stays gitlab, with the project id as repo and the pipeline id as the run id", () => {
54
+ const locator = resolveGateRunLocator(GITLAB)!;
55
+ expect(runOriginOfLocator(locator)).toEqual({
56
+ runId: "5150",
57
+ runOrigin: { forge: "gitlab", repo: "77", url: "https://gitlab.example/acme/app/-/jobs/31" },
58
+ });
59
+ });
60
+
61
+ test("the API base, attempt, job and workflow fields do not reach the origin", () => {
62
+ const { runOrigin } = runOriginOfLocator(resolveGateRunLocator(GITHUB)!);
63
+ expect(Object.keys(runOrigin).sort()).toEqual(["forge", "repo", "url"]);
64
+ });
65
+
66
+ test("a locator without a url maps to an origin without one", () => {
67
+ const { api, repo, run } = resolveGateRunLocator(GITHUB)!;
68
+ expect(runOriginOfLocator({ forge: "github", api, repo, run })).toEqual({ runId: "9001", runOrigin: { forge: "github", repo: "acme/app" } });
69
+ });
70
+ });
71
+
72
+ describe("a GateRunLocator and the RunOrigin written for the same CI job name the same run (#4020)", () => {
73
+ test.each([
74
+ ["GitHub", GITHUB, "github"],
75
+ ["Forgejo", FORGEJO, "github"],
76
+ ["GitLab", GITLAB, "gitlab"],
77
+ ] as const)("%s", (_name, env, forge) => {
78
+ const locator = resolveGateRunLocator(env)!;
79
+ const fromLocator = runOriginOfLocator(locator);
80
+ const written = resolveCiRun(env);
81
+ // A deploy record's run id is the one resolveRunId gives, and the gate's is the CI run id.
82
+ expect(resolveRunId(undefined, env).runId).toBe(fromLocator.runId);
83
+ expect(ciRunIdFromEnv(env)).toBe(fromLocator.runId);
84
+ expect(written?.runId).toBe(fromLocator.runId);
85
+ expect(written?.runOrigin.forge).toBe(forge);
86
+ expect(fromLocator.runOrigin.forge).toBe(written?.runOrigin.forge);
87
+ });
88
+
89
+ test("on GitHub and Forgejo the two origins are the same object, repo and url included", () => {
90
+ for (const env of [GITHUB, FORGEJO]) {
91
+ expect(runOriginOfLocator(resolveGateRunLocator(env)!).runOrigin).toEqual(resolveCiRun(env)!.runOrigin);
92
+ }
93
+ });
94
+
95
+ test("on GitLab the locator keeps the project id and the job page where the origin keeps the path and the pipeline page", () => {
96
+ expect(runOriginOfLocator(resolveGateRunLocator(GITLAB)!).runOrigin).toEqual({ forge: "gitlab", repo: "77", url: GITLAB.CI_JOB_URL });
97
+ expect(resolveCiRun(GITLAB)!.runOrigin).toEqual({ forge: "gitlab", repo: "acme/app", url: GITLAB.CI_PIPELINE_URL });
98
+ });
99
+ });
100
+
101
+ describe("the CI environment helpers", () => {
102
+ test("the commit is GITHUB_SHA or CI_COMMIT_SHA, and none outside CI", () => {
103
+ expect(ciCommitFromEnv(GITHUB)).toBe("a".repeat(40));
104
+ expect(ciCommitFromEnv(GITLAB)).toBe("b".repeat(40));
105
+ expect(ciCommitFromEnv({})).toBeUndefined();
106
+ });
107
+ test("the run id is none outside CI", () => {
108
+ expect(ciRunIdFromEnv({})).toBeUndefined();
109
+ });
110
+ });
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { ForgeKind } from "../pr-forge";
10
+ import type { RunOrigin } from "../lifecycle/release-ledger";
10
11
 
11
12
  /** Where the job that recorded a pending fact runs, and what its forge needs to start it again. */
12
13
  export interface GateRunLocator {
@@ -103,3 +104,37 @@ export function isGateRunLocator(value: unknown): value is GateRunLocator {
103
104
  if (v.forge === "forgejo") return typeof v.workflow === "string" && typeof v.ref === "string";
104
105
  return false;
105
106
  }
107
+
108
+ /**
109
+ * The CI run a {@link GateRunLocator} names, in the shape a run and a deploy
110
+ * record carry (#4020). `runId` is the locator's `run`. Forge `github` and
111
+ * `forgejo` both become `github`, as `RunOrigin.forge` documents (the `url`
112
+ * names the host), and `gitlab` stays `gitlab`. `repo` and `url` carry over.
113
+ * The API base, attempt, job and workflow fields stay on the locator, which
114
+ * exists to start the job again.
115
+ */
116
+ export function runOriginOfLocator(locator: GateRunLocator): { runId: string; runOrigin: RunOrigin } {
117
+ return {
118
+ runId: locator.run,
119
+ runOrigin: {
120
+ forge: locator.forge === "gitlab" ? "gitlab" : "github",
121
+ repo: locator.repo,
122
+ ...(locator.url ? { url: locator.url } : {}),
123
+ },
124
+ };
125
+ }
126
+
127
+ /**
128
+ * The id of the CI run this process is in, or undefined outside CI: the
129
+ * GitHub (and Forgejo) run id, else the GitLab pipeline id. The same
130
+ * precedence `resolveRunId` (../lifecycle/release-ledger.ts) uses for a
131
+ * deploy record, so a gate and a deploy name the run alike.
132
+ */
133
+ export function ciRunIdFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
134
+ return env.GITHUB_RUN_ID || env.CI_PIPELINE_ID || undefined;
135
+ }
136
+
137
+ /** The commit the CI job was started for: `GITHUB_SHA` (GitHub and Forgejo) or `CI_COMMIT_SHA` (GitLab). Undefined outside CI. */
138
+ export function ciCommitFromEnv(env: Record<string, string | undefined> = process.env): string | undefined {
139
+ return env.GITHUB_SHA || env.CI_COMMIT_SHA || undefined;
140
+ }