@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,341 @@
1
+ /**
2
+ * The release gate (#4045, ws-104 questions 2 and 6): the gate `chant
3
+ * components promote --release` passes for an environment.
4
+ *
5
+ * ## What it binds
6
+ *
7
+ * A promote under a release plans the release's change set for the
8
+ * environment it deploys to ({@link planReleaseChangeSet}), stores it on
9
+ * `chant/lifecycle` under its digest (`../lifecycle/gate-change-set.ts`), and
10
+ * decides every gate its components reach against three values:
11
+ *
12
+ * - the change-set document's `digest`, in `planDigest`;
13
+ * - the release, in `releaseDigest`;
14
+ * - the environment, in `environment`.
15
+ *
16
+ * An approval counts only when all three match (`evaluateGate` in
17
+ * `../op/gate.ts`). The document itself names the release and the environment
18
+ * it was planned for, and both are part of its digest, so a stored document
19
+ * names the one release its gate bound and two releases never share a digest.
20
+ *
21
+ * Every gate a promote under a release reaches is recorded in one ledger,
22
+ * {@link RELEASE_GATE_OP}, rather than in each component's. The gate name a
23
+ * composition authors is kept, so components that share a gate name share one
24
+ * gate per environment, and one approval of the release's change set passes
25
+ * each of them. A promote of one component (`--component`) plans that
26
+ * component's subset, which has its own digest and needs its own approval.
27
+ *
28
+ * ## Empty change sets
29
+ *
30
+ * When the change set changes nothing ({@link isEmptyChangeSet}), the
31
+ * promote writes an `empty: true` resolution for each gate, with the actor
32
+ * running it in `resolvedBy` ({@link emptyResolution}). A gate counts it only
33
+ * after it reads the stored document back and finds it empty
34
+ * ({@link emptyVerifier}). Quorum and signing rules don't apply to it.
35
+ *
36
+ * ## The members
37
+ *
38
+ * A component with a step whose capability can plan (`terraform-apply`) is
39
+ * planned by that capability, as a pull request's plan is. A plan that
40
+ * returns no change-set part is a hole, since nothing says what it changes.
41
+ *
42
+ * Any other component is a `chant` member compared with what the
43
+ * environment runs now: its latest deploy record and the release that record
44
+ * names. The artifact digest, each template the release's target records for
45
+ * the component, and the target's parameters are compared, and each one that
46
+ * differs is an attribute of one `update` entry (a `create` when the
47
+ * environment has no deploy of the component). When they are all equal the
48
+ * member has no entries. When the environment's deploy names no release, or
49
+ * the release it names can't be read, the member has a hole: nothing says
50
+ * what the environment runs, so the change can't be shown empty.
51
+ */
52
+
53
+ import type { CapabilityRegistry } from "./capability";
54
+ import { deployContext, resolveComponentGraph, resolveStepInput, type DriverComponent, type DriverGate, type DriverPhase, type DriverStep } from "./driver";
55
+ import { componentPlanDigest } from "./gate-plan";
56
+ import { gateName, RELEASE_GATE_OP } from "../op/gate-name";
57
+ import { capabilitySteps, readComponentOutputs } from "./wave-plan";
58
+ import {
59
+ composeChangeSet,
60
+ isEmptyChangeSet,
61
+ type ChangeSetAttribute,
62
+ type ChangeSetDocument,
63
+ type ChangeSetPart,
64
+ } from "../change-set";
65
+ import type { Release } from "../lifecycle/release-store";
66
+ import type { ReleaseRecord } from "../lifecycle/release-ledger";
67
+ import type { GateResolutionInput } from "../lifecycle/gate-ledger";
68
+ import type { StoredChangeSetRead } from "../lifecycle/change-set-store";
69
+
70
+ /** The gate ledger every release gate is recorded in: `_gates/release.jsonl`, under the member's prefix. */
71
+ export { RELEASE_GATE_OP };
72
+
73
+ /** The address and type a `chant` member's one entry has. */
74
+ export const RELEASE_COMPONENT_ENTRY_TYPE = "chant:component";
75
+
76
+ function isGate(step: DriverStep | DriverGate | DriverPhase): step is DriverGate {
77
+ return (step as { kind?: unknown }).kind === "gate";
78
+ }
79
+
80
+ function isPhase(step: DriverStep | DriverGate | DriverPhase): step is DriverPhase {
81
+ return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
82
+ }
83
+
84
+ /** The name of every gate the compositions declare, nested phases included, sorted and distinct. */
85
+ export function gateNamesOf(components: readonly DriverComponent[]): string[] {
86
+ const names = new Set<string>();
87
+ const walk = (steps: DriverPhase["steps"]): void => {
88
+ for (const step of steps) {
89
+ if (isPhase(step)) walk(step.steps);
90
+ else if (isGate(step)) names.add(gateName(step));
91
+ }
92
+ };
93
+ for (const c of components) for (const phase of c.deploy) walk(phase.steps);
94
+ return [...names].sort();
95
+ }
96
+
97
+ /** What an environment runs of one component now: its latest deploy record, and the release that record names when it can be read. */
98
+ export interface CurrentDeploy {
99
+ record: ReleaseRecord;
100
+ release: Release | null;
101
+ }
102
+
103
+ export interface ReleaseChangeSetInput {
104
+ release: Release;
105
+ /** The environment promoted to. */
106
+ env: string;
107
+ /** The promoted compositions, with build steps taken out and templates pinned to the release. */
108
+ components: readonly DriverComponent[];
109
+ /** The artifact digest each component is promoted at. */
110
+ digests: ReadonlyMap<string, string>;
111
+ registry: CapabilityRegistry;
112
+ /** Per component, what `env` runs now. A component missing here has never been deployed to it. */
113
+ current: ReadonlyMap<string, CurrentDeploy>;
114
+ /** Outputs supplied by the caller (`--seed-outputs`). */
115
+ seededOutputs?: Record<string, Record<string, unknown>>;
116
+ /** The chant version to record in the document. */
117
+ chant?: string;
118
+ }
119
+
120
+ const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
121
+
122
+ /** One attribute per key whose value differs between `before` and `after`, sorted by path. */
123
+ function differing(before: Map<string, string>, after: Map<string, string>, prefix: string): ChangeSetAttribute[] {
124
+ const out: ChangeSetAttribute[] = [];
125
+ for (const key of [...new Set([...before.keys(), ...after.keys()])].sort(byString)) {
126
+ const was = before.get(key);
127
+ const now = after.get(key);
128
+ if (was === now) continue;
129
+ out.push({ path: `${prefix}${key}`, ...(was !== undefined ? { before: was } : {}), ...(now !== undefined ? { after: now } : {}) });
130
+ }
131
+ return out;
132
+ }
133
+
134
+ /** What one target says a component deploys: its templates by path, and the target's parameters by name. */
135
+ function deployedInputs(release: Release, env: string, component: string): { templates: Map<string, string>; params: Map<string, string> } | null {
136
+ const target = release.targets.find((t) => t.env === env);
137
+ if (!target) return null;
138
+ return {
139
+ templates: new Map(target.templates.filter((t) => t.component === component).map((t) => [t.path, t.digest])),
140
+ params: new Map(target.params.map((p) => [p.name, JSON.stringify(p.value)])),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * A component no capability plans, as a `chant` member: what the release
146
+ * deploys compared with what the environment runs now. See the module doc.
147
+ */
148
+ export function chantComponentPart(input: {
149
+ component: DriverComponent;
150
+ release: Release;
151
+ env: string;
152
+ digest: string;
153
+ current: CurrentDeploy | undefined;
154
+ }): ChangeSetPart {
155
+ const { component, release, env, digest, current } = input;
156
+ const name = component.name;
157
+ const member = {
158
+ member: name,
159
+ lexicon: "chant",
160
+ planner: "chant" as const,
161
+ status: "planned" as const,
162
+ planDigest: componentPlanDigest({ environment: env, component, release: digest }),
163
+ holes: [] as Array<{ address: string; type?: string; reason: string }>,
164
+ };
165
+ const want = deployedInputs(release, env, name);
166
+ if (!want) {
167
+ member.holes.push({ address: name, type: RELEASE_COMPONENT_ENTRY_TYPE, reason: `release ${release.releaseDigest} has no target for "${env}"` });
168
+ return { member, entries: [] };
169
+ }
170
+ const entry = (action: "create" | "update", attributes: ChangeSetAttribute[]): ChangeSetPart => ({
171
+ member,
172
+ entries: [
173
+ {
174
+ member: name,
175
+ lexicon: "chant",
176
+ planner: "chant",
177
+ address: name,
178
+ type: RELEASE_COMPONENT_ENTRY_TYPE,
179
+ name,
180
+ action,
181
+ ...(action === "update" ? { disruption: "unknown" as const } : {}),
182
+ scope: env,
183
+ attributes,
184
+ },
185
+ ],
186
+ });
187
+ if (!current) {
188
+ return entry("create", [
189
+ { path: "artifact", after: digest },
190
+ ...differing(new Map(), want.templates, "template:"),
191
+ ...differing(new Map(), want.params, "param:"),
192
+ ]);
193
+ }
194
+ const named = current.record.releaseDigest;
195
+ if (named === undefined) {
196
+ member.holes.push({
197
+ address: name,
198
+ type: RELEASE_COMPONENT_ENTRY_TYPE,
199
+ reason: `the "${env}" deploy of ${name} (run ${current.record.runId}) names no release, so what it runs can't be compared with this release`,
200
+ });
201
+ return { member, entries: [] };
202
+ }
203
+ const have = current.release ? deployedInputs(current.release, env, name) : null;
204
+ if (!have) {
205
+ member.holes.push({
206
+ address: name,
207
+ type: RELEASE_COMPONENT_ENTRY_TYPE,
208
+ reason: current.release
209
+ ? `release ${named}, which the "${env}" deploy of ${name} names, has no target for "${env}"`
210
+ : `release ${named}, which the "${env}" deploy of ${name} names, can't be read from chant/lifecycle`,
211
+ });
212
+ return { member, entries: [] };
213
+ }
214
+ const attributes = [
215
+ ...differing(new Map([["", current.record.digest]]), new Map([["", digest]]), "artifact"),
216
+ ...differing(have.templates, want.templates, "template:"),
217
+ ...differing(have.params, want.params, "param:"),
218
+ ];
219
+ return attributes.length === 0 ? { member, entries: [] } : entry("update", attributes);
220
+ }
221
+
222
+ /**
223
+ * Plan the release's change set for `env`: one part per promoted component
224
+ * (or per member its planning capability names), composed into a document
225
+ * that names the release and the environment. Never throws for a component
226
+ * that fails to plan: it becomes a failed member.
227
+ */
228
+ export async function planReleaseChangeSet(input: ReleaseChangeSetInput): Promise<ChangeSetDocument> {
229
+ const { release, env, registry } = input;
230
+ const components = [...input.components];
231
+ const byName = new Map(components.map((c) => [c.name, c]));
232
+ const outputs: Record<string, Record<string, unknown>> = { ...(input.seededOutputs ?? {}) };
233
+ const read = new Set(Object.keys(outputs));
234
+ const parts: ChangeSetPart[] = [];
235
+ const { order } = resolveComponentGraph(components.map((c) => ({ ...c, dependsOn: (c.dependsOn ?? []).filter((d) => byName.has(d)) })));
236
+ for (const name of order) {
237
+ const component = byName.get(name)!;
238
+ const digest = input.digests.get(name) ?? "";
239
+ const plannable = capabilitySteps(component.deploy).filter((s) => registry.has(s.kind) && registry.resolve(s.kind).plan !== undefined);
240
+ if (plannable.length === 0) {
241
+ parts.push(chantComponentPart({ component, release, env, digest, current: input.current.get(name) }));
242
+ continue;
243
+ }
244
+ const ctx = deployContext({ env }, name);
245
+ try {
246
+ for (const dep of component.dependsOn ?? []) {
247
+ if (read.has(dep) || !byName.has(dep)) continue;
248
+ read.add(dep);
249
+ const found = await readComponentOutputs(byName.get(dep)!, registry, deployContext({ env }, dep), outputs);
250
+ if (found) outputs[dep] = found;
251
+ }
252
+ for (const step of plannable) {
253
+ const { kind, ...rest } = step;
254
+ const planned = await registry.resolve(kind).plan!(ctx, resolveStepInput(rest, {}, outputs) as never);
255
+ parts.push(
256
+ planned.changeSet ?? {
257
+ member: {
258
+ member: planned.member,
259
+ lexicon: "chant",
260
+ planner: "chant",
261
+ status: "planned",
262
+ planDigest: planned.planDigest,
263
+ holes: [{ address: planned.member, reason: `its ${kind} plan describes no changes, so what it changes is unknown` }],
264
+ },
265
+ entries: [],
266
+ },
267
+ );
268
+ }
269
+ } catch (err) {
270
+ parts.push({
271
+ member: {
272
+ member: name,
273
+ lexicon: "chant",
274
+ planner: "chant",
275
+ status: "failed",
276
+ error: err instanceof Error ? err.message : String(err),
277
+ planDigest: null,
278
+ holes: [],
279
+ },
280
+ entries: [],
281
+ });
282
+ }
283
+ }
284
+ return composeChangeSet(parts, {
285
+ ...(input.chant !== undefined ? { chant: input.chant } : {}),
286
+ planned: { releaseDigest: release.releaseDigest, environment: env },
287
+ });
288
+ }
289
+
290
+ /** The members a release gate refuses: provisional ones, which no approval of the document covers (`change-set.mdx`). */
291
+ export function provisionalMembers(doc: ChangeSetDocument): string[] {
292
+ return doc.members.filter((m) => m.provisional === true).map((m) => m.member);
293
+ }
294
+
295
+ /**
296
+ * The `verifyEmpty` a release gate decides with: the document stored under
297
+ * the digest, read back and checked against its digest by `read`, is the
298
+ * one planned for this release and environment, and it is empty.
299
+ */
300
+ export function emptyVerifier(
301
+ read: (digest: string) => Promise<StoredChangeSetRead>,
302
+ planned: { releaseDigest: string; environment: string },
303
+ ): (digest: string) => Promise<boolean> {
304
+ return async (digest) => {
305
+ const stored = await read(digest);
306
+ return (
307
+ stored.status === "found" &&
308
+ stored.document.releaseDigest === planned.releaseDigest &&
309
+ stored.document.environment === planned.environment &&
310
+ isEmptyChangeSet(stored.document)
311
+ );
312
+ };
313
+ }
314
+
315
+ /** The `empty: true` resolution a promote writes for one release gate over an empty change set. */
316
+ export function emptyResolution(input: {
317
+ gate: string;
318
+ doc: ChangeSetDocument;
319
+ actor: string;
320
+ timestamp: string;
321
+ gitSha?: string;
322
+ runId?: string;
323
+ }): GateResolutionInput {
324
+ const { doc } = input;
325
+ if (doc.releaseDigest === undefined || doc.environment === undefined) {
326
+ throw new Error(`change set ${doc.digest} names no release and environment, so no release gate can pass on it`);
327
+ }
328
+ return {
329
+ op: RELEASE_GATE_OP,
330
+ gate: input.gate,
331
+ resolvedBy: input.actor,
332
+ timestamp: input.timestamp,
333
+ note: `the change set of release ${doc.releaseDigest} for "${doc.environment}" is empty`,
334
+ planDigest: doc.digest,
335
+ environment: doc.environment,
336
+ releaseDigest: doc.releaseDigest,
337
+ empty: true,
338
+ ...(input.gitSha ? { gitSha: input.gitSha } : {}),
339
+ ...(input.runId ? { runId: input.runId } : {}),
340
+ };
341
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * A promote under a release reads the target's recorded templates and checks
3
+ * their digests (#4026, ws-104 question 5).
4
+ */
5
+
6
+ import { describe, test, expect } from "vitest";
7
+ import { contentDigest } from "../content-digest";
8
+ import { makeRelease } from "../lifecycle/release-store";
9
+ import type { DriverComponent } from "./driver";
10
+ import { isImplicitRelease, pinReleaseTemplates, releaseArtifactMismatches, removePinnedTemplates, targetFiles, targetFor, writePinnedTemplates, RELEASE_TEMPLATES_DIR } from "./release-promote";
11
+ import type { ReleaseRecord } from "../lifecycle/release-ledger";
12
+ import type { BuildArchiveManifest } from "./verbs/build-archive";
13
+ import { withTestDir } from "@intentius/chant-test-utils";
14
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ const prodTemplate = `{"env":"prod"}\n`;
18
+ const release = makeRelease({
19
+ member: null,
20
+ commit: "c".repeat(40),
21
+ artifactDigests: [],
22
+ targets: [
23
+ { env: "staging", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"staging"}\n`) }], params: [] },
24
+ { env: "prod", templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(prodTemplate) }], params: [] },
25
+ ],
26
+ });
27
+ const prod = release.targets.find((t) => t.env === "prod")!;
28
+
29
+ const api: DriverComponent = {
30
+ name: "api",
31
+ deploy: [
32
+ { phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
33
+ { phase: "Apply", steps: [{ kind: "gate", gate: "prod" }, { kind: "cfn-deploy", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
34
+ ],
35
+ };
36
+
37
+ const store = (content: string | null) => async (digest: string) => (digest === prod.templates[0].digest ? content : null);
38
+
39
+ describe("pinReleaseTemplates", () => {
40
+ test("points the deploy at the target's recorded template, by digest", async () => {
41
+ const pinned = await pinReleaseTemplates([api], release, prod, store(prodTemplate));
42
+ if ("errors" in pinned) throw new Error(pinned.errors.join("\n"));
43
+ const path = `${RELEASE_TEMPLATES_DIR}/${prod.templates[0].digest.replace(/:/g, "_")}/api.template.json`;
44
+ expect(pinned.files).toEqual([{ path, content: prodTemplate }]);
45
+ expect(pinned.components[0].deploy[1].steps[1]).toEqual({ kind: "cfn-deploy", template: path, imageRef: "@Publish.digest" });
46
+ // The publish step and the gate are left as they were.
47
+ expect(pinned.components[0].deploy[0]).toEqual(api.deploy[0]);
48
+ expect(pinned.components[0].deploy[1].steps[0]).toEqual({ kind: "gate", gate: "prod" });
49
+ });
50
+
51
+ test("refuses a template whose digest differs from the target's", async () => {
52
+ const pinned = await pinReleaseTemplates([api], release, prod, store(`{"env":"prod","edited":true}\n`));
53
+ expect("errors" in pinned && pinned.errors.join("\n")).toMatch(
54
+ new RegExp(`template "dist/api.template.json" hashes to sha256:[0-9a-f]{64}, but the release's target for "prod" records ${prod.templates[0].digest}; refusing to deploy it`),
55
+ );
56
+ });
57
+
58
+ test("refuses a template the target does not record, and one whose bytes are missing", async () => {
59
+ const other: DriverComponent = { name: "api", deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "dist/new.template.json" }] }] };
60
+ const missing = await pinReleaseTemplates([other], release, prod, store(prodTemplate));
61
+ expect("errors" in missing && missing.errors[0]).toMatch(/reads template "dist\/new.template.json", which release .* does not record for "prod"/);
62
+ const absent = await pinReleaseTemplates([api], release, prod, store(null));
63
+ expect("errors" in absent && absent.errors[0]).toMatch(/are not on the chant\/lifecycle branch/);
64
+ });
65
+
66
+ test("targetFor names the environments a release was compiled for", () => {
67
+ expect(targetFor(release, "prod")).toEqual({ target: prod });
68
+ expect(targetFor(release, "qa")).toEqual({ error: expect.stringMatching(/has no target for "qa"; it was compiled for prod, staging/) });
69
+ });
70
+ });
71
+
72
+ describe("an implicit release (#4046)", () => {
73
+ const yaml = "kind: Deployment\n";
74
+ const implicit = makeRelease({
75
+ member: null,
76
+ commit: "d".repeat(40),
77
+ artifactDigests: [],
78
+ targets: [{ env: "prod", templates: [{ component: null, path: "dist/build.yaml", digest: contentDigest(yaml) }], params: [] }],
79
+ });
80
+ const target = implicit.targets[0];
81
+
82
+ test("is a release with no artifacts whose templates all have no component", () => {
83
+ expect(isImplicitRelease(implicit)).toBe(true);
84
+ expect(isImplicitRelease(release)).toBe(false);
85
+ expect(isImplicitRelease(makeRelease({ ...implicit, targets: [{ env: "prod", templates: [], params: [] }] }))).toBe(false);
86
+ });
87
+
88
+ test("targetFiles gives the recorded bytes at their recorded paths, checked against the digest", async () => {
89
+ expect(await targetFiles(implicit, target, async () => yaml)).toEqual({ files: [{ path: "dist/build.yaml", content: yaml }] });
90
+ const tampered = await targetFiles(implicit, target, async () => "kind: Secret\n");
91
+ expect("errors" in tampered && tampered.errors[0]).toMatch(/hashes to sha256:.*refusing to write it/);
92
+ const missing = await targetFiles(implicit, target, async () => null);
93
+ expect("errors" in missing && missing.errors[0]).toMatch(/not on the chant\/lifecycle branch/);
94
+ });
95
+
96
+ test("targetFiles refuses a path that leaves the member's directory", async () => {
97
+ const escaping = { env: "prod", templates: [{ component: null, path: "../x.yaml", digest: contentDigest(yaml) }], params: [] };
98
+ const out = await targetFiles(implicit, escaping, async () => yaml);
99
+ expect("errors" in out && out.errors[0]).toMatch(/not a path inside the member's directory/);
100
+ });
101
+ });
102
+
103
+ describe("the artifact a promote under a release deploys (#4045)", () => {
104
+ const built = makeRelease({
105
+ member: null,
106
+ commit: "c".repeat(40),
107
+ artifactDigests: [{ component: "api", kind: "image", path: "api.tar", digest: "sha256:built" }],
108
+ targets: release.targets,
109
+ });
110
+ const source: ReleaseRecord = {
111
+ version: 1, component: "api", env: "staging", digest: "sha256:published", gitSha: "c".repeat(40), runId: "r1",
112
+ timestamp: "2026-10-01T00:00:00.000Z", actor: "ci", manifestDigest: "sha256:manifest",
113
+ };
114
+ const manifest = (digest: string): BuildArchiveManifest => ({
115
+ version: 1, component: "api", createdAt: "2026-10-01T00:00:00.000Z", manifestDigest: "sha256:manifest",
116
+ contents: [{ kind: "image", path: "api.tar", digest, mediaType: "application/vnd.oci.image.layout.v1+tar" }],
117
+ } as BuildArchiveManifest);
118
+
119
+ test("matches when the source deploy's build manifest holds the release's artifact at its path", async () => {
120
+ expect(await releaseArtifactMismatches(built, [{ component: "api", source }], async () => manifest("sha256:built"))).toEqual([]);
121
+ });
122
+
123
+ test("names a source deploy built from other bytes", async () => {
124
+ const out = await releaseArtifactMismatches(built, [{ component: "api", source }], async () => manifest("sha256:rebuilt"));
125
+ expect(out).toEqual([expect.stringMatching(/api: the staging deploy being promoted was built with api.tar at sha256:rebuilt, but release .* built it at sha256:built/)]);
126
+ });
127
+
128
+ test("a source deploy naming the release, or one with no readable manifest, says nothing", async () => {
129
+ const read = async () => manifest("sha256:rebuilt");
130
+ expect(await releaseArtifactMismatches(built, [{ component: "api", source: { ...source, releaseDigest: built.releaseDigest } }], read)).toEqual([]);
131
+ expect(await releaseArtifactMismatches(built, [{ component: "api", source: { ...source, manifestDigest: undefined } }], read)).toEqual([]);
132
+ expect(await releaseArtifactMismatches(built, [{ component: "api", source }], async () => null)).toEqual([]);
133
+ });
134
+ });
135
+
136
+ describe("removePinnedTemplates (#4045)", () => {
137
+ test("removes what the promote wrote, and the directory once it is empty", async () => {
138
+ await withTestDir(async (dir) => {
139
+ const files = [{ path: `${RELEASE_TEMPLATES_DIR}/sha256_ab/api.template.json`, content: "{}" }];
140
+ writePinnedTemplates(dir, files);
141
+ expect(existsSync(join(dir, files[0].path))).toBe(true);
142
+ removePinnedTemplates(dir, files);
143
+ expect(existsSync(join(dir, RELEASE_TEMPLATES_DIR))).toBe(false);
144
+ expect(existsSync(join(dir, ".chant"))).toBe(true);
145
+ });
146
+ });
147
+
148
+ test("keeps anything else under the directory", async () => {
149
+ await withTestDir(async (dir) => {
150
+ mkdirSync(join(dir, RELEASE_TEMPLATES_DIR, "other"), { recursive: true });
151
+ writeFileSync(join(dir, RELEASE_TEMPLATES_DIR, "other", "x"), "x");
152
+ const files = [{ path: `${RELEASE_TEMPLATES_DIR}/sha256_ab/api.template.json`, content: "{}" }];
153
+ writePinnedTemplates(dir, files);
154
+ removePinnedTemplates(dir, files);
155
+ expect(existsSync(join(dir, RELEASE_TEMPLATES_DIR, "other", "x"))).toBe(true);
156
+ expect(existsSync(join(dir, RELEASE_TEMPLATES_DIR, "sha256_ab"))).toBe(false);
157
+ });
158
+ });
159
+ });