@intentius/chant 0.124.28 → 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 (395) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +15 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +40 -3
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +61 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +24 -2
  41. package/dist/cli/registry.d.ts +15 -3
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +25 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +22 -4
  67. package/dist/components/promote.d.ts +23 -0
  68. package/dist/components/promote.d.ts.map +1 -1
  69. package/dist/components/promote.js +60 -2
  70. package/dist/components/release-compile.d.ts +9 -0
  71. package/dist/components/release-compile.d.ts.map +1 -1
  72. package/dist/components/release-compile.js +9 -3
  73. package/dist/components/release-gate.d.ts +11 -1
  74. package/dist/components/release-gate.d.ts.map +1 -1
  75. package/dist/components/release-gate.js +10 -4
  76. package/dist/components/wave-plan.d.ts +5 -0
  77. package/dist/components/wave-plan.d.ts.map +1 -1
  78. package/dist/components/wave-plan.js +10 -2
  79. package/dist/config.d.ts +38 -0
  80. package/dist/config.d.ts.map +1 -1
  81. package/dist/config.js +2 -0
  82. package/dist/flag-params.d.ts +54 -0
  83. package/dist/flag-params.d.ts.map +1 -0
  84. package/dist/flag-params.js +130 -0
  85. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  86. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  87. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  88. package/dist/lifecycle/change-set-store.d.ts +2 -1
  89. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  90. package/dist/lifecycle/change-set-store.js +2 -1
  91. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  92. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  93. package/dist/lifecycle/delivery-metrics.js +145 -0
  94. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  95. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  96. package/dist/lifecycle/deploy-outcome.js +101 -0
  97. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  98. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  99. package/dist/lifecycle/gate-change-set.js +35 -3
  100. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  101. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/git.d.ts +30 -1
  103. package/dist/lifecycle/git.d.ts.map +1 -1
  104. package/dist/lifecycle/git.js +31 -5
  105. package/dist/lifecycle/index.d.ts +4 -0
  106. package/dist/lifecycle/index.d.ts.map +1 -1
  107. package/dist/lifecycle/index.js +4 -0
  108. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  109. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  110. package/dist/lifecycle/mixed-environment.js +154 -0
  111. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  112. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  113. package/dist/lifecycle/publish-attachments.js +179 -0
  114. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  115. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-credentials.js +69 -0
  117. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  118. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-declaration.js +83 -0
  120. package/dist/lifecycle/publish-forge.d.ts +53 -0
  121. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-forge.js +298 -0
  123. package/dist/lifecycle/publish-npm.d.ts +61 -0
  124. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-npm.js +242 -0
  126. package/dist/lifecycle/publish-oci.d.ts +64 -0
  127. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-oci.js +383 -0
  129. package/dist/lifecycle/publish-record.d.ts +66 -0
  130. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-record.js +84 -0
  132. package/dist/lifecycle/publish-targets.d.ts +45 -0
  133. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-targets.js +85 -0
  135. package/dist/lifecycle/release-cut.d.ts +48 -1
  136. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  137. package/dist/lifecycle/release-cut.js +116 -23
  138. package/dist/lifecycle/release-join.d.ts +95 -0
  139. package/dist/lifecycle/release-join.d.ts.map +1 -0
  140. package/dist/lifecycle/release-join.js +139 -0
  141. package/dist/lifecycle/release-publish.d.ts +312 -0
  142. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  143. package/dist/lifecycle/release-publish.js +335 -0
  144. package/dist/lifecycle/release-revocation.d.ts +80 -0
  145. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  146. package/dist/lifecycle/release-revocation.js +123 -0
  147. package/dist/lifecycle/release-store.d.ts +12 -2
  148. package/dist/lifecycle/release-store.d.ts.map +1 -1
  149. package/dist/lifecycle/release-store.js +4 -2
  150. package/dist/lifecycle/release-verify.d.ts +61 -0
  151. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  152. package/dist/lifecycle/release-verify.js +92 -0
  153. package/dist/lifecycle/release-workstation.d.ts +91 -0
  154. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  155. package/dist/lifecycle/release-workstation.js +180 -0
  156. package/dist/lifecycle/retention-policy.d.ts +45 -0
  157. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  158. package/dist/lifecycle/retention-policy.js +43 -0
  159. package/dist/lifecycle/retention.d.ts +185 -0
  160. package/dist/lifecycle/retention.d.ts.map +1 -0
  161. package/dist/lifecycle/retention.js +400 -0
  162. package/dist/plan-summary.d.ts.map +1 -1
  163. package/dist/plan-summary.js +8 -2
  164. package/dist/pr-loop.d.ts +7 -0
  165. package/dist/pr-loop.d.ts.map +1 -1
  166. package/dist/pr-loop.js +23 -4
  167. package/dist/pr-report-url.d.ts +82 -0
  168. package/dist/pr-report-url.d.ts.map +1 -0
  169. package/dist/pr-report-url.js +174 -0
  170. package/dist/provenance.d.ts +2 -2
  171. package/dist/provenance.d.ts.map +1 -1
  172. package/dist/run-event-store.d.ts +29 -2
  173. package/dist/run-event-store.d.ts.map +1 -1
  174. package/dist/run-event-store.js +50 -3
  175. package/dist/run-event-stream.d.ts +2 -1
  176. package/dist/run-event-stream.d.ts.map +1 -1
  177. package/dist/run-event-stream.js +5 -1
  178. package/dist/run-event.d.ts +19 -1
  179. package/dist/run-event.d.ts.map +1 -1
  180. package/dist/run-event.js +2 -0
  181. package/dist/wave-runtime.mjs +260 -229
  182. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  183. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  184. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  185. package/dist/workspace/change-set.schema.json +5 -1
  186. package/dist/workspace/check.schema.json +4 -2
  187. package/dist/workspace/checks/records.d.ts +9 -1
  188. package/dist/workspace/checks/records.d.ts.map +1 -1
  189. package/dist/workspace/checks/records.js +35 -1
  190. package/dist/workspace/checks.d.ts +1 -0
  191. package/dist/workspace/checks.d.ts.map +1 -1
  192. package/dist/workspace/checks.js +2 -1
  193. package/dist/workspace/ci-green.d.ts +8 -0
  194. package/dist/workspace/ci-green.d.ts.map +1 -1
  195. package/dist/workspace/ci-green.js +10 -3
  196. package/dist/workspace/declaration.d.ts +16 -0
  197. package/dist/workspace/declaration.d.ts.map +1 -1
  198. package/dist/workspace/declaration.js +26 -0
  199. package/dist/workspace/declaration.schema.json +64 -0
  200. package/dist/workspace/declared-kinds.d.ts +16 -0
  201. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  202. package/dist/workspace/declared-kinds.js +25 -0
  203. package/dist/workspace/delivery-metrics.d.ts +91 -0
  204. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  205. package/dist/workspace/delivery-metrics.js +217 -0
  206. package/dist/workspace/delivery-metrics.schema.json +519 -0
  207. package/dist/workspace/flags-read.d.ts +37 -0
  208. package/dist/workspace/flags-read.d.ts.map +1 -0
  209. package/dist/workspace/flags-read.js +103 -0
  210. package/dist/workspace/flags.d.ts +100 -0
  211. package/dist/workspace/flags.d.ts.map +1 -0
  212. package/dist/workspace/flags.js +98 -0
  213. package/dist/workspace/graph-cli.js +1 -1
  214. package/dist/workspace/intent-cli.d.ts +11 -2
  215. package/dist/workspace/intent-cli.d.ts.map +1 -1
  216. package/dist/workspace/intent-cli.js +54 -5
  217. package/dist/workspace/intent-trace.d.ts +219 -0
  218. package/dist/workspace/intent-trace.d.ts.map +1 -0
  219. package/dist/workspace/intent-trace.js +314 -0
  220. package/dist/workspace/intent.d.ts +30 -6
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/intent.js +141 -21
  223. package/dist/workspace/intent.schema.json +702 -6
  224. package/dist/workspace/overlap.d.ts +146 -0
  225. package/dist/workspace/overlap.d.ts.map +1 -0
  226. package/dist/workspace/overlap.js +276 -0
  227. package/dist/workspace/reason-codes.d.ts +2 -0
  228. package/dist/workspace/reason-codes.d.ts.map +1 -1
  229. package/dist/workspace/reason-codes.js +3 -0
  230. package/dist/workspace/records.d.ts +15 -2
  231. package/dist/workspace/records.d.ts.map +1 -1
  232. package/dist/workspace/records.js +32 -0
  233. package/dist/workspace/records.schema.json +21 -2
  234. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  235. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  236. package/dist/workspace/releases.d.ts +9 -0
  237. package/dist/workspace/releases.d.ts.map +1 -1
  238. package/dist/workspace/releases.js +9 -0
  239. package/dist/workspace/releases.schema.json +123 -1
  240. package/dist/workspace/run-event.schema.json +10 -3
  241. package/dist/workspace/status-gates.d.ts +8 -0
  242. package/dist/workspace/status-gates.d.ts.map +1 -1
  243. package/dist/workspace/status.d.ts +78 -0
  244. package/dist/workspace/status.d.ts.map +1 -1
  245. package/dist/workspace/status.js +170 -3
  246. package/dist/workspace/status.schema.json +198 -1
  247. package/dist/workspace/wip.d.ts.map +1 -1
  248. package/dist/workspace/wip.js +11 -3
  249. package/package.json +1 -1
  250. package/src/build-params.test.ts +46 -0
  251. package/src/build-params.ts +55 -1
  252. package/src/change-set-graph.ts +10 -2
  253. package/src/change-set.ts +14 -3
  254. package/src/cli/build-params-cli.ts +22 -9
  255. package/src/cli/commands/build.ts +6 -3
  256. package/src/cli/handlers/approve-release.test.ts +5 -1
  257. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  258. package/src/cli/handlers/approve.ts +47 -0
  259. package/src/cli/handlers/build.test.ts +15 -0
  260. package/src/cli/handlers/build.ts +17 -4
  261. package/src/cli/handlers/fan-out.test.ts +180 -1
  262. package/src/cli/handlers/fan-out.ts +38 -3
  263. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  264. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  265. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  266. package/src/cli/handlers/pr.test.ts +148 -1
  267. package/src/cli/handlers/pr.ts +139 -28
  268. package/src/cli/handlers/promote.test.ts +280 -2
  269. package/src/cli/handlers/promote.ts +210 -34
  270. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  271. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  272. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  273. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  274. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  275. package/src/cli/handlers/release.e2e.test.ts +146 -32
  276. package/src/cli/handlers/release.ts +415 -28
  277. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  278. package/src/cli/handlers/run-release.test.ts +55 -0
  279. package/src/cli/handlers/run.ts +69 -5
  280. package/src/cli/main.test.ts +1 -0
  281. package/src/cli/main.ts +61 -10
  282. package/src/cli/parse-args.ts +19 -2
  283. package/src/cli/registry.ts +15 -3
  284. package/src/cli/run-events.ts +5 -2
  285. package/src/components/auto-release.test.ts +9 -0
  286. package/src/components/auto-release.ts +3 -0
  287. package/src/components/capability.ts +9 -0
  288. package/src/components/driver.test.ts +24 -0
  289. package/src/components/driver.ts +36 -0
  290. package/src/components/fan-out-inputs.test.ts +85 -0
  291. package/src/components/fan-out-inputs.ts +192 -0
  292. package/src/components/fan-out-record.test.ts +10 -0
  293. package/src/components/fan-out-record.ts +19 -0
  294. package/src/components/fan-out-run.ts +48 -0
  295. package/src/components/fan-out.ts +4 -0
  296. package/src/components/pr-pipeline.ts +35 -4
  297. package/src/components/promote.ts +68 -2
  298. package/src/components/release-compile.ts +17 -3
  299. package/src/components/release-gate.ts +18 -4
  300. package/src/components/wave-plan.ts +15 -2
  301. package/src/config.ts +8 -0
  302. package/src/flag-params.test.ts +144 -0
  303. package/src/flag-params.ts +134 -0
  304. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  305. package/src/lifecycle/change-set-store.ts +2 -1
  306. package/src/lifecycle/delivery-metrics.ts +263 -0
  307. package/src/lifecycle/deploy-outcome.ts +147 -0
  308. package/src/lifecycle/gate-change-set.ts +37 -6
  309. package/src/lifecycle/gate-ledger.ts +2 -2
  310. package/src/lifecycle/git.ts +61 -8
  311. package/src/lifecycle/index.ts +4 -0
  312. package/src/lifecycle/mixed-environment.test.ts +97 -0
  313. package/src/lifecycle/mixed-environment.ts +204 -0
  314. package/src/lifecycle/publish-attachments.ts +196 -0
  315. package/src/lifecycle/publish-credentials.ts +84 -0
  316. package/src/lifecycle/publish-declaration.ts +95 -0
  317. package/src/lifecycle/publish-forge.ts +325 -0
  318. package/src/lifecycle/publish-npm.ts +259 -0
  319. package/src/lifecycle/publish-oci.ts +396 -0
  320. package/src/lifecycle/publish-record.ts +113 -0
  321. package/src/lifecycle/publish-resume.test.ts +168 -0
  322. package/src/lifecycle/publish-targets.test.ts +494 -0
  323. package/src/lifecycle/publish-targets.ts +100 -0
  324. package/src/lifecycle/release-cut.test.ts +74 -2
  325. package/src/lifecycle/release-cut.ts +158 -22
  326. package/src/lifecycle/release-flag.test.ts +112 -0
  327. package/src/lifecycle/release-join.test.ts +68 -0
  328. package/src/lifecycle/release-join.ts +182 -0
  329. package/src/lifecycle/release-publish.test.ts +225 -0
  330. package/src/lifecycle/release-publish.ts +568 -0
  331. package/src/lifecycle/release-revocation.ts +147 -0
  332. package/src/lifecycle/release-store.ts +15 -4
  333. package/src/lifecycle/release-verify.test.ts +110 -0
  334. package/src/lifecycle/release-verify.ts +120 -0
  335. package/src/lifecycle/release-workstation.test.ts +204 -0
  336. package/src/lifecycle/release-workstation.ts +216 -0
  337. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  338. package/src/lifecycle/retention-policy.ts +63 -0
  339. package/src/lifecycle/retention.test.ts +363 -0
  340. package/src/lifecycle/retention.ts +508 -0
  341. package/src/plan-summary.test.ts +20 -0
  342. package/src/plan-summary.ts +8 -1
  343. package/src/pr-loop.ts +25 -4
  344. package/src/pr-report-url.test.ts +131 -0
  345. package/src/pr-report-url.ts +209 -0
  346. package/src/provenance.ts +2 -2
  347. package/src/run-event-store.ts +63 -3
  348. package/src/run-event-stream.ts +4 -1
  349. package/src/run-event.ts +21 -1
  350. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  351. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  352. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  353. package/src/workspace/change-set.schema.json +5 -1
  354. package/src/workspace/check.schema.json +4 -2
  355. package/src/workspace/checks/records.ts +35 -1
  356. package/src/workspace/checks.ts +2 -1
  357. package/src/workspace/ci-green.ts +14 -3
  358. package/src/workspace/declaration.schema.json +64 -0
  359. package/src/workspace/declaration.test.ts +28 -0
  360. package/src/workspace/declaration.ts +38 -0
  361. package/src/workspace/declared-kinds.ts +40 -1
  362. package/src/workspace/delivery-metrics.schema.json +519 -0
  363. package/src/workspace/delivery-metrics.test.ts +236 -0
  364. package/src/workspace/delivery-metrics.ts +286 -0
  365. package/src/workspace/flags-read.ts +121 -0
  366. package/src/workspace/flags.test.ts +141 -0
  367. package/src/workspace/flags.ts +157 -0
  368. package/src/workspace/graph-cli.ts +1 -1
  369. package/src/workspace/intent-cli.ts +46 -5
  370. package/src/workspace/intent-trace.test.ts +173 -0
  371. package/src/workspace/intent-trace.ts +484 -0
  372. package/src/workspace/intent.schema.json +702 -6
  373. package/src/workspace/intent.ts +148 -25
  374. package/src/workspace/older-records.test.ts +412 -0
  375. package/src/workspace/overlap.test.ts +165 -0
  376. package/src/workspace/overlap.ts +340 -0
  377. package/src/workspace/read-contract.e2e.test.ts +2 -1
  378. package/src/workspace/reason-codes.test.ts +5 -0
  379. package/src/workspace/reason-codes.ts +3 -0
  380. package/src/workspace/record-assets.test.ts +2 -1
  381. package/src/workspace/records-contract.e2e.test.ts +2 -1
  382. package/src/workspace/records.schema.json +21 -2
  383. package/src/workspace/records.ts +36 -2
  384. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  385. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  386. package/src/workspace/releases.schema.json +123 -1
  387. package/src/workspace/releases.ts +11 -0
  388. package/src/workspace/run-event.schema.json +10 -3
  389. package/src/workspace/status-contract.test.ts +29 -0
  390. package/src/workspace/status-gates.ts +8 -0
  391. package/src/workspace/status-mixed.test.ts +152 -0
  392. package/src/workspace/status-revoked.test.ts +153 -0
  393. package/src/workspace/status.schema.json +198 -1
  394. package/src/workspace/status.ts +254 -3
  395. package/src/workspace/wip.ts +11 -2
