@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,161 @@
1
+ /**
2
+ * An implicit release of a project with no components, end to end (#4046):
3
+ * the getting-started example, given two environments and a build parameter
4
+ * set per environment, is cut from the same commit in two clones. Each
5
+ * environment's build output must come out byte for byte the same, and the
6
+ * two cuts must give one release digest. No cluster is touched: the release
7
+ * is exported to files and never deployed.
8
+ */
9
+
10
+ import { describe, test, expect } from "vitest";
11
+ import { spawnSync } from "node:child_process";
12
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { join, relative } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+ import { contract, REPO } from "../../workspace/__fixtures__/contract-repo";
17
+ import releasesSchema from "../../workspace/releases.schema.json";
18
+ import { contentDigest } from "../../content-digest";
19
+
20
+ const CHANT = [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")];
21
+ const EXAMPLE = join(REPO, "examples", "getting-started");
22
+
23
+ function chant(cwd: string, ...args: string[]): { code: number; stdout: string; stderr: string } {
24
+ const env: NodeJS.ProcessEnv = { ...process.env, USER: "tester" };
25
+ for (const k of ["GITHUB_RUN_ID", "CI_PIPELINE_ID", "GITHUB_ACTOR", "GITLAB_USER_LOGIN", "CHANT_ENV", "GITHUB_SHA", "CI"]) delete env[k];
26
+ const r = spawnSync(CHANT[0], [...CHANT.slice(1), ...args], { cwd, encoding: "utf-8", env, timeout: 180_000 });
27
+ return { code: r.status ?? 1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
28
+ }
29
+
30
+ function git(cwd: string, ...args: string[]): string {
31
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
32
+ if (r.status !== 0) throw new Error(r.stderr);
33
+ return r.stdout.trim();
34
+ }
35
+
36
+ /** Every file under `dir`, by its path relative to `dir`, as bytes. */
37
+ function filesUnder(dir: string): Map<string, Buffer> {
38
+ const out = new Map<string, Buffer>();
39
+ const walk = (d: string): void => {
40
+ for (const e of readdirSync(d, { withFileTypes: true })) {
41
+ const p = join(d, e.name);
42
+ if (e.isDirectory()) walk(p);
43
+ else out.set(relative(dir, p), readFileSync(p));
44
+ }
45
+ };
46
+ walk(dir);
47
+ return out;
48
+ }
49
+
50
+ /** The example as a project of its own with two environments and a parameter that differs between them. */
51
+ function writeProject(dir: string): void {
52
+ cpSync(join(EXAMPLE, "src"), join(dir, "src"), { recursive: true });
53
+ // The deploy Op goes under the build root, so the build writes src/dist/ops/deploy/op.json too: an Op's output goes under its build root.
54
+ cpSync(join(EXAMPLE, "deploy.op.ts"), join(dir, "src", "deploy.op.ts"));
55
+ const web = readFileSync(join(dir, "src", "web.ts"), "utf-8");
56
+ writeFileSync(
57
+ join(dir, "src", "web.ts"),
58
+ `import { params } from "@intentius/chant/params";\n${web.replace("replicas: 2,", "replicas: params.replicas as number,")}`,
59
+ );
60
+ writeFileSync(
61
+ join(dir, "chant.config.ts"),
62
+ `export default {\n lexicons: ["k8s"],\n sourceDir: "src",\n environments: ["dev", "prod"],\n buildParams: { replicas: { type: "number", default: 2 } },\n};\n`,
63
+ );
64
+ writeFileSync(join(dir, "params.dev.json"), `{ "replicas": 1 }\n`);
65
+ writeFileSync(join(dir, "params.prod.json"), `{ "replicas": 3 }\n`);
66
+ writeFileSync(join(dir, ".gitignore"), "node_modules\ndist/\nout/\n");
67
+ }
68
+
69
+ describe("an implicit release of a project with no components", () => {
70
+ test(
71
+ "two cuts of one commit, in two clones, give byte-identical build output per environment and one release digest",
72
+ () => {
73
+ const root = mkdtempSync(join(tmpdir(), "chant-release-implicit-"));
74
+ try {
75
+ const a = join(root, "a");
76
+ const b = join(root, "b");
77
+ mkdirSync(a);
78
+ writeProject(a);
79
+ git(a, "init", "-q", "-b", "main");
80
+ git(a, "config", "user.email", "t@chant.dev");
81
+ git(a, "config", "user.name", "T");
82
+ git(a, "add", ".");
83
+ git(a, "commit", "-q", "-m", "init");
84
+ const sha = git(a, "rev-parse", "HEAD");
85
+ // Each clone has a bare remote of its own holding main, so the commit is on trunk and pushed (#4027).
86
+ git(root, "init", "-q", "--bare", "-b", "main", "a.git");
87
+ git(a, "remote", "add", "origin", join(root, "a.git"));
88
+ git(a, "push", "-q", "origin", "main");
89
+ git(a, "fetch", "-q", "origin");
90
+ git(root, "clone", "-q", join(root, "a.git"), b);
91
+ git(root, "init", "-q", "--bare", "-b", "main", "b.git");
92
+ git(b, "remote", "set-url", "origin", join(root, "b.git"));
93
+ git(b, "push", "-q", "origin", "main");
94
+ git(b, "fetch", "-q", "origin");
95
+ for (const d of [a, b]) git(d, "tag", `ci/green/${sha}`);
96
+ for (const d of [a, b]) symlinkSync(join(REPO, "node_modules"), join(d, "node_modules"), "dir");
97
+
98
+ const cuts = [a, b].map((d, i) => {
99
+ const r = chant(d, "release", "cut", ...(i === 0 ? ["v1"] : []), "--params-file", "params.{env}.json", "--yes", "--json");
100
+ expect(r.code, r.stderr).toBe(0);
101
+ return JSON.parse(r.stdout);
102
+ });
103
+ const [first, second] = cuts;
104
+ expect(second.release.releaseDigest).toBe(first.release.releaseDigest);
105
+ expect(second.release).toEqual(first.release);
106
+ expect(first.stored && second.stored).toBe(true);
107
+
108
+ const release = first.release;
109
+ expect(release.commit).toBe(sha);
110
+ expect(release.artifactDigests).toEqual([]);
111
+ expect(release.targets.map((t: { env: string }) => t.env)).toEqual(["dev", "prod"]);
112
+ for (const t of release.targets) {
113
+ expect(t.templates.map((x: { component: unknown; path: string }) => [x.component, x.path])).toEqual([
114
+ [null, "dist/build.yaml"],
115
+ [null, "src/dist/ops/deploy/op.json"],
116
+ ]);
117
+ expect(t.params).toEqual([{ name: "replicas", value: t.env === "prod" ? 3 : 1, source: "params-file" }]);
118
+ }
119
+ // The parameter reaches the output, so the two environments' manifests differ.
120
+ expect(release.targets[0].templates[0].digest).not.toBe(release.targets[1].templates[0].digest);
121
+ // Nothing was written into the member's directory by the cut.
122
+ expect(existsSync(join(a, "dist")) || existsSync(join(a, "src", "dist"))).toBe(false);
123
+
124
+ // Each clone's stored bytes, written out per environment, are the same bytes.
125
+ for (const env of ["dev", "prod"]) {
126
+ const [fromA, fromB] = [a, b].map((d) => {
127
+ const ref = d === a ? "v1" : release.releaseDigest;
128
+ const r = chant(d, "release", "export", ref, "--env", env, "-o", `out/${env}`);
129
+ expect(r.code, r.stderr).toBe(0);
130
+ return filesUnder(join(d, "out", env));
131
+ });
132
+ expect([...fromA.keys()].sort()).toEqual(["dist/build.yaml", "src/dist/ops/deploy/op.json"]);
133
+ expect([...fromB.keys()].sort()).toEqual([...fromA.keys()].sort());
134
+ for (const [path, bytes] of fromA) {
135
+ expect(fromB.get(path)!.equals(bytes), `${env} ${path}`).toBe(true);
136
+ const recorded = release.targets.find((t: { env: string }) => t.env === env).templates.find((x: { path: string }) => x.path === path);
137
+ expect(contentDigest(bytes.toString("utf-8"))).toBe(recorded.digest);
138
+ }
139
+ expect(fromA.get("dist/build.yaml")!.toString("utf-8")).toContain(`replicas: ${env === "prod" ? 3 : 1}`);
140
+ }
141
+
142
+ // `release list --json` shows it as it shows any release.
143
+ const list = chant(a, "release", "list", "--json");
144
+ expect(list.code, list.stderr).toBe(0);
145
+ const doc = JSON.parse(list.stdout);
146
+ contract(releasesSchema).expectValid(doc);
147
+ expect(doc.releases).toHaveLength(1);
148
+ expect(doc.releases[0]).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1", artifactDigests: [], environments: [] });
149
+
150
+ // Promote has no component to deploy, and says what to run instead.
151
+ const promote = chant(a, "components", "promote", "--from", "dev", "--to", "prod", "--release", "v1");
152
+ expect(promote.code).toBe(1);
153
+ expect(promote.stderr).toMatch(/release v1 is of a project with no components/);
154
+ expect(promote.stderr).toMatch(/chant release export v1 --env prod/);
155
+ } finally {
156
+ rmSync(root, { recursive: true, force: true });
157
+ }
158
+ },
159
+ 480_000,
160
+ );
161
+ });
@@ -0,0 +1,185 @@
1
+ /**
2
+ * A commit, a release, a release gate and a deploy record join through the
3
+ * release digest (#4043). Everything runs in a temp repo with no remote, so
4
+ * nothing is pushed: a green tag, a real cut, a real `components promote
5
+ * --release` that stops at its gate, a real `chant approve`, and the promote
6
+ * that follows. Only the capability registry is replaced, so no deploy step
7
+ * reaches a cloud.
8
+ */
9
+
10
+ import { afterEach, describe, expect, test, vi } from "vitest";
11
+ import { withTestDir } from "@intentius/chant-test-utils";
12
+ import { spawnSync } from "node:child_process";
13
+ import { writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { CapabilityRegistry, type DeployContext } from "../../components/capability";
16
+ import type { DriverComponent } from "../../components/driver";
17
+ import type { ParsedArgs } from "../registry";
18
+ import { appendReleaseRecord, readReleaseLedger } from "../../lifecycle/release-ledger";
19
+ import { readGateLedger } from "../../lifecycle/gate-ledger";
20
+ import { cutRelease } from "../../lifecycle/release-cut";
21
+ import { joinRelease, type ReleaseJoinStart } from "../../lifecycle/release-join";
22
+ import { resetGateOrigin } from "../../lifecycle/gate-origin";
23
+ import type { ReleaseArtifact } from "../../lifecycle/release-store";
24
+ import type { CompiledTarget, TargetCompiler } from "../../components/release-compile";
25
+
26
+ const DIGEST = `sha256:${"a".repeat(64)}`;
27
+
28
+ const api: DriverComponent = {
29
+ name: "api",
30
+ deploy: [
31
+ { phase: "Publish", steps: [{ kind: "publish-image", from: "archive:api.tar" }] },
32
+ { phase: "Apply", steps: [{ kind: "gate", gate: "prod-release" }, { kind: "apply", template: "dist/api.template.json", imageRef: "@Publish.digest" }] },
33
+ ],
34
+ };
35
+
36
+ vi.mock("../../config", async () => {
37
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
38
+ return { ...actual, loadChantConfig: async () => ({ config: {} }) };
39
+ });
40
+ vi.mock("../../components/cli-support", () => ({
41
+ isComponentList: () => false,
42
+ resolveComponentTargets: async () => ({ success: true, targets: [api] }),
43
+ }));
44
+ vi.mock("../../components/fan-out-support", () => ({
45
+ fanOutRegistry: async () => {
46
+ const registry = new CapabilityRegistry();
47
+ registry.register({ kind: "publish-image", async run(_ctx: DeployContext) { return { digest: DIGEST, uri: `reg/api@${DIGEST}` }; } } as never);
48
+ registry.register({ kind: "apply", async run() { return {}; } } as never);
49
+ return registry;
50
+ },
51
+ }));
52
+ vi.mock("../../op/discover", () => ({ discoverOps: async () => ({ errors: [], ops: new Map() }) }));
53
+ vi.mock("./run", () => ({ GATED_EXIT_CODE: 3 }));
54
+
55
+ const { runComponentsPromote, runComponentsRedeploy } = await import("./promote");
56
+ const { recordGateApproval } = await import("./approve");
57
+
58
+ function git(dir: string, ...args: string[]): string {
59
+ const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
60
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
61
+ return r.stdout.trim();
62
+ }
63
+
64
+ /** A compiler that compiles each environment to its own template and builds one image. */
65
+ const compiler: TargetCompiler = {
66
+ async compile(env): Promise<CompiledTarget> {
67
+ return {
68
+ env,
69
+ params: [],
70
+ components: ["api"],
71
+ buildSteps: { api: [{ kind: "docker-build", context: "api", into: "api.tar" }] },
72
+ buildInputs: { api: ["api"] },
73
+ componentFiles: { api: "api.component.ts" },
74
+ templates: [{ component: "api", path: "dist/api.template.json", content: `{"env":"${env}"}\n` }],
75
+ };
76
+ },
77
+ async buildArtifacts(_env, components): Promise<ReleaseArtifact[]> {
78
+ return components.map((c) => ({ component: c, kind: "image" as const, path: `${c}.tar`, digest: DIGEST }));
79
+ },
80
+ };
81
+
82
+ function args(overrides: Partial<ParsedArgs>): ParsedArgs {
83
+ return {
84
+ command: "components", path: "promote", format: "", fix: false, watch: false, verbose: false, help: false, live: false,
85
+ migrateFrom: "staging", migrateTo: "prod", actor: "bob", runId: "run-9", ...overrides,
86
+ } as ParsedArgs;
87
+ }
88
+ const ctx = (a: Partial<ParsedArgs>) => ({ args: args(a), plugins: [] }) as never;
89
+
90
+ describe("commit, release, gate and deploy record join through the release digest (#4043)", () => {
91
+ const cwd = process.cwd();
92
+ afterEach(() => {
93
+ process.chdir(cwd);
94
+ resetGateOrigin();
95
+ vi.restoreAllMocks();
96
+ });
97
+
98
+ test(
99
+ "a cut, a gated promote, an approval and a redeploy join from the commit, the release, the gate and the deploy",
100
+ async () => {
101
+ vi.spyOn(console, "error").mockImplementation(() => {});
102
+ vi.spyOn(console, "log").mockImplementation(() => {});
103
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
104
+ await withTestDir(async (dir) => {
105
+ git(dir, "init", "-q", "-b", "main");
106
+ git(dir, "config", "user.email", "t@chant.dev");
107
+ git(dir, "config", "user.name", "T");
108
+ writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev", "staging", "prod"] };\n`);
109
+ writeFileSync(join(dir, "api.component.ts"), "export const api = {};\n");
110
+ git(dir, "add", ".");
111
+ git(dir, "commit", "-q", "-m", "init");
112
+ const sha = git(dir, "rev-parse", "HEAD");
113
+ git(dir, "tag", `ci/green/${sha}`, sha);
114
+ process.chdir(dir);
115
+
116
+ // The release is cut from the green commit.
117
+ const cut = await cutRelease({ cwd: dir, compiler, label: "v1.0.0", actor: "alice", env: {}, now: () => new Date("2026-10-16T10:00:00.000Z") });
118
+ const releaseDigest = cut.release.releaseDigest;
119
+
120
+ // Staging is what prod promotes from. It is a plain deploy of the same commit.
121
+ await appendReleaseRecord(
122
+ { component: "api", env: "staging", digest: DIGEST, gitSha: sha, runId: "run-1", timestamp: "2026-10-16T10:30:00.000Z", actor: "ci" },
123
+ { cwd: dir },
124
+ );
125
+
126
+ // The promote stops at the release gate and leaves a pending fact for the release.
127
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(3);
128
+ const afterStop = await readGateLedger("release", { cwd: dir });
129
+ expect(afterStop.pending).toEqual([expect.objectContaining({ gate: "prod-release", environment: "prod", releaseDigest })]);
130
+ expect((await readReleaseLedger("prod", { cwd: dir })).records).toEqual([]);
131
+
132
+ // A person approves it, and the promote then deploys.
133
+ const approved = await recordGateApproval("release", "prod-release", { origin: "cli", actor: "alice", cwd: dir, environment: "prod" });
134
+ expect(approved.ok).toBe(true);
135
+ expect(await runComponentsPromote(ctx({ release: "v1.0.0" }))).toBe(0);
136
+ const [deploy] = (await readReleaseLedger("prod", { cwd: dir })).records;
137
+ expect(deploy).toMatchObject({ env: "prod", component: "api", releaseDigest, approver: "alice", admission: { op: "release", gate: "prod-release" } });
138
+
139
+ // A redeploy puts the same deploy back; it stops at the component's own gate first.
140
+ const redeployArgs = { path: "redeploy", component: "api", migrateTo: undefined, migrateFrom: undefined, extraPositional: "prod", runId: "run-10" };
141
+ const stopped = await runComponentsRedeploy(ctx(redeployArgs));
142
+ expect(stopped).toBe(3);
143
+ const gateOp = (await readGateLedger("api", { cwd: dir })).pending.at(-1)!;
144
+ expect((await recordGateApproval(gateOp.op, gateOp.gate, { origin: "cli", actor: "alice", cwd: dir, environment: "prod" })).ok).toBe(true);
145
+ expect(await runComponentsRedeploy(ctx(redeployArgs))).toBe(0);
146
+ const prodRecords = (await readReleaseLedger("prod", { cwd: dir })).records;
147
+ expect(prodRecords).toHaveLength(2);
148
+ expect(prodRecords[1]).toMatchObject({ releaseDigest, redeploys: { env: "prod", runId: deploy.runId } });
149
+
150
+ // From each starting point the same release, gate and deploys come back.
151
+ const resolution = (await readGateLedger("release", { cwd: dir })).resolutions[0];
152
+ const pending = (await readGateLedger("release", { cwd: dir })).pending[0];
153
+ const starts: Array<[string, ReleaseJoinStart]> = [
154
+ ["commit", { commit: sha }],
155
+ ["commit prefix", { commit: sha.slice(0, 10) }],
156
+ ["release label", { release: "v1.0.0" }],
157
+ ["release digest", { release: releaseDigest }],
158
+ ["deploy record", { deploy: deploy }],
159
+ ["gate resolution", { gate: resolution }],
160
+ ["pending fact", { gate: pending }],
161
+ ["deploy record without releaseDigest, by its admission", { deploy: { admission: deploy.admission } }],
162
+ ];
163
+ for (const [name, from] of starts) {
164
+ const joined = await joinRelease(from, { cwd: dir });
165
+ expect(joined.releases, name).toHaveLength(1);
166
+ const [one] = joined.releases;
167
+ expect(one.releaseDigest, name).toBe(releaseDigest);
168
+ expect(one.commit, name).toBe(sha);
169
+ expect(one.label, name).toBe("v1.0.0");
170
+ expect(one.gates.resolutions.map((r) => [r.gate, r.resolvedBy, r.environment]), name).toEqual([["prod-release", "alice", "prod"]]);
171
+ expect(one.gates.pending.map((p) => p.planDigest), name).toEqual([resolution.planDigest]);
172
+ expect(one.deploys.map((d) => [d.env, d.runId]), name).toEqual([["prod", deploy.runId], ["prod", "run-10"]]);
173
+ // The deploy names the gate's resolution by address, and the join finds that very resolution.
174
+ expect(one.deploys[0].admission).toMatchObject({ op: "release", gate: one.gates.resolutions[0].gate, resolvedAt: one.gates.resolutions[0].timestamp });
175
+ }
176
+
177
+ // An unknown commit and a deploy that came from no release join to nothing, with a reason.
178
+ expect(await joinRelease({ commit: "f".repeat(40) }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/no release was cut/) });
179
+ expect(await joinRelease({ deploy: { releaseDigest: undefined } }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/not made from one/) });
180
+ expect(await joinRelease({ commit: "abc" }, { cwd: dir })).toMatchObject({ releases: [], reason: expect.stringMatching(/at least 7/) });
181
+ });
182
+ },
183
+ 240_000,
184
+ );
185
+ });
@@ -0,0 +1,295 @@
1
+ /**
2
+ * `chant release publish` with targets, end to end (#4047): a member that
3
+ * declares an npm target of two packages and an OCI target is cut, then
4
+ * published to stand-ins only, a fake `npm` first on PATH and a registry fake
5
+ * on 127.0.0.1 that this test starts and closes. The publish is a dry run
6
+ * without --yes; with --yes and a CI OIDC token it stages each package under
7
+ * next with --provenance and the image by digest, then moves latest and the
8
+ * tag (#4048); no credential appears in its output or on `chant/lifecycle`;
9
+ * and a checkout that moved past the release's commit is refused. A publish
10
+ * that fails part way is resumed from a fresh clone, as a CI job would, from
11
+ * the record it pushed, and publishes nothing again.
12
+ *
13
+ * #4049: the first release is cut signed with a throwaway key the signers
14
+ * file lists, and published as CI publishes it: each package carries its
15
+ * SBOM, the image gets its SBOM and the release's provenance statement as
16
+ * referrers, and the fake npm records `npm audit signatures` run over what
17
+ * was published. The second is published unsigned under --allow-unsigned.
18
+ */
19
+
20
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
21
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
22
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { pathToFileURL } from "node:url";
26
+ import { REPO } from "../../workspace/__fixtures__/contract-repo";
27
+ import { readFakeNpmRegistry, startFakeRegistry, writeFakeNpm, writeLockfile, writeOciLayout, writePackage, type FakeRegistry } from "../../lifecycle/__fixtures__/publish-fakes";
28
+ import { hasSshKeygen } from "../../workspace/trust/test-repo";
29
+
30
+ const CHANT = [process.execPath, "--import", pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href, join(REPO, "packages", "core", "src", "cli", "main.ts")];
31
+ const CI_VARS = [
32
+ "CI",
33
+ "GITHUB_ACTIONS",
34
+ "GITHUB_RUN_ID",
35
+ "GITHUB_SHA",
36
+ "GITHUB_ACTOR",
37
+ "GITHUB_REPOSITORY",
38
+ "GITHUB_SERVER_URL",
39
+ "GITHUB_API_URL",
40
+ "GITHUB_TOKEN",
41
+ "ACTIONS_ID_TOKEN_REQUEST_URL",
42
+ "ACTIONS_ID_TOKEN_REQUEST_TOKEN",
43
+ "GITLAB_CI",
44
+ "CI_PIPELINE_ID",
45
+ "GITLAB_USER_LOGIN",
46
+ "CHANT_ENV",
47
+ "NODE_AUTH_TOKEN",
48
+ "OCI_USERNAME",
49
+ "OCI_PASSWORD",
50
+ ];
51
+
52
+ /** Run chant without blocking this process, so the registry fake in it can answer. */
53
+ function chant(cwd: string, args: string[], extra: Record<string, string> = {}): Promise<{ code: number; stdout: string; stderr: string }> {
54
+ const env: NodeJS.ProcessEnv = { ...process.env, USER: "tester" };
55
+ for (const k of CI_VARS) delete env[k];
56
+ Object.assign(env, extra);
57
+ return new Promise((resolve) => {
58
+ const child = spawn(CHANT[0]!, [...CHANT.slice(1), ...args], { cwd, env });
59
+ let stdout = "";
60
+ let stderr = "";
61
+ child.stdout.on("data", (c: Buffer) => (stdout += c.toString()));
62
+ child.stderr.on("data", (c: Buffer) => (stderr += c.toString()));
63
+ child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
64
+ });
65
+ }
66
+
67
+ function git(cwd: string, ...args: string[]): string {
68
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
69
+ if (r.status !== 0) throw new Error(r.stderr);
70
+ return r.stdout.trim();
71
+ }
72
+
73
+ const BUILD = `mkdir -p dist && printf '{"env":"%s"}' "$CHANT_ENV" > dist/api.template.json`;
74
+ const NPM_TOKEN = "npm_e2eSecretToken0042";
75
+ const OCI_PASSWORD = "oci-e2e-password-0042";
76
+ const REGISTRY_TOKEN = "registry-e2e-token-0042";
77
+
78
+ let reg: FakeRegistry;
79
+ let root: string;
80
+ beforeAll(async () => {
81
+ reg = await startFakeRegistry({ username: "robot", password: OCI_PASSWORD, token: REGISTRY_TOKEN });
82
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-release-publish-targets-")));
83
+ });
84
+ afterAll(async () => {
85
+ await reg.close();
86
+ rmSync(root, { recursive: true, force: true });
87
+ });
88
+
89
+ describe("chant release publish with targets", () => {
90
+ test.skipIf(!hasSshKeygen)(
91
+ "a dry run by default; with --yes publishes a signed release's two npm packages in order with --provenance, audits their signatures, and an image by digest with its SBOM and provenance; prints no credential, and refuses a moved checkout",
92
+ async () => {
93
+ const dir = join(root, "work");
94
+ const remote = join(root, "origin.git");
95
+ git(root, "init", "-q", "--bare", "-b", "main", remote);
96
+ git(root, "init", "-q", "-b", "main", dir);
97
+ git(dir, "config", "user.email", "t@chant.dev");
98
+ git(dir, "config", "user.name", "T");
99
+ const publish = [
100
+ { type: "npm", packages: ["packages/core", "packages/cli"], access: "public" },
101
+ { type: "oci", image: "image", repository: `${reg.host}/acme/api`, usernameEnv: "OCI_USERNAME", passwordEnv: "OCI_PASSWORD" },
102
+ ];
103
+ writeFileSync(join(dir, "chant.config.ts"), `export default { environments: ["dev"], release: { publish: ${JSON.stringify(publish)} } };\n`);
104
+ writeFileSync(
105
+ join(dir, "api.component.ts"),
106
+ `export const api = { name: "api", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "dist/api.template.json" }] }] };\n`,
107
+ );
108
+ writeFileSync(join(dir, ".gitignore"), "dist/\n");
109
+ writePackage(join(dir, "packages", "core"), "@acme/core", "1.0.0");
110
+ writePackage(join(dir, "packages", "cli"), "@acme/cli", "1.0.0");
111
+ writeLockfile(dir);
112
+ const { digest } = writeOciLayout(join(dir, "image"));
113
+ // #4049: alice's throwaway key, which the signers file lists.
114
+ const key = join(root, "alice-key");
115
+ execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "alice", "-f", key]);
116
+ const alicePub = readFileSync(`${key}.pub`, "utf-8").trim().split(" ").slice(0, 2).join(" ");
117
+ mkdirSync(join(dir, ".chant"), { recursive: true });
118
+ writeFileSync(join(dir, ".chant", "allowed_signers"), `alice ${alicePub}\n`);
119
+ git(dir, "add", ".");
120
+ git(dir, "commit", "-q", "-m", "init");
121
+ git(dir, "remote", "add", "origin", remote);
122
+ git(dir, "push", "-q", "origin", "main");
123
+ git(dir, "fetch", "-q", "origin");
124
+ const sha = git(dir, "rev-parse", "HEAD");
125
+ git(dir, "tag", `ci/green/${sha}`);
126
+
127
+ const log = join(root, "npm.log");
128
+ const bin = join(root, "bin");
129
+ writeFakeNpm(bin, log);
130
+ const path = { PATH: `${bin}:${process.env.PATH}` };
131
+ const calls = () => (existsSync(log) ? readFileSync(log, "utf-8").trim().split("\n").filter(Boolean).map((l) => JSON.parse(l) as { args: string[]; tokenSet: boolean }) : []);
132
+
133
+ const dryCut = await chant(dir, ["release", "cut", "v1", "--build", BUILD], path);
134
+ expect(dryCut.code, dryCut.stderr).toBe(0);
135
+ expect(dryCut.stdout).toContain("chant release publish v1: npm (2 packages) to npm the registry npm's config names");
136
+ expect(dryCut.stdout).toContain(`chant release publish v1: oci ${reg.host}/acme/api to ${reg.host}/acme/api`);
137
+ const cut = await chant(dir, ["release", "cut", "v1", "--build", BUILD, "--sign", key, "--yes"], path);
138
+ expect(cut.code, cut.stderr).toBe(0);
139
+ expect(cut.stderr).toContain("signed by alice");
140
+
141
+ const secrets = { NODE_AUTH_TOKEN: NPM_TOKEN, OCI_USERNAME: "robot", OCI_PASSWORD };
142
+ const oidc = { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://token.example", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "oidc-request-token-0042" };
143
+
144
+ const dry = await chant(dir, ["release", "publish", "v1"], { ...path, ...secrets, ...oidc });
145
+ expect(dry.code, dry.stderr).toBe(0);
146
+ expect(dry.stdout).toContain("What would be published where:");
147
+ expect(dry.stdout).toContain(" npm (2 packages), with --provenance:\n @acme/core@1.0.0 to the registry npm's config names, dist-tag next, then latest, carrying sbom.spdx.json\n @acme/cli@1.0.0 to");
148
+ expect(dry.stdout).toContain(` image (tag v1) to ${reg.host}/acme/api@${digest}`);
149
+ expect(dry.stderr).toContain("Dry run: nothing was published. Run again with --yes to publish: chant release publish v1 --yes");
150
+ expect(calls()).toEqual([]);
151
+ expect(reg.requests).toEqual([]);
152
+
153
+ const pub = await chant(dir, ["release", "publish", "v1", "--yes"], { ...path, ...secrets, ...oidc });
154
+ expect(pub.code, pub.stderr).toBe(0);
155
+ expect(pub.stderr).toContain("signed by alice");
156
+ expect(calls().map((c) => c.args)).toEqual([
157
+ ["view", "@acme/core@1.0.0", "version"],
158
+ ["publish", join(dir, "packages/core"), "--tag", "next", "--access", "public", "--provenance"],
159
+ ["view", "@acme/cli@1.0.0", "version"],
160
+ ["publish", join(dir, "packages/cli"), "--tag", "next", "--access", "public", "--provenance"],
161
+ ["dist-tag", "add", "@acme/core@1.0.0", "latest"],
162
+ ["dist-tag", "add", "@acme/cli@1.0.0", "latest"],
163
+ // #4049: in CI, what was published is installed in an empty directory and its signatures and attestations audited.
164
+ ["install", "--ignore-scripts", "--no-audit", "--no-fund"],
165
+ ["audit", "signatures"],
166
+ ]);
167
+ expect(pub.stderr).toContain("2 packages have verified attestations");
168
+ expect(calls().every((c) => c.tokenSet)).toBe(true);
169
+ expect(reg.manifests.has(digest)).toBe(true);
170
+ expect(reg.manifests.has("v1")).toBe(true);
171
+ expect(pub.stderr).toContain(`published: image (tag v1) to ${reg.host}/acme/api@${digest}`);
172
+ expect(pub.stderr).toContain("published: @acme/core@1.0.0 to the registry npm's config names, dist-tag latest, carrying sbom.spdx.json");
173
+ expect(pub.stderr).toMatch(/published: sbom\.spdx\.json \(SBOM\) to .*@sha256:[0-9a-f]{64}, a referrer of the image/);
174
+ expect(pub.stderr).toMatch(/published: release\.intoto\.json \(provenance\) to .*@sha256:[0-9a-f]{64}, a referrer of the image/);
175
+ expect(pub.stderr).toContain("Published v1 to 2 target(s)");
176
+ // The image's referrers: each names the image as its subject, and the provenance statement carries the cut's signature.
177
+ const referrers = [...reg.manifests.entries()]
178
+ .filter(([k]) => k.startsWith("sha256:") && k !== digest)
179
+ .map(([, m]) => JSON.parse(m.bytes.toString("utf-8")) as { artifactType: string; subject: { digest: string }; layers: Array<{ digest: string }> });
180
+ expect(referrers.map((m) => m.artifactType).sort()).toEqual(["application/spdx+json", "application/vnd.in-toto+json"]);
181
+ expect(referrers.every((m) => m.subject.digest === digest)).toBe(true);
182
+ const statement = JSON.parse(reg.blobs.get(referrers.find((m) => m.artifactType === "application/vnd.in-toto+json")!.layers[0]!.digest)!.toString("utf-8"));
183
+ expect(statement).toMatchObject({
184
+ _type: "https://in-toto.io/Statement/v1",
185
+ subject: [{ digest: { sha256: digest.slice(7) } }],
186
+ predicateType: "https://intentius.io/chant/release/v1",
187
+ predicate: { label: "v1", cut: { actor: "alice", namespace: "chant-release", signature: expect.stringMatching(/^-----BEGIN SSH SIGNATURE-----/) }, provenance: { kind: "oidc", ci: "github" } },
188
+ });
189
+
190
+ // The release verifies offline against the signers file at its commit.
191
+ const verified = await chant(dir, ["release", "verify", "v1", "--json"]);
192
+ expect(verified.code, verified.stderr).toBe(0);
193
+ expect(JSON.parse(verified.stdout)).toMatchObject({ label: "v1", status: "verified", verifiedBy: { actor: "alice" } });
194
+
195
+ // No credential in the output, or anywhere on chant/lifecycle.
196
+ const lifecycle = git(dir, "log", "-p", "--all");
197
+ for (const secret of [NPM_TOKEN, OCI_PASSWORD, REGISTRY_TOKEN, "oidc-request-token-0042"]) {
198
+ for (const text of [dry.stdout, dry.stderr, pub.stdout, pub.stderr, lifecycle]) expect(text).not.toContain(secret);
199
+ }
200
+ expect(pub.stderr).toContain("npm notice token [redacted]");
201
+
202
+ // From a workstation the publish warns that it carries no provenance, and the plan offers none.
203
+ const asJson = await chant(dir, ["release", "publish", "v1", "--json"], { ...path, ...secrets });
204
+ expect(asJson.code, asJson.stderr).toBe(0);
205
+ expect(asJson.stderr).toContain("this publish runs without OIDC");
206
+ const doc = JSON.parse(asJson.stdout);
207
+ expect(doc).toMatchObject({ dryRun: true, label: "v1", provenance: { oidc: false } });
208
+ expect(doc.targets.map((t: { kind: string }) => t.kind)).toEqual(["npm", "oci"]);
209
+
210
+ // A checkout past the release's commit is refused before anything runs.
211
+ git(dir, "commit", "-q", "--allow-empty", "-m", "later");
212
+ const moved = await chant(dir, ["release", "publish", "v1", "--yes"], { ...path, ...secrets, ...oidc });
213
+ expect(moved.code).toBe(1);
214
+ expect(moved.stderr).toMatch(/\[release-head-mismatch\]/);
215
+ expect(calls()).toHaveLength(8);
216
+ },
217
+ 240_000,
218
+ );
219
+ test(
220
+ "a publish that failed at the image is resumed from a fresh clone with the record it pushed, and stages no package again",
221
+ async () => {
222
+ const work = join(root, "resume");
223
+ const remote = join(root, "resume-origin.git");
224
+ git(root, "init", "-q", "--bare", "-b", "main", remote);
225
+ git(root, "init", "-q", "-b", "main", work);
226
+ git(work, "config", "user.email", "t@chant.dev");
227
+ git(work, "config", "user.name", "T");
228
+ const publish = [
229
+ { type: "npm", packages: ["packages/core", "packages/cli"] },
230
+ { type: "oci", image: "image", repository: `${reg.host}/acme/resume` },
231
+ ];
232
+ writeFileSync(join(work, "chant.config.ts"), `export default { environments: ["dev"], release: { publish: ${JSON.stringify(publish)} } };\n`);
233
+ writeFileSync(
234
+ join(work, "api.component.ts"),
235
+ `export const api = { name: "api", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", template: "dist/api.template.json" }] }] };\n`,
236
+ );
237
+ writeFileSync(join(work, ".gitignore"), "dist/\n");
238
+ writePackage(join(work, "packages", "core"), "@acme/rcore", "3.0.0");
239
+ writePackage(join(work, "packages", "cli"), "@acme/rcli", "3.0.0");
240
+ writeLockfile(work);
241
+ const { digest } = writeOciLayout(join(work, "image"), "resume layer");
242
+ git(work, "add", ".");
243
+ git(work, "commit", "-q", "-m", "init");
244
+ git(work, "remote", "add", "origin", remote);
245
+ git(work, "push", "-q", "origin", "main");
246
+ git(work, "fetch", "-q", "origin");
247
+ git(work, "tag", `ci/green/${git(work, "rev-parse", "HEAD")}`);
248
+
249
+ const log = join(root, "resume-npm.log");
250
+ const bin = join(root, "resume-bin");
251
+ writeFakeNpm(bin, log);
252
+ const path = { PATH: `${bin}:${process.env.PATH}` };
253
+ const calls = () => (existsSync(log) ? readFileSync(log, "utf-8").trim().split("\n").filter(Boolean).map((l) => (JSON.parse(l) as { args: string[] }).args) : []);
254
+ const cut = await chant(work, ["release", "cut", "r3", "--build", BUILD, "--yes"], path);
255
+ expect(cut.code, cut.stderr).toBe(0);
256
+
257
+ // The registry refuses the password: npm staged, the image did not, and no tag moved. These packages
258
+ // are new, and npm makes a new package's first version latest whatever tag it was published under.
259
+ const failed = await chant(work, ["release", "publish", "r3", "--allow-unsigned", "--yes"], { ...path, OCI_USERNAME: "robot", OCI_PASSWORD: "wrong-password-0042" });
260
+ expect(failed.code).toBe(1);
261
+ expect(failed.stderr).toMatch(/\[publish-oci-failed\]/);
262
+ expect(failed.stderr).toContain("nothing is visible yet; staged: npm (2 packages)");
263
+ expect(failed.stderr).toContain("Run the same command again to resume; what went out is not published again: chant release publish r3 --allow-unsigned --yes");
264
+ expect(failed.stderr).not.toContain("wrong-password-0042");
265
+ expect(readFakeNpmRegistry(log).packages["@acme/rcore"]!.distTags).toEqual({ next: "3.0.0", latest: "3.0.0" });
266
+ expect(calls().filter((a) => a[0] === "dist-tag")).toEqual([]);
267
+ expect([...reg.manifests.keys()].filter((k) => k === "r3")).toEqual([]);
268
+
269
+ // A CI job resumes on a fresh clone of the release's commit: the record came with chant/lifecycle.
270
+ const fresh = join(root, "resume-fresh");
271
+ git(root, "clone", "-q", remote, fresh);
272
+ git(fresh, "config", "user.email", "ci@chant.dev");
273
+ git(fresh, "config", "user.name", "CI");
274
+ const before = calls().length;
275
+ const dry = await chant(fresh, ["release", "publish", "r3", "--allow-unsigned"], path);
276
+ expect(dry.code, dry.stderr).toBe(0);
277
+ expect(dry.stdout).toContain("npm (2 packages) (staged by an earlier run; only revealed):");
278
+ const resumed = await chant(fresh, ["release", "publish", "r3", "--allow-unsigned", "--yes", "--json"], { ...path, OCI_USERNAME: "robot", OCI_PASSWORD });
279
+ expect(resumed.code, resumed.stderr).toBe(0);
280
+ expect(calls().slice(before)).toEqual([
281
+ ["dist-tag", "add", "@acme/rcore@3.0.0", "latest"],
282
+ ["dist-tag", "add", "@acme/rcli@3.0.0", "latest"],
283
+ ]);
284
+ expect(reg.manifests.has(digest)).toBe(true);
285
+ expect(reg.manifests.has("r3")).toBe(true);
286
+ const doc = JSON.parse(resumed.stdout);
287
+ expect(doc.resumed).toEqual([{ target: "npm (2 packages)", step: "staged" }]);
288
+ expect(doc.targets.map((t: { progress: string }) => t.progress)).toEqual(["staged", "none"]);
289
+ const record = git(fresh, "show", "chant/lifecycle:_releases/publishes.jsonl").split("\n").map((l) => JSON.parse(l) as { kind: string; step: string });
290
+ expect(record.map((l) => `${l.kind} ${l.step}`)).toEqual(["npm staged", "oci staged", "npm revealed", "oci revealed"]);
291
+ expect(git(fresh, "log", "-p", "--all")).not.toContain(OCI_PASSWORD);
292
+ },
293
+ 240_000,
294
+ );
295
+ });