@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
@@ -12,14 +12,24 @@
12
12
  * failed or stopped at a gate, so the ledger never misses a deploy that
13
13
  * happened. Unlike an auto-recorded release, the record is what these
14
14
  * commands are for, so a failed write is an error rather than a warning.
15
+ * A run that applied partly, or failed, also appends a deploy outcome
16
+ * (#4035, ../../lifecycle/deploy-outcome.ts) naming what it did to each
17
+ * component, so `workspace status` reads the environment as mixed.
18
+ *
19
+ * `rollback --release` (#4035) is a promote of an earlier release into the
20
+ * environment: every component of the release at the digest the environment
21
+ * ran from it, through the release gate `promote --release` binds (#4045).
15
22
  */
16
23
 
17
24
  import { resolve } from "node:path";
18
25
  import { readFileSync } from "node:fs";
19
26
  import { loadChantConfig, type ChantConfig } from "../../config";
27
+ import { promotionOrder } from "../../workspace/promotion-order";
28
+ import { resolvePromotionSource } from "../../components/promote-order";
20
29
  import { fetchLifecycle, getHeadCommit, pushLifecycle } from "../../lifecycle/git";
21
30
  import {
22
31
  appendReleaseRecord,
32
+ latestPerComponent,
23
33
  readReleaseLedger,
24
34
  resolveRunId,
25
35
  type ReleaseRecord,
@@ -30,10 +40,12 @@ import { applyConfigDefaults } from "../../components/config-defaults";
30
40
  import { fanOutRegistry } from "../../components/fan-out-support";
31
41
  import { renderDriverHuman } from "../../components/driver-output";
32
42
  import { ndjsonProgressSink } from "../../components/run-progress";
43
+ import { openCliRunEvents, refusesBothStreams } from "../run-events";
33
44
  import {
34
45
  parseDigestPins,
35
46
  planPromotion,
36
47
  planRollback,
48
+ planReleaseRollback,
37
49
  planRedeploy,
38
50
  withoutBuildSteps,
39
51
  runPromotion,
@@ -41,27 +53,38 @@ import {
41
53
  rollbackRecord,
42
54
  redeployRecord,
43
55
  gateApprover,
56
+ gateAdmission,
44
57
  type DeployRunInfo,
45
58
  type PromotionItem,
46
59
  type PromotionPlan,
47
60
  } from "../../components/promote";
48
61
  import type { DriverComponent } from "../../components/driver";
49
62
  import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
50
- import { approveCommand, describeGateMismatch, gateIsSealed } from "../../op/gate";
63
+ import { approveCommand, describeGateMismatch, gateIsSealed, gitGateLedgerPort, type GateLedgerPort } from "../../op/gate";
64
+ import type { ReleaseGateBinding } from "../../components/driver";
65
+ import type { ChangeSetDocument } from "../../change-set";
51
66
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli";
52
67
  import { formatError, formatWarning, formatSuccess, formatBold, formatInfo } from "../format";
53
68
  import { GATED_EXIT_CODE } from "./run";
54
69
  import type { CommandContext } from "../registry";
70
+ import { redeployLine, releaseRollbackLine, rollbackTarget, unsettledComponents } from "../../lifecycle/mixed-environment";
55
71
 
56
72
  const PROMOTE_USAGE =
57
- "chant components promote --from <env> --to <env> [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
73
+ "chant components promote --to <env> [--from <env>] [--allow-skip] [--release <label|digest>] [--component <name> [--digest <sha256:...>] | --digest <component>=<sha256:...> ...] [--dry-run] [--json]";
58
74
  const ROLLBACK_USAGE =
59
- "chant components rollback <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
75
+ "chant components rollback <env> (--component <name> [--digest <sha256:...>] | --release <label|digest>) [--dry-run] [--json]";
60
76
  const REDEPLOY_USAGE =
61
77
  "chant components redeploy <env> --component <name> [--digest <sha256:...>] [--dry-run] [--json]";
62
78
 
63
79
  type Verb = "promote" | "rollback" | "redeploy";
64
80
 
81
+ /** The release a promote deploys under (#4026), and its target for the environment promoted to. */
82
+ interface UnderRelease {
83
+ release: import("../../lifecycle/release-store").Release;
84
+ label: string | null;
85
+ target: import("../../lifecycle/release-store").ReleaseTarget;
86
+ }
87
+
65
88
  function renderPlan(verb: Verb, plan: PromotionPlan, removed: Map<string, string[]>): void {
66
89
  console.error(formatBold(verb === "promote" ? `promote ${plan.from} -> ${plan.to}` : `${verb} ${plan.to}`));
67
90
  for (const item of plan.items) {
@@ -129,6 +152,124 @@ async function readLedger(env: string): Promise<ReleaseRecord[]> {
129
152
  return ledger.records;
130
153
  }
131
154
 
155
+ /**
156
+ * Plan, store and bind the release gate for a promote under a release
157
+ * (#4045, ws-104 questions 2 and 6). Undefined when no promoted composition
158
+ * declares a gate, so there is nothing to bind. A number is the exit code of
159
+ * a refusal already printed: a provisional member, or a member that failed
160
+ * to plan. On an empty change set, each gate gets an `empty: true`
161
+ * resolution the run then counts once it reads the stored document back.
162
+ */
163
+ async function bindReleaseGate(input: {
164
+ plan: PromotionPlan;
165
+ prepared: DriverComponent[];
166
+ under: UnderRelease;
167
+ registry: Awaited<ReturnType<typeof fanOutRegistry>>;
168
+ port: GateLedgerPort;
169
+ seededOutputs: Record<string, Record<string, unknown>>;
170
+ actor: string;
171
+ projectPath: string;
172
+ head: string | undefined;
173
+ }): Promise<{ binding: ReleaseGateBinding; doc: ChangeSetDocument; empty: boolean } | number | undefined> {
174
+ const { plan, under, port } = input;
175
+ const gate = await import("../../components/release-gate");
176
+ const gates = gate.gateNamesOf(input.prepared);
177
+ if (gates.length === 0) return undefined;
178
+ const env = plan.to;
179
+ const releaseDigest = under.release.releaseDigest;
180
+
181
+ // What the environment runs now, to compare each component with.
182
+ const { readRelease } = await import("../../lifecycle/release-store");
183
+ const envRecords = (await readLedger(env)).filter((r) => r.env === env);
184
+ const latest = latestPerComponent(envRecords);
185
+ const releases = new Map<string, import("../../lifecycle/release-store").Release | null>();
186
+ const current = new Map<string, import("../../components/release-gate").CurrentDeploy>();
187
+ for (const item of plan.items) {
188
+ const record = latest.get(item.component);
189
+ if (!record) continue;
190
+ const named = record.releaseDigest;
191
+ if (named !== undefined && !releases.has(named)) releases.set(named, await readRelease(named).catch(() => null));
192
+ current.set(item.component, { record, release: named !== undefined ? (releases.get(named) ?? null) : null });
193
+ }
194
+
195
+ // #4035: a component a run left failed or gated runs something the ledger doesn't say.
196
+ const { readDeployOutcomes } = await import("../../lifecycle/deploy-outcome");
197
+ const unsettled = new Map<string, string>();
198
+ for (const [component, u] of unsettledComponents(envRecords, await readDeployOutcomes(env).catch(() => []))) {
199
+ unsettled.set(component, `the ${u.outcome.verb} run ${u.outcome.runId} left ${component} ${u.state} in "${env}" and nothing has been recorded for it since, so what it runs is not known`);
200
+ }
201
+
202
+ const doc = await gate.planReleaseChangeSet({
203
+ release: under.release,
204
+ env,
205
+ components: input.prepared,
206
+ digests: new Map(plan.items.map((i) => [i.component, i.digest])),
207
+ registry: input.registry,
208
+ current,
209
+ unsettled,
210
+ seededOutputs: input.seededOutputs,
211
+ });
212
+ const provisional = gate.provisionalMembers(doc);
213
+ if (provisional.length > 0) {
214
+ console.error(formatError({
215
+ message: `the change set of release ${under.label ?? releaseDigest} for "${env}" has provisional members (${provisional.join(", ")}), which no approval covers; nothing was deployed`,
216
+ hint: "Deploy what they read first, so they plan against real values.",
217
+ }));
218
+ return 1;
219
+ }
220
+ const failed = doc.members.filter((m) => m.status === "failed");
221
+ if (failed.length > 0) {
222
+ for (const m of failed) console.error(formatError({ message: `${m.member} failed to plan for "${env}"${m.error ? `: ${m.error}` : ""}` }));
223
+ console.error(formatError({ message: "nothing was deployed" }));
224
+ return 1;
225
+ }
226
+
227
+ // The document is stored before the gate is decided, so the plan a pending
228
+ // gate waits on can be read, and an empty pass can be checked.
229
+ const { storeGateChangeSet } = await import("../../lifecycle/gate-change-set");
230
+ try {
231
+ const stored = await storeGateChangeSet({ doc, cwd: input.projectPath, at: input.head ?? null });
232
+ if (stored.warning) console.error(formatWarning({ message: stored.warning }));
233
+ } catch (err) {
234
+ console.error(formatWarning({ message: `could not store the change set ${doc.digest}: ${err instanceof Error ? err.message : String(err)}` }));
235
+ }
236
+ const { isEmptyChangeSet } = await import("../../change-set");
237
+ const empty = isEmptyChangeSet(doc);
238
+ console.error(formatInfo(
239
+ `release gate: change set ${doc.digest} for "${env}"` +
240
+ (empty ? ", empty: its gates pass without an approval" : `, ${doc.summary.entries} entr${doc.summary.entries === 1 ? "y" : "ies"}, ${doc.summary.holes} hole(s)`) +
241
+ ` (chant change-set show ${doc.digest})`,
242
+ ));
243
+
244
+ if (empty) {
245
+ if (!port.appendResolution) throw new Error("the gate ledger can't record a resolution, so an empty change set can't pass its gates");
246
+ const { resolutions } = await port.read(gate.RELEASE_GATE_OP);
247
+ const timestamp = new Date().toISOString();
248
+ for (const name of gates) {
249
+ const standing = resolutions.some(
250
+ (r) => r.empty === true && r.gate === name && r.environment === env && r.releaseDigest === releaseDigest && r.planDigest === doc.digest,
251
+ );
252
+ if (standing) continue;
253
+ // Pushed with the resolution; the deploy records the promote writes push the same branch.
254
+ await port.appendResolution(
255
+ gate.emptyResolution({ gate: name, doc, actor: input.actor, timestamp, ...(input.head ? { gitSha: input.head } : {}) }),
256
+ );
257
+ }
258
+ }
259
+
260
+ const { readStoredChangeSet } = await import("../../lifecycle/change-set-store");
261
+ return {
262
+ doc,
263
+ empty,
264
+ binding: {
265
+ op: gate.RELEASE_GATE_OP,
266
+ planDigest: doc.digest,
267
+ releaseDigest,
268
+ verifyEmpty: gate.emptyVerifier((digest) => readStoredChangeSet(digest, { cwd: input.projectPath }), { releaseDigest, environment: env }),
269
+ },
270
+ };
271
+ }
272
+
132
273
  /**
133
274
  * Prepare, run and record a plan. Shared by promote, rollback and redeploy,
134
275
  * which differ only in how the plan was chosen, whether the publish step runs,
@@ -141,8 +282,10 @@ async function deployPlan(
141
282
  project: { projectPath: string; config: ChantConfig; targets: DriverComponent[] },
142
283
  actor: string | undefined,
143
284
  record: (item: PromotionItem, env: string, run: DeployRunInfo) => ReleaseRecordInput,
285
+ under?: UnderRelease,
144
286
  ): Promise<number> {
145
287
  const { args } = ctx;
288
+ if (refusesBothStreams(args)) return 1;
146
289
  const byName = new Map(project.targets.map((c) => [c.name, applyConfigDefaults(c, project.config)]));
147
290
  const prepared: DriverComponent[] = [];
148
291
  const removed = new Map<string, string[]>();
@@ -166,6 +309,37 @@ async function deployPlan(
166
309
  return 1;
167
310
  }
168
311
 
312
+ // #4026: under a release, the deploy reads the templates the release
313
+ // recorded for this environment, checked against their digests.
314
+ let pinnedFiles: import("../../components/release-promote").PinnedTemplate[] = [];
315
+ if (under) {
316
+ const { pinReleaseTemplates } = await import("../../components/release-promote");
317
+ const { readReleaseTemplate } = await import("../../lifecycle/release-store");
318
+ const pinned = await pinReleaseTemplates(prepared, under.release, under.target, (digest) => readReleaseTemplate(digest));
319
+ if ("errors" in pinned) {
320
+ for (const message of pinned.errors) console.error(formatError({ message }));
321
+ console.error(formatError({ message: `nothing was deployed` }));
322
+ return 1;
323
+ }
324
+ prepared.splice(0, prepared.length, ...pinned.components);
325
+ pinnedFiles = pinned.files;
326
+ for (const item of plan.items) {
327
+ const named = item.source.releaseDigest;
328
+ if (named !== undefined && named !== under.release.releaseDigest) {
329
+ console.error(formatWarning({ message: `${item.component}: the ${plan.from} deploy being promoted came from release ${named}, not ${under.release.releaseDigest}` }));
330
+ }
331
+ }
332
+ // #4045: the artifact deployed against the artifacts the release built.
333
+ const { releaseArtifactMismatches } = await import("../../components/release-promote");
334
+ const { readBuildManifest } = await import("../../lifecycle/build-ledger-store");
335
+ const mismatches = await releaseArtifactMismatches(
336
+ under.release,
337
+ plan.items.map((i) => ({ component: i.component, source: i.source })),
338
+ (digest) => readBuildManifest(digest).catch(() => null),
339
+ );
340
+ for (const message of mismatches) console.error(formatWarning({ message }));
341
+ }
342
+
169
343
  // The composition deployed is this checkout's. Say so when it is not the
170
344
  // commit the artifact was built from.
171
345
  const head = await getHeadCommit().catch(() => undefined);
@@ -178,12 +352,24 @@ async function deployPlan(
178
352
  }
179
353
  }
180
354
 
355
+ const releaseJson: { release?: { releaseDigest: string; label: string | null; templates: unknown[] } } = under
356
+ ? { release: { releaseDigest: under.release.releaseDigest, label: under.label, templates: under.target.templates } }
357
+ : {};
181
358
  if (args.dryRun) {
182
- if (args.json) console.log(JSON.stringify(planJson(plan, removed), null, 2));
183
- else renderPlan(verb, plan, removed);
359
+ if (args.json) console.log(JSON.stringify({ ...planJson(plan, removed), ...releaseJson }, null, 2));
360
+ else {
361
+ renderPlan(verb, plan, removed);
362
+ if (under) console.error(` under release ${under.label ?? under.release.releaseDigest}: ${under.target.templates.length} template(s) from its "${plan.to}" target`);
363
+ }
184
364
  return 0;
185
365
  }
186
366
  if (!args.json) renderPlan(verb, plan, removed);
367
+ // #4045: the pinned templates are written for this run and removed after it.
368
+ const { writePinnedTemplates, removePinnedTemplates } = await import("../../components/release-promote");
369
+ const tidy = () => {
370
+ if (pinnedFiles.length > 0) removePinnedTemplates(project.projectPath, pinnedFiles);
371
+ };
372
+ if (pinnedFiles.length > 0) writePinnedTemplates(project.projectPath, pinnedFiles);
187
373
 
188
374
  const seededOutputs: Record<string, Record<string, unknown>> = {};
189
375
  for (const file of args.seedOutputs ?? []) {
@@ -191,37 +377,85 @@ async function deployPlan(
191
377
  Object.assign(seededOutputs, JSON.parse(readFileSync(resolve(file), "utf8")));
192
378
  } catch (err) {
193
379
  console.error(formatError({ message: `--seed-outputs: could not read "${file}": ${err instanceof Error ? err.message : String(err)}` }));
380
+ tidy();
194
381
  return 1;
195
382
  }
196
383
  }
197
384
 
198
385
  const registry = await fanOutRegistry(project.projectPath, project.config);
199
- const run = await runPromotion({
200
- plan,
201
- components: prepared,
202
- registry,
203
- componentOutputs: seededOutputs,
204
- ...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
386
+ const port = gitGateLedgerPort();
387
+ // #4045: under a release, every gate binds the release's change set for this environment.
388
+ let releaseGate: ReleaseGateBinding | undefined;
389
+ let changeSetJson: { changeSet: string; empty: boolean } | undefined;
390
+ if (under) {
391
+ try {
392
+ const bound = await bindReleaseGate({ plan, prepared, under, registry, port, seededOutputs, actor: actor!, projectPath: project.projectPath, head });
393
+ if (typeof bound === "number") {
394
+ tidy();
395
+ return bound;
396
+ }
397
+ if (bound) {
398
+ releaseGate = bound.binding;
399
+ changeSetJson = { changeSet: bound.doc.digest, empty: bound.empty };
400
+ }
401
+ } catch (err) {
402
+ tidy();
403
+ throw err;
404
+ }
405
+ }
406
+ // The run id the release records carry, chosen before the run so that
407
+ // `--events-json` (#4041) names the run by it too.
408
+ const { runId, runOrigin } = resolveRunId(args.runId);
409
+ const events = await openCliRunEvents(args, {
410
+ kind: "component",
411
+ name: `${verb} ${plan.items.map((i) => i.component).join(",")} in ${plan.to}`,
412
+ runId,
413
+ cwd: project.projectPath,
205
414
  });
415
+ let run: Awaited<ReturnType<typeof runPromotion>>;
416
+ try {
417
+ run = await runPromotion({
418
+ plan,
419
+ components: prepared,
420
+ registry,
421
+ componentOutputs: seededOutputs,
422
+ gates: port,
423
+ ...(releaseGate ? { releaseGate } : {}),
424
+ ...(args.progressJson ? { onProgress: ndjsonProgressSink() } : {}),
425
+ ...(events ? { onRunEvent: events.emit } : {}),
426
+ });
427
+ } catch (err) {
428
+ events?.settle("failed", err instanceof Error ? err.message : String(err));
429
+ throw err;
430
+ } finally {
431
+ tidy();
432
+ }
433
+ // The driver settles the stream; this only ends one it left open.
434
+ events?.settle(run.status === "fail" ? "failed" : run.status);
206
435
  if (!args.json) renderDriverHuman(run);
207
436
 
208
437
  // Record every component that deployed, whatever happened to the rest: a
209
438
  // component that reached the environment and is missing from its ledger
210
439
  // would be exactly the unrecorded deploy the ledger exists to rule out.
211
440
  const deployed = plan.items.filter((item) => run.results.some((r) => r.component === item.component && r.status === "ok"));
212
- const { runId, runOrigin } = resolveRunId(args.runId);
213
441
  const timestamp = new Date().toISOString();
214
442
  const recorded: ReleaseRecord[] = [];
215
443
  let recordError: string | undefined;
216
- if (deployed.length > 0) {
444
+ // #4035: a run that applied partly, or failed, records what it did to each
445
+ // component, so the environment reads as mixed until it is settled.
446
+ const { deployOutcomeOf, appendDeployOutcome } = await import("../../lifecycle/deploy-outcome");
447
+ const outcome = deployOutcomeOf({ verb, plan, run, releaseDigest: under?.release.releaseDigest ?? null, runId, timestamp, actor: actor! });
448
+ if (deployed.length > 0 || outcome) {
217
449
  try {
218
450
  for (const item of deployed) {
219
- const approver = gateApprover(run.results.find((r) => r.component === item.component));
220
- const { record: written } = await appendReleaseRecord(
221
- record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}) }),
222
- );
451
+ const result = run.results.find((r) => r.component === item.component);
452
+ const approver = gateApprover(result);
453
+ const admission = gateAdmission(result);
454
+ const input = record(item, plan.to, { runId, ...(runOrigin ? { runOrigin } : {}), actor: actor!, timestamp, ...(approver ? { approver } : {}), ...(admission ? { admission } : {}) });
455
+ const { record: written } = await appendReleaseRecord(under ? { ...input, releaseDigest: under.release.releaseDigest } : input);
223
456
  recorded.push(written);
224
457
  }
458
+ if (outcome) await appendDeployOutcome(outcome);
225
459
  await pushLifecycle();
226
460
  } catch (err) {
227
461
  recordError = err instanceof Error ? err.message : String(err);
@@ -229,7 +463,8 @@ async function deployPlan(
229
463
  }
230
464
 
231
465
  if (args.json) {
232
- console.log(JSON.stringify({ ...planJson(plan, removed), run, recorded }, null, 2));
466
+ const withChangeSet = under && changeSetJson ? { release: { ...releaseJson.release!, ...changeSetJson } } : releaseJson;
467
+ console.log(JSON.stringify({ ...planJson(plan, removed), ...withChangeSet, run, recorded, ...(outcome ? { outcome } : {}) }, null, 2));
233
468
  } else {
234
469
  for (const r of recorded) {
235
470
  console.error(formatSuccess(
@@ -242,9 +477,20 @@ async function deployPlan(
242
477
  }
243
478
  }
244
479
 
480
+ if (outcome && recordError === undefined) {
481
+ const unsettled = outcome.components.filter((c) => c.outcome === "failed" || c.outcome === "gated");
482
+ console.error(formatWarning({
483
+ message: `"${plan.to}" now runs a mix: ${outcome.components.map((c) => `${c.component} ${c.outcome}`).join(", ")}. ` +
484
+ `chant workspace status ${plan.to} --json lists what each component runs and the rollback line`,
485
+ }));
486
+ for (const c of unsettled) console.error(formatInfo(`settle ${c.component}: ${redeployLine(plan.to, c.component)}, or run the same ${verb} again`));
487
+ }
488
+
245
489
  if (recordError !== undefined) {
246
490
  console.error(formatError({
247
- message: `deployed to "${plan.to}", but the release record was not written: ${recordError}`,
491
+ message: deployed.length > 0
492
+ ? `deployed to "${plan.to}", but the release record was not written: ${recordError}`
493
+ : `the outcome of the ${verb} in "${plan.to}" was not written: ${recordError}`,
248
494
  hint: `Pull the chant/lifecycle branch and check \`chant components status ${plan.to}\` before running this again.`,
249
495
  }));
250
496
  return 1;
@@ -294,12 +540,120 @@ function actorOrRefuse(ctx: CommandContext): { actor: string | undefined } | num
294
540
  return { actor };
295
541
  }
296
542
 
543
+ /** The revoked commits, or the exit code of a refusal already printed when the ci tags can't be judged (#4033). */
544
+ async function revokedOrRefuse(what: string): Promise<Map<string, import("../../lifecycle/release-revocation").ReleaseRevocation> | number> {
545
+ const { readRevokedCommits } = await import("../../lifecycle/release-revocation");
546
+ try {
547
+ return await readRevokedCommits(process.cwd());
548
+ } catch (err) {
549
+ console.error(formatError({
550
+ message: `the ci tags can't be judged, so whether ${what} is revoked is unknown: ${err instanceof Error ? err.message : String(err)} [declaration-invalid]`,
551
+ hint: "Fix ci.green in chant.workspace.json at base; chant workspace check names the problem.",
552
+ }));
553
+ return 1;
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Resolve `--release` for a promote or a rollback to `env` (#4026, #4035):
559
+ * the release, its label and its target for `env`. Refused, with the exit
560
+ * code returned, when the release can't be found, has no target for `env`,
561
+ * is of a project with no components (#4046), or was cut from a revoked
562
+ * commit (#4033), so a gate pending for it can't pass.
563
+ */
564
+ async function resolveUnderRelease(ref: string, env: string, verb: "promote" | "rollback", fix: (release: { releaseDigest: string }) => Promise<string> | string): Promise<UnderRelease | number> {
565
+ const { resolveRelease } = await import("../../lifecycle/release-store");
566
+ const { targetFor, isImplicitRelease } = await import("../../components/release-promote");
567
+ const found = await resolveRelease(ref);
568
+ if (!found) {
569
+ console.error(formatError({ message: `no release is labelled or digested "${ref}" in this member`, hint: "List them with: chant release list" }));
570
+ return 1;
571
+ }
572
+ const target = targetFor(found.release, env);
573
+ if ("error" in target) {
574
+ console.error(formatError({ message: target.error }));
575
+ return 1;
576
+ }
577
+ // #4046: a release of a project with no components records build output
578
+ // and no component, so there is no deploy step here to point at it.
579
+ if (isImplicitRelease(found.release)) {
580
+ const name = found.label ?? found.release.releaseDigest;
581
+ console.error(formatError({
582
+ message: `release ${name} is of a project with no components, so ${verb} has no component to deploy`,
583
+ hint: `Write its "${env}" build output with \`chant release export ${name} --env ${env}\`, then run the project's own deploy against those files.`,
584
+ }));
585
+ return 1;
586
+ }
587
+ // #4033 (ws-105): a release cut from a revoked commit is never deployed,
588
+ // so a gate pending for it can't pass. Read from local tags, as the cut reads them.
589
+ const revoked = await revokedOrRefuse(`release ${found.label ?? found.release.releaseDigest}`);
590
+ if (typeof revoked === "number") return revoked;
591
+ const { revocationOf, revokedRefusal } = await import("../../lifecycle/release-revocation");
592
+ const revocation = revocationOf(found.release, revoked);
593
+ if (revocation) {
594
+ console.error(formatError(revokedRefusal({ releaseDigest: found.release.releaseDigest, label: found.label }, revocation, await fix(found.release))));
595
+ return 1;
596
+ }
597
+ return { release: found.release, label: found.label, target: target.target };
598
+ }
599
+
600
+ /**
601
+ * The line that rolls `env` back past `exclude` (#4035): to the newest
602
+ * release in its ledger that is still kept and not revoked, by label, or
603
+ * null when there is none.
604
+ */
605
+ async function rollbackLineFor(env: string, records: readonly ReleaseRecord[], exclude: ReadonlySet<string>, revoked: ReadonlyMap<string, import("../../lifecycle/release-revocation").ReleaseRevocation>): Promise<string | null> {
606
+ const { readRelease, readCuts, labelsByDigest } = await import("../../lifecycle/release-store");
607
+ const { revocationOf } = await import("../../lifecycle/release-revocation");
608
+ const target = await rollbackTarget({
609
+ records,
610
+ exclude,
611
+ check: async (digest) => {
612
+ const release = await readRelease(digest).catch(() => null);
613
+ return { kept: release !== null, revoked: release !== null && revocationOf(release, revoked) !== null };
614
+ },
615
+ });
616
+ if (!target) return null;
617
+ const labels = await readCuts().then((c) => labelsByDigest(c.cuts), () => new Map<string, string>());
618
+ return releaseRollbackLine(env, target, labels.get(target));
619
+ }
620
+
621
+ /**
622
+ * Refuse a rollback or a redeploy that would put back a release cut from a
623
+ * revoked commit (#4033, #4035), naming the release to go back to instead.
624
+ * Returns the exit code of the refusal, or undefined when nothing restored
625
+ * names a revoked release.
626
+ */
627
+ async function refuseRevokedRestore(verb: "rollback" | "redeploy", plan: PromotionPlan, records: readonly ReleaseRecord[]): Promise<number | undefined> {
628
+ const named = [...new Set(plan.items.map((i) => i.source.releaseDigest).filter((d): d is string => d !== undefined))];
629
+ if (named.length === 0) return undefined;
630
+ const revoked = await revokedOrRefuse(`the release this ${verb} restores`);
631
+ if (typeof revoked === "number") return revoked;
632
+ if (revoked.size === 0) return undefined;
633
+ const { readRelease, readCuts, labelsByDigest } = await import("../../lifecycle/release-store");
634
+ const { revocationOf, revokedRefusal } = await import("../../lifecycle/release-revocation");
635
+ for (const digest of named) {
636
+ const release = await readRelease(digest).catch(() => null);
637
+ const revocation = release ? revocationOf(release, revoked) : null;
638
+ if (!revocation) continue;
639
+ const labels = await readCuts().then((c) => labelsByDigest(c.cuts), () => new Map<string, string>());
640
+ const back = await rollbackLineFor(plan.to, records, new Set([digest]), revoked);
641
+ console.error(formatError(revokedRefusal(
642
+ { releaseDigest: digest, label: labels.get(digest) ?? null },
643
+ revocation,
644
+ back ? `Restore a release cut from a green commit instead: ${back}` : `No other release "${plan.to}" ran is kept and green; promote a release cut from a green commit (\`chant ci last-green\` names the newest).`,
645
+ )));
646
+ console.error(formatError({ message: "nothing was deployed" }));
647
+ return 1;
648
+ }
649
+ return undefined;
650
+ }
651
+
297
652
  export async function runComponentsPromote(ctx: CommandContext): Promise<number> {
298
653
  const { args } = ctx;
299
- const from = args.migrateFrom;
300
654
  const to = args.migrateTo;
301
- if (!from || !to) {
302
- console.error(formatError({ message: "--from <env> and --to <env> are required", hint: PROMOTE_USAGE }));
655
+ if (!to) {
656
+ console.error(formatError({ message: "--to <env> is required", hint: PROMOTE_USAGE }));
303
657
  return 1;
304
658
  }
305
659
  const who = actorOrRefuse(ctx);
@@ -308,6 +662,25 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
308
662
  const project = await loadProject(ctx, args.component ?? "all");
309
663
  if (typeof project === "number") return project;
310
664
 
665
+ // The member's `environments` array is its promotion order (#4030). It
666
+ // defaults --from and refuses a skipped step; a member that declares none
667
+ // has no order, so --from is required and nothing is refused.
668
+ const resolved = resolvePromotionSource({
669
+ order: promotionOrder(project.config.environments),
670
+ to,
671
+ ...(args.migrateFrom ? { from: args.migrateFrom } : {}),
672
+ ...(args.allowSkip ? { allowSkip: true } : {}),
673
+ });
674
+ if ("error" in resolved) {
675
+ console.error(formatError({ message: `${resolved.error.message} [${resolved.error.code}]`, hint: resolved.error.fix }));
676
+ return 1;
677
+ }
678
+ const from = resolved.from;
679
+ if (!from) {
680
+ console.error(formatError({ message: "--from <env> is required: this member declares no environments, so there is no promotion order to default it from", hint: PROMOTE_USAGE }));
681
+ return 1;
682
+ }
683
+
311
684
  // Without --component, each --digest is <component>=<digest> (#2602): a
312
685
  // generated promote job pins every component to the release its own
313
686
  // pipeline run recorded, rather than whatever is latest in --from.
@@ -326,6 +699,17 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
326
699
  }
327
700
 
328
701
  await fetchLifecycle().catch(() => false);
702
+
703
+ // #4026: --release names the release this promote deploys, by label or digest.
704
+ let under: UnderRelease | undefined;
705
+ if (args.release) {
706
+ const found = await resolveUnderRelease(args.release, to, "promote", () =>
707
+ "Promote a release cut from a green commit: `chant ci last-green` names the newest, and `chant release list` marks the revoked releases. Fetch the tags first: git fetch origin --tags",
708
+ );
709
+ if (typeof found === "number") return found;
710
+ under = found;
711
+ }
712
+
329
713
  const plan = planPromotion({
330
714
  from,
331
715
  to,
@@ -339,37 +723,85 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
339
723
  console.error(formatError({ message: plan.error, hint: PROMOTE_USAGE }));
340
724
  return 1;
341
725
  }
342
- return deployPlan(ctx, "promote", plan, project, who.actor, promotionRecord);
726
+ return deployPlan(ctx, "promote", plan, project, who.actor, promotionRecord, under);
343
727
  }
344
728
 
345
729
  export async function runComponentsRollback(ctx: CommandContext): Promise<number> {
346
730
  const { args } = ctx;
347
731
  const env = args.extraPositional;
348
- if (!env || !args.component) {
349
- console.error(formatError({ message: "an environment and --component <name> are required", hint: ROLLBACK_USAGE }));
732
+ if (!env || (!args.component && !args.release)) {
733
+ console.error(formatError({ message: "an environment and --component <name> or --release <label|digest> are required", hint: ROLLBACK_USAGE }));
734
+ return 1;
735
+ }
736
+ if (args.component && args.release) {
737
+ console.error(formatError({ message: "--release rolls back every component of a release, so it does not take --component", hint: ROLLBACK_USAGE }));
738
+ return 1;
739
+ }
740
+ if (args.release && args.digest) {
741
+ console.error(formatError({ message: "--digest picks one release of one component, so it needs --component, not --release", hint: ROLLBACK_USAGE }));
350
742
  return 1;
351
743
  }
352
744
  const who = actorOrRefuse(ctx);
353
745
  if (typeof who === "number") return who;
746
+ if (args.release) return runReleaseRollback(ctx, env, args.release, who.actor);
354
747
 
355
- const project = await loadProject(ctx, args.component);
748
+ const project = await loadProject(ctx, args.component!);
356
749
  if (typeof project === "number") return project;
357
750
 
358
751
  await fetchLifecycle().catch(() => false);
752
+ const records = await readLedger(env);
359
753
  const plan = planRollback({
360
754
  env,
361
- records: await readLedger(env),
755
+ records,
362
756
  declared: project.targets.map((c) => c.name),
363
- component: args.component,
757
+ component: args.component!,
364
758
  ...(args.digest ? { digest: args.digest } : {}),
365
759
  });
366
760
  if ("error" in plan) {
367
761
  console.error(formatError({ message: plan.error, hint: ROLLBACK_USAGE }));
368
762
  return 1;
369
763
  }
764
+ const refused = await refuseRevokedRestore("rollback", plan, records);
765
+ if (refused !== undefined) return refused;
370
766
  return deployPlan(ctx, "rollback", plan, project, who.actor, rollbackRecord);
371
767
  }
372
768
 
769
+ /**
770
+ * `chant components rollback <env> --release <label|digest>` (#4035): a
771
+ * gated promote of an earlier release into `env`. Every component of the
772
+ * release is restored at the digest `env` recorded running it from that
773
+ * release, its deploy reads the templates the release recorded for `env`,
774
+ * and its gates bind the release's change set planned against what `env`
775
+ * runs now, as `promote --release` binds them (#4045).
776
+ */
777
+ async function runReleaseRollback(ctx: CommandContext, env: string, ref: string, actor: string | undefined): Promise<number> {
778
+ const project = await loadProject(ctx, "all");
779
+ if (typeof project === "number") return project;
780
+ await fetchLifecycle().catch(() => false);
781
+ const records = await readLedger(env);
782
+ const under = await resolveUnderRelease(ref, env, "rollback", async (release) => {
783
+ const revoked = await revokedOrRefuse("the release before it").then((r) => (typeof r === "number" ? new Map() : r));
784
+ const back = await rollbackLineFor(env, records, new Set([release.releaseDigest]), revoked);
785
+ return back ? `Roll back to a release cut from a green commit: ${back}` : "Roll back to a release cut from a green commit; `chant release list` marks the revoked releases.";
786
+ });
787
+ if (typeof under === "number") return under;
788
+ const { readDeployOutcomes } = await import("../../lifecycle/deploy-outcome");
789
+ const outcomes = await readDeployOutcomes(env).catch(() => []);
790
+ const plan = planReleaseRollback({
791
+ env,
792
+ records,
793
+ declared: project.targets.map((c) => c.name),
794
+ release: under.release,
795
+ label: under.label,
796
+ unsettled: new Set(unsettledComponents(records, outcomes).keys()),
797
+ });
798
+ if ("error" in plan) {
799
+ console.error(formatError({ message: plan.error, hint: ROLLBACK_USAGE }));
800
+ return 1;
801
+ }
802
+ return deployPlan(ctx, "rollback", plan, project, actor, rollbackRecord, under);
803
+ }
804
+
373
805
  export async function runComponentsRedeploy(ctx: CommandContext): Promise<number> {
374
806
  const { args } = ctx;
375
807
  const env = args.extraPositional;
@@ -384,9 +816,10 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
384
816
  if (typeof project === "number") return project;
385
817
 
386
818
  await fetchLifecycle().catch(() => false);
819
+ const records = await readLedger(env);
387
820
  const plan = planRedeploy({
388
821
  env,
389
- records: await readLedger(env),
822
+ records,
390
823
  declared: project.targets.map((c) => c.name),
391
824
  component: args.component,
392
825
  ...(args.digest ? { digest: args.digest } : {}),
@@ -395,5 +828,7 @@ export async function runComponentsRedeploy(ctx: CommandContext): Promise<number
395
828
  console.error(formatError({ message: plan.error, hint: REDEPLOY_USAGE }));
396
829
  return 1;
397
830
  }
831
+ const refused = await refuseRevokedRestore("redeploy", plan, records);
832
+ if (refused !== undefined) return refused;
398
833
  return deployPlan(ctx, "redeploy", plan, project, who.actor, redeployRecord);
399
834
  }