@@ -177,6 +177,17 @@
177
177
  }
178
178
  }
179
179
  }
180
+ },
181
+ "revoked": {
182
+ "description": "Added within contract version 1 by #4033, written by chant 0.124.29 and newer. The ci/revoked/<sha> tag on the release's commit, or null when the commit is not revoked. A revoked release is never promoted (chant components promote --release refuses it) and its pending release gates refuse approval. Null too when the ci tags can't be judged, as when ci.green.signedSince names no commit here, which chant workspace check reports.",
183
+ "oneOf": [
184
+ {
185
+ "type": "null"
186
+ },
187
+ {
188
+ "$ref": "#/$defs/revocation"
189
+ }
190
+ ]
180
191
  }
181
192
  }
182
193
  },
@@ -280,6 +291,13 @@
280
291
  "type": "string",
281
292
  "description": "ISO 8601."
282
293
  },
294
+ "runId": {
295
+ "type": [
296
+ "string",
297
+ "null"
298
+ ],
299
+ "description": "Added by #4027, written by chant 0.124.29 and newer. The run the cut ran in: the CI run's id in CI (GITHUB_RUN_ID, CI_PIPELINE_ID), or outside CI an id minted on the machine that cut, local-<epoch ms>. Null on a cut written before 0.124.29."
300
+ },
283
301
  "runOrigin": {
284
302
  "oneOf": [
285
303
  {
@@ -308,7 +326,7 @@
308
326
  }
309
327
  }
310
328
  ],
