@intentius/chant 0.124.27 → 0.124.29

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 (681) 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/build-params.d.ts +34 -1
  20. package/dist/build-params.d.ts.map +1 -1
  21. package/dist/build-params.js +31 -1
  22. package/dist/change-set-graph.d.ts +63 -0
  23. package/dist/change-set-graph.d.ts.map +1 -0
  24. package/dist/change-set-graph.js +130 -0
  25. package/dist/change-set.d.ts +62 -3
  26. package/dist/change-set.d.ts.map +1 -1
  27. package/dist/change-set.js +36 -4
  28. package/dist/chant-version.d.ts +9 -0
  29. package/dist/chant-version.d.ts.map +1 -0
  30. package/dist/chant-version.js +16 -0
  31. package/dist/cli/build-params-cli.d.ts +7 -1
  32. package/dist/cli/build-params-cli.d.ts.map +1 -1
  33. package/dist/cli/build-params-cli.js +17 -8
  34. package/dist/cli/commands/build.d.ts +8 -0
  35. package/dist/cli/commands/build.d.ts.map +1 -1
  36. package/dist/cli/commands/build.js +15 -11
  37. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  38. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  39. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  40. package/dist/cli/commands/check-lexicon.js +13 -2
  41. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  42. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  43. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  44. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  45. package/dist/cli/commands/init-lexicon.js +17 -8
  46. package/dist/cli/handlers/approve.d.ts.map +1 -1
  47. package/dist/cli/handlers/approve.js +64 -4
  48. package/dist/cli/handlers/build.d.ts.map +1 -1
  49. package/dist/cli/handlers/build.js +43 -4
  50. package/dist/cli/handlers/change-set.d.ts +10 -0
  51. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  52. package/dist/cli/handlers/change-set.js +51 -1
  53. package/dist/cli/handlers/fan-out.d.ts +10 -1
  54. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  55. package/dist/cli/handlers/fan-out.js +54 -6
  56. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  57. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  58. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  59. package/dist/cli/handlers/pr.d.ts +16 -2
  60. package/dist/cli/handlers/pr.d.ts.map +1 -1
  61. package/dist/cli/handlers/pr.js +269 -37
  62. package/dist/cli/handlers/promote.d.ts +7 -0
  63. package/dist/cli/handlers/promote.d.ts.map +1 -1
  64. package/dist/cli/handlers/promote.js +440 -28
  65. package/dist/cli/handlers/release.d.ts +91 -0
  66. package/dist/cli/handlers/release.d.ts.map +1 -0
  67. package/dist/cli/handlers/release.js +589 -0
  68. package/dist/cli/handlers/run.d.ts +38 -2
  69. package/dist/cli/handlers/run.d.ts.map +1 -1
  70. package/dist/cli/handlers/run.js +234 -16
  71. package/dist/cli/main.d.ts.map +1 -1
  72. package/dist/cli/main.js +143 -17
  73. package/dist/cli/mcp/workspace-tools.js +1 -1
  74. package/dist/cli/parse-args.d.ts.map +1 -1
  75. package/dist/cli/parse-args.js +73 -5
  76. package/dist/cli/registry.d.ts +43 -5
  77. package/dist/cli/registry.d.ts.map +1 -1
  78. package/dist/cli/run-events.d.ts +30 -0
  79. package/dist/cli/run-events.d.ts.map +1 -0
  80. package/dist/cli/run-events.js +49 -0
  81. package/dist/cli/version.d.ts +3 -5
  82. package/dist/cli/version.d.ts.map +1 -1
  83. package/dist/cli/version.js +3 -13
  84. package/dist/codegen/docs-in-site.d.ts +71 -0
  85. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  86. package/dist/codegen/docs-in-site.js +177 -0
  87. package/dist/codegen/docs-types.d.ts +14 -1
  88. package/dist/codegen/docs-types.d.ts.map +1 -1
  89. package/dist/codegen/docs.d.ts +18 -1
  90. package/dist/codegen/docs.d.ts.map +1 -1
  91. package/dist/codegen/docs.js +40 -7
  92. package/dist/codegen/rehype-base-url.mjs +15 -9
  93. package/dist/components/auto-release.d.ts +6 -1
  94. package/dist/components/auto-release.d.ts.map +1 -1
  95. package/dist/components/auto-release.js +3 -0
  96. package/dist/components/capability.d.ts +9 -0
  97. package/dist/components/capability.d.ts.map +1 -1
  98. package/dist/components/cli-support.d.ts +6 -0
  99. package/dist/components/cli-support.d.ts.map +1 -1
  100. package/dist/components/cli-support.js +10 -9
  101. package/dist/components/driver.d.ts +50 -9
  102. package/dist/components/driver.d.ts.map +1 -1
  103. package/dist/components/driver.js +116 -54
  104. package/dist/components/fan-out-inputs.d.ts +69 -0
  105. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  106. package/dist/components/fan-out-inputs.js +169 -0
  107. package/dist/components/fan-out-record.d.ts +13 -0
  108. package/dist/components/fan-out-record.d.ts.map +1 -1
  109. package/dist/components/fan-out-record.js +16 -0
  110. package/dist/components/fan-out-run.d.ts +7 -0
  111. package/dist/components/fan-out-run.d.ts.map +1 -1
  112. package/dist/components/fan-out-run.js +85 -26
  113. package/dist/components/fan-out.d.ts +4 -0
  114. package/dist/components/fan-out.d.ts.map +1 -1
  115. package/dist/components/pr-pipeline.d.ts +25 -0
  116. package/dist/components/pr-pipeline.d.ts.map +1 -1
  117. package/dist/components/pr-pipeline.js +22 -4
  118. package/dist/components/pr-run.d.ts +11 -0
  119. package/dist/components/pr-run.d.ts.map +1 -1
  120. package/dist/components/pr-run.js +20 -1
  121. package/dist/components/promote-order.d.ts +38 -0
  122. package/dist/components/promote-order.d.ts.map +1 -0
  123. package/dist/components/promote-order.js +62 -0
  124. package/dist/components/promote.d.ts +38 -2
  125. package/dist/components/promote.d.ts.map +1 -1
  126. package/dist/components/promote.js +77 -2
  127. package/dist/components/release-compile.d.ts +142 -0
  128. package/dist/components/release-compile.d.ts.map +1 -0
  129. package/dist/components/release-compile.js +382 -0
  130. package/dist/components/release-gate.d.ts +135 -0
  131. package/dist/components/release-gate.d.ts.map +1 -0
  132. package/dist/components/release-gate.js +288 -0
  133. package/dist/components/release-promote.d.ts +96 -0
  134. package/dist/components/release-promote.d.ts.map +1 -0
  135. package/dist/components/release-promote.js +204 -0
  136. package/dist/components/run-progress.d.ts +7 -0
  137. package/dist/components/run-progress.d.ts.map +1 -1
  138. package/dist/components/wave-plan.d.ts +8 -1
  139. package/dist/components/wave-plan.d.ts.map +1 -1
  140. package/dist/components/wave-plan.js +11 -3
  141. package/dist/config.d.ts +45 -0
  142. package/dist/config.d.ts.map +1 -1
  143. package/dist/config.js +3 -0
  144. package/dist/flag-params.d.ts +54 -0
  145. package/dist/flag-params.d.ts.map +1 -0
  146. package/dist/flag-params.js +130 -0
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +2 -0
  150. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  151. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  152. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  153. package/dist/lifecycle/change-set-store.d.ts +68 -0
  154. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  155. package/dist/lifecycle/change-set-store.js +97 -0
  156. package/dist/lifecycle/commit-sha.d.ts +13 -0
  157. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  158. package/dist/lifecycle/commit-sha.js +21 -0
  159. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  160. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  161. package/dist/lifecycle/delivery-metrics.js +145 -0
  162. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  163. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  164. package/dist/lifecycle/deploy-outcome.js +101 -0
  165. package/dist/lifecycle/gate-change-set.d.ts +78 -0
  166. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  167. package/dist/lifecycle/gate-change-set.js +138 -0
  168. package/dist/lifecycle/gate-ledger.d.ts +51 -4
  169. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/gate-ledger.js +18 -0
  171. package/dist/lifecycle/git.d.ts +30 -1
  172. package/dist/lifecycle/git.d.ts.map +1 -1
  173. package/dist/lifecycle/git.js +31 -5
  174. package/dist/lifecycle/index.d.ts +4 -0
  175. package/dist/lifecycle/index.d.ts.map +1 -1
  176. package/dist/lifecycle/index.js +4 -0
  177. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  178. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  179. package/dist/lifecycle/mixed-environment.js +154 -0
  180. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  181. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  182. package/dist/lifecycle/publish-attachments.js +179 -0
  183. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  184. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  185. package/dist/lifecycle/publish-credentials.js +69 -0
  186. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  187. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  188. package/dist/lifecycle/publish-declaration.js +83 -0
  189. package/dist/lifecycle/publish-forge.d.ts +53 -0
  190. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  191. package/dist/lifecycle/publish-forge.js +298 -0
  192. package/dist/lifecycle/publish-npm.d.ts +61 -0
  193. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  194. package/dist/lifecycle/publish-npm.js +242 -0
  195. package/dist/lifecycle/publish-oci.d.ts +64 -0
  196. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  197. package/dist/lifecycle/publish-oci.js +383 -0
  198. package/dist/lifecycle/publish-record.d.ts +66 -0
  199. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  200. package/dist/lifecycle/publish-record.js +84 -0
  201. package/dist/lifecycle/publish-targets.d.ts +45 -0
  202. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  203. package/dist/lifecycle/publish-targets.js +85 -0
  204. package/dist/lifecycle/release-cut.d.ts +154 -0
  205. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  206. package/dist/lifecycle/release-cut.js +366 -0
  207. package/dist/lifecycle/release-join.d.ts +95 -0
  208. package/dist/lifecycle/release-join.d.ts.map +1 -0
  209. package/dist/lifecycle/release-join.js +139 -0
  210. package/dist/lifecycle/release-ledger.d.ts +54 -0
  211. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  212. package/dist/lifecycle/release-ledger.js +53 -20
  213. package/dist/lifecycle/release-publish.d.ts +312 -0
  214. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  215. package/dist/lifecycle/release-publish.js +335 -0
  216. package/dist/lifecycle/release-revocation.d.ts +80 -0
  217. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  218. package/dist/lifecycle/release-revocation.js +123 -0
  219. package/dist/lifecycle/release-store.d.ts +200 -0
  220. package/dist/lifecycle/release-store.d.ts.map +1 -0
  221. package/dist/lifecycle/release-store.js +320 -0
  222. package/dist/lifecycle/release-verify.d.ts +61 -0
  223. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  224. package/dist/lifecycle/release-verify.js +92 -0
  225. package/dist/lifecycle/release-workstation.d.ts +91 -0
  226. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  227. package/dist/lifecycle/release-workstation.js +180 -0
  228. package/dist/lifecycle/retention-policy.d.ts +45 -0
  229. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  230. package/dist/lifecycle/retention-policy.js +43 -0
  231. package/dist/lifecycle/retention.d.ts +185 -0
  232. package/dist/lifecycle/retention.d.ts.map +1 -0
  233. package/dist/lifecycle/retention.js +400 -0
  234. package/dist/lifecycle/run-ledger.d.ts +4 -0
  235. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  236. package/dist/lifecycle/run-ledger.js +2 -0
  237. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  238. package/dist/op/activities/change-coverage.js +4 -2
  239. package/dist/op/activities/source-release.d.ts.map +1 -1
  240. package/dist/op/activities/source-release.js +7 -3
  241. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  242. package/dist/op/activities/source-rollback.js +7 -3
  243. package/dist/op/gate-name.d.ts +11 -2
  244. package/dist/op/gate-name.d.ts.map +1 -1
  245. package/dist/op/gate-name.js +12 -3
  246. package/dist/op/gate-run-locator.d.ts +22 -0
  247. package/dist/op/gate-run-locator.d.ts.map +1 -1
  248. package/dist/op/gate-run-locator.js +31 -0
  249. package/dist/op/gate.d.ts +27 -3
  250. package/dist/op/gate.d.ts.map +1 -1
  251. package/dist/op/gate.js +57 -3
  252. package/dist/op/local-executor.d.ts +36 -1
  253. package/dist/op/local-executor.d.ts.map +1 -1
  254. package/dist/op/local-executor.js +158 -52
  255. package/dist/op/op-waves-run.d.ts.map +1 -1
  256. package/dist/op/op-waves-run.js +4 -0
  257. package/dist/op/run-live.d.ts +2 -0
  258. package/dist/op/run-live.d.ts.map +1 -1
  259. package/dist/op/run-live.js +1 -1
  260. package/dist/op/runtime.d.ts +46 -0
  261. package/dist/op/runtime.d.ts.map +1 -1
  262. package/dist/op/runtimes/local.d.ts.map +1 -1
  263. package/dist/op/runtimes/local.js +8 -0
  264. package/dist/plan-summary.d.ts.map +1 -1
  265. package/dist/plan-summary.js +8 -2
  266. package/dist/point-decision.d.ts +2 -0
  267. package/dist/point-decision.d.ts.map +1 -1
  268. package/dist/point-decision.js +9 -2
  269. package/dist/pr-loop.d.ts +7 -0
  270. package/dist/pr-loop.d.ts.map +1 -1
  271. package/dist/pr-loop.js +23 -4
  272. package/dist/pr-report-url.d.ts +82 -0
  273. package/dist/pr-report-url.d.ts.map +1 -0
  274. package/dist/pr-report-url.js +174 -0
  275. package/dist/provenance.d.ts +2 -2
  276. package/dist/provenance.d.ts.map +1 -1
  277. package/dist/run-event-store.d.ts +104 -0
  278. package/dist/run-event-store.d.ts.map +1 -0
  279. package/dist/run-event-store.js +251 -0
  280. package/dist/run-event-stream.d.ts +133 -0
  281. package/dist/run-event-stream.d.ts.map +1 -0
  282. package/dist/run-event-stream.js +242 -0
  283. package/dist/run-event.d.ts +285 -0
  284. package/dist/run-event.d.ts.map +1 -0
  285. package/dist/run-event.js +209 -0
  286. package/dist/wave-runtime.mjs +262 -226
  287. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  288. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  289. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  290. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  291. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  292. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  293. package/dist/workspace/change-set-show.d.ts +56 -0
  294. package/dist/workspace/change-set-show.d.ts.map +1 -0
  295. package/dist/workspace/change-set-show.js +54 -0
  296. package/dist/workspace/change-set-show.schema.json +102 -0
  297. package/dist/workspace/change-set.schema.json +32 -1
  298. package/dist/workspace/changes-cli.d.ts.map +1 -1
  299. package/dist/workspace/changes-cli.js +10 -1
  300. package/dist/workspace/changes.d.ts +37 -0
  301. package/dist/workspace/changes.d.ts.map +1 -1
  302. package/dist/workspace/changes.js +49 -2
  303. package/dist/workspace/changes.schema.json +135 -0
  304. package/dist/workspace/check.schema.json +4 -2
  305. package/dist/workspace/checks/records.d.ts +9 -1
  306. package/dist/workspace/checks/records.d.ts.map +1 -1
  307. package/dist/workspace/checks/records.js +35 -1
  308. package/dist/workspace/checks.d.ts +1 -0
  309. package/dist/workspace/checks.d.ts.map +1 -1
  310. package/dist/workspace/checks.js +2 -1
  311. package/dist/workspace/ci-green-cli.d.ts +19 -4
  312. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  313. package/dist/workspace/ci-green-cli.js +75 -12
  314. package/dist/workspace/ci-green-forge.d.ts +20 -3
  315. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  316. package/dist/workspace/ci-green-forge.js +6 -4
  317. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  318. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  319. package/dist/workspace/ci-green-workflow.js +6 -0
  320. package/dist/workspace/ci-green.d.ts +281 -13
  321. package/dist/workspace/ci-green.d.ts.map +1 -1
  322. package/dist/workspace/ci-green.js +456 -38
  323. package/dist/workspace/ci-last-green.schema.json +32 -2
  324. package/dist/workspace/ci-list.schema.json +525 -0
  325. package/dist/workspace/commit-links.d.ts +85 -0
  326. package/dist/workspace/commit-links.d.ts.map +1 -0
  327. package/dist/workspace/commit-links.js +163 -0
  328. package/dist/workspace/declaration.d.ts +35 -0
  329. package/dist/workspace/declaration.d.ts.map +1 -1
  330. package/dist/workspace/declaration.js +27 -1
  331. package/dist/workspace/declaration.schema.json +76 -0
  332. package/dist/workspace/declared-kinds.d.ts +16 -0
  333. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  334. package/dist/workspace/declared-kinds.js +25 -0
  335. package/dist/workspace/delivery-metrics.d.ts +91 -0
  336. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  337. package/dist/workspace/delivery-metrics.js +217 -0
  338. package/dist/workspace/delivery-metrics.schema.json +519 -0
  339. package/dist/workspace/flags-read.d.ts +37 -0
  340. package/dist/workspace/flags-read.d.ts.map +1 -0
  341. package/dist/workspace/flags-read.js +103 -0
  342. package/dist/workspace/flags.d.ts +100 -0
  343. package/dist/workspace/flags.d.ts.map +1 -0
  344. package/dist/workspace/flags.js +98 -0
  345. package/dist/workspace/graph-cli.js +1 -1
  346. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  347. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  348. package/dist/workspace/identity-gate-rule.js +72 -7
  349. package/dist/workspace/identity.d.ts +12 -0
  350. package/dist/workspace/identity.d.ts.map +1 -1
  351. package/dist/workspace/identity.js +13 -1
  352. package/dist/workspace/intent-cli.d.ts +11 -2
  353. package/dist/workspace/intent-cli.d.ts.map +1 -1
  354. package/dist/workspace/intent-cli.js +54 -5
  355. package/dist/workspace/intent-trace.d.ts +219 -0
  356. package/dist/workspace/intent-trace.d.ts.map +1 -0
  357. package/dist/workspace/intent-trace.js +314 -0
  358. package/dist/workspace/intent.d.ts +36 -7
  359. package/dist/workspace/intent.d.ts.map +1 -1
  360. package/dist/workspace/intent.js +192 -22
  361. package/dist/workspace/intent.schema.json +702 -6
  362. package/dist/workspace/ls.d.ts +4 -0
  363. package/dist/workspace/ls.d.ts.map +1 -1
  364. package/dist/workspace/ls.js +2 -0
  365. package/dist/workspace/ls.schema.json +3 -1
  366. package/dist/workspace/member-pipeline.d.ts +6 -0
  367. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  368. package/dist/workspace/member-pipeline.js +6 -0
  369. package/dist/workspace/overlap.d.ts +146 -0
  370. package/dist/workspace/overlap.d.ts.map +1 -0
  371. package/dist/workspace/overlap.js +276 -0
  372. package/dist/workspace/promotion-order.d.ts +41 -0
  373. package/dist/workspace/promotion-order.d.ts.map +1 -0
  374. package/dist/workspace/promotion-order.js +95 -0
  375. package/dist/workspace/reason-codes.d.ts +4 -0
  376. package/dist/workspace/reason-codes.d.ts.map +1 -1
  377. package/dist/workspace/reason-codes.js +7 -1
  378. package/dist/workspace/records.d.ts +15 -2
  379. package/dist/workspace/records.d.ts.map +1 -1
  380. package/dist/workspace/records.js +32 -0
  381. package/dist/workspace/records.schema.json +21 -2
  382. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  383. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  384. package/dist/workspace/releases.d.ts +81 -0
  385. package/dist/workspace/releases.d.ts.map +1 -0
  386. package/dist/workspace/releases.js +103 -0
  387. package/dist/workspace/releases.schema.json +513 -0
  388. package/dist/workspace/run-event.schema.json +295 -0
  389. package/dist/workspace/status-gates.d.ts +34 -1
  390. package/dist/workspace/status-gates.d.ts.map +1 -1
  391. package/dist/workspace/status-gates.js +31 -2
  392. package/dist/workspace/status-stewards.d.ts +4 -0
  393. package/dist/workspace/status-stewards.d.ts.map +1 -1
  394. package/dist/workspace/status-stewards.js +2 -0
  395. package/dist/workspace/status.d.ts +117 -0
  396. package/dist/workspace/status.d.ts.map +1 -1
  397. package/dist/workspace/status.js +218 -4
  398. package/dist/workspace/status.schema.json +268 -2
  399. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  400. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  401. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  402. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  403. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  404. package/dist/workspace/trust/op-run-statement.js +143 -0
  405. package/dist/workspace/trust/rotation.d.ts +18 -2
  406. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  407. package/dist/workspace/trust/rotation.js +65 -16
  408. package/dist/workspace/trust/seal.d.ts +11 -0
  409. package/dist/workspace/trust/seal.d.ts.map +1 -1
  410. package/dist/workspace/trust/seal.js +9 -0
  411. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  412. package/dist/workspace/trust/ssh-commit.js +10 -10
  413. package/dist/workspace/wip.d.ts.map +1 -1
  414. package/dist/workspace/wip.js +11 -3
  415. package/package.json +1 -1
  416. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  417. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  418. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  419. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  420. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  421. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  422. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  423. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  424. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  425. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  426. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  427. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  428. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  429. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  430. package/src/__golden__/wave-loop/op-spend.json +3 -0
  431. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  432. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  433. package/src/build-params.test.ts +46 -0
  434. package/src/build-params.ts +55 -1
  435. package/src/change-set-graph.test.ts +72 -0
  436. package/src/change-set-graph.ts +141 -0
  437. package/src/change-set.ts +87 -6
  438. package/src/chant-version.ts +16 -0
  439. package/src/cli/build-params-cli.ts +22 -9
  440. package/src/cli/commands/build.ts +25 -11
  441. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  442. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  443. package/src/cli/commands/check-lexicon.ts +14 -2
  444. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  445. package/src/cli/commands/init-lexicon.test.ts +25 -0
  446. package/src/cli/commands/init-lexicon.ts +18 -8
  447. package/src/cli/handlers/approve-release.test.ts +84 -0
  448. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  449. package/src/cli/handlers/approve.ts +65 -2
  450. package/src/cli/handlers/build.test.ts +67 -0
  451. package/src/cli/handlers/build.ts +46 -4
  452. package/src/cli/handlers/change-set.ts +49 -1
  453. package/src/cli/handlers/fan-out.test.ts +228 -1
  454. package/src/cli/handlers/fan-out.ts +53 -6
  455. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  456. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  457. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  458. package/src/cli/handlers/operator.test.ts +37 -0
  459. package/src/cli/handlers/pr.test.ts +341 -0
  460. package/src/cli/handlers/pr.ts +292 -47
  461. package/src/cli/handlers/promote.test.ts +669 -7
  462. package/src/cli/handlers/promote.ts +464 -29
  463. package/src/cli/handlers/release-implicit.e2e.test.ts +161 -0
  464. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  465. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  466. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  467. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  468. package/src/cli/handlers/release.e2e.test.ts +199 -0
  469. package/src/cli/handlers/release.ts +593 -0
  470. package/src/cli/handlers/run-auto-release.test.ts +80 -0
  471. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  472. package/src/cli/handlers/run-release.test.ts +55 -0
  473. package/src/cli/handlers/run.test.ts +30 -0
  474. package/src/cli/handlers/run.ts +243 -18
  475. package/src/cli/main.test.ts +8 -0
  476. package/src/cli/main.ts +143 -17
  477. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  478. package/src/cli/mcp/workspace-tools.ts +1 -1
  479. package/src/cli/parse-args.ts +56 -5
  480. package/src/cli/registry.ts +43 -5
  481. package/src/cli/run-events.ts +57 -0
  482. package/src/cli/version.ts +3 -13
  483. package/src/codegen/docs-in-site.test.ts +166 -0
  484. package/src/codegen/docs-in-site.ts +182 -0
  485. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  486. package/src/codegen/docs-types.ts +14 -1
  487. package/src/codegen/docs.ts +60 -7
  488. package/src/codegen/rehype-base-url.d.mts +3 -1
  489. package/src/codegen/rehype-base-url.mjs +15 -9
  490. package/src/codegen/rehype-base-url.test.ts +34 -0
  491. package/src/components/auto-release.test.ts +30 -0
  492. package/src/components/auto-release.ts +9 -1
  493. package/src/components/capability.ts +9 -0
  494. package/src/components/cli-support.test.ts +82 -0
  495. package/src/components/cli-support.ts +16 -9
  496. package/src/components/driver.test.ts +24 -0
  497. package/src/components/driver.ts +165 -56
  498. package/src/components/fan-out-inputs.test.ts +85 -0
  499. package/src/components/fan-out-inputs.ts +192 -0
  500. package/src/components/fan-out-record.test.ts +10 -0
  501. package/src/components/fan-out-record.ts +19 -0
  502. package/src/components/fan-out-run.ts +104 -35
  503. package/src/components/fan-out.ts +4 -0
  504. package/src/components/pr-pipeline.ts +35 -4
  505. package/src/components/pr-run.ts +30 -1
  506. package/src/components/promote-order.test.ts +90 -0
  507. package/src/components/promote-order.ts +75 -0
  508. package/src/components/promote.test.ts +10 -1
  509. package/src/components/promote.ts +94 -3
  510. package/src/components/release-compile.test.ts +147 -0
  511. package/src/components/release-compile.ts +459 -0
  512. package/src/components/release-gate.test.ts +215 -0
  513. package/src/components/release-gate.ts +355 -0
  514. package/src/components/release-promote.test.ts +159 -0
  515. package/src/components/release-promote.ts +232 -0
  516. package/src/components/run-progress.ts +7 -0
  517. package/src/components/wave-plan.ts +16 -3
  518. package/src/config.ts +15 -1
  519. package/src/flag-params.test.ts +144 -0
  520. package/src/flag-params.ts +134 -0
  521. package/src/index.ts +2 -0
  522. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  523. package/src/lifecycle/change-set-store.test.ts +217 -0
  524. package/src/lifecycle/change-set-store.ts +113 -0
  525. package/src/lifecycle/commit-sha.ts +23 -0
  526. package/src/lifecycle/delivery-metrics.ts +263 -0
  527. package/src/lifecycle/deploy-outcome.ts +147 -0
  528. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  529. package/src/lifecycle/gate-change-set.ts +164 -0
  530. package/src/lifecycle/gate-ledger.test.ts +33 -0
  531. package/src/lifecycle/gate-ledger.ts +68 -3
  532. package/src/lifecycle/git.ts +61 -8
  533. package/src/lifecycle/index.ts +4 -0
  534. package/src/lifecycle/mixed-environment.test.ts +97 -0
  535. package/src/lifecycle/mixed-environment.ts +204 -0
  536. package/src/lifecycle/publish-attachments.ts +196 -0
  537. package/src/lifecycle/publish-credentials.ts +84 -0
  538. package/src/lifecycle/publish-declaration.ts +95 -0
  539. package/src/lifecycle/publish-forge.ts +325 -0
  540. package/src/lifecycle/publish-npm.ts +259 -0
  541. package/src/lifecycle/publish-oci.ts +396 -0
  542. package/src/lifecycle/publish-record.ts +113 -0
  543. package/src/lifecycle/publish-resume.test.ts +168 -0
  544. package/src/lifecycle/publish-targets.test.ts +494 -0
  545. package/src/lifecycle/publish-targets.ts +100 -0
  546. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  547. package/src/lifecycle/release-cut.test.ts +483 -0
  548. package/src/lifecycle/release-cut.ts +503 -0
  549. package/src/lifecycle/release-flag.test.ts +112 -0
  550. package/src/lifecycle/release-join.test.ts +68 -0
  551. package/src/lifecycle/release-join.ts +182 -0
  552. package/src/lifecycle/release-ledger.test.ts +14 -0
  553. package/src/lifecycle/release-ledger.ts +85 -17
  554. package/src/lifecycle/release-publish.test.ts +225 -0
  555. package/src/lifecycle/release-publish.ts +568 -0
  556. package/src/lifecycle/release-revocation.ts +147 -0
  557. package/src/lifecycle/release-store.ts +434 -0
  558. package/src/lifecycle/release-verify.test.ts +110 -0
  559. package/src/lifecycle/release-verify.ts +120 -0
  560. package/src/lifecycle/release-workstation.test.ts +204 -0
  561. package/src/lifecycle/release-workstation.ts +216 -0
  562. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  563. package/src/lifecycle/retention-policy.ts +63 -0
  564. package/src/lifecycle/retention.test.ts +363 -0
  565. package/src/lifecycle/retention.ts +508 -0
  566. package/src/lifecycle/run-ledger.test.ts +10 -0
  567. package/src/lifecycle/run-ledger.ts +6 -0
  568. package/src/op/activities/change-coverage.ts +4 -1
  569. package/src/op/activities/source-release.ts +8 -3
  570. package/src/op/activities/source-rollback.ts +8 -4
  571. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  572. package/src/op/gate-name.ts +13 -3
  573. package/src/op/gate-release.test.ts +126 -0
  574. package/src/op/gate-run-locator.test.ts +110 -0
  575. package/src/op/gate-run-locator.ts +35 -0
  576. package/src/op/gate.ts +88 -6
  577. package/src/op/local-executor.ts +207 -52
  578. package/src/op/op-waves-run.ts +4 -0
  579. package/src/op/run-events.test.ts +166 -0
  580. package/src/op/run-live.ts +1 -1
  581. package/src/op/runtime.ts +39 -0
  582. package/src/op/runtimes/local.test.ts +16 -0
  583. package/src/op/runtimes/local.ts +9 -0
  584. package/src/plan-summary.test.ts +20 -0
  585. package/src/plan-summary.ts +8 -1
  586. package/src/point-decision.ts +12 -2
  587. package/src/pr-loop.ts +25 -4
  588. package/src/pr-report-url.test.ts +131 -0
  589. package/src/pr-report-url.ts +209 -0
  590. package/src/provenance.ts +2 -2
  591. package/src/run-event-store.test.ts +144 -0
  592. package/src/run-event-store.ts +273 -0
  593. package/src/run-event-stream.test.ts +175 -0
  594. package/src/run-event-stream.ts +309 -0
  595. package/src/run-event.test.ts +277 -0
  596. package/src/run-event.ts +471 -0
  597. package/src/wave-loop-golden.test.ts +9 -0
  598. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  599. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  600. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  601. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  602. package/src/workspace/ancestry-index.test.ts +93 -0
  603. package/src/workspace/change-set-show.schema.json +102 -0
  604. package/src/workspace/change-set-show.ts +72 -0
  605. package/src/workspace/change-set.schema.json +32 -1
  606. package/src/workspace/changes-cli.ts +10 -1
  607. package/src/workspace/changes.schema.json +135 -0
  608. package/src/workspace/changes.test.ts +1 -1
  609. package/src/workspace/changes.ts +84 -2
  610. package/src/workspace/check.schema.json +4 -2
  611. package/src/workspace/checks/records.ts +35 -1
  612. package/src/workspace/checks.ts +2 -1
  613. package/src/workspace/ci-green-cli.ts +75 -13
  614. package/src/workspace/ci-green-forge.ts +24 -4
  615. package/src/workspace/ci-green-runs.test.ts +242 -0
  616. package/src/workspace/ci-green-signed.test.ts +302 -0
  617. package/src/workspace/ci-green-workflow.test.ts +16 -0
  618. package/src/workspace/ci-green-workflow.ts +12 -0
  619. package/src/workspace/ci-green.test.ts +131 -2
  620. package/src/workspace/ci-green.ts +635 -42
  621. package/src/workspace/ci-last-green.schema.json +32 -2
  622. package/src/workspace/ci-list.schema.json +525 -0
  623. package/src/workspace/commit-links.test.ts +279 -0
  624. package/src/workspace/commit-links.ts +183 -0
  625. package/src/workspace/declaration.schema.json +76 -0
  626. package/src/workspace/declaration.test.ts +28 -0
  627. package/src/workspace/declaration.ts +60 -2
  628. package/src/workspace/declared-kinds.ts +40 -1
  629. package/src/workspace/delivery-metrics.schema.json +519 -0
  630. package/src/workspace/delivery-metrics.test.ts +236 -0
  631. package/src/workspace/delivery-metrics.ts +286 -0
  632. package/src/workspace/flags-read.ts +121 -0
  633. package/src/workspace/flags.test.ts +141 -0
  634. package/src/workspace/flags.ts +157 -0
  635. package/src/workspace/graph-cli.ts +1 -1
  636. package/src/workspace/identity-gate-rule.ts +91 -7
  637. package/src/workspace/identity.ts +14 -1
  638. package/src/workspace/intent-cli.ts +46 -5
  639. package/src/workspace/intent-trace.test.ts +173 -0
  640. package/src/workspace/intent-trace.ts +484 -0
  641. package/src/workspace/intent.schema.json +702 -6
  642. package/src/workspace/intent.ts +196 -26
  643. package/src/workspace/ls.schema.json +3 -1
  644. package/src/workspace/ls.ts +6 -0
  645. package/src/workspace/member-pipeline.test.ts +17 -0
  646. package/src/workspace/member-pipeline.ts +9 -0
  647. package/src/workspace/older-records.test.ts +412 -0
  648. package/src/workspace/overlap.test.ts +165 -0
  649. package/src/workspace/overlap.ts +340 -0
  650. package/src/workspace/promotion-order.ts +97 -0
  651. package/src/workspace/read-contract.e2e.test.ts +5 -1
  652. package/src/workspace/reason-codes.test.ts +7 -1
  653. package/src/workspace/reason-codes.ts +7 -1
  654. package/src/workspace/record-assets.test.ts +2 -1
  655. package/src/workspace/records-contract.e2e.test.ts +2 -1
  656. package/src/workspace/records.schema.json +21 -2
  657. package/src/workspace/records.ts +36 -2
  658. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  659. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  660. package/src/workspace/releases.schema.json +513 -0
  661. package/src/workspace/releases.test.ts +108 -0
  662. package/src/workspace/releases.ts +133 -0
  663. package/src/workspace/run-event.schema.json +295 -0
  664. package/src/workspace/status-contract.test.ts +81 -1
  665. package/src/workspace/status-gates.ts +64 -1
  666. package/src/workspace/status-mixed.test.ts +152 -0
  667. package/src/workspace/status-revoked.test.ts +153 -0
  668. package/src/workspace/status-stewards.ts +6 -0
  669. package/src/workspace/status.schema.json +268 -2
  670. package/src/workspace/status.ts +334 -4
  671. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  672. package/src/workspace/trust/op-run-statement.ts +148 -0
  673. package/src/workspace/trust/rotation.ts +61 -14
  674. package/src/workspace/trust/seal.ts +10 -0
  675. package/src/workspace/trust/ssh-commit.ts +10 -9
  676. package/src/workspace/wip.ts +11 -2
  677. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  678. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  679. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  680. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  681. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,180 @@
