@intentius/chant 0.124.27 → 0.124.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (681) hide show
  1. package/README.md +1 -1
  2. package/dist/__golden__/wave-loop/fan-out-gated.json +8 -0
  3. package/dist/__golden__/wave-loop/fan-out-no-members.json +4 -0
  4. package/dist/__golden__/wave-loop/fan-out-only.json +6 -0
  5. package/dist/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  6. package/dist/__golden__/wave-loop/op-cost-member.json +3 -0
  7. package/dist/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  8. package/dist/__golden__/wave-loop/op-held-on.json +2 -0
  9. package/dist/__golden__/wave-loop/op-hooks-held.json +2 -0
  10. package/dist/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  11. package/dist/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  12. package/dist/__golden__/wave-loop/op-hooks-share.json +3 -0
  13. package/dist/__golden__/wave-loop/op-per-run.json +7 -0
  14. package/dist/__golden__/wave-loop/op-pr-review.json +7 -0
  15. package/dist/__golden__/wave-loop/op-sealed.json +3 -0
  16. package/dist/__golden__/wave-loop/op-spend.json +3 -0
  17. package/dist/__golden__/wave-loop/op-wave-commands.json +7 -0
  18. package/dist/__golden__/wave-loop/op-wide-waiting.json +4 -0
  19. package/dist/build-params.d.ts +34 -1
  20. package/dist/build-params.d.ts.map +1 -1
  21. package/dist/build-params.js +31 -1
  22. package/dist/change-set-graph.d.ts +63 -0
  23. package/dist/change-set-graph.d.ts.map +1 -0
  24. package/dist/change-set-graph.js +130 -0
  25. package/dist/change-set.d.ts +62 -3
  26. package/dist/change-set.d.ts.map +1 -1
  27. package/dist/change-set.js +36 -4
  28. package/dist/chant-version.d.ts +9 -0
  29. package/dist/chant-version.d.ts.map +1 -0
  30. package/dist/chant-version.js +16 -0
  31. package/dist/cli/build-params-cli.d.ts +7 -1
  32. package/dist/cli/build-params-cli.d.ts.map +1 -1
  33. package/dist/cli/build-params-cli.js +17 -8
  34. package/dist/cli/commands/build.d.ts +8 -0
  35. package/dist/cli/commands/build.d.ts.map +1 -1
  36. package/dist/cli/commands/build.js +15 -11
  37. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  38. package/dist/cli/commands/check-lexicon-docs.js +30 -2
  39. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  40. package/dist/cli/commands/check-lexicon.js +13 -2
  41. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +4 -1
  42. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  43. package/dist/cli/commands/init-lexicon/templates/docs.js +6 -6
  44. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  45. package/dist/cli/commands/init-lexicon.js +17 -8
  46. package/dist/cli/handlers/approve.d.ts.map +1 -1
  47. package/dist/cli/handlers/approve.js +64 -4
  48. package/dist/cli/handlers/build.d.ts.map +1 -1
  49. package/dist/cli/handlers/build.js +43 -4
  50. package/dist/cli/handlers/change-set.d.ts +10 -0
  51. package/dist/cli/handlers/change-set.d.ts.map +1 -1
  52. package/dist/cli/handlers/change-set.js +51 -1
  53. package/dist/cli/handlers/fan-out.d.ts +10 -1
  54. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  55. package/dist/cli/handlers/fan-out.js +54 -6
  56. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  57. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  58. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  59. package/dist/cli/handlers/pr.d.ts +16 -2
  60. package/dist/cli/handlers/pr.d.ts.map +1 -1
  61. package/dist/cli/handlers/pr.js +269 -37
  62. package/dist/cli/handlers/promote.d.ts +7 -0
  63. package/dist/cli/handlers/promote.d.ts.map +1 -1
  64. package/dist/cli/handlers/promote.js +440 -28
  65. package/dist/cli/handlers/release.d.ts +91 -0
  66. package/dist/cli/handlers/release.d.ts.map +1 -0
  67. package/dist/cli/handlers/release.js +589 -0
  68. package/dist/cli/handlers/run.d.ts +38 -2
  69. package/dist/cli/handlers/run.d.ts.map +1 -1
  70. package/dist/cli/handlers/run.js +234 -16
  71. package/dist/cli/main.d.ts.map +1 -1
  72. package/dist/cli/main.js +143 -17
  73. package/dist/cli/mcp/workspace-tools.js +1 -1
  74. package/dist/cli/parse-args.d.ts.map +1 -1
  75. package/dist/cli/parse-args.js +73 -5
  76. package/dist/cli/registry.d.ts +43 -5
  77. package/dist/cli/registry.d.ts.map +1 -1
  78. package/dist/cli/run-events.d.ts +30 -0
  79. package/dist/cli/run-events.d.ts.map +1 -0
  80. package/dist/cli/run-events.js +49 -0
  81. package/dist/cli/version.d.ts +3 -5
  82. package/dist/cli/version.d.ts.map +1 -1
  83. package/dist/cli/version.js +3 -13
  84. package/dist/codegen/docs-in-site.d.ts +71 -0
  85. package/dist/codegen/docs-in-site.d.ts.map +1 -0
  86. package/dist/codegen/docs-in-site.js +177 -0
  87. package/dist/codegen/docs-types.d.ts +14 -1
  88. package/dist/codegen/docs-types.d.ts.map +1 -1
  89. package/dist/codegen/docs.d.ts +18 -1
  90. package/dist/codegen/docs.d.ts.map +1 -1
  91. package/dist/codegen/docs.js +40 -7
  92. package/dist/codegen/rehype-base-url.mjs +15 -9
  93. package/dist/components/auto-release.d.ts +6 -1
  94. package/dist/components/auto-release.d.ts.map +1 -1
  95. package/dist/components/auto-release.js +3 -0
  96. package/dist/components/capability.d.ts +9 -0
  97. package/dist/components/capability.d.ts.map +1 -1
  98. package/dist/components/cli-support.d.ts +6 -0
  99. package/dist/components/cli-support.d.ts.map +1 -1
  100. package/dist/components/cli-support.js +10 -9
  101. package/dist/components/driver.d.ts +50 -9
  102. package/dist/components/driver.d.ts.map +1 -1
  103. package/dist/components/driver.js +116 -54
  104. package/dist/components/fan-out-inputs.d.ts +69 -0
  105. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  106. package/dist/components/fan-out-inputs.js +169 -0
  107. package/dist/components/fan-out-record.d.ts +13 -0
  108. package/dist/components/fan-out-record.d.ts.map +1 -1
  109. package/dist/components/fan-out-record.js +16 -0
  110. package/dist/components/fan-out-run.d.ts +7 -0
  111. package/dist/components/fan-out-run.d.ts.map +1 -1
  112. package/dist/components/fan-out-run.js +85 -26
  113. package/dist/components/fan-out.d.ts +4 -0
  114. package/dist/components/fan-out.d.ts.map +1 -1
  115. package/dist/components/pr-pipeline.d.ts +25 -0
  116. package/dist/components/pr-pipeline.d.ts.map +1 -1
  117. package/dist/components/pr-pipeline.js +22 -4
  118. package/dist/components/pr-run.d.ts +11 -0
  119. package/dist/components/pr-run.d.ts.map +1 -1
  120. package/dist/components/pr-run.js +20 -1
  121. package/dist/components/promote-order.d.ts +38 -0
  122. package/dist/components/promote-order.d.ts.map +1 -0
  123. package/dist/components/promote-order.js +62 -0
  124. package/dist/components/promote.d.ts +38 -2
  125. package/dist/components/promote.d.ts.map +1 -1
  126. package/dist/components/promote.js +77 -2
  127. package/dist/components/release-compile.d.ts +142 -0
  128. package/dist/components/release-compile.d.ts.map +1 -0
  129. package/dist/components/release-compile.js +382 -0
  130. package/dist/components/release-gate.d.ts +135 -0
  131. package/dist/components/release-gate.d.ts.map +1 -0
  132. package/dist/components/release-gate.js +288 -0
  133. package/dist/components/release-promote.d.ts +96 -0
  134. package/dist/components/release-promote.d.ts.map +1 -0
  135. package/dist/components/release-promote.js +204 -0
  136. package/dist/components/run-progress.d.ts +7 -0
  137. package/dist/components/run-progress.d.ts.map +1 -1
  138. package/dist/components/wave-plan.d.ts +8 -1
  139. package/dist/components/wave-plan.d.ts.map +1 -1
  140. package/dist/components/wave-plan.js +11 -3
  141. package/dist/config.d.ts +45 -0
  142. package/dist/config.d.ts.map +1 -1
  143. package/dist/config.js +3 -0
  144. package/dist/flag-params.d.ts +54 -0
  145. package/dist/flag-params.d.ts.map +1 -0
  146. package/dist/flag-params.js +130 -0
  147. package/dist/index.d.ts +1 -0
  148. package/dist/index.d.ts.map +1 -1
  149. package/dist/index.js +2 -0
  150. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  151. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  152. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  153. package/dist/lifecycle/change-set-store.d.ts +68 -0
  154. package/dist/lifecycle/change-set-store.d.ts.map +1 -0
  155. package/dist/lifecycle/change-set-store.js +97 -0
  156. package/dist/lifecycle/commit-sha.d.ts +13 -0
  157. package/dist/lifecycle/commit-sha.d.ts.map +1 -0
  158. package/dist/lifecycle/commit-sha.js +21 -0
  159. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  160. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  161. package/dist/lifecycle/delivery-metrics.js +145 -0
  162. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  163. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  164. package/dist/lifecycle/deploy-outcome.js +101 -0
  165. package/dist/lifecycle/gate-change-set.d.ts +78 -0
  166. package/dist/lifecycle/gate-change-set.d.ts.map +1 -0
  167. package/dist/lifecycle/gate-change-set.js +138 -0
  168. package/dist/lifecycle/gate-ledger.d.ts +51 -4
  169. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/gate-ledger.js +18 -0
  171. package/dist/lifecycle/git.d.ts +30 -1
  172. package/dist/lifecycle/git.d.ts.map +1 -1
  173. package/dist/lifecycle/git.js +31 -5
  174. package/dist/lifecycle/index.d.ts +4 -0
  175. package/dist/lifecycle/index.d.ts.map +1 -1
  176. package/dist/lifecycle/index.js +4 -0
  177. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  178. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  179. package/dist/lifecycle/mixed-environment.js +154 -0
  180. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  181. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  182. package/dist/lifecycle/publish-attachments.js +179 -0
  183. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  184. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  185. package/dist/lifecycle/publish-credentials.js +69 -0
  186. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  187. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  188. package/dist/lifecycle/publish-declaration.js +83 -0
  189. package/dist/lifecycle/publish-forge.d.ts +53 -0
  190. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  191. package/dist/lifecycle/publish-forge.js +298 -0
  192. package/dist/lifecycle/publish-npm.d.ts +61 -0
  193. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  194. package/dist/lifecycle/publish-npm.js +242 -0
  195. package/dist/lifecycle/publish-oci.d.ts +64 -0
  196. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  197. package/dist/lifecycle/publish-oci.js +383 -0
  198. package/dist/lifecycle/publish-record.d.ts +66 -0
  199. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  200. package/dist/lifecycle/publish-record.js +84 -0
  201. package/dist/lifecycle/publish-targets.d.ts +45 -0
  202. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  203. package/dist/lifecycle/publish-targets.js +85 -0
  204. package/dist/lifecycle/release-cut.d.ts +154 -0
  205. package/dist/lifecycle/release-cut.d.ts.map +1 -0
  206. package/dist/lifecycle/release-cut.js +366 -0
  207. package/dist/lifecycle/release-join.d.ts +95 -0
  208. package/dist/lifecycle/release-join.d.ts.map +1 -0
  209. package/dist/lifecycle/release-join.js +139 -0
  210. package/dist/lifecycle/release-ledger.d.ts +54 -0
  211. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  212. package/dist/lifecycle/release-ledger.js +53 -20
  213. package/dist/lifecycle/release-publish.d.ts +312 -0
  214. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  215. package/dist/lifecycle/release-publish.js +335 -0
  216. package/dist/lifecycle/release-revocation.d.ts +80 -0
  217. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  218. package/dist/lifecycle/release-revocation.js +123 -0
  219. package/dist/lifecycle/release-store.d.ts +200 -0
  220. package/dist/lifecycle/release-store.d.ts.map +1 -0
  221. package/dist/lifecycle/release-store.js +320 -0
  222. package/dist/lifecycle/release-verify.d.ts +61 -0
  223. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  224. package/dist/lifecycle/release-verify.js +92 -0
  225. package/dist/lifecycle/release-workstation.d.ts +91 -0
  226. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  227. package/dist/lifecycle/release-workstation.js +180 -0
  228. package/dist/lifecycle/retention-policy.d.ts +45 -0
  229. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  230. package/dist/lifecycle/retention-policy.js +43 -0
  231. package/dist/lifecycle/retention.d.ts +185 -0
  232. package/dist/lifecycle/retention.d.ts.map +1 -0
  233. package/dist/lifecycle/retention.js +400 -0
  234. package/dist/lifecycle/run-ledger.d.ts +4 -0
  235. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  236. package/dist/lifecycle/run-ledger.js +2 -0
  237. package/dist/op/activities/change-coverage.d.ts.map +1 -1
  238. package/dist/op/activities/change-coverage.js +4 -2
  239. package/dist/op/activities/source-release.d.ts.map +1 -1
  240. package/dist/op/activities/source-release.js +7 -3
  241. package/dist/op/activities/source-rollback.d.ts.map +1 -1
  242. package/dist/op/activities/source-rollback.js +7 -3
  243. package/dist/op/gate-name.d.ts +11 -2
  244. package/dist/op/gate-name.d.ts.map +1 -1
  245. package/dist/op/gate-name.js +12 -3
  246. package/dist/op/gate-run-locator.d.ts +22 -0
  247. package/dist/op/gate-run-locator.d.ts.map +1 -1
  248. package/dist/op/gate-run-locator.js +31 -0
  249. package/dist/op/gate.d.ts +27 -3
  250. package/dist/op/gate.d.ts.map +1 -1
  251. package/dist/op/gate.js +57 -3
  252. package/dist/op/local-executor.d.ts +36 -1
  253. package/dist/op/local-executor.d.ts.map +1 -1
  254. package/dist/op/local-executor.js +158 -52
  255. package/dist/op/op-waves-run.d.ts.map +1 -1
  256. package/dist/op/op-waves-run.js +4 -0
  257. package/dist/op/run-live.d.ts +2 -0
  258. package/dist/op/run-live.d.ts.map +1 -1
  259. package/dist/op/run-live.js +1 -1
  260. package/dist/op/runtime.d.ts +46 -0
  261. package/dist/op/runtime.d.ts.map +1 -1
  262. package/dist/op/runtimes/local.d.ts.map +1 -1
  263. package/dist/op/runtimes/local.js +8 -0
  264. package/dist/plan-summary.d.ts.map +1 -1
  265. package/dist/plan-summary.js +8 -2
  266. package/dist/point-decision.d.ts +2 -0
  267. package/dist/point-decision.d.ts.map +1 -1
  268. package/dist/point-decision.js +9 -2
  269. package/dist/pr-loop.d.ts +7 -0
  270. package/dist/pr-loop.d.ts.map +1 -1
  271. package/dist/pr-loop.js +23 -4
  272. package/dist/pr-report-url.d.ts +82 -0
  273. package/dist/pr-report-url.d.ts.map +1 -0
  274. package/dist/pr-report-url.js +174 -0
  275. package/dist/provenance.d.ts +2 -2
  276. package/dist/provenance.d.ts.map +1 -1
  277. package/dist/run-event-store.d.ts +104 -0
  278. package/dist/run-event-store.d.ts.map +1 -0
  279. package/dist/run-event-store.js +251 -0
  280. package/dist/run-event-stream.d.ts +133 -0
  281. package/dist/run-event-stream.d.ts.map +1 -0
  282. package/dist/run-event-stream.js +242 -0
  283. package/dist/run-event.d.ts +285 -0
  284. package/dist/run-event.d.ts.map +1 -0
  285. package/dist/run-event.js +209 -0
  286. package/dist/wave-runtime.mjs +262 -226
  287. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  288. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  289. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  290. package/dist/workspace/__fixtures__/run-event-check.d.ts +19 -0
  291. package/dist/workspace/__fixtures__/run-event-check.d.ts.map +1 -0
  292. package/dist/workspace/__fixtures__/run-event-check.js +59 -0
  293. package/dist/workspace/change-set-show.d.ts +56 -0
  294. package/dist/workspace/change-set-show.d.ts.map +1 -0
  295. package/dist/workspace/change-set-show.js +54 -0
  296. package/dist/workspace/change-set-show.schema.json +102 -0
  297. package/dist/workspace/change-set.schema.json +32 -1
  298. package/dist/workspace/changes-cli.d.ts.map +1 -1
  299. package/dist/workspace/changes-cli.js +10 -1
  300. package/dist/workspace/changes.d.ts +37 -0
  301. package/dist/workspace/changes.d.ts.map +1 -1
  302. package/dist/workspace/changes.js +49 -2
  303. package/dist/workspace/changes.schema.json +135 -0
  304. package/dist/workspace/check.schema.json +4 -2
  305. package/dist/workspace/checks/records.d.ts +9 -1
  306. package/dist/workspace/checks/records.d.ts.map +1 -1
  307. package/dist/workspace/checks/records.js +35 -1
  308. package/dist/workspace/checks.d.ts +1 -0
  309. package/dist/workspace/checks.d.ts.map +1 -1
  310. package/dist/workspace/checks.js +2 -1
  311. package/dist/workspace/ci-green-cli.d.ts +19 -4
  312. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  313. package/dist/workspace/ci-green-cli.js +75 -12
  314. package/dist/workspace/ci-green-forge.d.ts +20 -3
  315. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  316. package/dist/workspace/ci-green-forge.js +6 -4
  317. package/dist/workspace/ci-green-workflow.d.ts +11 -0
  318. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  319. package/dist/workspace/ci-green-workflow.js +6 -0
  320. package/dist/workspace/ci-green.d.ts +281 -13
  321. package/dist/workspace/ci-green.d.ts.map +1 -1
  322. package/dist/workspace/ci-green.js +456 -38
  323. package/dist/workspace/ci-last-green.schema.json +32 -2
  324. package/dist/workspace/ci-list.schema.json +525 -0
  325. package/dist/workspace/commit-links.d.ts +85 -0
  326. package/dist/workspace/commit-links.d.ts.map +1 -0
  327. package/dist/workspace/commit-links.js +163 -0
  328. package/dist/workspace/declaration.d.ts +35 -0
  329. package/dist/workspace/declaration.d.ts.map +1 -1
  330. package/dist/workspace/declaration.js +27 -1
  331. package/dist/workspace/declaration.schema.json +76 -0
  332. package/dist/workspace/declared-kinds.d.ts +16 -0
  333. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  334. package/dist/workspace/declared-kinds.js +25 -0
  335. package/dist/workspace/delivery-metrics.d.ts +91 -0
  336. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  337. package/dist/workspace/delivery-metrics.js +217 -0
  338. package/dist/workspace/delivery-metrics.schema.json +519 -0
  339. package/dist/workspace/flags-read.d.ts +37 -0
  340. package/dist/workspace/flags-read.d.ts.map +1 -0
  341. package/dist/workspace/flags-read.js +103 -0
  342. package/dist/workspace/flags.d.ts +100 -0
  343. package/dist/workspace/flags.d.ts.map +1 -0
  344. package/dist/workspace/flags.js +98 -0
  345. package/dist/workspace/graph-cli.js +1 -1
  346. package/dist/workspace/identity-gate-rule.d.ts +37 -0
  347. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  348. package/dist/workspace/identity-gate-rule.js +72 -7
  349. package/dist/workspace/identity.d.ts +12 -0
  350. package/dist/workspace/identity.d.ts.map +1 -1
  351. package/dist/workspace/identity.js +13 -1
  352. package/dist/workspace/intent-cli.d.ts +11 -2
  353. package/dist/workspace/intent-cli.d.ts.map +1 -1
  354. package/dist/workspace/intent-cli.js +54 -5
  355. package/dist/workspace/intent-trace.d.ts +219 -0
  356. package/dist/workspace/intent-trace.d.ts.map +1 -0
  357. package/dist/workspace/intent-trace.js +314 -0
  358. package/dist/workspace/intent.d.ts +36 -7
  359. package/dist/workspace/intent.d.ts.map +1 -1
  360. package/dist/workspace/intent.js +192 -22
  361. package/dist/workspace/intent.schema.json +702 -6
  362. package/dist/workspace/ls.d.ts +4 -0
  363. package/dist/workspace/ls.d.ts.map +1 -1
  364. package/dist/workspace/ls.js +2 -0
  365. package/dist/workspace/ls.schema.json +3 -1
  366. package/dist/workspace/member-pipeline.d.ts +6 -0
  367. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  368. package/dist/workspace/member-pipeline.js +6 -0
  369. package/dist/workspace/overlap.d.ts +146 -0
  370. package/dist/workspace/overlap.d.ts.map +1 -0
  371. package/dist/workspace/overlap.js +276 -0
  372. package/dist/workspace/promotion-order.d.ts +41 -0
  373. package/dist/workspace/promotion-order.d.ts.map +1 -0
  374. package/dist/workspace/promotion-order.js +95 -0
  375. package/dist/workspace/reason-codes.d.ts +4 -0
  376. package/dist/workspace/reason-codes.d.ts.map +1 -1
  377. package/dist/workspace/reason-codes.js +7 -1
  378. package/dist/workspace/records.d.ts +15 -2
  379. package/dist/workspace/records.d.ts.map +1 -1
  380. package/dist/workspace/records.js +32 -0
  381. package/dist/workspace/records.schema.json +21 -2
  382. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  383. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  384. package/dist/workspace/releases.d.ts +81 -0
  385. package/dist/workspace/releases.d.ts.map +1 -0
  386. package/dist/workspace/releases.js +103 -0
  387. package/dist/workspace/releases.schema.json +513 -0
  388. package/dist/workspace/run-event.schema.json +295 -0
  389. package/dist/workspace/status-gates.d.ts +34 -1
  390. package/dist/workspace/status-gates.d.ts.map +1 -1
  391. package/dist/workspace/status-gates.js +31 -2
  392. package/dist/workspace/status-stewards.d.ts +4 -0
  393. package/dist/workspace/status-stewards.d.ts.map +1 -1
  394. package/dist/workspace/status-stewards.js +2 -0
  395. package/dist/workspace/status.d.ts +117 -0
  396. package/dist/workspace/status.d.ts.map +1 -1
  397. package/dist/workspace/status.js +218 -4
  398. package/dist/workspace/status.schema.json +268 -2
  399. package/dist/workspace/trust/ci-tag-statement.d.ts +102 -0
  400. package/dist/workspace/trust/ci-tag-statement.d.ts.map +1 -0
  401. package/dist/workspace/trust/ci-tag-statement.js +123 -0
  402. package/dist/workspace/trust/op-run-statement.d.ts +91 -0
  403. package/dist/workspace/trust/op-run-statement.d.ts.map +1 -0
  404. package/dist/workspace/trust/op-run-statement.js +143 -0
  405. package/dist/workspace/trust/rotation.d.ts +18 -2
  406. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  407. package/dist/workspace/trust/rotation.js +65 -16
  408. package/dist/workspace/trust/seal.d.ts +11 -0
  409. package/dist/workspace/trust/seal.d.ts.map +1 -1
  410. package/dist/workspace/trust/seal.js +9 -0
  411. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  412. package/dist/workspace/trust/ssh-commit.js +10 -10
  413. package/dist/workspace/wip.d.ts.map +1 -1
  414. package/dist/workspace/wip.js +11 -3
  415. package/package.json +1 -1
  416. package/src/__golden__/wave-loop/fan-out-gated.json +8 -0
  417. package/src/__golden__/wave-loop/fan-out-no-members.json +4 -0
  418. package/src/__golden__/wave-loop/fan-out-only.json +6 -0
  419. package/src/__golden__/wave-loop/op-by-apply-hold.json +2 -0
  420. package/src/__golden__/wave-loop/op-cost-member.json +3 -0
  421. package/src/__golden__/wave-loop/op-held-on-before-apply.json +1 -0
  422. package/src/__golden__/wave-loop/op-held-on.json +2 -0
  423. package/src/__golden__/wave-loop/op-hooks-held.json +2 -0
  424. package/src/__golden__/wave-loop/op-hooks-hold-before-apply.json +2 -0
  425. package/src/__golden__/wave-loop/op-hooks-refused-at-base.json +3 -0
  426. package/src/__golden__/wave-loop/op-hooks-share.json +3 -0
  427. package/src/__golden__/wave-loop/op-per-run.json +7 -0
  428. package/src/__golden__/wave-loop/op-pr-review.json +7 -0
  429. package/src/__golden__/wave-loop/op-sealed.json +3 -0
  430. package/src/__golden__/wave-loop/op-spend.json +3 -0
  431. package/src/__golden__/wave-loop/op-wave-commands.json +7 -0
  432. package/src/__golden__/wave-loop/op-wide-waiting.json +4 -0
  433. package/src/build-params.test.ts +46 -0
  434. package/src/build-params.ts +55 -1
  435. package/src/change-set-graph.test.ts +72 -0
  436. package/src/change-set-graph.ts +141 -0
  437. package/src/change-set.ts +87 -6
  438. package/src/chant-version.ts +16 -0
  439. package/src/cli/build-params-cli.ts +22 -9
  440. package/src/cli/commands/build.ts +25 -11
  441. package/src/cli/commands/check-lexicon-docs.test.ts +51 -0
  442. package/src/cli/commands/check-lexicon-docs.ts +39 -2
  443. package/src/cli/commands/check-lexicon.ts +14 -2
  444. package/src/cli/commands/init-lexicon/templates/docs.ts +7 -6
  445. package/src/cli/commands/init-lexicon.test.ts +25 -0
  446. package/src/cli/commands/init-lexicon.ts +18 -8
  447. package/src/cli/handlers/approve-release.test.ts +84 -0
  448. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  449. package/src/cli/handlers/approve.ts +65 -2
  450. package/src/cli/handlers/build.test.ts +67 -0
  451. package/src/cli/handlers/build.ts +46 -4
  452. package/src/cli/handlers/change-set.ts +49 -1
  453. package/src/cli/handlers/fan-out.test.ts +228 -1
  454. package/src/cli/handlers/fan-out.ts +53 -6
  455. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  456. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  457. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  458. package/src/cli/handlers/operator.test.ts +37 -0
  459. package/src/cli/handlers/pr.test.ts +341 -0
  460. package/src/cli/handlers/pr.ts +292 -47
  461. package/src/cli/handlers/promote.test.ts +669 -7
  462. package/src/cli/handlers/promote.ts +464 -29
  463. package/src/cli/handlers/release-implicit.e2e.test.ts +161 -0
  464. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  465. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  466. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  467. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  468. package/src/cli/handlers/release.e2e.test.ts +199 -0
  469. package/src/cli/handlers/release.ts +593 -0
  470. package/src/cli/handlers/run-auto-release.test.ts +80 -0
  471. package/src/cli/handlers/run-events-cmd.test.ts +92 -0
  472. package/src/cli/handlers/run-release.test.ts +55 -0
  473. package/src/cli/handlers/run.test.ts +30 -0
  474. package/src/cli/handlers/run.ts +243 -18
  475. package/src/cli/main.test.ts +8 -0
  476. package/src/cli/main.ts +143 -17
  477. package/src/cli/mcp/never-over-mcp.test.ts +2 -0
  478. package/src/cli/mcp/workspace-tools.ts +1 -1
  479. package/src/cli/parse-args.ts +56 -5
  480. package/src/cli/registry.ts +43 -5
  481. package/src/cli/run-events.ts +57 -0
  482. package/src/cli/version.ts +3 -13
  483. package/src/codegen/docs-in-site.test.ts +166 -0
  484. package/src/codegen/docs-in-site.ts +182 -0
  485. package/src/codegen/docs-site/AgentPrompt.astro +26 -1
  486. package/src/codegen/docs-types.ts +14 -1
  487. package/src/codegen/docs.ts +60 -7
  488. package/src/codegen/rehype-base-url.d.mts +3 -1
  489. package/src/codegen/rehype-base-url.mjs +15 -9
  490. package/src/codegen/rehype-base-url.test.ts +34 -0
  491. package/src/components/auto-release.test.ts +30 -0
  492. package/src/components/auto-release.ts +9 -1
  493. package/src/components/capability.ts +9 -0
  494. package/src/components/cli-support.test.ts +82 -0
  495. package/src/components/cli-support.ts +16 -9
  496. package/src/components/driver.test.ts +24 -0
  497. package/src/components/driver.ts +165 -56
  498. package/src/components/fan-out-inputs.test.ts +85 -0
  499. package/src/components/fan-out-inputs.ts +192 -0
  500. package/src/components/fan-out-record.test.ts +10 -0
  501. package/src/components/fan-out-record.ts +19 -0
  502. package/src/components/fan-out-run.ts +104 -35
  503. package/src/components/fan-out.ts +4 -0
  504. package/src/components/pr-pipeline.ts +35 -4
  505. package/src/components/pr-run.ts +30 -1
  506. package/src/components/promote-order.test.ts +90 -0
  507. package/src/components/promote-order.ts +75 -0
  508. package/src/components/promote.test.ts +10 -1
  509. package/src/components/promote.ts +94 -3
  510. package/src/components/release-compile.test.ts +147 -0
  511. package/src/components/release-compile.ts +459 -0
  512. package/src/components/release-gate.test.ts +215 -0
  513. package/src/components/release-gate.ts +355 -0
  514. package/src/components/release-promote.test.ts +159 -0
  515. package/src/components/release-promote.ts +232 -0
  516. package/src/components/run-progress.ts +7 -0
  517. package/src/components/wave-plan.ts +16 -3
  518. package/src/config.ts +15 -1
  519. package/src/flag-params.test.ts +144 -0
  520. package/src/flag-params.ts +134 -0
  521. package/src/index.ts +2 -0
  522. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  523. package/src/lifecycle/change-set-store.test.ts +217 -0
  524. package/src/lifecycle/change-set-store.ts +113 -0
  525. package/src/lifecycle/commit-sha.ts +23 -0
  526. package/src/lifecycle/delivery-metrics.ts +263 -0
  527. package/src/lifecycle/deploy-outcome.ts +147 -0
  528. package/src/lifecycle/gate-change-set.e2e.test.ts +122 -0
  529. package/src/lifecycle/gate-change-set.ts +164 -0
  530. package/src/lifecycle/gate-ledger.test.ts +33 -0
  531. package/src/lifecycle/gate-ledger.ts +68 -3
  532. package/src/lifecycle/git.ts +61 -8
  533. package/src/lifecycle/index.ts +4 -0
  534. package/src/lifecycle/mixed-environment.test.ts +97 -0
  535. package/src/lifecycle/mixed-environment.ts +204 -0
  536. package/src/lifecycle/publish-attachments.ts +196 -0
  537. package/src/lifecycle/publish-credentials.ts +84 -0
  538. package/src/lifecycle/publish-declaration.ts +95 -0
  539. package/src/lifecycle/publish-forge.ts +325 -0
  540. package/src/lifecycle/publish-npm.ts +259 -0
  541. package/src/lifecycle/publish-oci.ts +396 -0
  542. package/src/lifecycle/publish-record.ts +113 -0
  543. package/src/lifecycle/publish-resume.test.ts +168 -0
  544. package/src/lifecycle/publish-targets.test.ts +494 -0
  545. package/src/lifecycle/publish-targets.ts +100 -0
  546. package/src/lifecycle/release-cut-signed.test.ts +180 -0
  547. package/src/lifecycle/release-cut.test.ts +483 -0
  548. package/src/lifecycle/release-cut.ts +503 -0
  549. package/src/lifecycle/release-flag.test.ts +112 -0
  550. package/src/lifecycle/release-join.test.ts +68 -0
  551. package/src/lifecycle/release-join.ts +182 -0
  552. package/src/lifecycle/release-ledger.test.ts +14 -0
  553. package/src/lifecycle/release-ledger.ts +85 -17
  554. package/src/lifecycle/release-publish.test.ts +225 -0
  555. package/src/lifecycle/release-publish.ts +568 -0
  556. package/src/lifecycle/release-revocation.ts +147 -0
  557. package/src/lifecycle/release-store.ts +434 -0
  558. package/src/lifecycle/release-verify.test.ts +110 -0
  559. package/src/lifecycle/release-verify.ts +120 -0
  560. package/src/lifecycle/release-workstation.test.ts +204 -0
  561. package/src/lifecycle/release-workstation.ts +216 -0
  562. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  563. package/src/lifecycle/retention-policy.ts +63 -0
  564. package/src/lifecycle/retention.test.ts +363 -0
  565. package/src/lifecycle/retention.ts +508 -0
  566. package/src/lifecycle/run-ledger.test.ts +10 -0
  567. package/src/lifecycle/run-ledger.ts +6 -0
  568. package/src/op/activities/change-coverage.ts +4 -1
  569. package/src/op/activities/source-release.ts +8 -3
  570. package/src/op/activities/source-rollback.ts +8 -4
  571. package/src/op/commit-run-gate-deploy-join.test.ts +117 -0
  572. package/src/op/gate-name.ts +13 -3
  573. package/src/op/gate-release.test.ts +126 -0
  574. package/src/op/gate-run-locator.test.ts +110 -0
  575. package/src/op/gate-run-locator.ts +35 -0
  576. package/src/op/gate.ts +88 -6
  577. package/src/op/local-executor.ts +207 -52
  578. package/src/op/op-waves-run.ts +4 -0
  579. package/src/op/run-events.test.ts +166 -0
  580. package/src/op/run-live.ts +1 -1
  581. package/src/op/runtime.ts +39 -0
  582. package/src/op/runtimes/local.test.ts +16 -0
  583. package/src/op/runtimes/local.ts +9 -0
  584. package/src/plan-summary.test.ts +20 -0
  585. package/src/plan-summary.ts +8 -1
  586. package/src/point-decision.ts +12 -2
  587. package/src/pr-loop.ts +25 -4
  588. package/src/pr-report-url.test.ts +131 -0
  589. package/src/pr-report-url.ts +209 -0
  590. package/src/provenance.ts +2 -2
  591. package/src/run-event-store.test.ts +144 -0
  592. package/src/run-event-store.ts +273 -0
  593. package/src/run-event-stream.test.ts +175 -0
  594. package/src/run-event-stream.ts +309 -0
  595. package/src/run-event.test.ts +277 -0
  596. package/src/run-event.ts +471 -0
  597. package/src/wave-loop-golden.test.ts +9 -0
  598. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  599. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  600. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  601. package/src/workspace/__fixtures__/run-event-check.ts +58 -0
  602. package/src/workspace/ancestry-index.test.ts +93 -0
  603. package/src/workspace/change-set-show.schema.json +102 -0
  604. package/src/workspace/change-set-show.ts +72 -0
  605. package/src/workspace/change-set.schema.json +32 -1
  606. package/src/workspace/changes-cli.ts +10 -1
  607. package/src/workspace/changes.schema.json +135 -0
  608. package/src/workspace/changes.test.ts +1 -1
  609. package/src/workspace/changes.ts +84 -2
  610. package/src/workspace/check.schema.json +4 -2
  611. package/src/workspace/checks/records.ts +35 -1
  612. package/src/workspace/checks.ts +2 -1
  613. package/src/workspace/ci-green-cli.ts +75 -13
  614. package/src/workspace/ci-green-forge.ts +24 -4
  615. package/src/workspace/ci-green-runs.test.ts +242 -0
  616. package/src/workspace/ci-green-signed.test.ts +302 -0
  617. package/src/workspace/ci-green-workflow.test.ts +16 -0
  618. package/src/workspace/ci-green-workflow.ts +12 -0
  619. package/src/workspace/ci-green.test.ts +131 -2
  620. package/src/workspace/ci-green.ts +635 -42
  621. package/src/workspace/ci-last-green.schema.json +32 -2
  622. package/src/workspace/ci-list.schema.json +525 -0
  623. package/src/workspace/commit-links.test.ts +279 -0
  624. package/src/workspace/commit-links.ts +183 -0
  625. package/src/workspace/declaration.schema.json +76 -0
  626. package/src/workspace/declaration.test.ts +28 -0
  627. package/src/workspace/declaration.ts +60 -2
  628. package/src/workspace/declared-kinds.ts +40 -1
  629. package/src/workspace/delivery-metrics.schema.json +519 -0
  630. package/src/workspace/delivery-metrics.test.ts +236 -0
  631. package/src/workspace/delivery-metrics.ts +286 -0
  632. package/src/workspace/flags-read.ts +121 -0
  633. package/src/workspace/flags.test.ts +141 -0
  634. package/src/workspace/flags.ts +157 -0
  635. package/src/workspace/graph-cli.ts +1 -1
  636. package/src/workspace/identity-gate-rule.ts +91 -7
  637. package/src/workspace/identity.ts +14 -1
  638. package/src/workspace/intent-cli.ts +46 -5
  639. package/src/workspace/intent-trace.test.ts +173 -0
  640. package/src/workspace/intent-trace.ts +484 -0
  641. package/src/workspace/intent.schema.json +702 -6
  642. package/src/workspace/intent.ts +196 -26
  643. package/src/workspace/ls.schema.json +3 -1
  644. package/src/workspace/ls.ts +6 -0
  645. package/src/workspace/member-pipeline.test.ts +17 -0
  646. package/src/workspace/member-pipeline.ts +9 -0
  647. package/src/workspace/older-records.test.ts +412 -0
  648. package/src/workspace/overlap.test.ts +165 -0
  649. package/src/workspace/overlap.ts +340 -0
  650. package/src/workspace/promotion-order.ts +97 -0
  651. package/src/workspace/read-contract.e2e.test.ts +5 -1
  652. package/src/workspace/reason-codes.test.ts +7 -1
  653. package/src/workspace/reason-codes.ts +7 -1
  654. package/src/workspace/record-assets.test.ts +2 -1
  655. package/src/workspace/records-contract.e2e.test.ts +2 -1
  656. package/src/workspace/records.schema.json +21 -2
  657. package/src/workspace/records.ts +36 -2
  658. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  659. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  660. package/src/workspace/releases.schema.json +513 -0
  661. package/src/workspace/releases.test.ts +108 -0
  662. package/src/workspace/releases.ts +133 -0
  663. package/src/workspace/run-event.schema.json +295 -0
  664. package/src/workspace/status-contract.test.ts +81 -1
  665. package/src/workspace/status-gates.ts +64 -1
  666. package/src/workspace/status-mixed.test.ts +152 -0
  667. package/src/workspace/status-revoked.test.ts +153 -0
  668. package/src/workspace/status-stewards.ts +6 -0
  669. package/src/workspace/status.schema.json +268 -2
  670. package/src/workspace/status.ts +334 -4
  671. package/src/workspace/trust/ci-tag-statement.ts +151 -0
  672. package/src/workspace/trust/op-run-statement.ts +148 -0
  673. package/src/workspace/trust/rotation.ts +61 -14
  674. package/src/workspace/trust/seal.ts +10 -0
  675. package/src/workspace/trust/ssh-commit.ts +10 -9
  676. package/src/workspace/wip.ts +11 -2
  677. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -14
  678. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +0 -16
  679. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +0 -8
  680. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +0 -15
  681. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +0 -10
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The join through the release digest (#4043), against a temp repo: two
3
+ * releases of one commit, and deploys of one release in two environments.
4
+ * The end-to-end chain, with a real cut, promote and approval, is in
5
+ * cli/handlers/release-join.e2e.test.ts.
6
+ */
7
+ import { describe, test, expect } 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 { joinRelease } from "./release-join";
13
+ import { appendCut, makeRelease, persistRelease } from "./release-store";
14
+ import { appendReleaseRecord } from "./release-ledger";
15
+
16
+ function git(dir: string, ...args: string[]): string {
17
+ const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
18
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
19
+ return r.stdout.trim();
20
+ }
21
+
22
+ const subject = (commit: string, digest: string) => ({
23
+ member: null,
24
+ commit,
25
+ artifactDigests: [{ component: "api", kind: "image" as const, path: "api.tar", digest }],
26
+ targets: [{ env: "dev", templates: [], params: [] }],
27
+ });
28
+
29
+ describe("joinRelease", () => {
30
+ test("a commit joins to every release cut from it, each with its own deploys across environments", async () => {
31
+ await withTestDir(async (dir) => {
32
+ git(dir, "init", "-q", "-b", "main");
33
+ git(dir, "config", "user.email", "t@chant.dev");
34
+ git(dir, "config", "user.name", "T");
35
+ writeFileSync(join(dir, "README.md"), "x\n");
36
+ git(dir, "add", ".");
37
+ git(dir, "commit", "-q", "-m", "init");
38
+ const sha = git(dir, "rev-parse", "HEAD");
39
+ const one = makeRelease(subject(sha, "sha256:1"));
40
+ const two = makeRelease(subject(sha, "sha256:2"));
41
+ for (const [r, label] of [[one, "v1"], [two, null]] as const) {
42
+ await persistRelease(r, [], { cwd: dir });
43
+ await appendCut({ releaseDigest: r.releaseDigest, label, actor: "alice", timestamp: "2026-10-16T10:00:00.000Z", runOrigin: null, signature: null }, { cwd: dir });
44
+ }
45
+ const rec = (env: string, runId: string, timestamp: string, releaseDigest?: string) =>
46
+ appendReleaseRecord({ component: "api", env, digest: "sha256:1", gitSha: sha, runId, timestamp, actor: "ci", ...(releaseDigest ? { releaseDigest } : {}) }, { cwd: dir });
47
+ await rec("prod", "r3", "2026-10-16T13:00:00.000Z", one.releaseDigest);
48
+ await rec("dev", "r1", "2026-10-16T11:00:00.000Z", one.releaseDigest);
49
+ await rec("dev", "r2", "2026-10-16T12:00:00.000Z"); // a deploy of the commit that named no release
50
+ await rec("dev", "r4", "2026-10-16T14:00:00.000Z", two.releaseDigest);
51
+
52
+ const { releases } = await joinRelease({ commit: sha }, { cwd: dir });
53
+ expect(releases.map((r) => [r.label, r.deploys.map((d) => `${d.env}:${d.runId}`)])).toEqual(
54
+ expect.arrayContaining([["v1", ["dev:r1", "prod:r3"]], [null, ["dev:r4"]]]),
55
+ );
56
+ expect(releases).toHaveLength(2);
57
+ expect(releases.every((r) => r.commit === sha)).toBe(true);
58
+ expect(releases.find((r) => r.label === "v1")!.gates).toEqual({ pending: [], resolutions: [] });
59
+
60
+ // The unlabelled release is reached by its digest, and the record that named no release joins nowhere.
61
+ expect((await joinRelease({ release: two.releaseDigest }, { cwd: dir })).releases[0].deploys.map((d) => d.runId)).toEqual(["r4"]);
62
+ expect((await joinRelease({ deploy: { releaseDigest: undefined } }, { cwd: dir })).releases).toEqual([]);
63
+ expect((await joinRelease({ release: "nope" }, { cwd: dir })).reason).toMatch(/no release is labelled or digested "nope"/);
64
+ const ghost = `jcs1-sha256:${"9".repeat(64)}`;
65
+ expect((await joinRelease({ gate: { releaseDigest: ghost } }, { cwd: dir })).reason).toMatch(/file is not on this checkout/);
66
+ });
67
+ });
68
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Join a commit, a release, its gates and its deploy records through the
3
+ * release digest (#4043, ws-104).
4
+ *
5
+ * The records already name one another. A release file holds its `commit`; a
6
+ * release gate's pending fact and resolution hold the `releaseDigest`; a deploy
7
+ * record holds the `releaseDigest` when the deploy was made from a release.
8
+ * From any one of the four, the others are one lookup away:
9
+ *
10
+ * commit to releases `_releases/*.json` `commit`
11
+ * release to gates `releaseDigest` on the records of `_gates/release.jsonl`
12
+ * release to deploys `releaseDigest` on the records of every `<env>/releases.jsonl`
13
+ *
14
+ * This is the one place that does it, for the readers that need the chain
15
+ * (the why chain of #4023, behold). Release plus a deploy record's `env`
16
+ * names the release target.
17
+ *
18
+ * Read-only. Reads the local `chant/lifecycle` branch; fetch it first for the
19
+ * remote's records. A release digest or commit that nothing recorded joins to
20
+ * an empty list rather than an error.
21
+ */
22
+
23
+ import { RELEASE_GATE_OP } from "../op/gate-name";
24
+ import { readGateLedger, type GateResolutionRecord, type PendingGateRecord } from "./gate-ledger";
25
+ import { listReleaseEnvironments, readReleaseLedger, type ReleaseRecord } from "./release-ledger";
26
+ import { labelsByDigest, readAllReleases, readCuts, type Release, type ReleaseCut } from "./release-store";
27
+ import type { LedgerLocation } from "./git";
28
+
29
+ /** Where a join starts. Each names the others one lookup away. */
30
+ export type ReleaseJoinStart =
31
+ /** A full commit sha, or a prefix of at least 7 characters. Joins to every release cut from it. */
32
+ | { commit: string }
33
+ /**
34
+ * Several full commit shas, or prefixes of at least 7 characters. Joins to
35
+ * every release cut from any of them, reading the ledgers once (#4023).
36
+ */
37
+ | { commits: readonly string[] }
38
+ /** A release's label or its `releaseDigest`. */
39
+ | { release: string }
40
+ /**
41
+ * A deploy record. It joins through its `releaseDigest`; a record with none
42
+ * but an admission by the release gate joins through the resolution that
43
+ * admission names.
44
+ */
45
+ | { deploy: Pick<ReleaseRecord, "releaseDigest" | "admission"> }
46
+ /** A release gate's pending fact or resolution. */
47
+ | { gate: Pick<PendingGateRecord | GateResolutionRecord, "releaseDigest"> };
48
+
49
+ /** One release with everything the digest joins to it. */
50
+ export interface JoinedRelease {
51
+ releaseDigest: string;
52
+ /** The label of the release, or null when it was cut with none. */
53
+ label: string | null;
54
+ /** The green commit the release was cut from. */
55
+ commit: string;
56
+ release: Release;
57
+ /** Every cut of the release, oldest first. */
58
+ cuts: ReleaseCut[];
59
+ /** The release gate's facts for this release (`_gates/release.jsonl`), oldest first. */
60
+ gates: { pending: PendingGateRecord[]; resolutions: GateResolutionRecord[] };
61
+ /** Every deploy record that names the release, across environments, oldest first: by its `releaseDigest`, or, without one, by the release gate resolution its admission names. Each record's `env` with the release names the target. */
62
+ deploys: ReleaseRecord[];
63
+ }
64
+
65
+ /** What {@link joinRelease} returns. */
66
+ export interface ReleaseJoin {
67
+ /** The releases the starting point joins to: one for a release, a gate or a deploy, any number for a commit. */
68
+ releases: JoinedRelease[];
69
+ /** Why `releases` is empty, when the starting point could not be followed. Absent otherwise. */
70
+ reason?: string;
71
+ }
72
+
73
+ /** The version of the shape above, for a reader that stores it. Chant 0.124.29 and newer. */
74
+ export const RELEASE_JOIN_VERSION = 1;
75
+
76
+ const MIN_COMMIT_PREFIX = 7;
77
+
78
+ /**
79
+ * Follow one record to the rest of its chain. Never throws for an unknown
80
+ * starting point; a ledger that can't be read does throw, as those readers do.
81
+ */
82
+ export async function joinRelease(from: ReleaseJoinStart, loc: LedgerLocation = {}): Promise<ReleaseJoin> {
83
+ const { releases } = await readAllReleases(loc);
84
+ const { cuts } = await readCuts(loc);
85
+ const labels = labelsByDigest(cuts);
86
+
87
+ let digests: string[];
88
+ let reason: string | undefined;
89
+ if ("commit" in from) {
90
+ const sha = from.commit.toLowerCase();
91
+ if (sha.length < MIN_COMMIT_PREFIX) {
92
+ return { releases: [], reason: `a commit needs at least ${MIN_COMMIT_PREFIX} characters to join on` };
93
+ }
94
+ digests = releases.filter((r) => r.commit.toLowerCase().startsWith(sha)).map((r) => r.releaseDigest);
95
+ if (digests.length === 0) reason = `no release was cut from commit ${from.commit}`;
96
+ } else if ("commits" in from) {
97
+ const shas = from.commits.map((c) => c.toLowerCase());
98
+ if (shas.some((sha) => sha.length < MIN_COMMIT_PREFIX)) {
99
+ return { releases: [], reason: `a commit needs at least ${MIN_COMMIT_PREFIX} characters to join on` };
100
+ }
101
+ digests = releases.filter((r) => shas.some((sha) => r.commit.toLowerCase().startsWith(sha))).map((r) => r.releaseDigest);
102
+ if (digests.length === 0) reason = shas.length === 0 ? "no commit was given to join on" : `no release was cut from ${shas.length === 1 ? `commit ${from.commits[0]}` : `any of ${shas.length} commits`}`;
103
+ } else if ("release" in from) {
104
+ const ref = from.release;
105
+ const byDigest = releases.find((r) => r.releaseDigest === ref);
106
+ const byLabel = [...labels].find(([, label]) => label === ref)?.[0];
107
+ const found = byDigest?.releaseDigest ?? byLabel;
108
+ digests = found ? [found] : [];
109
+ if (digests.length === 0) reason = `no release is labelled or digested "${ref}"`;
110
+ } else if ("deploy" in from) {
111
+ const digest = from.deploy.releaseDigest ?? (await releaseOfAdmission(from.deploy.admission, loc));
112
+ digests = digest ? [digest] : [];
113
+ if (digests.length === 0) reason = "the deploy record names no release: it was not made from one";
114
+ } else {
115
+ const digest = from.gate.releaseDigest;
116
+ digests = digest ? [digest] : [];
117
+ if (digests.length === 0) reason = "the gate binds no release: it is not a release gate";
118
+ }
119
+
120
+ const known = new Map(releases.map((r) => [r.releaseDigest, r]));
121
+ const wanted = [...new Set(digests)].filter((d) => known.has(d));
122
+ if (digests.length > 0 && wanted.length === 0) reason = `release ${digests[0]} is named but its file is not on this checkout's chant/lifecycle`;
123
+ if (wanted.length === 0) return { releases: [], ...(reason ? { reason } : {}) };
124
+
125
+ const gateLedger = await readGateLedger(RELEASE_GATE_OP, loc);
126
+ // A record names its release by `releaseDigest`, or, with none, through the
127
+ // release gate resolution its admission names (#4045), as releaseOfDeploy reads it.
128
+ const records: { record: ReleaseRecord; digest: string }[] = [];
129
+ for (const env of await listReleaseEnvironments(loc)) {
130
+ for (const record of (await readReleaseLedger(env, loc)).records) {
131
+ const digest = releaseOfDeploy(record, gateLedger.resolutions);
132
+ if (digest !== undefined && wanted.includes(digest)) records.push({ record, digest });
133
+ }
134
+ }
135
+ const byTime = (a: { timestamp: string }, b: { timestamp: string }): number => (a.timestamp < b.timestamp ? -1 : a.timestamp > b.timestamp ? 1 : 0);
136
+
137
+ return {
138
+ releases: wanted.map((digest) => {
139
+ const release = known.get(digest)!;
140
+ return {
141
+ releaseDigest: digest,
142
+ label: labels.get(digest) ?? null,
143
+ commit: release.commit,
144
+ release,
145
+ cuts: cuts.filter((c) => c.releaseDigest === digest),
146
+ gates: {
147
+ pending: gateLedger.pending.filter((p) => p.releaseDigest === digest),
148
+ resolutions: gateLedger.resolutions.filter((r) => r.releaseDigest === digest),
149
+ },
150
+ deploys: records.filter((r) => r.digest === digest).map((r) => r.record).sort(byTime),
151
+ };
152
+ }),
153
+ };
154
+ }
155
+
156
+ /**
157
+ * The release a deploy record's admission names, when the record itself has no
158
+ * `releaseDigest`: the resolution it points at, in the release gate's ledger,
159
+ * carries one (#4045).
160
+ */
161
+ async function releaseOfAdmission(admission: ReleaseRecord["admission"], loc: LedgerLocation): Promise<string | undefined> {
162
+ if (!admission || admission.op !== RELEASE_GATE_OP) return undefined;
163
+ const { resolutions } = await readGateLedger(RELEASE_GATE_OP, loc);
164
+ return releaseOfDeploy({ admission }, resolutions);
165
+ }
166
+
167
+ /**
168
+ * The release a deploy record names, over the release gate's resolutions
169
+ * already read: its own `releaseDigest`, else the one on the resolution its
170
+ * admission names. Undefined when it was not made from a release. The rule
171
+ * {@link joinRelease} follows, for a reader that holds the ledgers already,
172
+ * such as retention's garbage collection (#4036).
173
+ */
174
+ export function releaseOfDeploy(
175
+ record: Pick<ReleaseRecord, "releaseDigest" | "admission">,
176
+ releaseGateResolutions: readonly Pick<GateResolutionRecord, "gate" | "timestamp" | "releaseDigest">[],
177
+ ): string | undefined {
178
+ if (record.releaseDigest !== undefined) return record.releaseDigest;
179
+ const admission = record.admission;
180
+ if (!admission || admission.op !== RELEASE_GATE_OP) return undefined;
181
+ return releaseGateResolutions.find((r) => r.gate === admission.gate && r.timestamp === admission.resolvedAt && r.releaseDigest !== undefined)?.releaseDigest;
182
+ }
@@ -146,6 +146,20 @@ describe("release-ledger", () => {
146
146
  });
147
147
  });
148
148
 
149
+ test("records the gate resolution that admitted the deploy, and an older record without it still reads (#4020)", async () => {
150
+ await withTestDir(async (dir) => {
151
+ await initRepo(dir);
152
+ const admission = { op: "search-service", gate: "approve-prod", resolvedAt: "2026-01-01T00:05:00.000Z" };
153
+ await appendReleaseRecord(makeInput({ timestamp: "2026-01-01T00:06:00.000Z", approver: "alice@corp", admission }), { cwd: dir });
154
+ await appendReleaseRecord(makeInput({ component: "worker", timestamp: "2026-01-01T00:07:00.000Z" }), { cwd: dir });
155
+ const { records, malformed } = await readReleaseLedger("prod", { cwd: dir });
156
+ expect(malformed).toBe(0);
157
+ expect(records[0].admission).toEqual(admission);
158
+ expect(records[0].approver).toBe("alice@corp");
159
+ expect(records[1].admission).toBeUndefined();
160
+ });
161
+ });
162
+
149
163
  test("approver is absent for an ungated change (#1035)", async () => {
150
164
  await withTestDir(async (dir) => {
151
165
  await initRepo(dir);
@@ -32,6 +32,7 @@ import { sortedJsonReplacer } from "../utils";
32
32
  import { warnOnLegacyDigests, withoutReadFlags, type LegacyDigestFlag } from "./legacy-digest";
33
33
  import { appendReleaseRecordLine, readReleaseLedgerLines, listLedgerEnvironments as gitListLedgerEnvironments, type LedgerLocation } from "./git";
34
34
  import { parseReleaseLedger, validateReleaseRecord } from "./release-read";
35
+ import type { GateResolutionRecord } from "./gate-ledger";
35
36
 
36
37
  // The pure readers live apart, so a bundle can import them without git (#3865).
37
38
  export { validateReleaseRecord, parseReleaseLedger, releaseLedgerLines, readReleaseRecords, listReleaseEnvironmentsOn } from "./release-read";
@@ -56,6 +57,40 @@ export interface RunOrigin {
56
57
  url?: string;
57
58
  }
58
59
 
60
+ /**
61
+ * The gate resolution that admitted a deploy (#4020): the gate's address in the
62
+ * gate ledger (`_gates/<op>.jsonl`, `op` and `gate`) and when the resolution
63
+ * was recorded. With `approver` on the same record it names the line a reader
64
+ * opens to see what was approved. Chant 0.124.28 and newer write it.
65
+ */
66
+ export interface ReleaseAdmission {
67
+ /** The op, or for a component gate the component, whose gate ledger holds the resolution. */
68
+ op: string;
69
+ /** The gate's name. */
70
+ gate: string;
71
+ /** ISO-8601 time the resolution was recorded (`GateResolutionRecord.timestamp`). When several approvals counted, the newest one. */
72
+ resolvedAt: string;
73
+ }
74
+
75
+ /**
76
+ * The approver and admission a release records for the resolutions of its gate
77
+ * (#4020): those of `gate` on `op` bound to `planDigest`. The approver is the
78
+ * distinct `resolvedBy` names, comma joined, as `approver` has always been
79
+ * written by the release activities; `resolvedAt` is the newest one's time.
80
+ * Undefined when no resolution is bound to the plan.
81
+ */
82
+ export function admissionOf(
83
+ resolutions: readonly GateResolutionRecord[],
84
+ at: { op: string; gate: string; planDigest?: string },
85
+ ): { approver: string; admission: ReleaseAdmission } | undefined {
86
+ const bound = resolutions.filter((r) => r.gate === at.gate && r.planDigest === at.planDigest);
87
+ if (bound.length === 0) return undefined;
88
+ return {
89
+ approver: [...new Set(bound.map((r) => r.resolvedBy))].join(", "),
90
+ admission: { op: at.op, gate: at.gate, resolvedAt: bound.map((r) => r.timestamp).sort().at(-1)! },
91
+ };
92
+ }
93
+
59
94
  /**
60
95
  * Points at one earlier release record (#2530, #2531, #2604). A record has no id of its own,
61
96
  * so it is named by the environment whose ledger holds it plus the run id and
@@ -126,6 +161,13 @@ export interface ReleaseRecord {
126
161
  * the one who triggered the deploy (separation of duties).
127
162
  */
128
163
  approver?: string;
164
+ /**
165
+ * The gate resolution that admitted this deploy (#4020), for a gated change:
166
+ * which gate (`op` and `gate`) and when it was resolved. The approver is
167
+ * `approver`. Optional and omitted for an ungated change and for a record
168
+ * written before 0.124.28, whose gate cannot be recovered from the record.
169
+ */
170
+ admission?: ReleaseAdmission;
129
171
  /** Optional: the archive's own manifest digest (../components/verbs/build-archive.ts's `manifestDigest`), when the caller has it — lets a reader recover full build contents/provenance, not just the promoted image digest. */
130
172
  manifestDigest?: string;
131
173
  /**
@@ -175,6 +217,13 @@ export interface ReleaseRecord {
175
217
  * partway. Absent on every other record.
176
218
  */
177
219
  redeploys?: ReleaseRef;
220
+ /**
221
+ * Optional: the release this deploy came from (#4026, ws-104), the
222
+ * `releaseDigest` of `_releases/<releaseDigest>.json`. Written by
223
+ * `chant components promote --release` since 0.124.28. Absent on every
224
+ * deploy that was not made from a release.
225
+ */
226
+ releaseDigest?: string;
178
227
  }
179
228
 
180
229
  /** Thrown by `appendReleaseRecord` when the record is missing a required field — a release record can never be recorded partially, since it is never editable afterward. */
@@ -213,29 +262,48 @@ export function resolveRunId(
213
262
  explicit?: string,
214
263
  env: NodeJS.ProcessEnv = process.env,
215
264
  ): { runId: string; runOrigin?: RunOrigin } {
216
- const githubOrigin = (id: string): RunOrigin => ({
217
- forge: "github",
218
- ...(env.GITHUB_REPOSITORY ? { repo: env.GITHUB_REPOSITORY } : {}),
219
- ...(env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY
220
- ? { url: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${id}` }
221
- : {}),
222
- });
223
- const gitlabOrigin = (): RunOrigin => ({
224
- forge: "gitlab",
225
- ...(env.CI_PROJECT_PATH ? { repo: env.CI_PROJECT_PATH } : {}),
226
- ...(env.CI_PIPELINE_URL ? { url: env.CI_PIPELINE_URL } : {}),
227
- });
228
-
265
+ const ci = resolveCiRun(env);
229
266
  if (explicit) {
230
- if (env.GITHUB_RUN_ID && explicit === env.GITHUB_RUN_ID) return { runId: explicit, runOrigin: githubOrigin(explicit) };
231
- if (env.CI_PIPELINE_ID && explicit === env.CI_PIPELINE_ID) return { runId: explicit, runOrigin: gitlabOrigin() };
267
+ if (ci && explicit === ci.runId) return ci;
232
268
  return { runId: explicit };
233
269
  }
234
- if (env.GITHUB_RUN_ID) return { runId: env.GITHUB_RUN_ID, runOrigin: githubOrigin(env.GITHUB_RUN_ID) };
235
- if (env.CI_PIPELINE_ID) return { runId: env.CI_PIPELINE_ID, runOrigin: gitlabOrigin() };
270
+ if (ci) return ci;
236
271
  return { runId: `local-${Date.now()}`, runOrigin: { forge: "local" } };
237
272
  }
238
273
 
274
+ /**
275
+ * The CI run the environment names, with the origin that locates it, or
276
+ * undefined outside CI (#4020). `resolveRunId` takes its CI branches from
277
+ * here, and an Op run record takes its `runOrigin` from here, so a run and the
278
+ * deploy it made name the run the same way.
279
+ */
280
+ export function resolveCiRun(env: NodeJS.ProcessEnv = process.env): { runId: string; runOrigin: RunOrigin } | undefined {
281
+ if (env.GITHUB_RUN_ID) {
282
+ const id = env.GITHUB_RUN_ID;
283
+ return {
284
+ runId: id,
285
+ runOrigin: {
286
+ forge: "github",
287
+ ...(env.GITHUB_REPOSITORY ? { repo: env.GITHUB_REPOSITORY } : {}),
288
+ ...(env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY
289
+ ? { url: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${id}` }
290
+ : {}),
291
+ },
292
+ };
293
+ }
294
+ if (env.CI_PIPELINE_ID) {
295
+ return {
296
+ runId: env.CI_PIPELINE_ID,
297
+ runOrigin: {
298
+ forge: "gitlab",
299
+ ...(env.CI_PROJECT_PATH ? { repo: env.CI_PROJECT_PATH } : {}),
300
+ ...(env.CI_PIPELINE_URL ? { url: env.CI_PIPELINE_URL } : {}),
301
+ },
302
+ };
303
+ }
304
+ return undefined;
305
+ }
306
+
239
307
  /**
240
308
  * Append one immutable release record to the `<env>` ledger on the
241
309
  * `chant/lifecycle` orphan branch. Throws `InvalidReleaseRecordError` before
@@ -0,0 +1,225 @@
1
+ /**
2
+ * #4027: `chant release publish`'s contract. It refuses a release no cut
3
+ * names, a revoked one, and one with no verified signature (#4049; under
4
+ * identity.gates.release even with --allow-unsigned); it warns when the run has no OIDC token; and with no
5
+ * publish target declared it has nothing to publish. With a target (#4047)
6
+ * it reads them from release.publish and refuses a checkout that is not the
7
+ * release's commit with a clean tree.
8
+ */
9
+
10
+ import { afterAll, describe, expect, test } from "vitest";
11
+ import { rmSync } from "node:fs";
12
+ import { dirname } from "node:path";
13
+ import { cutRelease } from "./release-cut";
14
+ import { detectProvenance, preparePublish, type PublishPlan, type PublishRefusal } from "./release-publish";
15
+ import type { ReleaseArtifact } from "./release-store";
16
+ import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
17
+ import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
18
+
19
+ const repos: TestRepo[] = [];
20
+ afterAll(() => {
21
+ for (const r of repos) rmSync(dirname(r.dir), { recursive: true, force: true });
22
+ });
23
+
24
+ const compiler: TargetCompiler = {
25
+ async compile(env): Promise<CompiledTarget> {
26
+ return {
27
+ env,
28
+ params: [],
29
+ components: ["api"],
30
+ buildSteps: {},
31
+ buildInputs: {},
32
+ componentFiles: { api: "api.component.ts" },
33
+ templates: [{ component: "api", path: "dist/api.json", content: `{"env":"${env}"}\n` }],
34
+ };
35
+ },
36
+ async buildArtifacts(): Promise<ReleaseArtifact[]> {
37
+ return [];
38
+ },
39
+ };
40
+
41
+ /** A project, a workspace member when `declaration` is given, with alice and bob as signers and alice a releaser. */
42
+ function setup(declaration: Record<string, unknown> | null, opts: { signers?: boolean } = {}): { r: TestRepo; alice: Key; bob: Key; head: string } {
43
+ const r = new TestRepo("release-publish");
44
+ repos.push(r);
45
+ const alice = r.key("alice");
46
+ const bob = r.key("bob");
47
+ if (declaration) {
48
+ r.write("chant.workspace.json", JSON.stringify({ name: "acme", schema: 1, members: [{ name: "root", dir: ".", kind: "other", because: "the publish fixture" }], ...declaration }, null, 2));
49
+ r.write(".chant/allowed_signers", `alice ${alice.pub}\nbob ${bob.pub}\n`);
50
+ } else if (opts.signers) {
51
+ // A plain project with a signers file and no workspace declaration (#4049).
52
+ r.write(".chant/allowed_signers", `alice ${alice.pub}\n`);
53
+ }
54
+ r.write("chant.config.ts", `export default { environments: ["dev", "prod"] };\n`);
55
+ r.write("api.component.ts", "export const api = {};\n");
56
+ const head = r.commit("init");
57
+ r.git(["tag", `ci/green/${head}`]);
58
+ return { r, alice, bob, head };
59
+ }
60
+
61
+ const cut = (r: TestRepo, label: string, sign?: Key, actor = "alice") =>
62
+ cutRelease({ cwd: r.dir, compiler, label, actor, env: {}, now: () => new Date("2026-10-17T10:00:00.000Z"), ...(sign ? { sign: sign.file } : {}) });
63
+ const refusal = (p: PublishPlan | { refusal: PublishRefusal }): PublishRefusal | null => ("refusal" in p ? p.refusal : null);
64
+ const WORKSTATION: NodeJS.ProcessEnv = { PATH: process.env.PATH };
65
+
66
+ describe("provenance", () => {
67
+ test("GitHub Actions has OIDC with id-token: write, GitLab with an ID token, a workstation never", () => {
68
+ expect(detectProvenance({})).toEqual({ oidc: false, ci: null, reason: "this run is not in CI, and a workstation has no OIDC token" });
69
+ expect(detectProvenance({ GITHUB_ACTIONS: "true" })).toMatchObject({ oidc: false, ci: "github", reason: expect.stringMatching(/id-token: write/) });
70
+ expect(detectProvenance({ GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" })).toMatchObject({ oidc: true, ci: "github" });
71
+ expect(detectProvenance({ GITLAB_CI: "true" })).toMatchObject({ oidc: false, ci: "gitlab", reason: expect.stringMatching(/id_tokens/) });
72
+ expect(detectProvenance({ GITLAB_CI: "true", SIGSTORE_ID_TOKEN: "t" })).toMatchObject({ oidc: true, ci: "gitlab" });
73
+ // A CI variable alone is not a token.
74
+ expect(detectProvenance({ CI: "true", GITHUB_RUN_ID: "1" }).oidc).toBe(false);
75
+ });
76
+ });
77
+
78
+ describe("chant release publish", () => {
79
+ test("warns on a workstation that what it publishes carries no provenance, and has no target to publish to", async () => {
80
+ const { r } = setup(null);
81
+ await cut(r, "v1");
82
+ const plan = await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true });
83
+ expect(refusal(plan)).toBeNull();
84
+ const p = plan as PublishPlan;
85
+ expect(p.label).toBe("v1");
86
+ expect(p.signature.status).toBe("unsigned");
87
+ expect(p.provenance.oidc).toBe(false);
88
+ expect(p.warning).toBe(
89
+ "this publish runs without OIDC, so what it publishes carries no provenance: this run is not in CI, and a workstation has no OIDC token. Publish from CI to get provenance.",
90
+ );
91
+ expect(p.targets).toEqual([]);
92
+ expect(p.releaseTag).toBe("release/v1");
93
+
94
+ const inCi = (await preparePublish({
95
+ cwd: r.dir,
96
+ ref: p.release.releaseDigest,
97
+ env: { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" },
98
+ allowUnsigned: true,
99
+ })) as PublishPlan;
100
+ expect(inCi.warning).toBeNull();
101
+ expect(inCi.label).toBe("v1");
102
+ });
103
+
104
+ test("refuses a release no cut names, and one cut from a revoked commit", async () => {
105
+ const { r, head } = setup(null);
106
+ await cut(r, "v1");
107
+ expect(refusal(await preparePublish({ cwd: r.dir, ref: "v9", env: WORKSTATION }))?.code).toBe("release-not-found");
108
+ r.git(["tag", `ci/revoked/${head}`]);
109
+ const revoked = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION }));
110
+ expect(revoked?.code).toBe("release-revoked");
111
+ expect(revoked?.message).toMatch(new RegExp(`ci/revoked/${head}`));
112
+ });
113
+
114
+ test.skipIf(!hasSshKeygen)("under identity.gates.release, refuses an unsigned cut and publishes a signed one", async () => {
115
+ const gate = { identity: { gates: { release: {} } } };
116
+ const { r, alice } = setup(gate);
117
+ // Cut before the gate applies to the cut, as an older chant or another branch could: write it through the store's own path.
118
+ const { appendCut, makeRelease, persistRelease } = await import("./release-store");
119
+ const head = r.head();
120
+ const unsigned = makeRelease({ member: "root", commit: head, artifactDigests: [], targets: [{ env: "dev", params: [], templates: [] }] });
121
+ await persistRelease(unsigned, [], { cwd: r.dir });
122
+ await appendCut({ releaseDigest: unsigned.releaseDigest, label: "v0", actor: "alice", timestamp: "2026-10-17T09:00:00.000Z", runOrigin: null }, { cwd: r.dir });
123
+ const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v0", env: WORKSTATION }));
124
+ expect(refused?.code).toBe("release-unsigned");
125
+ expect(refused?.fix).toMatch(/chant release cut v0 --sign --yes/);
126
+
127
+ await cut(r, "v1", alice);
128
+ const signed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
129
+ expect(refusal(signed)).toBeNull();
130
+ expect(signed.signature).toMatchObject({ status: "verified", actor: "alice", key: expect.stringMatching(/^SHA256:/) });
131
+ });
132
+
133
+ test.skipIf(!hasSshKeygen)("under identity.gates.release, refuses a cut whose signature does not verify for its actor", async () => {
134
+ const { r, bob } = setup({ identity: { gates: { release: {} } } });
135
+ const { appendCut, makeRelease, persistRelease } = await import("./release-store");
136
+ const { sealReleaseCut } = await import("../workspace/trust/seal");
137
+ const release = makeRelease({ member: "root", commit: r.head(), artifactDigests: [], targets: [{ env: "dev", params: [], templates: [] }] });
138
+ await persistRelease(release, [], { cwd: r.dir });
139
+ // Signed by bob's key, in alice's name.
140
+ const line = { releaseDigest: release.releaseDigest, label: "v2", actor: "alice", timestamp: "2026-10-17T09:00:00.000Z" };
141
+ const signature = sealReleaseCut(bob.file, line).signature;
142
+ await appendCut({ ...line, runOrigin: null, signature }, { cwd: r.dir });
143
+ const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION }));
144
+ expect(refused?.code).toBe("release-signature-unverified");
145
+ // --allow-unsigned never overrides the release gate.
146
+ expect(refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION, allowUnsigned: true }))?.code).toBe("release-signature-unverified");
147
+ });
148
+
149
+ test("refuses an unsigned release with no release gate, and publishes it under --allow-unsigned with a warning", async () => {
150
+ const { r } = setup(null);
151
+ await cut(r, "v1");
152
+ const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION }));
153
+ expect(refused?.code).toBe("release-unsigned");
154
+ expect(refused?.message).toBe("v1 has no signed cut, and chant release publish publishes only a release signed by a key in .chant/allowed_signers");
155
+ expect(refused?.fix).toContain("chant release cut v1 --sign --yes");
156
+ expect(refused?.fix).toContain("--allow-unsigned");
157
+
158
+ const allowed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true })) as PublishPlan;
159
+ expect(refusal(allowed)).toBeNull();
160
+ expect(allowed.signatureWarning).toBe("v1 has no signed cut; publishing it anyway under --allow-unsigned, so chant release verify v1 will fail for it");
161
+ expect(allowed.cut).toMatchObject({ actor: "alice", label: "v1", signature: null });
162
+ });
163
+
164
+ test.skipIf(!hasSshKeygen)("with no release gate, publishes a release signed by a listed key and refuses one signed by a key the signers file does not list", async () => {
165
+ const { r, alice, bob } = setup(null, { signers: true });
166
+ await cut(r, "v1", alice);
167
+ const signed = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION })) as PublishPlan;
168
+ expect(refusal(signed)).toBeNull();
169
+ expect(signed.signature).toMatchObject({ status: "verified", actor: "alice" });
170
+ expect(signed.signatureWarning).toBeNull();
171
+ expect(signed.cut?.signature).toMatch(/^-----BEGIN SSH SIGNATURE-----/);
172
+
173
+ // bob signs in his own name, and the signers file lists only alice.
174
+ r.write("later.txt", "x\n");
175
+ const later = r.commit("later");
176
+ r.git(["tag", `ci/green/${later}`]);
177
+ await cut(r, "v2", bob, "bob");
178
+ const unlisted = refusal(await preparePublish({ cwd: r.dir, ref: "v2", env: WORKSTATION }));
179
+ expect(unlisted?.code).toBe("release-signature-unverified");
180
+ expect(unlisted?.message).toMatch(/bob has no key in \.chant\/allowed_signers at base/);
181
+ });
182
+
183
+ test("--require-provenance refuses a run with no OIDC token and passes one in CI", async () => {
184
+ const { r } = setup(null);
185
+ await cut(r, "v1");
186
+ const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true, requireProvenance: true }));
187
+ expect(refused?.code).toBe("publish-provenance-missing");
188
+ expect(refused?.message).toMatch(/a workstation has no OIDC token/);
189
+ const ci = { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://x", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" };
190
+ expect(refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: ci, allowUnsigned: true, requireProvenance: true }))).toBeNull();
191
+ });
192
+
193
+ test("reads the targets release.publish declares, and with one refuses a checkout that is not the release's commit or has changes", async () => {
194
+ const { r } = setup(null);
195
+ r.write("chant.config.ts", `export default { environments: ["dev"], release: { publish: [{ type: "npm", packages: ["pkg"] }, { type: "oci", image: "img", repository: "ghcr.io/acme/api" }] } };\n`);
196
+ const head = r.commit("declare targets");
197
+ r.git(["tag", `ci/green/${head}`]);
198
+ await cut(r, "v1");
199
+ const p = (await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true })) as PublishPlan;
200
+ expect(refusal(p)).toBeNull();
201
+ expect(p.targets.map((t) => [t.kind, t.name, t.destination])).toEqual([
202
+ ["npm", "npm (1 package)", "npm the registry npm's config names"],
203
+ ["oci", "oci ghcr.io/acme/api", "ghcr.io/acme/api"],
204
+ ]);
205
+ expect(p.targets[1]!.credentialEnv).toEqual(["OCI_PASSWORD"]);
206
+
207
+ r.write("untracked.txt", "x\n");
208
+ expect(refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }))?.code).toBe("release-worktree-dirty");
209
+ rmSync(`${r.dir}/untracked.txt`);
210
+ r.commit("later");
211
+ const moved = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }));
212
+ expect(moved?.code).toBe("release-head-mismatch");
213
+ expect(moved?.fix).toContain(`git checkout ${head}`);
214
+ });
215
+
216
+ test("refuses a release.publish block that does not validate, before anything is read", async () => {
217
+ const { r } = setup(null);
218
+ await cut(r, "v1");
219
+ r.write("chant.config.ts", `export default { environments: ["dev"], release: { publish: [{ type: "npm", packages: [] }] } };\n`);
220
+ const refused = refusal(await preparePublish({ cwd: r.dir, ref: "v1", env: WORKSTATION, allowUnsigned: true }));
221
+ expect(refused?.code).toBe("publish-config-invalid");
222
+ expect(refused?.message).toMatch(/release\.publish\.0\.packages/);
223
+ });
224
+ });
225
+