311
- "description": "The CI run the cut ran in: the forge (github for GitHub and Forgejo, gitlab), the repo or project, and a link to the run. Null outside CI."
329
+ "description": "The run the cut ran in: the forge (github for GitHub and Forgejo, gitlab, or local for an id minted outside CI), the repo or project, and a link to the run. From chant 0.124.29 a cut outside CI writes { forge: local } (#4027); a cut written before reads null outside CI."
312
330
  },
313
331
  "signature": {
314
332
  "type": [
@@ -386,6 +404,110 @@
386
404
  }
387
405
  }
388
406
  }
407
+ },
408
+ "revocation": {
409
+ "type": "object",
410
+ "description": "A revoked tag on a release's commit (#4033, ws-105), as chant ci list --json shows a commit's revoked tag, with the commit. Read from local tags at read time and judged as release cut judges them: with ci.green.signedSince, a revoked tag without a valid runner statement is ignored and revokes nothing.",
411
+ "required": [
412
+ "commit",
413
+ "tag",
414
+ "tagged",
415
+ "phase",
416
+ "run",
417
+ "signer"
418
+ ],
419
+ "properties": {
420
+ "commit": {
421
+ "type": "string",
422
+ "description": "The release's commit, which the tag names."
423
+ },
424
+ "tag": {
425
+ "type": "string",
426
+ "description": "ci/revoked/<sha>."
427
+ },
428
+ "tagged": {
429
+ "type": [
430
+ "string",
431
+ "null"
432
+ ],
433
+ "description": "When the tag was made, ISO 8601, or null for a lightweight tag."
434
+ },
435
+ "phase": {
436
+ "type": [
437
+ "string",
438
+ "null"
439
+ ],
440
+ "description": "The phase the annotation names as failed, or null when the annotation can't be read."
441
+ },
442
+ "run": {
443
+ "description": "The run the annotation names as failed, or null.",
444
+ "oneOf": [
445
+ {
446
+ "type": "null"
447
+ },
448
+ {
449
+ "type": "object",
450
+ "required": [
451
+ "name",
452
+ "id",
453
+ "conclusion",
454
+ "url"
455
+ ],
456
+ "properties": {
457
+ "name": {
458
+ "type": "string"
459
+ },
460
+ "id": {
461
+ "type": [
462
+ "integer",
463
+ "null"
464
+ ],
465
+ "description": "The check run's id, or null for a chant Op run."
466
+ },
467
+ "conclusion": {
468
+ "type": [
469
+ "string",
470
+ "null"
471
+ ]
472
+ },
473
+ "url": {
474
+ "type": [
475
+ "string",
476
+ "null"
477
+ ]
478
+ },
479
+ "opRun": {
480
+ "type": "object",
481
+ "required": [
482
+ "id",
483
+ "env",
484
+ "signer"
485
+ ],
486
+ "properties": {
487
+ "id": {
488
+ "type": "string"
489
+ },
490
+ "env": {
491
+ "type": "string"
492
+ },
493
+ "signer": {
494
+ "type": "string"
495
+ }
496
+ },
497
+ "description": "Present only when the run is a chant Op run (ci.green.source runs, #4044): its id, the env it is recorded under and the runner whose statement it carries. id above is then null."
498
+ }
499
+ }
500
+ }
501
+ ]
502
+ },
503
+ "signer": {
504
+ "type": [
505
+ "string",
506
+ "null"
507
+ ],
508
+ "description": "The runner principal whose statement the tag carries, or null when it counts unsigned."
509
+ }
510
+ }
389
511
  }