1
+ /**
2
+ * #4031: who may cut a release, and the cut's signature. The declaration's
3
+ * `identity.gates.release` names the class that may cut; a cut is signed
4
+ * with the actor's ssh key over the release digest, label, actor and time,
5
+ * and `release list` and `workspace status` verify it on every read. With
6
+ * `ci.green.signedSince`, a hand-pushed green tag cuts nothing.
7
+ */
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import { afterAll, describe, expect, test } from "vitest";
11
+ import { cutRelease, ReleaseCutError } from "./release-cut";
12
+ import { readCuts, type ReleaseArtifact } from "./release-store";
13
+ import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
14
+ import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
15
+ import { releasesDocument } from "../workspace/releases";
16
+ import { workspaceStatus, type StatusDocument } from "../workspace/status";
17
+ import { checkReleaseSignature, type GateAdmissionRule } from "../workspace/identity-gate-rule";
18
+ import { contract } from "../workspace/__fixtures__/contract-repo";
19
+ import releasesSchema from "../workspace/releases.schema.json";
20
+ import statusSchema from "../workspace/status.schema.json";
21
+ import { rmSync } from "node:fs";
22
+ import { dirname } from "node:path";
23
+
24
+ const repos: TestRepo[] = [];
25
+ afterAll(() => {
26
+ for (const r of repos) rmSync(dirname(r.dir), { recursive: true, force: true });
27
+ });
28
+
29
+ function compiler(): TargetCompiler {
30
+ return {
31
+ async compile(env): Promise<CompiledTarget> {
32
+ return {
33
+ env,
34
+ params: [],
35
+ components: ["api"],
36
+ buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
37
+ buildInputs: { api: ["api"] },
38
+ componentFiles: { api: "api.component.ts" },
39
+ templates: [{ component: "api", path: "dist/api.json", content: `{"env":"${env}"}\n` }],
40
+ };
41
+ },
42
+ async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
43
+ return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: `sha256:${"b".repeat(64)}` }));
44
+ },
45
+ };
46
+ }
47
+
48
+ /** A domain class, `releaser`, from a path pin, granted by the `releaser` role. */
49
+ const PLUGIN = {
50
+ "classes/package.json": JSON.stringify({ name: "@acme/classes", version: "1.0.0", exports: { "./workspace-principals": "./principals.json" } }),
51
+ "classes/principals.json": JSON.stringify({ schema: 1, classes: [{ name: "releaser", description: "people who may cut a release", role: "releaser" }] }),
52
+ };
53
+
54
+ /**
55
+ * A workspace whose root member is a chant project with three environments,
56
+ * signed by alice and bob, with alice holding the releaser role. `extra`
57
+ * goes into the declaration.
58
+ */
59
+ function setup(extra: Record<string, unknown>): { r: TestRepo; alice: Key; bob: Key; head: string } {
60
+ const r = new TestRepo("release-cut");
61
+ repos.push(r);
62
+ const alice = r.key("alice");
63
+ const bob = r.key("bob");
64
+ r.write("chant.workspace.json", JSON.stringify({ name: "acme", schema: 1, pins: [{ path: "classes" }], members: [{ name: "root", dir: ".", kind: "other", because: "the release fixture" }], ...extra }, null, 2));
65
+ for (const [path, text] of Object.entries(PLUGIN)) r.write(path, text);
66
+ r.write(".chant/allowed_signers", `alice ${alice.pub}\nbob ${bob.pub}\n`);
67
+ r.write(".chant/trust.json", JSON.stringify({ schema: 1, roles: { releaser: ["alice"] } }));
68
+ r.write("chant.config.ts", `export default { environments: ["dev", "prod"] };\n`);
69
+ r.write("api.component.ts", "export const api = {};\n");
70
+ r.write("api/Dockerfile", "FROM scratch\n");
71
+ const head = r.commit("init");
72
+ return { r, alice, bob, head };
73
+ }
74
+
75
+ const NOW = () => new Date("2026-10-16T10:00:00.000Z");
76
+ const cut = (r: TestRepo, actor: string, sign?: Key, label: string | null = "v1.0.0") =>
77
+ cutRelease({ cwd: r.dir, compiler: compiler(), label, actor, env: {}, now: NOW, ...(sign ? { sign: sign.file } : {}) });
78
+ const code = async (p: Promise<unknown>) => ((await p.catch((e: unknown) => e)) as ReleaseCutError).code;
79
+ const noLedger = (r: TestRepo) => spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/lifecycle"], { cwd: r.dir }).status !== 0;
80
+
81
+ describe.skipIf(!hasSshKeygen)("who may cut a release (#4031)", () => {
82
+ const RELEASE_GATE = { identity: { gates: { release: { class: "releaser" } } } };
83
+
84
+ test("cutting as someone outside the release gate's class is refused, and nothing is written", async () => {
85
+ const { r, bob } = setup(RELEASE_GATE);
86
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
87
+ const err = await cut(r, "bob", bob).catch((e: unknown) => e);
88
+ expect(err).toBeInstanceOf(ReleaseCutError);
89
+ expect((err as ReleaseCutError).code).toBe("release-signer-refused");
90
+ expect((err as ReleaseCutError).message).toMatch(/bob is not in the class releaser that gate release needs/);
91
+ expect(noLedger(r)).toBe(true);
92
+ });
93
+
94
+ test("an unsigned cut, or one signed with another signer's key, is refused under the gate", async () => {
95
+ const { r, alice, bob } = setup(RELEASE_GATE);
96
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
97
+ expect(await code(cut(r, "alice"))).toBe("release-unsigned");
98
+ const forged = await cut(r, "alice", bob).catch((e: unknown) => e);
99
+ expect((forged as ReleaseCutError).code).toBe("release-signer-refused");
100
+ expect((forged as ReleaseCutError).message).toMatch(/does not verify for alice/);
101
+ expect(noLedger(r)).toBe(true);
102
+ expect(alice.name).toBe("alice");
103
+ });
104
+
105
+ test("a cut by a releaser carries a signature that release list and workspace status verify", async () => {
106
+ const { r, alice } = setup(RELEASE_GATE);
107
+ const head = r.git(["rev-parse", "HEAD"]).trim();
108
+ r.git(["tag", `ci/green/${head}`]);
109
+ const result = await cut(r, "alice", alice);
110
+ expect(result.cut?.signature).toMatch(/^-----BEGIN SSH SIGNATURE-----/);
111
+ const { cuts } = await readCuts({ cwd: r.dir });
112
+ expect(cuts).toHaveLength(1);
113
+ expect(cuts[0]).toMatchObject({ label: "v1.0.0", actor: "alice", signature: result.cut!.signature });
114
+
115
+ const list = await releasesDocument({ cwd: r.dir });
116
+ contract(releasesSchema).expectValid(list);
117
+ const entry = (list as Extract<typeof list, { releases: unknown }>).releases[0];
118
+ expect(entry.cuts[0].signatureCheck).toMatchObject({ status: "verified", key: expect.stringMatching(/^SHA256:/), reason: "signed by alice, in the class releaser" });
119
+
120
+ const status = (await workspaceStatus({ cwd: r.dir, env: "dev" })) as Extract<StatusDocument, { members: unknown }>;
121
+ contract(statusSchema).expectValid(status);
122
+ expect(status.members[0].releaseCuts).toEqual([
123
+ {
124
+ releaseDigest: result.release.releaseDigest,
125
+ label: "v1.0.0",
126
+ commit: head,
127
+ actor: "alice",
128
+ timestamp: "2026-10-16T10:00:00.000Z",
129
+ signature: result.cut!.signature,
130
+ signatureCheck: { status: "verified", key: entry.cuts[0].signatureCheck.key, reason: "signed by alice, in the class releaser" },
131
+ },
132
+ ]);
133
+
134
+ // A cut line edited after the fact no longer verifies: the label is signed.
135
+ const rule: GateAdmissionRule = { gate: "release", class: null, signers: { signersPath: ".chant/allowed_signers", active: true, signers: [{ principal: "alice", key: alice.pub, line: 1 }] }, roles: {}, classes: [] };
136
+ expect(checkReleaseSignature(rule, { ...cuts[0] }).status).toBe("verified");
137
+ expect(checkReleaseSignature(rule, { ...cuts[0], label: "v9.9.9" })).toMatchObject({ status: "unverified", reason: expect.stringMatching(/does not verify for alice/) });
138
+ expect(checkReleaseSignature(rule, { ...cuts[0], signature: null }).status).toBe("unsigned");
139
+ });
140
+
141
+ test("without a release gate a cut may be unsigned, and a signed one is still verified on read", async () => {
142
+ const { r, bob } = setup({});
143
+ r.git(["tag", `ci/green/${r.git(["rev-parse", "HEAD"]).trim()}`]);
144
+ const plain = await cut(r, "carol", undefined, "v1");
145
+ expect(plain.cut?.signature).toBeNull();
146
+ r.write("README.md", "next\n");
147
+ const next = r.commit("next");
148
+ r.git(["tag", `ci/green/${next}`]);
149
+ const signed = await cut(r, "bob", bob, "v2");
150
+ expect(signed.cut?.signature).toMatch(/BEGIN SSH SIGNATURE/);
151
+ const list = (await releasesDocument({ cwd: r.dir })) as Extract<Awaited<ReturnType<typeof releasesDocument>>, { releases: unknown }>;
152
+ // Both cuts share one time, so the list's order between them is the digests'.
153
+ expect(list.releases.map((x) => [x.label, x.cuts[0].signatureCheck.status]).sort()).toEqual([
154
+ ["v1", "unsigned"],
155
+ ["v2", "verified"],
156
+ ]);
157
+ });
158
+
159
+ test("with ci.green.signedSince, a hand-pushed green tag cuts nothing", async () => {
160
+ const green = { branch: "main", phases: { test: ["test"] }, require: ["test"] };
161
+ const { r, alice, head: pivot } = setup({ ...RELEASE_GATE, ci: { green } });
162
+ // Tags on commits from the pivot on need a statement; the declaration says so in the next commit.
163
+ const decl = JSON.parse(r.git(["show", "HEAD:chant.workspace.json"]));
164
+ r.write("chant.workspace.json", JSON.stringify({ ...decl, ci: { green: { ...green, signedSince: pivot } } }, null, 2));
165
+ const head = r.commit("require signed ci tags");
166
+ r.git(["tag", "-a", "-m", "ci: by hand", `ci/green/${head}`, head]);
167
+ const err = await cut(r, "alice", alice).catch((e: unknown) => e);
168
+ expect((err as ReleaseCutError).code).toBe("release-commit-not-green");
169
+ expect((err as ReleaseCutError).message).toMatch(/no ci\/green tag that counts.*ignored: ci\/green\/[0-9a-f]+ \(unsigned\)/);
170
+ expect(noLedger(r)).toBe(true);
171
+
172
+ // A signedSince that names no commit here refuses the cut.
173
+ r.write("chant.workspace.json", JSON.stringify({ ...decl, ci: { green: { ...green, signedSince: "f".repeat(40) } } }, null, 2));
174
+ const broken = r.commit("a signedSince from another clone");
175
+ r.git(["tag", `ci/green/${broken}`, broken]);
176
+ const refused = await cut(r, "alice", alice).catch((e: unknown) => e);
177
+ expect((refused as ReleaseCutError).code).toBe("declaration-invalid");
178
+ expect(noLedger(r)).toBe(true);
179
+ });
180
+ });
@@ -0,0 +1,483 @@
1
+ /**
2
+ * The release cut from a green commit (#4026, ws-104), against temp git repos
3
+ * and a compiler that hands back fixtures.
4
+ */
5
+
6
+ import { describe, test, expect } from "vitest";
7
+ import { withTestDir } from "@intentius/chant-test-utils";
8
+ import { spawnSync } from "node:child_process";
9
+ import { mkdirSync, writeFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { cutRelease, differingBuildSteps, planRelease, ReleaseCutError } from "./release-cut";
12
+ import {
13
+ appendCut,
14
+ computeReleaseDigest,
15
+ makeRelease,
16
+ persistRelease,
17
+ readAllReleases,
18
+ readCuts,
19
+ readRelease,
20
+ readReleaseTemplate,
21
+ ReleaseLabelError,
22
+ resolveRelease,
23
+ verifyReleaseDigest,
24
+ type ReleaseArtifact,
25
+ } from "./release-store";
26
+ import { readBlobFromPath, writeLedgerFiles } from "./git";
27
+ import { contentDigest } from "../content-digest";
28
+ import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
29
+ import { isImplicitRelease } from "../components/release-promote";
30
+ import { releasesDocument } from "../workspace/releases";
31
+
32
+ function git(dir: string, ...args: string[]): string {
33
+ const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
34
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
35
+ return r.stdout.trim();
36
+ }
37
+
38
+ function initRepo(dir: string): string {
39
+ git(dir, "init", "-q", "-b", "main");
40
+ git(dir, "config", "user.email", "test@chant.dev");
41
+ git(dir, "config", "user.name", "Test");
42
+ writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "staging", "prod"] };\n`);
43
+ mkdirSync(join(dir, "api"), { recursive: true });
44
+ writeFileSync(join(dir, "api", "Dockerfile"), "FROM scratch\n");
45
+ writeFileSync(join(dir, "api.component.ts"), "export const api = {};\n");
46
+ writeFileSync(join(dir, "README.md"), "fixture\n");
47
+ git(dir, "add", ".");
48
+ git(dir, "commit", "-q", "-m", "init");
49
+ return git(dir, "rev-parse", "HEAD");
50
+ }
51
+
52
+ function commitFile(dir: string, file: string, content: string): string {
53
+ writeFileSync(join(dir, file), content);
54
+ git(dir, "add", ".");
55
+ git(dir, "commit", "-q", "-m", `change ${file}`);
56
+ return git(dir, "rev-parse", "HEAD");
57
+ }
58
+
59
+ const green = (dir: string, sha: string) => git(dir, "tag", `ci/green/${sha}`, sha);
60
+ const revoke = (dir: string, sha: string) => git(dir, "tag", `ci/revoked/${sha}`, sha);
61
+
62
+ /** A compiler that compiles each environment to its own template and builds one image. */
63
+ function fixtureCompiler(opts: { buildArgs?: (env: string) => string; imageDigest?: string } = {}): TargetCompiler & { built: string[][]; compiled: string[] } {
64
+ const built: string[][] = [];
65
+ const compiled: string[] = [];
66
+ return {
67
+ built,
68
+ compiled,
69
+ async compile(env): Promise<CompiledTarget> {
70
+ compiled.push(env);
71
+ return {
72
+ env,
73
+ params: [
74
+ { name: "tier", value: env === "prod" ? "large" : "small", source: "default" },
75
+ { name: "region", value: "eu", source: "env" },
76
+ ],
77
+ components: ["api"],
78
+ buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar", buildArgs: { ENV: opts.buildArgs?.(env) ?? "same" } }] },
79
+ buildInputs: { api: ["api", "api/Dockerfile"] },
80
+ componentFiles: { api: "api.component.ts" },
81
+ templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
82
+ };
83
+ },
84
+ async buildArtifacts(env, components): Promise<ReleaseArtifact[]> {
85
+ built.push([env, ...components]);
86
+ return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: opts.imageDigest ?? `sha256:${"a".repeat(64)}` }));
87
+ },
88
+ };
89
+ }
90
+
91
+ const cut = (dir: string, compiler: TargetCompiler, label: string | null, actor = "alice") =>
92
+ cutRelease({ cwd: dir, compiler, label, actor, env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
93
+
94
+ describe("the release digest", () => {
95
+ const subject = {
96
+ member: null,
97
+ commit: "c".repeat(40),
98
+ artifactDigests: [{ component: "api", kind: "image" as const, path: "api.tar", digest: "sha256:1" }],
99
+ targets: [{ env: "dev", templates: [{ component: "api", path: "t.json", digest: "sha256:2" }], params: [{ name: "tier", value: "small", source: "default" }] }],
100
+ };
101
+
102
+ test("is a jcs1 plan digest of kind release, unchanged by list order and by a param's source", () => {
103
+ const digest = computeReleaseDigest(subject);
104
+ expect(digest).toMatch(/^jcs1-sha256:[0-9a-f]{64}$/);
105
+ const reordered = { ...subject, targets: [{ ...subject.targets[0], params: [{ name: "tier", value: "small", source: "cli" }] }] };
106
+ expect(computeReleaseDigest(reordered)).toBe(digest);
107
+ });
108
+
109
+ test("changes with a param's value, a template's digest, the commit or the member", () => {
110
+ const digest = computeReleaseDigest(subject);
111
+ expect(computeReleaseDigest({ ...subject, targets: [{ ...subject.targets[0], params: [{ name: "tier", value: "large", source: "default" }] }] })).not.toBe(digest);
112
+ expect(computeReleaseDigest({ ...subject, targets: [{ ...subject.targets[0], templates: [{ component: "api", path: "t.json", digest: "sha256:3" }] }] })).not.toBe(digest);
113
+ expect(computeReleaseDigest({ ...subject, commit: "d".repeat(40) })).not.toBe(digest);
114
+ expect(computeReleaseDigest({ ...subject, member: "api" })).not.toBe(digest);
115
+ });
116
+
117
+ test("verifyReleaseDigest catches a release whose content no longer matches its digest", () => {
118
+ const release = makeRelease(subject);
119
+ expect(verifyReleaseDigest(release)).toBe(true);
120
+ expect(verifyReleaseDigest({ ...release, commit: "e".repeat(40) })).toBe(false);
121
+ });
122
+ });
123
+
124
+ describe("cutRelease", () => {
125
+ test("cuts a release from a green commit and stores it apart from the deploy records", async () => {
126
+ await withTestDir(async (dir) => {
127
+ const sha = initRepo(dir);
128
+ green(dir, sha);
129
+ const compiler = fixtureCompiler();
130
+ const result = await cut(dir, compiler, "v1.0.0");
131
+
132
+ expect(compiler.compiled).toEqual(["dev", "staging", "prod"]);
133
+ expect(compiler.built).toEqual([["dev", "api"]]);
134
+ expect(result.label).toBe("v1.0.0");
135
+ expect(result.stored).toBe(true);
136
+ const { release } = result;
137
+ expect(release).toMatchObject({ version: 1, member: null, commit: sha });
138
+ expect(release.artifactDigests).toEqual([{ component: "api", kind: "image", path: "api.tar", digest: `sha256:${"a".repeat(64)}` }]);
139
+ expect(release.targets.map((t) => t.env)).toEqual(["dev", "prod", "staging"]);
140
+ const prod = release.targets.find((t) => t.env === "prod")!;
141
+ expect(prod.templates).toEqual([{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"prod"}\n`) }]);
142
+ expect(prod.params).toEqual([
143
+ { name: "region", value: "eu", source: "env" },
144
+ { name: "tier", value: "large", source: "default" },
145
+ ]);
146
+
147
+ // Stored under _releases/, never in a releases.jsonl.
148
+ expect(await readRelease(release.releaseDigest, { cwd: dir })).toEqual(release);
149
+ expect(await readReleaseTemplate(prod.templates[0].digest, { cwd: dir })).toBe(`{"env":"prod"}\n`);
150
+ const { cuts } = await readCuts({ cwd: dir });
151
+ expect(cuts).toEqual([
152
+ {
153
+ version: 1,
154
+ releaseDigest: release.releaseDigest,
155
+ label: "v1.0.0",
156
+ actor: "alice",
157
+ timestamp: "2026-10-16T10:00:00.000Z",
158
+ runId: `local-${Date.parse("2026-10-16T10:00:00.000Z")}`,
159
+ runOrigin: { forge: "local" },
160
+ signature: null,
161
+ },
162
+ ]);
163
+ expect(git(dir, "ls-tree", "-r", "--name-only", "chant/lifecycle").split("\n").filter((p) => p.endsWith("releases.jsonl"))).toEqual([]);
164
+ expect(await resolveRelease("v1.0.0", { cwd: dir })).toEqual({ release, label: "v1.0.0" });
165
+ });
166
+ });
167
+
168
+ test("refuses a commit that isn't green, and writes nothing", async () => {
169
+ await withTestDir(async (dir) => {
170
+ initRepo(dir);
171
+ const compiler = fixtureCompiler();
172
+ const err = await cut(dir, compiler, "v1").catch((e: unknown) => e);
173
+ expect(err).toBeInstanceOf(ReleaseCutError);
174
+ expect((err as ReleaseCutError).code).toBe("release-commit-not-green");
175
+ expect(compiler.compiled).toEqual([]);
176
+ expect(spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/lifecycle"], { cwd: dir }).status).not.toBe(0);
177
+ });
178
+ });
179
+
180
+ test("refuses a green commit that was revoked", async () => {
181
+ await withTestDir(async (dir) => {
182
+ const sha = initRepo(dir);
183
+ green(dir, sha);
184
+ revoke(dir, sha);
185
+ const err = await cut(dir, fixtureCompiler(), "v1").catch((e: unknown) => e);
186
+ expect((err as ReleaseCutError).code).toBe("release-commit-revoked");
187
+ });
188
+ });
189
+
190
+ test("refuses a working tree with uncommitted changes to tracked files", async () => {
191
+ await withTestDir(async (dir) => {
192
+ const sha = initRepo(dir);
193
+ green(dir, sha);
194
+ writeFileSync(join(dir, "README.md"), "edited\n");
195
+ const err = await cut(dir, fixtureCompiler(), "v1").catch((e: unknown) => e);
196
+ expect((err as ReleaseCutError).code).toBe("release-worktree-dirty");
197
+ });
198
+ });
199
+
200
+ test("two cuts of one commit give the same releaseDigest; the second is a no-op that keeps the label", async () => {
201
+ await withTestDir(async (dir) => {
202
+ const sha = initRepo(dir);
203
+ green(dir, sha);
204
+ const first = await cut(dir, fixtureCompiler(), "v1.0.0", "alice");
205
+ const second = await cutRelease({ cwd: dir, compiler: fixtureCompiler(), label: null, actor: "bob", env: { GITHUB_RUN_ID: "9" }, now: () => new Date("2026-10-17T00:00:00.000Z") });
206
+ expect(second.release.releaseDigest).toBe(first.release.releaseDigest);
207
+ expect(second.cut).toBeNull();
208
+ expect(second.label).toBe("v1.0.0");
209
+ expect(second.stored).toBe(false);
210
+ expect((await readCuts({ cwd: dir })).cuts).toHaveLength(1);
211
+
212
+ // The same digest under another label is refused.
213
+ const err = await cut(dir, fixtureCompiler(), "v2.0.0").catch((e: unknown) => e);
214
+ expect(err).toBeInstanceOf(ReleaseLabelError);
215
+ expect((err as ReleaseLabelError).code).toBe("release-already-labelled");
216
+ });
217
+ });
218
+
219
+ test("a label names one digest: a second release under a taken label is refused", async () => {
220
+ await withTestDir(async (dir) => {
221
+ const first = initRepo(dir);
222
+ green(dir, first);
223
+ await cut(dir, fixtureCompiler(), "v1");
224
+ const second = commitFile(dir, "api/main.go", "package main\n");
225
+ green(dir, second);
226
+ const err = await cut(dir, fixtureCompiler({ imageDigest: `sha256:${"b".repeat(64)}` }), "v1").catch((e: unknown) => e);
227
+ expect((err as ReleaseLabelError).code).toBe("release-label-taken");
228
+ });
229
+ });
230
+
231
+ test("refuses a component whose build steps differ between environments, before anything is built", async () => {
232
+ await withTestDir(async (dir) => {
233
+ const sha = initRepo(dir);
234
+ green(dir, sha);
235
+ const compiler = fixtureCompiler({ buildArgs: (env) => env });
236
+ const err = await cut(dir, compiler, "v1").catch((e: unknown) => e);
237
+ expect((err as ReleaseCutError).code).toBe("release-build-inputs-differ");
238
+ expect((err as Error).message).toMatch(/"api"/);
239
+ expect(compiler.built).toEqual([]);
240
+ });
241
+ });
242
+
243
+ test("a config-only commit gets a release of its own that carries the previous artifact digests", async () => {
244
+ await withTestDir(async (dir) => {
245
+ const first = initRepo(dir);
246
+ green(dir, first);
247
+ const r1 = await cut(dir, fixtureCompiler(), "v1");
248
+
249
+ const second = commitFile(dir, "flags.json", `{"newCheckout":true}\n`);
250
+ green(dir, second);
251
+ // A rebuild would give another digest; the source did not change, so nothing is rebuilt.
252
+ const compiler = fixtureCompiler({ imageDigest: `sha256:${"f".repeat(64)}` });
253
+ const r2 = await cut(dir, compiler, "v2");
254
+ expect(compiler.built).toEqual([]);
255
+ expect(r2.reused).toEqual([{ component: "api", from: r1.release.releaseDigest }]);
256
+ expect(r2.release.artifactDigests).toEqual(r1.release.artifactDigests);
257
+ expect(r2.release.releaseDigest).not.toBe(r1.release.releaseDigest);
258
+
259
+ // A change under the build context is built again.
260
+ const third = commitFile(dir, "api/main.go", "package main\n");
261
+ green(dir, third);
262
+ const rebuilt = fixtureCompiler({ imageDigest: `sha256:${"f".repeat(64)}` });
263
+ const r3 = await cut(dir, rebuilt, "v3");
264
+ expect(rebuilt.built).toEqual([["dev", "api"]]);
265
+ expect(r3.release.artifactDigests[0].digest).toBe(`sha256:${"f".repeat(64)}`);
266
+ });
267
+ });
268
+
269
+ test("refuses a member that declares no environments", async () => {
270
+ await withTestDir(async (dir) => {
271
+ const sha = initRepo(dir);
272
+ green(dir, sha);
273
+ const err = await cutRelease({ cwd: dir, compiler: fixtureCompiler(), label: null, actor: "a", environments: [], env: {} }).catch((e: unknown) => e);
274
+ expect((err as ReleaseCutError).code).toBe("release-no-environments");
275
+ });
276
+ });
277
+ });
278
+
279
+ /** A project with no components (#4046): each environment compiles to the files its build writes, with no component. */
280
+ function implicitCompiler(): TargetCompiler & { built: string[][] } {
281
+ const built: string[][] = [];
282
+ return {
283
+ built,
284
+ async compile(env): Promise<CompiledTarget> {
285
+ return {
286
+ env,
287
+ params: [{ name: "replicas", value: env === "prod" ? 3 : 1, source: "params-file" }],
288
+ components: [],
289
+ buildSteps: {},
290
+ buildInputs: {},
291
+ componentFiles: {},
292
+ templates: [
293
+ { component: null, path: "dist/build.yaml", content: `replicas: ${env === "prod" ? 3 : 1}\n` },
294
+ { component: null, path: "dist/ops/deploy/op.json", content: `{"name":"deploy"}\n` },
295
+ ],
296
+ };
297
+ },
298
+ async buildArtifacts(env, components): Promise<ReleaseArtifact[]> {
299
+ built.push([env, ...components]);
300
+ return [];
301
+ },
302
+ };
303
+ }
304
+
305
+ describe("an implicit release, of a project with no components (#4046)", () => {
306
+ function initPlainRepo(dir: string): string {
307
+ git(dir, "init", "-q", "-b", "main");
308
+ git(dir, "config", "user.email", "test@chant.dev");
309
+ git(dir, "config", "user.name", "Test");
310
+ writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "prod"] };\n`);
311
+ git(dir, "add", ".");
312
+ git(dir, "commit", "-q", "-m", "init");
313
+ return git(dir, "rev-parse", "HEAD");
314
+ }
315
+
316
+ test("is cut with no artifacts and a target per environment holding the build output, and lists like any release", async () => {
317
+ await withTestDir(async (dir) => {
318
+ const sha = initPlainRepo(dir);
319
+ green(dir, sha);
320
+ const compiler = implicitCompiler();
321
+ const result = await cut(dir, compiler, "v1");
322
+ expect(compiler.built).toEqual([]);
323
+ const { release } = result;
324
+ expect(release.artifactDigests).toEqual([]);
325
+ expect(release.targets).toEqual([
326
+ {
327
+ env: "dev",
328
+ templates: [
329
+ { component: null, path: "dist/build.yaml", digest: contentDigest("replicas: 1\n") },
330
+ { component: null, path: "dist/ops/deploy/op.json", digest: contentDigest(`{"name":"deploy"}\n`) },
331
+ ],
332
+ params: [{ name: "replicas", value: 1, source: "params-file" }],
333
+ },
334
+ {
335
+ env: "prod",
336
+ templates: [
337
+ { component: null, path: "dist/build.yaml", digest: contentDigest("replicas: 3\n") },
338
+ { component: null, path: "dist/ops/deploy/op.json", digest: contentDigest(`{"name":"deploy"}\n`) },
339
+ ],
340
+ params: [{ name: "replicas", value: 3, source: "params-file" }],
341
+ },
342
+ ]);
343
+ expect(await readReleaseTemplate(contentDigest("replicas: 3\n"), { cwd: dir })).toBe("replicas: 3\n");
344
+ expect(isImplicitRelease(release)).toBe(true);
345
+
346
+ // A second cut of the commit is the same release.
347
+ const again = await cut(dir, implicitCompiler(), null, "bob");
348
+ expect(again.release.releaseDigest).toBe(release.releaseDigest);
349
+ expect(again.cut).toBeNull();
350
+
351
+ const doc = await releasesDocument({ cwd: dir });
352
+ if ("error" in doc) throw new Error(doc.error.message);
353
+ expect(doc.releases).toHaveLength(1);
354
+ expect(doc.releases[0]).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1", artifactDigests: [], environments: [] });
355
+ expect(doc.releases[0].targets[1].templates[0]).toEqual({ component: null, path: "dist/build.yaml", digest: contentDigest("replicas: 3\n") });
356
+ });
357
+ });
358
+
359
+ test("is still refused when components are declared but none is enabled and nothing was compiled", async () => {
360
+ await withTestDir(async (dir) => {
361
+ const sha = initPlainRepo(dir);
362
+ green(dir, sha);
363
+ const empty: TargetCompiler = {
364
+ async compile(env) {
365
+ return { env, params: [], components: [], buildSteps: {}, buildInputs: {}, componentFiles: {}, templates: [] };
366
+ },
367
+ async buildArtifacts() {
368
+ return [];
369
+ },
370
+ };
371
+ const err = await cut(dir, empty, "v1").catch((e: unknown) => e);
372
+ expect((err as ReleaseCutError).code).toBe("release-no-components");
373
+ });
374
+ });
375
+ });
376
+
377
+ describe("the release store", () => {
378
+ test("a release is written once and never rewritten", async () => {
379
+ await withTestDir(async (dir) => {
380
+ initRepo(dir);
381
+ const release = makeRelease({ member: null, commit: "c".repeat(40), artifactDigests: [], targets: [{ env: "dev", templates: [], params: [] }] });
382
+ expect((await persistRelease(release, [], { cwd: dir })).written).toBe(true);
383
+ const tip = git(dir, "rev-parse", "chant/lifecycle");
384
+ expect((await persistRelease(release, [], { cwd: dir })).written).toBe(false);
385
+ expect(git(dir, "rev-parse", "chant/lifecycle")).toBe(tip);
386
+ });
387
+ });
388
+
389
+ test("a stored release whose content does not hash to its digest is not read, and counts as malformed", async () => {
390
+ await withTestDir(async (dir) => {
391
+ initRepo(dir);
392
+ const release = makeRelease({ member: null, commit: "c".repeat(40), artifactDigests: [], targets: [{ env: "dev", templates: [], params: [] }] });
393
+ await persistRelease(release, [], { cwd: dir });
394
+ const name = `${release.releaseDigest.replace(/:/g, "_")}.json`;
395
+ const stored = (await readBlobFromPath("_releases", name, { cwd: dir }))!;
396
+ await writeLedgerFiles([{ path: `_releases/${name}`, content: stored.replace("c".repeat(40), "d".repeat(40)) }], "tamper", { cwd: dir });
397
+ expect(await readRelease(release.releaseDigest, { cwd: dir })).toBeNull();
398
+ expect(await readAllReleases({ cwd: dir })).toEqual({ releases: [], malformed: 1 });
399
+ });
400
+ });
401
+
402
+ test("an unlabelled cut can be labelled once; an invalid label is refused", async () => {
403
+ await withTestDir(async (dir) => {
404
+ initRepo(dir);
405
+ const base = { releaseDigest: `jcs1-sha256:${"1".repeat(64)}`, actor: "a", timestamp: "2026-10-16T00:00:00.000Z", runOrigin: null };
406
+ expect(await appendCut({ ...base, label: null }, { cwd: dir })).toMatchObject({ appended: true });
407
+ expect(await appendCut({ ...base, label: null }, { cwd: dir })).toEqual({ appended: false, label: null });
408
+ expect(await appendCut({ ...base, label: "v1" }, { cwd: dir })).toMatchObject({ appended: true });
409
+ expect(await appendCut({ ...base, label: "v1" }, { cwd: dir })).toEqual({ appended: false, label: "v1" });
410
+ await expect(appendCut({ ...base, label: "has space" }, { cwd: dir })).rejects.toMatchObject({ code: "release-label-invalid" });
411
+ });
412
+ });
413
+ });
414
+
415
+ describe("differingBuildSteps", () => {
416
+ test("names the components whose steps differ, and ignores one present in only some environments", () => {
417
+ const t = (env: string, steps: Record<string, unknown[]>): CompiledTarget => ({ env, params: [], components: Object.keys(steps), buildSteps: steps, buildInputs: {}, componentFiles: {}, templates: [] });
418
+ expect(differingBuildSteps([t("dev", { a: [{ x: 1 }], b: [{ y: 1 }] }), t("prod", { a: [{ x: 2 }], b: [{ y: 1 }] }), t("qa", { c: [] })])).toEqual([{ component: "a", envs: ["dev", "prod"] }]);
419
+ });
420
+ });
421
+
422
+ describe("the dry run of a cut (#4027)", () => {
423
+ const plan = (dir: string, compiler: TargetCompiler, label: string | null) =>
424
+ planRelease({ cwd: dir, compiler, label, actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
425
+
426
+ test("compiles every environment and says what it would build, without building, storing or cutting", () =>
427
+ withTestDir(async (dir) => {
428
+ green(dir, initRepo(dir));
429
+ const compiler = fixtureCompiler();
430
+ const p = await plan(dir, compiler, "v1.0.0");
431
+ expect(compiler.compiled).toEqual(["dev", "staging", "prod"]);
432
+ expect(compiler.built).toEqual([]);
433
+ expect(p).toMatchObject({ label: "v1.0.0", releaseDigest: null, alreadyCut: null, build: [{ component: "api", env: "dev" }], reused: [], signatureRequired: false });
434
+ expect(p.targets.map((t) => [t.env, t.templates])).toEqual([
435
+ ["dev", [{ component: "api", path: "dist/api.template.json" }]],
436
+ ["staging", [{ component: "api", path: "dist/api.template.json" }]],
437
+ ["prod", [{ component: "api", path: "dist/api.template.json" }]],
438
+ ]);
439
+ expect(spawnSync("git", ["rev-parse", "--verify", "--quiet", "refs/heads/chant/lifecycle"], { cwd: dir }).status).not.toBe(0);
440
+ }));
441
+
442
+ test("knows the digest when every artifact is carried, and that it is already cut", () =>
443
+ withTestDir(async (dir) => {
444
+ green(dir, initRepo(dir));
445
+ const first = await cut(dir, fixtureCompiler(), "v1.0.0");
446
+ const sha = commitFile(dir, "README.md", "docs only\n");
447
+ green(dir, sha);
448
+ const p = await plan(dir, fixtureCompiler(), "v1.1.0");
449
+ expect(p.build).toEqual([]);
450
+ expect(p.reused).toEqual([{ component: "api", from: first.release.releaseDigest }]);
451
+ const second = await cut(dir, fixtureCompiler(), "v1.1.0");
452
+ expect(p.releaseDigest).toBe(second.release.releaseDigest);
453
+ expect((await plan(dir, fixtureCompiler(), "v1.1.0")).alreadyCut).toBe(second.release.releaseDigest);
454
+ expect((await plan(dir, fixtureCompiler(), null)).alreadyCut).toBe(second.release.releaseDigest);
455
+ }));
456
+
457
+ test("refuses a label another commit's release holds, and one that is not a label", () =>
458
+ withTestDir(async (dir) => {
459
+ green(dir, initRepo(dir));
460
+ await cut(dir, fixtureCompiler(), "v1.0.0");
461
+ green(dir, commitFile(dir, "api/Dockerfile", "FROM busybox\n"));
462
+ const taken = await plan(dir, fixtureCompiler(), "v1.0.0").catch((e: unknown) => e);
463
+ expect(taken).toBeInstanceOf(ReleaseLabelError);
464
+ expect((taken as ReleaseLabelError).code).toBe("release-label-taken");
465
+ expect((taken as ReleaseLabelError).message).toMatch(/cut from commit [0-9a-f]{12}/);
466
+ expect(((await plan(dir, fixtureCompiler(), "-bad").catch((e: unknown) => e)) as ReleaseLabelError).code).toBe("release-label-invalid");
467
+ }));
468
+
469
+ test("lists each component's publish steps with the environments whose deploy runs them", () =>
470
+ withTestDir(async (dir) => {
471
+ green(dir, initRepo(dir));
472
+ const base = fixtureCompiler();
473
+ const compiler: TargetCompiler = {
474
+ ...base,
475
+ async compile(env) {
476
+ const t = await base.compile(env);
477
+ return { ...t, publishSteps: { api: [{ kind: "publish-image", from: "archive", to: "$env.registry" }] } };
478
+ },
479
+ };
480
+ const p = await plan(dir, compiler, null);
481
+ expect(p.publishSteps).toEqual([{ component: "api", kind: "publish-image", to: "$env.registry", envs: ["dev", "staging", "prod"] }]);
482
+ }));
483
+ });