390
512
  }
391
513
  }
@@ -8,6 +8,12 @@
8
8
  * `releaseDigest`. A promote under a release writes that field; deploys made
9
9
  * any other way do not name a release yet (#4043), so they list nowhere.
10
10
  *
11
+ * Each release carries `revoked` (#4033, ws-105): the `ci/revoked/<sha>` tag
12
+ * on its commit, judged as `release cut` judges it, or null. It is read from
13
+ * local tags at read time; nothing records it. When the tags can't be judged
14
+ * (a `ci.green.signedSince` naming no commit here) every release reads null,
15
+ * and `chant workspace check` reports the declaration.
16
+ *
11
17
  * Each cut carries `signatureCheck` (#4031): whether its signature verifies
12
18
  * for its actor against the signers at base, and, when the declaration names
13
19
  * a `release` gate with a class, whether the actor is in it.
@@ -24,6 +30,7 @@ import { readAllReleases, readCuts, type Release, type ReleaseCut } from "../lif
24
30
  import { resolveMemberLedger } from "../lifecycle/member-ledger";
25
31
  import { readerVersion } from "./declaration";
26
32
  import { checkReleaseSignature, type GateAdmissionRule, type ReleaseSignatureCheck } from "./identity-gate-rule";
33
+ import { readRevokedCommits, revocationOf, type ReleaseRevocation } from "../lifecycle/release-revocation";
27
34
 
28
35
  export const RELEASES_CONTRACT_VERSION = 1;
29
36
  export const RELEASES_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/releases/v1/releases.schema.json";
@@ -41,6 +48,8 @@ export interface ReleaseEntry extends Omit<Release, "version"> {
41
48
  cuts: Array<Omit<ReleaseCut, "version" | "releaseDigest"> & { signatureCheck: ReleaseSignatureCheck }>;
42
49
  /** The environments that run it, and in each the components whose latest deploy names it. Sorted by env. */
43
50
  environments: Array<{ env: string; components: string[] }>;
51
+ /** The revoked tag on its commit (#4033), or null when the commit is not revoked. */
52
+ revoked: ReleaseRevocation | null;
44
53
  }
45
54
 
46
55
  export type ReleasesDocument =
@@ -93,6 +102,7 @@ export async function releasesDocument(req: { cwd: string }): Promise<ReleasesDo
93
102
  }
94
103
 
95
104
  const rule = await releaseRule(req.cwd);
105
+ const revoked = await readRevokedCommits(req.cwd).catch(() => new Map<string, ReleaseRevocation>());
96
106
  const entries: ReleaseEntry[] = releases.map(({ version: _v, ...release }) => {
97
107
  const own = cuts.filter((c) => c.releaseDigest === release.releaseDigest);
98
108
  return {
@@ -102,6 +112,7 @@ export async function releasesDocument(req: { cwd: string }): Promise<ReleasesDo
102
112
  environments: [...(running.get(release.releaseDigest) ?? new Map<string, string[]>())]
103
113
  .map(([env, components]) => ({ env, components }))
104
114
  .sort((a, b) => (a.env < b.env ? -1 : 1)),
115
+ revoked: revocationOf(release, revoked),
105
116
  };
106
117
  });
107
118
  const latest = (e: ReleaseEntry): string => e.cuts.map((c) => c.timestamp).sort().at(-1) ?? "";
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://intentius.io/chant/schemas/workspace/run-event/v1/run-event.schema.json",
4
4
  "title": "chant run event",
5
- "description": "One event of a run's stream (#4019, behold #678): one JSON object per line, written by writeRunEvent and createRunEventWriter in src/run-event.ts. It covers Op runs, component runs and pipeline runs alike. Every event names the run, what it ran, the commit it built from and the runtime it ran on, so a reader shows a run the same way wherever it happened. A run's stream opens with run-start and ends with run-settle, whose status is the run's verdict; seq counts the events from 0. Version 1 of the read contract. Readers ignore fields they do not know; a field is only ever added within a version. The status sets are closed. Contract version 1 of this document is written by chant 0.124.28 and newer; the read contract as a whole by chant 0.81.0 and newer.",
5
+ "description": "One event of a run's stream (#4019, behold #678): one JSON object per line, written by writeRunEvent and createRunEventWriter in src/run-event.ts. It covers Op runs, component runs and pipeline runs alike. Every event names the run, what it ran, the commit it built from and the runtime it ran on, so a reader shows a run the same way wherever it happened. A run's stream opens with run-start and ends with run-settle, whose status is the run's verdict; seq counts the events from 0. Version 1 of the read contract. Readers ignore fields they do not know; a field is only ever added within a version. A status set gains a value only as an addition within a version, which the read contract lists with the chant that first writes it; a reader treats a status it does not know as a settle it cannot classify and reads on. Contract version 1 of this document is written by chant 0.124.28 and newer; the read contract as a whole by chant 0.81.0 and newer.",
6
6
  "oneOf": [
7
7
  { "$ref": "#/$defs/run-start" },
8
8
  { "$ref": "#/$defs/run-settle" },
@@ -150,13 +150,20 @@
150
150
  { "$ref": "#/$defs/envelope" },
151
151
  {
152
152
  "type": "object",
153
- "description": "A component's composition settled, after any rollback the run did for it.",
153
+ "description": "A component's composition settled, after any rollback the run did for it. A component the run did not start settles too, with no component-start before it: blocked when a dependency failed or stopped at a gate, already-applied when an earlier attempt the run resumed applied it.",
154
154
  "required": ["type", "wave", "component", "status"],
155
155
  "properties": {
156
156
  "type": { "const": "component-settle" },
157
157
  "wave": { "type": "integer", "minimum": 1 },
158
158
  "component": { "type": "string" },
159
- "status": { "$ref": "#/$defs/settleStatus" }
159
+ "status": {
160
+ "enum": ["ok", "failed", "gated", "waiting", "blocked", "already-applied"],
161
+ "description": "The settle statuses of a run, wave or phase, and two more for a component the run did not start, added within contract version 1 by #4007 and written by chant 0.124.29 and newer. blocked: a dependency failed or stopped at a gate, so it never ran; blockedBy names the dependency. already-applied: an earlier attempt the run resumed applied it, so it did not run again; its wave is the wave it had in that attempt's derivation. A reader that knows only the four settle statuses can read either as a component that did not run in this run."
162
+ },
163
+ "blockedBy": {
164
+ "type": "string",
165
+ "description": "With blocked: the failed or gated component the run reached this one from. Added within contract version 1 by #4007, written by chant 0.124.29 and newer."
166
+ }
160
167
  }
161
168
  }
162
169
  ]
@@ -478,6 +478,35 @@ describe("commit, run and gate on the records status --json lists (#4020)", () =
478
478
  });
479
479
  });
480
480
 
481
+ describe("the release a deploy was made from, on the records status --json lists (#4043)", () => {
482
+ test("a release item carries the releaseDigest and the label it was cut with, and null when there is none", async () => {
483
+ const root = twoLayouts();
484
+ const rd = `jcs1-sha256:${"e".repeat(64)}`;
485
+ const cut = JSON.stringify({ version: 1, releaseDigest: rd, label: "v1.2.0", actor: "alice", timestamp: "2026-09-19T00:00:00.000Z", runOrigin: null, signature: null });
486
+ lifecycle(root, {
487
+ "_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("a"), "a".repeat(40), { releaseDigest: rd })),
488
+ "_members/web/_releases/cuts.jsonl": `${cut}\n`,
489
+ "staging/releases.jsonl": jsonl(release("api", "staging", D("b"), "b".repeat(40))),
490
+ });
491
+ const doc = result(await workspaceStatus({ cwd: root, env: "staging", now: NOW }));
492
+ expectValid(doc);
493
+ const [, web, api] = doc.members;
494
+ expect(web.environments[0].releases[0]).toMatchObject({ releaseDigest: rd, releaseLabel: "v1.2.0" });
495
+ expect(api.environments[0].releases[0]).toMatchObject({ releaseDigest: null, releaseLabel: null });
496
+ });
497
+
498
+ test("a release with no cut line keeps its digest and has no label", async () => {
499
+ const root = twoLayouts();
500
+ const rd = `jcs1-sha256:${"e".repeat(64)}`;
501
+ lifecycle(root, {
502
+ "_members/web/staging/releases.jsonl": jsonl(release("web", "staging", D("a"), "a".repeat(40), { releaseDigest: rd })),
503
+ });
504
+ const doc = result(await workspaceStatus({ cwd: root, env: "staging", now: NOW }));
505
+ expectValid(doc);
506
+ expect(doc.members[1].environments[0].releases[0]).toMatchObject({ releaseDigest: rd, releaseLabel: null });
507
+ });
508
+ });
509
+
481
510
  describe("gate state in chant workspace status --json (#2674)", () => {
482
511
  test("each member lists its gates with state, approvals, quorum and the approve line", async () => {
483
512
  const doc = result(await workspaceStatus({ cwd: gated(), env: "staging", compareTo: "prod", now: NOW }));
@@ -25,6 +25,7 @@ import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-
25
25
  import { isPendingGateExpired, latestPendingGate, latestResolutionForPlan, parseGateLedger } from "../lifecycle/gate-ledger";
26
26
  import { humanQuorumSince, tallyGateApprovals, type GateApprovalRule } from "../op/gate";
27
27
  import type { ReasonCode } from "./reason-codes";
28
+ import type { ReleaseRevocation } from "../lifecycle/release-revocation";
28
29
  import { CHANGE_SETS_DIR, changeSetFile } from "../lifecycle/change-set-store";
29
30
 
30
31
  /** The stored change-set directory beside a gate ledger directory: `<prefix>_change-sets`. */
@@ -73,6 +74,13 @@ export interface StatusGate {
73
74
  */
74
75
  releaseDigest?: string;
75
76
  state: GateState;
77
+ /**
78
+ * Set on a release gate whose release was cut from a revoked commit
79
+ * (#4033, ws-105): the revoked tag on that commit. The gate is then
80
+ * `superseded` (or `expired`), and `chant approve` refuses it with
81
+ * `release-revoked`. Added by `workspace status`; absent otherwise.
82
+ */
83
+ revoked?: ReleaseRevocation;
76
84
  /** When the run recorded the pending fact the state is read against. */
77
85
  recordedAt: string;
78
86
  /** The commit the run reached the gate at (#4020), from the pending fact. Absent on a fact written before chant 0.124.28. */
@@ -0,0 +1,152 @@
1
+ /**
2
+ * #4035: an environment a promote left mixed, as `workspace status --json`
3
+ * shows it: what each component runs, and the redeploy and rollback lines
4
+ * that settle it. Against temp repos; nothing is pushed.
5
+ */
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { withTestDir } from "@intentius/chant-test-utils";
9
+ import { spawnSync } from "node:child_process";
10
+ import { writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { Ajv2020 } from "ajv/dist/2020";
13
+ import statusSchema from "./status.schema.json";
14
+ import { formatStatus, workspaceStatus, type StatusDocument } from "./status";
15
+ import { appendCut, makeRelease, persistRelease, type Release } from "../lifecycle/release-store";
16
+ import { appendReleaseRecord, type ReleaseRecordInput } from "../lifecycle/release-ledger";
17
+ import { appendDeployOutcome } from "../lifecycle/deploy-outcome";
18
+ import { contentDigest } from "../content-digest";
19
+
20
+ const validateStatus = new Ajv2020({ strict: true, allErrors: true }).compile(statusSchema);
21
+ const NOW = "2026-10-17T14:00:00.000Z";
22
+
23
+ function git(cwd: string, ...args: string[]): string {
24
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
25
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
26
+ return r.stdout.trim();
27
+ }
28
+
29
+ function result(doc: StatusDocument): Extract<StatusDocument, { members: unknown }> {
30
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
31
+ expect(validateStatus(doc), JSON.stringify(validateStatus.errors, null, 2)).toBe(true);
32
+ return doc;
33
+ }
34
+
35
+ /** A workspace whose root member has releases v1.0.0 and v1.1.0 of api and web, and runs v1.0.0 in prod. */
36
+ async function setup(dir: string): Promise<{ v1: Release; v2: Release; deploy: (input: Partial<ReleaseRecordInput> & { component: string; timestamp: string; releaseDigest: string }) => Promise<void> }> {
37
+ git(dir, "init", "-q", "-b", "main");
38
+ git(dir, "config", "user.email", "t@chant.dev");
39
+ git(dir, "config", "user.name", "T");
40
+ writeFileSync(
41
+ join(dir, "chant.workspace.json"),
42
+ JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the fixture" }] }, null, 2),
43
+ );
44
+ git(dir, "add", ".");
45
+ git(dir, "commit", "-q", "-m", "init");
46
+ const first = git(dir, "rev-parse", "HEAD");
47
+ git(dir, "commit", "-q", "--allow-empty", "-m", "next");
48
+ const second = git(dir, "rev-parse", "HEAD");
49
+ const at = (commit: string) =>
50
+ makeRelease({
51
+ member: null,
52
+ commit,
53
+ artifactDigests: ["api", "web"].map((component) => ({ component, kind: "image" as const, path: `${component}.tar`, digest: contentDigest(`${component} ${commit}`) })),
54
+ targets: [{ env: "prod", templates: [], params: [] }],
55
+ });
56
+ const v1 = at(first);
57
+ const v2 = at(second);
58
+ for (const [r, label, ts] of [[v1, "v1.0.0", "10"], [v2, "v1.1.0", "11"]] as const) {
59
+ await persistRelease(r, [], { cwd: dir });
60
+ await appendCut({ releaseDigest: r.releaseDigest, label, actor: "alice", timestamp: `2026-10-17T${ts}:00:00.000Z`, runOrigin: null }, { cwd: dir });
61
+ }
62
+ const deploy = async (input: Partial<ReleaseRecordInput> & { component: string; timestamp: string; releaseDigest: string }) => {
63
+ await appendReleaseRecord(
64
+ { env: "prod", digest: contentDigest(`${input.component} ${input.releaseDigest}`), gitSha: first, runId: "r", actor: "ci", ...input },
65
+ { cwd: dir },
66
+ );
67
+ };
68
+ for (const component of ["api", "web"]) await deploy({ component, timestamp: "2026-10-17T12:00:00.000Z", releaseDigest: v1.releaseDigest });
69
+ return { v1, v2, deploy };
70
+ }
71
+
72
+ describe("a mixed environment in workspace status (#4035)", () => {
73
+ test("a partly applied promote shows the environment mixed, with the redeploy and rollback lines, until it is settled", async () => {
74
+ await withTestDir(async (dir) => {
75
+ const { v1, v2, deploy } = await setup(dir);
76
+
77
+ // Before the promote: one release, nothing mixed.
78
+ const before = result(await workspaceStatus({ cwd: dir, env: "prod", now: NOW }));
79
+ expect(before.members[0].environments[0]).not.toHaveProperty("mixed");
80
+
81
+ // The promote of v1.1.0 deploys api and fails on web.
82
+ const at = "2026-10-17T13:00:00.000Z";
83
+ await deploy({ component: "api", timestamp: at, releaseDigest: v2.releaseDigest, runId: "run-9", promotedFrom: { env: "staging", runId: "run-8", timestamp: "2026-10-17T12:30:00.000Z" } });
84
+ await appendDeployOutcome(
85
+ {
86
+ version: 1, env: "prod", verb: "promote", runId: "run-9", timestamp: at, actor: "bob", releaseDigest: v2.releaseDigest, status: "fail",
87
+ components: [
88
+ { component: "api", outcome: "deployed", digest: contentDigest(`api ${v2.releaseDigest}`), releaseDigest: v2.releaseDigest },
89
+ { component: "web", outcome: "failed", digest: contentDigest(`web ${v2.releaseDigest}`), releaseDigest: v2.releaseDigest },
90
+ ],
91
+ },
92
+ { cwd: dir },
93
+ );
94
+
95
+ const doc = result(await workspaceStatus({ cwd: dir, env: "prod", now: NOW }));
96
+ const prod = doc.members[0].environments[0];
97
+ expect(prod.mixed).toEqual({
98
+ components: [
99
+ { component: "api", digest: contentDigest(`api ${v2.releaseDigest}`), releaseDigest: v2.releaseDigest, releaseLabel: "v1.1.0", state: "settled", attempt: null },
100
+ {
101
+ component: "web",
102
+ digest: contentDigest(`web ${v1.releaseDigest}`),
103
+ releaseDigest: v1.releaseDigest,
104
+ releaseLabel: "v1.0.0",
105
+ state: "failed",
106
+ attempt: { verb: "promote", runId: "run-9", timestamp: at, digest: contentDigest(`web ${v2.releaseDigest}`), releaseDigest: v2.releaseDigest, releaseLabel: "v1.1.0" },
107
+ },
108
+ ],
109
+ redeploy: ["chant components redeploy prod --component web"],
110
+ rollback: "chant components rollback prod --release v1.0.0",
111
+ });
112
+ expect(formatStatus(doc)).toContain(" prod is mixed: api runs release v1.1.0; web failed (promote run-9), recorded release v1.0.0");
113
+ expect(formatStatus(doc)).toContain(" redeploy: chant components redeploy prod --component web");
114
+ expect(formatStatus(doc)).toContain(" roll back: chant components rollback prod --release v1.0.0");
115
+
116
+ // A redeploy of web settles it, but prod still runs two releases.
117
+ await deploy({ component: "web", timestamp: "2026-10-17T13:10:00.000Z", releaseDigest: v1.releaseDigest, redeploys: { env: "prod", runId: "r", timestamp: "2026-10-17T12:00:00.000Z" } });
118
+ const redeployed = result(await workspaceStatus({ cwd: dir, env: "prod", now: NOW })).members[0].environments[0];
119
+ expect(redeployed.mixed).toMatchObject({
120
+ components: [{ component: "api", state: "settled", releaseLabel: "v1.1.0" }, { component: "web", state: "settled", releaseLabel: "v1.0.0" }],
121
+ redeploy: [],
122
+ rollback: "chant components rollback prod --release v1.0.0",
123
+ });
124
+
125
+ // The same promote run again puts web on v1.1.0: one release, nothing mixed.
126
+ await deploy({ component: "web", timestamp: "2026-10-17T13:20:00.000Z", releaseDigest: v2.releaseDigest });
127
+ const settled = result(await workspaceStatus({ cwd: dir, env: "prod", now: NOW })).members[0].environments[0];
128
+ expect(settled).not.toHaveProperty("mixed");
129
+ });
130
+ });
131
+
132
+ test("a revoked release's rollback line names the release the environment ran before it", async () => {
133
+ await withTestDir(async (dir) => {
134
+ const { v2, deploy } = await setup(dir);
135
+ for (const component of ["api", "web"]) await deploy({ component, timestamp: "2026-10-17T13:00:00.000Z", releaseDigest: v2.releaseDigest });
136
+ writeFileSync(
137
+ join(dir, "chant.workspace.json"),
138
+ JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the fixture" }], ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"] } } }, null, 2),
139
+ );
140
+ git(dir, "commit", "-q", "-am", "ci.green");
141
+ const record = { schema: 1, tag: "revoked", commit: v2.commit, branch: "main", at: "2026-10-17T13:30:00.000Z", phase: "test", run: { name: "test", id: 77, conclusion: "failure", url: null } };
142
+ git(dir, "tag", "-a", "-m", `ci: phase test failed\n\n${JSON.stringify(record, null, 2)}\n`, `ci/revoked/${v2.commit}`, v2.commit);
143
+
144
+ const prod = result(await workspaceStatus({ cwd: dir, env: "prod", now: NOW })).members[0].environments[0];
145
+ expect(prod.releases.map((r) => [r.component, r.revoked?.rollback])).toEqual([
146
+ ["api", "chant components rollback prod --release v1.0.0"],
147
+ ["web", "chant components rollback prod --release v1.0.0"],
148
+ ]);
149
+ expect(prod).not.toHaveProperty("mixed");
150
+ });
151
+ });
152
+ });
@@ -0,0 +1,153 @@
1
+ /**
2
+ * #4033 (ws-105): a release cut from a revoked commit, read at read time from
3
+ * the `ci/revoked/<sha>` tag. `workspace status` flags an environment that
4
+ * runs it, with the rollback line, and reads its pending gate as superseded;
5
+ * `release list` marks it. Against temp repos; nothing is pushed.
6
+ */
7
+
8
+ import { afterEach, describe, expect, test, vi } from "vitest";
9
+ import { withTestDir } from "@intentius/chant-test-utils";
10
+ import { spawnSync } from "node:child_process";
11
+ import { writeFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { Ajv2020 } from "ajv/dist/2020";
14
+ import statusSchema from "./status.schema.json";
15
+ import releasesSchema from "./releases.schema.json";
16
+ import { formatStatus, workspaceStatus, type StatusDocument } from "./status";
17
+ import { releasesDocument } from "./releases";
18
+ import { contract } from "./__fixtures__/contract-repo";
19
+ import { appendCut, makeRelease, persistRelease, type Release } from "../lifecycle/release-store";
20
+ import { appendReleaseRecord } from "../lifecycle/release-ledger";
21
+ import { appendPendingGate } from "../lifecycle/gate-ledger";
22
+ import type { CommandContext } from "../cli/registry";
23
+ import { contentDigest } from "../content-digest";
24
+
25
+ const validateStatus = new Ajv2020({ strict: true, allErrors: true }).compile(statusSchema);
26
+ const { expectValid: expectValidReleases } = contract(releasesSchema);
27
+
28
+ function git(cwd: string, ...args: string[]): string {
29
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
30
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
31
+ return r.stdout.trim();
32
+ }
33
+
34
+ const DOC = `jcs1-sha256:${"d".repeat(64)}`;
35
+ const NOW = "2026-10-16T14:00:00.000Z";
36
+
37
+ /** A workspace whose root member runs release v1.0.0 in prod and waits at its staging gate, and a second release v1.1.0 from a later commit. */
38
+ async function setup(dir: string): Promise<{ old: string; release: Release; next: Release }> {
39
+ git(dir, "init", "-q", "-b", "main");
40
+ git(dir, "config", "user.email", "t@chant.dev");
41
+ git(dir, "config", "user.name", "T");
42
+ writeFileSync(
43
+ join(dir, "chant.workspace.json"),
44
+ JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the fixture" }], ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"] } } }, null, 2),
45
+ );
46
+ git(dir, "add", ".");
47
+ git(dir, "commit", "-q", "-m", "init");
48
+ const old = git(dir, "rev-parse", "HEAD");
49
+ git(dir, "commit", "-q", "--allow-empty", "-m", "next");
50
+ const later = git(dir, "rev-parse", "HEAD");
51
+ const at = (commit: string) =>
52
+ makeRelease({ member: null, commit, artifactDigests: [], targets: ["staging", "prod"].map((env) => ({ env, templates: [{ component: "api", path: "t.json", digest: `sha256:${"c".repeat(64)}` }], params: [] })) });
53
+ const release = at(old);
54
+ const next = at(later);
55
+ for (const [r, label, ts] of [[release, "v1.0.0", "10"], [next, "v1.1.0", "11"]] as const) {
56
+ await persistRelease(r, [], { cwd: dir });
57
+ await appendCut({ releaseDigest: r.releaseDigest, label, actor: "alice", timestamp: `2026-10-16T${ts}:00:00.000Z`, runOrigin: null }, { cwd: dir });
58
+ }
59
+ const deploy = { component: "api", digest: contentDigest("api image"), runId: "r", actor: "ci" };
60
+ await appendReleaseRecord({ ...deploy, env: "prod", gitSha: old, timestamp: "2026-10-16T12:00:00.000Z", releaseDigest: release.releaseDigest }, { cwd: dir });
61
+ await appendReleaseRecord({ ...deploy, env: "staging", gitSha: later, timestamp: "2026-10-16T12:00:00.000Z", releaseDigest: next.releaseDigest }, { cwd: dir });
62
+ for (const [r, env] of [[release, "staging"], [next, "prod"]] as const) {
63
+ await appendPendingGate(
64
+ { op: "release", gate: `${env}-release`, timestamp: "2026-10-16T13:00:00.000Z", expiresAt: "2999-01-01T00:00:00.000Z", planDigest: DOC, environment: env, releaseDigest: r.releaseDigest },
65
+ { cwd: dir },
66
+ );
67
+ }
68
+ return { old, release, next };
69
+ }
70
+
71
+ function revoke(dir: string, sha: string): void {
72
+ const record = { schema: 1, tag: "revoked", commit: sha, branch: "main", at: "2026-10-16T12:30:00.000Z", phase: "test", run: { name: "test", id: 77, conclusion: "failure", url: "https://example.test/runs/77" } };
73
+ git(dir, "tag", "-a", "-m", `ci: phase test failed in test (failure)\n\n${JSON.stringify(record, null, 2)}\n`, `ci/revoked/${sha}`, sha);
74
+ }
75
+
76
+ function result(doc: StatusDocument): Extract<StatusDocument, { members: unknown }> {
77
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
78
+ expect(validateStatus(doc), JSON.stringify(validateStatus.errors, null, 2)).toBe(true);
79
+ return doc;
80
+ }
81
+
82
+ describe("a revoked release in workspace status and release list (#4033)", () => {
83
+ test("an environment running a release cut from a revoked commit is flagged with the rollback line, and the release's pending gate is superseded", async () => {
84
+ await withTestDir(async (dir) => {
85
+ const { old, release, next } = await setup(dir);
86
+
87
+ // Before the revocation nothing is flagged.
88
+ const before = result(await workspaceStatus({ cwd: dir, env: "prod", compareTo: "staging", now: NOW }));
89
+ expect(before.members[0].environments[0].releases[0]).not.toHaveProperty("revoked");
90
+ expect(before.members[0].gates.map((g) => [g.name, g.state])).toEqual([["prod-release", "pending"], ["staging-release", "pending"]]);
91
+
92
+ revoke(dir, old);
93
+ const doc = result(await workspaceStatus({ cwd: dir, env: "prod", compareTo: "staging", now: NOW }));
94
+ const [prod, staging] = doc.members[0].environments;
95
+ expect(prod.releases[0].revoked).toEqual({
96
+ commit: old,
97
+ tag: `ci/revoked/${old}`,
98
+ tagged: expect.any(String),
99
+ phase: "test",
100
+ run: { name: "test", id: 77, conclusion: "failure", url: "https://example.test/runs/77" },
101
+ signer: null,
102
+ releaseDigest: release.releaseDigest,
103
+ label: "v1.0.0",
104
+ rollback: "chant components rollback prod --component api",
105
+ });
106
+ // staging runs the later release, which is not revoked.
107
+ expect(staging.releases[0]).not.toHaveProperty("revoked");
108
+
109
+ const [prodGate, stagingGate] = doc.members[0].gates;
110
+ expect(prodGate).toMatchObject({ name: "prod-release", releaseDigest: next.releaseDigest, state: "pending" });
111
+ expect(prodGate).not.toHaveProperty("revoked");
112
+ expect(stagingGate).toMatchObject({ name: "staging-release", releaseDigest: release.releaseDigest, state: "superseded", revoked: { commit: old, tag: `ci/revoked/${old}`, phase: "test" } });
113
+
114
+ expect(formatStatus(doc)).toContain(
115
+ ` prod: api runs release v1.0.0, revoked by ci/revoked/${old} (phase test failed in test (run 77)); roll back: chant components rollback prod --component api`,
116
+ );
117
+ });
118
+ });
119
+
120
+ test("release list marks the revoked release, in JSON and in text", async () => {
121
+ await withTestDir(async (dir) => {
122
+ const { old } = await setup(dir);
123
+ revoke(dir, old);
124
+ const doc = await releasesDocument({ cwd: dir });
125
+ expectValidReleases(doc);
126
+ if ("error" in doc) throw new Error(doc.error.message);
127
+ expect(doc.releases.map((r) => [r.label, r.revoked?.tag ?? null])).toEqual([
128
+ ["v1.1.0", null],
129
+ ["v1.0.0", `ci/revoked/${old}`],
130
+ ]);
131
+
132
+ const out: string[] = [];
133
+ const err: string[] = [];
134
+ vi.spyOn(console, "log").mockImplementation((...m: unknown[]) => { out.push(m.join(" ")); });
135
+ vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { err.push(m.join(" ")); });
136
+ const cwd = process.cwd();
137
+ try {
138
+ process.chdir(dir);
139
+ const { runReleaseList } = await import("../cli/handlers/release");
140
+ expect(await runReleaseList({ args: {} } as unknown as CommandContext)).toBe(0);
141
+ } finally {
142
+ process.chdir(cwd);
143
+ }
144
+ expect(out.find((l) => l.startsWith("v1.0.0"))).toMatch(/ {2}revoked$/);
145
+ expect(out.find((l) => l.startsWith("v1.1.0"))).not.toMatch(/revoked/);
146
+ expect(err).toContain(` v1.0.0: revoked by ci/revoked/${old}, phase test failed in test (run 77)`);
147
+ });
148
+ });
149
+
150
+ afterEach(() => {
151
+ vi.restoreAllMocks();
152
+ });
153
+ });