@intentius/chant 0.124.28 → 0.124.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +15 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +40 -3
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +61 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +24 -2
  41. package/dist/cli/registry.d.ts +15 -3
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +25 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +22 -4
  67. package/dist/components/promote.d.ts +23 -0
  68. package/dist/components/promote.d.ts.map +1 -1
  69. package/dist/components/promote.js +60 -2
  70. package/dist/components/release-compile.d.ts +9 -0
  71. package/dist/components/release-compile.d.ts.map +1 -1
  72. package/dist/components/release-compile.js +9 -3
  73. package/dist/components/release-gate.d.ts +11 -1
  74. package/dist/components/release-gate.d.ts.map +1 -1
  75. package/dist/components/release-gate.js +10 -4
  76. package/dist/components/wave-plan.d.ts +5 -0
  77. package/dist/components/wave-plan.d.ts.map +1 -1
  78. package/dist/components/wave-plan.js +10 -2
  79. package/dist/config.d.ts +38 -0
  80. package/dist/config.d.ts.map +1 -1
  81. package/dist/config.js +2 -0
  82. package/dist/flag-params.d.ts +54 -0
  83. package/dist/flag-params.d.ts.map +1 -0
  84. package/dist/flag-params.js +130 -0
  85. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  86. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  87. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  88. package/dist/lifecycle/change-set-store.d.ts +2 -1
  89. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  90. package/dist/lifecycle/change-set-store.js +2 -1
  91. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  92. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  93. package/dist/lifecycle/delivery-metrics.js +145 -0
  94. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  95. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  96. package/dist/lifecycle/deploy-outcome.js +101 -0
  97. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  98. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  99. package/dist/lifecycle/gate-change-set.js +35 -3
  100. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  101. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/git.d.ts +30 -1
  103. package/dist/lifecycle/git.d.ts.map +1 -1
  104. package/dist/lifecycle/git.js +31 -5
  105. package/dist/lifecycle/index.d.ts +4 -0
  106. package/dist/lifecycle/index.d.ts.map +1 -1
  107. package/dist/lifecycle/index.js +4 -0
  108. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  109. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  110. package/dist/lifecycle/mixed-environment.js +154 -0
  111. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  112. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  113. package/dist/lifecycle/publish-attachments.js +179 -0
  114. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  115. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-credentials.js +69 -0
  117. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  118. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-declaration.js +83 -0
  120. package/dist/lifecycle/publish-forge.d.ts +53 -0
  121. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-forge.js +298 -0
  123. package/dist/lifecycle/publish-npm.d.ts +61 -0
  124. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-npm.js +242 -0
  126. package/dist/lifecycle/publish-oci.d.ts +64 -0
  127. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-oci.js +383 -0
  129. package/dist/lifecycle/publish-record.d.ts +66 -0
  130. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-record.js +84 -0
  132. package/dist/lifecycle/publish-targets.d.ts +45 -0
  133. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-targets.js +85 -0
  135. package/dist/lifecycle/release-cut.d.ts +48 -1
  136. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  137. package/dist/lifecycle/release-cut.js +116 -23
  138. package/dist/lifecycle/release-join.d.ts +95 -0
  139. package/dist/lifecycle/release-join.d.ts.map +1 -0
  140. package/dist/lifecycle/release-join.js +139 -0
  141. package/dist/lifecycle/release-publish.d.ts +312 -0
  142. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  143. package/dist/lifecycle/release-publish.js +335 -0
  144. package/dist/lifecycle/release-revocation.d.ts +80 -0
  145. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  146. package/dist/lifecycle/release-revocation.js +123 -0
  147. package/dist/lifecycle/release-store.d.ts +12 -2
  148. package/dist/lifecycle/release-store.d.ts.map +1 -1
  149. package/dist/lifecycle/release-store.js +4 -2
  150. package/dist/lifecycle/release-verify.d.ts +61 -0
  151. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  152. package/dist/lifecycle/release-verify.js +92 -0
  153. package/dist/lifecycle/release-workstation.d.ts +91 -0
  154. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  155. package/dist/lifecycle/release-workstation.js +180 -0
  156. package/dist/lifecycle/retention-policy.d.ts +45 -0
  157. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  158. package/dist/lifecycle/retention-policy.js +43 -0
  159. package/dist/lifecycle/retention.d.ts +185 -0
  160. package/dist/lifecycle/retention.d.ts.map +1 -0
  161. package/dist/lifecycle/retention.js +400 -0
  162. package/dist/plan-summary.d.ts.map +1 -1
  163. package/dist/plan-summary.js +8 -2
  164. package/dist/pr-loop.d.ts +7 -0
  165. package/dist/pr-loop.d.ts.map +1 -1
  166. package/dist/pr-loop.js +23 -4
  167. package/dist/pr-report-url.d.ts +82 -0
  168. package/dist/pr-report-url.d.ts.map +1 -0
  169. package/dist/pr-report-url.js +174 -0
  170. package/dist/provenance.d.ts +2 -2
  171. package/dist/provenance.d.ts.map +1 -1
  172. package/dist/run-event-store.d.ts +29 -2
  173. package/dist/run-event-store.d.ts.map +1 -1
  174. package/dist/run-event-store.js +50 -3
  175. package/dist/run-event-stream.d.ts +2 -1
  176. package/dist/run-event-stream.d.ts.map +1 -1
  177. package/dist/run-event-stream.js +5 -1
  178. package/dist/run-event.d.ts +19 -1
  179. package/dist/run-event.d.ts.map +1 -1
  180. package/dist/run-event.js +2 -0
  181. package/dist/wave-runtime.mjs +260 -229
  182. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  183. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  184. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  185. package/dist/workspace/change-set.schema.json +5 -1
  186. package/dist/workspace/check.schema.json +4 -2
  187. package/dist/workspace/checks/records.d.ts +9 -1
  188. package/dist/workspace/checks/records.d.ts.map +1 -1
  189. package/dist/workspace/checks/records.js +35 -1
  190. package/dist/workspace/checks.d.ts +1 -0
  191. package/dist/workspace/checks.d.ts.map +1 -1
  192. package/dist/workspace/checks.js +2 -1
  193. package/dist/workspace/ci-green.d.ts +8 -0
  194. package/dist/workspace/ci-green.d.ts.map +1 -1
  195. package/dist/workspace/ci-green.js +10 -3
  196. package/dist/workspace/declaration.d.ts +16 -0
  197. package/dist/workspace/declaration.d.ts.map +1 -1
  198. package/dist/workspace/declaration.js +26 -0
  199. package/dist/workspace/declaration.schema.json +64 -0
  200. package/dist/workspace/declared-kinds.d.ts +16 -0
  201. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  202. package/dist/workspace/declared-kinds.js +25 -0
  203. package/dist/workspace/delivery-metrics.d.ts +91 -0
  204. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  205. package/dist/workspace/delivery-metrics.js +217 -0
  206. package/dist/workspace/delivery-metrics.schema.json +519 -0
  207. package/dist/workspace/flags-read.d.ts +37 -0
  208. package/dist/workspace/flags-read.d.ts.map +1 -0
  209. package/dist/workspace/flags-read.js +103 -0
  210. package/dist/workspace/flags.d.ts +100 -0
  211. package/dist/workspace/flags.d.ts.map +1 -0
  212. package/dist/workspace/flags.js +98 -0
  213. package/dist/workspace/graph-cli.js +1 -1
  214. package/dist/workspace/intent-cli.d.ts +11 -2
  215. package/dist/workspace/intent-cli.d.ts.map +1 -1
  216. package/dist/workspace/intent-cli.js +54 -5
  217. package/dist/workspace/intent-trace.d.ts +219 -0
  218. package/dist/workspace/intent-trace.d.ts.map +1 -0
  219. package/dist/workspace/intent-trace.js +314 -0
  220. package/dist/workspace/intent.d.ts +30 -6
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/intent.js +141 -21
  223. package/dist/workspace/intent.schema.json +702 -6
  224. package/dist/workspace/overlap.d.ts +146 -0
  225. package/dist/workspace/overlap.d.ts.map +1 -0
  226. package/dist/workspace/overlap.js +276 -0
  227. package/dist/workspace/reason-codes.d.ts +2 -0
  228. package/dist/workspace/reason-codes.d.ts.map +1 -1
  229. package/dist/workspace/reason-codes.js +3 -0
  230. package/dist/workspace/records.d.ts +15 -2
  231. package/dist/workspace/records.d.ts.map +1 -1
  232. package/dist/workspace/records.js +32 -0
  233. package/dist/workspace/records.schema.json +21 -2
  234. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  235. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  236. package/dist/workspace/releases.d.ts +9 -0
  237. package/dist/workspace/releases.d.ts.map +1 -1
  238. package/dist/workspace/releases.js +9 -0
  239. package/dist/workspace/releases.schema.json +123 -1
  240. package/dist/workspace/run-event.schema.json +10 -3
  241. package/dist/workspace/status-gates.d.ts +8 -0
  242. package/dist/workspace/status-gates.d.ts.map +1 -1
  243. package/dist/workspace/status.d.ts +78 -0
  244. package/dist/workspace/status.d.ts.map +1 -1
  245. package/dist/workspace/status.js +170 -3
  246. package/dist/workspace/status.schema.json +198 -1
  247. package/dist/workspace/wip.d.ts.map +1 -1
  248. package/dist/workspace/wip.js +11 -3
  249. package/package.json +1 -1
  250. package/src/build-params.test.ts +46 -0
  251. package/src/build-params.ts +55 -1
  252. package/src/change-set-graph.ts +10 -2
  253. package/src/change-set.ts +14 -3
  254. package/src/cli/build-params-cli.ts +22 -9
  255. package/src/cli/commands/build.ts +6 -3
  256. package/src/cli/handlers/approve-release.test.ts +5 -1
  257. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  258. package/src/cli/handlers/approve.ts +47 -0
  259. package/src/cli/handlers/build.test.ts +15 -0
  260. package/src/cli/handlers/build.ts +17 -4
  261. package/src/cli/handlers/fan-out.test.ts +180 -1
  262. package/src/cli/handlers/fan-out.ts +38 -3
  263. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  264. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  265. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  266. package/src/cli/handlers/pr.test.ts +148 -1
  267. package/src/cli/handlers/pr.ts +139 -28
  268. package/src/cli/handlers/promote.test.ts +280 -2
  269. package/src/cli/handlers/promote.ts +210 -34
  270. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  271. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  272. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  273. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  274. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  275. package/src/cli/handlers/release.e2e.test.ts +146 -32
  276. package/src/cli/handlers/release.ts +415 -28
  277. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  278. package/src/cli/handlers/run-release.test.ts +55 -0
  279. package/src/cli/handlers/run.ts +69 -5
  280. package/src/cli/main.test.ts +1 -0
  281. package/src/cli/main.ts +61 -10
  282. package/src/cli/parse-args.ts +19 -2
  283. package/src/cli/registry.ts +15 -3
  284. package/src/cli/run-events.ts +5 -2
  285. package/src/components/auto-release.test.ts +9 -0
  286. package/src/components/auto-release.ts +3 -0
  287. package/src/components/capability.ts +9 -0
  288. package/src/components/driver.test.ts +24 -0
  289. package/src/components/driver.ts +36 -0
  290. package/src/components/fan-out-inputs.test.ts +85 -0
  291. package/src/components/fan-out-inputs.ts +192 -0
  292. package/src/components/fan-out-record.test.ts +10 -0
  293. package/src/components/fan-out-record.ts +19 -0
  294. package/src/components/fan-out-run.ts +48 -0
  295. package/src/components/fan-out.ts +4 -0
  296. package/src/components/pr-pipeline.ts +35 -4
  297. package/src/components/promote.ts +68 -2
  298. package/src/components/release-compile.ts +17 -3
  299. package/src/components/release-gate.ts +18 -4
  300. package/src/components/wave-plan.ts +15 -2
  301. package/src/config.ts +8 -0
  302. package/src/flag-params.test.ts +144 -0
  303. package/src/flag-params.ts +134 -0
  304. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  305. package/src/lifecycle/change-set-store.ts +2 -1
  306. package/src/lifecycle/delivery-metrics.ts +263 -0
  307. package/src/lifecycle/deploy-outcome.ts +147 -0
  308. package/src/lifecycle/gate-change-set.ts +37 -6
  309. package/src/lifecycle/gate-ledger.ts +2 -2
  310. package/src/lifecycle/git.ts +61 -8
  311. package/src/lifecycle/index.ts +4 -0
  312. package/src/lifecycle/mixed-environment.test.ts +97 -0
  313. package/src/lifecycle/mixed-environment.ts +204 -0
  314. package/src/lifecycle/publish-attachments.ts +196 -0
  315. package/src/lifecycle/publish-credentials.ts +84 -0
  316. package/src/lifecycle/publish-declaration.ts +95 -0
  317. package/src/lifecycle/publish-forge.ts +325 -0
  318. package/src/lifecycle/publish-npm.ts +259 -0
  319. package/src/lifecycle/publish-oci.ts +396 -0
  320. package/src/lifecycle/publish-record.ts +113 -0
  321. package/src/lifecycle/publish-resume.test.ts +168 -0
  322. package/src/lifecycle/publish-targets.test.ts +494 -0
  323. package/src/lifecycle/publish-targets.ts +100 -0
  324. package/src/lifecycle/release-cut.test.ts +74 -2
  325. package/src/lifecycle/release-cut.ts +158 -22
  326. package/src/lifecycle/release-flag.test.ts +112 -0
  327. package/src/lifecycle/release-join.test.ts +68 -0
  328. package/src/lifecycle/release-join.ts +182 -0
  329. package/src/lifecycle/release-publish.test.ts +225 -0
  330. package/src/lifecycle/release-publish.ts +568 -0
  331. package/src/lifecycle/release-revocation.ts +147 -0
  332. package/src/lifecycle/release-store.ts +15 -4
  333. package/src/lifecycle/release-verify.test.ts +110 -0
  334. package/src/lifecycle/release-verify.ts +120 -0
  335. package/src/lifecycle/release-workstation.test.ts +204 -0
  336. package/src/lifecycle/release-workstation.ts +216 -0
  337. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  338. package/src/lifecycle/retention-policy.ts +63 -0
  339. package/src/lifecycle/retention.test.ts +363 -0
  340. package/src/lifecycle/retention.ts +508 -0
  341. package/src/plan-summary.test.ts +20 -0
  342. package/src/plan-summary.ts +8 -1
  343. package/src/pr-loop.ts +25 -4
  344. package/src/pr-report-url.test.ts +131 -0
  345. package/src/pr-report-url.ts +209 -0
  346. package/src/provenance.ts +2 -2
  347. package/src/run-event-store.ts +63 -3
  348. package/src/run-event-stream.ts +4 -1
  349. package/src/run-event.ts +21 -1
  350. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  351. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  352. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  353. package/src/workspace/change-set.schema.json +5 -1
  354. package/src/workspace/check.schema.json +4 -2
  355. package/src/workspace/checks/records.ts +35 -1
  356. package/src/workspace/checks.ts +2 -1
  357. package/src/workspace/ci-green.ts +14 -3
  358. package/src/workspace/declaration.schema.json +64 -0
  359. package/src/workspace/declaration.test.ts +28 -0
  360. package/src/workspace/declaration.ts +38 -0
  361. package/src/workspace/declared-kinds.ts +40 -1
  362. package/src/workspace/delivery-metrics.schema.json +519 -0
  363. package/src/workspace/delivery-metrics.test.ts +236 -0
  364. package/src/workspace/delivery-metrics.ts +286 -0
  365. package/src/workspace/flags-read.ts +121 -0
  366. package/src/workspace/flags.test.ts +141 -0
  367. package/src/workspace/flags.ts +157 -0
  368. package/src/workspace/graph-cli.ts +1 -1
  369. package/src/workspace/intent-cli.ts +46 -5
  370. package/src/workspace/intent-trace.test.ts +173 -0
  371. package/src/workspace/intent-trace.ts +484 -0
  372. package/src/workspace/intent.schema.json +702 -6
  373. package/src/workspace/intent.ts +148 -25
  374. package/src/workspace/older-records.test.ts +412 -0
  375. package/src/workspace/overlap.test.ts +165 -0
  376. package/src/workspace/overlap.ts +340 -0
  377. package/src/workspace/read-contract.e2e.test.ts +2 -1
  378. package/src/workspace/reason-codes.test.ts +5 -0
  379. package/src/workspace/reason-codes.ts +3 -0
  380. package/src/workspace/record-assets.test.ts +2 -1
  381. package/src/workspace/records-contract.e2e.test.ts +2 -1
  382. package/src/workspace/records.schema.json +21 -2
  383. package/src/workspace/records.ts +36 -2
  384. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  385. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  386. package/src/workspace/releases.schema.json +123 -1
  387. package/src/workspace/releases.ts +11 -0
  388. package/src/workspace/run-event.schema.json +10 -3
  389. package/src/workspace/status-contract.test.ts +29 -0
  390. package/src/workspace/status-gates.ts +8 -0
  391. package/src/workspace/status-mixed.test.ts +152 -0
  392. package/src/workspace/status-revoked.test.ts +153 -0
  393. package/src/workspace/status.schema.json +198 -1
  394. package/src/workspace/status.ts +254 -3
  395. package/src/workspace/wip.ts +11 -2
@@ -1,10 +1,22 @@
1
1
  /**
2
2
  * `chant release` (#4026, ws-104): the release is what graduates.
3
3
  *
4
- * release cut [<label>] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
5
- * cut a release of this member from the checkout's HEAD, which must be green;
4
+ * release cut [<label>] [--yes] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
5
+ * a dry run of a cut of this member from the checkout's HEAD, which must be
6
+ * clean, on trunk and pushed, and green (#4027); `--yes` cuts, records,
7
+ * and tags the commit `release/<label>` locally;
6
8
  * --sign signs the cut with the actor's ssh key (#4031); `--src`, `-o` and
7
9
  * `--lexicon` shape the build of a project with no components (#4046)
10
+ * release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json]
11
+ * publish a cut release (#4027) to the targets release.publish declares
12
+ * (#4047): a dry run unless --yes; refuses a revoked one, one with no
13
+ * signature that verifies against .chant/allowed_signers unless
14
+ * --allow-unsigned (#4049), and with a target a checkout that is not
15
+ * the release's commit; warns with no OIDC provenance, and refuses
16
+ * under --require-provenance; attaches SBOMs and provenance (#4049)
17
+ * release verify <label|digest> [--json]
18
+ * check the release's signature offline against .chant/allowed_signers
19
+ * at the release's commit (#4049)
8
20
  * release list [--json]
9
21
  * every release of this member with its targets, its cuts and where it runs;
10
22
  * `--json` prints the read contract's `releases` document
@@ -13,18 +25,21 @@
13
25
  * release compile --env <env> [--build <command>] [--artifacts --component <a,b>] --json
14
26
  * internal: what the cut runs in a child process per environment
15
27
  *
16
- * The workstation UX for `cut` grows in #4027. A declaration whose
17
- * `identity.gates` names a `release` gate requires a signed cut by an actor
18
- * in its class (#4031).
28
+ * The workstation checks, the actor and the tag are in
29
+ * ../../lifecycle/release-workstation.ts, and the publish contract in
30
+ * ../../lifecycle/release-publish.ts. A declaration whose `identity.gates`
31
+ * names a `release` gate requires a signed cut by an actor in its class (#4031).
19
32
  */
20
33
 
34
+ import { execFileSync } from "node:child_process";
35
+ import { existsSync } from "node:fs";
21
36
  import { resolve } from "node:path";
22
37
  import { formatBold, formatError, formatSuccess, formatWarning } from "../format";
23
38
  import { parseParamFlags } from "../build-params-cli";
24
39
  import type { CommandContext } from "../registry";
25
40
 
26
41
  const USAGE =
27
- "chant release cut [<label>] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
42
+ "chant release cut [<label>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
28
43
 
29
44
  /** The build inputs `release cut` and `release compile` share. */
30
45
  function compileInputs(ctx: CommandContext, params: Record<string, string> | undefined): import("../../components/release-compile").CompileInputs {
@@ -40,10 +55,54 @@ function compileInputs(ctx: CommandContext, params: Record<string, string> | und
40
55
  };
41
56
  }
42
57
 
43
- function resolveActor(ctx: CommandContext): string | undefined {
44
- return ctx.args.actor ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER;
58
+ /** What to run after a cut (#4027): publish from CI, or push the tag so CI publishes, or deploy the first environment. */
59
+ export function nextCommands(req: {
60
+ ref: string;
61
+ tag: string | null;
62
+ inCi: boolean;
63
+ firstEnv: string | null;
64
+ /** A project with no components deploys with `release export`, not promote (#4046). */
65
+ noComponents: boolean;
66
+ }): string[] {
67
+ const publish = `chant release publish ${req.ref}`;
68
+ const deploy = req.firstEnv === null
69
+ ? null
70
+ : req.noComponents
71
+ ? `chant release export ${req.ref} --env ${req.firstEnv}`
72
+ : `chant components promote --release ${req.ref} --to ${req.firstEnv}`;
73
+ if (req.inCi) return [publish, ...(deploy ? [deploy] : [])];
74
+ return [...(req.tag ? [`git push origin ${req.tag}`] : []), publish, ...(deploy ? [deploy] : [])];
45
75
  }
46
76
 
77
+ /** The command line that ran, without `--json`, with `--yes` added: what a dry run says to run next. */
78
+ export function sameWithYes(argv: readonly string[]): string {
79
+ const quote = (a: string) => (/^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`);
80
+ return ["chant", ...argv.filter((a) => a !== "--json" && a !== "--yes"), "--yes"].map(quote).join(" ");
81
+ }
82
+
83
+ function describeActorSource(source: import("../../lifecycle/release-workstation").ActorSource): string {
84
+ switch (source) {
85
+ case "flag":
86
+ return "--actor";
87
+ case "signer":
88
+ return "the signers file at base, for the signing key";
89
+ case "ci":
90
+ return "the CI's actor";
91
+ case "git":
92
+ return "git user.email";
93
+ case "user":
94
+ return "$USER";
95
+ }
96
+ }
97
+
98
+ /**
99
+ * `release cut [<label>]` (#4026, #4027). A dry run unless `--yes`: it
100
+ * refuses a dirty tree, a commit that is not on trunk and pushed, and one
101
+ * that is not green, compiles every environment, and prints what the cut
102
+ * would record, build and tag, and what publishes where. With `--yes` it
103
+ * cuts, signs with `--sign`, records the cut with its actor and run id,
104
+ * tags the commit locally and prints the next command.
105
+ */
47
106
  export async function runReleaseCut(ctx: CommandContext): Promise<number> {
48
107
  const { args } = ctx;
49
108
  const positional = args.extraPositional;
@@ -52,44 +111,193 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
52
111
  return 1;
53
112
  }
54
113
  const label = positional ?? args.label ?? null;
55
- const actor = resolveActor(ctx);
56
- if (!actor) {
57
- console.error(formatError({ message: "Could not resolve --actor from the environment", hint: "Pass --actor explicitly, or set GITHUB_ACTOR / GITLAB_USER_LOGIN / USER." }));
58
- return 1;
59
- }
114
+ const dryRun = args.yes !== true;
60
115
  const cwd = resolve(".");
61
116
  const { spawnTargetCompiler, ReleaseCompileError } = await import("../../components/release-compile");
62
- const { cutRelease, ReleaseCutError } = await import("../../lifecycle/release-cut");
117
+ const { cutRelease, planRelease, ReleaseCutError } = await import("../../lifecycle/release-cut");
63
118
  const { ReleaseLabelError } = await import("../../lifecycle/release-store");
64
- const { pushLifecycleReport } = await import("../../lifecycle/git");
65
- const params = parseParamFlags(args.param);
66
- const compiler = spawnTargetCompiler(compileInputs(ctx, params));
119
+ const { pushLifecycleReport, getHeadCommit } = await import("../../lifecycle/git");
120
+ const { resolveCiRun } = await import("../../lifecycle/release-ledger");
121
+ const ws = await import("../../lifecycle/release-workstation");
122
+ const { detectProvenance, releasePublishTargets } = await import("../../lifecycle/release-publish");
123
+ const fail = (code: string, message: string, fix?: string): number => {
124
+ console.error(formatError({ message: `${message} [${code}]`, ...(fix ? { hint: fix } : {}) }));
125
+ return 1;
126
+ };
127
+
67
128
  try {
68
- const result = await cutRelease({ cwd, compiler, label, actor, ...(args.sign !== undefined ? { sign: args.sign } : {}) });
129
+ // The workstation checks come first, in the order #4027 lists them; the
130
+ // green check and the rest are the cut's own.
131
+ const top = (() => {
132
+ try {
133
+ return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
134
+ } catch {
135
+ return null;
136
+ }
137
+ })();
138
+ let commit: string | null = null;
139
+ let trunk: import("../../lifecycle/release-workstation").Trunk | null = null;
140
+ let signers: readonly import("../../workspace/trust/trust-files").Signer[] = [];
141
+ let member: string | null = null;
142
+ if (top) {
143
+ commit = await getHeadCommit({ cwd });
144
+ ws.checkCleanTree(top, commit);
145
+ const { scopeSource } = await import("../../workspace/write-scope");
146
+ const { releaseRuleFrom } = await import("../../workspace/identity");
147
+ const source = scopeSource(cwd);
148
+ trunk = ws.resolveTrunk(top, source.declaration?.ci?.green?.branch ?? null);
149
+ ws.checkOnTrunk(top, commit, trunk);
150
+ signers = releaseRuleFrom(source).rule.signers.signers;
151
+ const { resolveMemberLedger } = await import("../../lifecycle/member-ledger");
152
+ const ledger = await resolveMemberLedger(cwd);
153
+ member = ledger.members.length > 0 ? ledger.members.join("/") : null;
154
+ }
155
+
156
+ // The signing key is resolved now, so a dry run shows a key that can't be read.
157
+ let publicKey: string | null = null;
158
+ if (args.sign !== undefined) {
159
+ const { resolveSigningKey, SealError } = await import("../../workspace/trust/seal");
160
+ try {
161
+ const key = resolveSigningKey(args.sign, cwd);
162
+ try {
163
+ if (!existsSync(key.file)) return fail("release-signer-refused", `no release was cut: the signing key ${key.file} does not exist`);
164
+ publicKey = ws.publicKeyOf(key.file);
165
+ } finally {
166
+ key.cleanup();
167
+ }
168
+ } catch (err) {
169
+ if (!(err instanceof SealError)) throw err;
170
+ return fail("release-signer-refused", `no release was cut: ${err.message}`);
171
+ }
172
+ }
173
+ const who = ws.resolveCutActor({ cwd, ...(args.actor ? { explicit: args.actor } : {}), publicKey, signers });
174
+ if (!who) {
175
+ console.error(formatError({ message: "Could not resolve --actor", hint: "Pass --actor explicitly, or set git's user.email, GITHUB_ACTOR, GITLAB_USER_LOGIN or USER." }));
176
+ return 1;
177
+ }
178
+ const actor = who.actor;
179
+ const tag = label !== null && commit !== null && top ? { name: ws.releaseTagName(member, label), state: ws.checkReleaseTag(top, ws.releaseTagName(member, label), commit) } : null;
180
+ const ci = resolveCiRun(process.env);
181
+ const params = parseParamFlags(args.param);
182
+ const compiler = spawnTargetCompiler(compileInputs(ctx, params));
183
+ const request = { cwd, compiler, label, actor, ...(args.sign !== undefined ? { sign: args.sign } : {}) };
184
+
185
+ if (dryRun) {
186
+ const plan = await planRelease(request);
187
+ // Under the release gate, a key the signers file at base lists for someone else is refused now, not after the build.
188
+ if (plan.signatureRequired && publicKey !== null && !signers.some((s) => s.key === publicKey && s.principal.trim().toLowerCase() === actor.trim().toLowerCase())) {
189
+ return fail(
190
+ "release-signer-refused",
191
+ `no release would be cut: the signing key is not listed for ${actor} in the signers file at base, and identity.gates.release counts only a cut its signer signs`,
192
+ "Pass --actor <principal> naming the key's signer, or sign with that principal's key.",
193
+ );
194
+ }
195
+ const ref = plan.label ?? plan.alreadyCut ?? plan.releaseDigest;
196
+ const provenance = detectProvenance();
197
+ // The targets release publish would run, as the config here declares them; their files are read at publish.
198
+ const publishTargets = await releasePublishTargets(cwd).then(
199
+ (ts) => ts.map((t) => `${t.name} to ${t.destination}`),
200
+ (err: unknown) => (err instanceof Error ? err : new Error(String(err))),
201
+ );
202
+ const deployRef = plan.label ?? "<label|digest>";
203
+ const doc = {
204
+ dryRun: true,
205
+ ...plan,
206
+ actor,
207
+ actorSource: who.source,
208
+ run: ci ? { runId: ci.runId, runOrigin: ci.runOrigin } : { runId: null, runOrigin: { forge: "local" as const } },
209
+ trunk,
210
+ sign: args.sign !== undefined ? { key: publicKey !== null ? publicKey.split(" ")[0] : null, readable: publicKey !== null } : null,
211
+ tag: tag ? { name: tag.name, exists: tag.state === "present" } : null,
212
+ publish: { targets: Array.isArray(publishTargets) ? publishTargets : [], provenance },
213
+ next: [sameWithYes(process.argv.slice(2))],
214
+ };
215
+ if (args.json) {
216
+ console.log(JSON.stringify(doc, null, 2));
217
+ return 0;
218
+ }
219
+ const name = plan.label ? formatBold(plan.label) : "an unlabelled release";
220
+ console.log(`Dry run: cut ${name} from commit ${plan.commit.slice(0, 12)}${trunk ? ` (on ${trunk.remote}/${trunk.branch}, green)` : ""}`);
221
+ if (plan.alreadyCut) console.log(` already cut as ${plan.alreadyCut}: the cut would record nothing new`);
222
+ console.log(` actor ${actor} (from ${describeActorSource(who.source)})`);
223
+ console.log(` run ${ci ? `${ci.runId} (${ci.runOrigin.forge})` : "minted on this machine at the cut (local)"}`);
224
+ console.log(
225
+ ` ${args.sign !== undefined ? `signed with ${publicKey ? publicKey.split(" ")[0] : "a key whose public half can't be read until it signs"}` : "not signed"}` +
226
+ (plan.signatureRequired ? "; identity.gates.release requires a signed cut" : ""),
227
+ );
228
+ console.log(` tag ${tag ? `${tag.name}${tag.state === "present" ? " (exists)" : ""}, made here and not pushed` : "none, since the release has no label"}`);
229
+ for (const t of plan.targets) {
230
+ console.log(` ${t.env}: ${t.templates.length} template(s)${t.params.length > 0 ? `, ${t.params.map((p) => `${p.name}=${p.value}`).join(" ")}` : ""}`);
231
+ }
232
+ for (const b of plan.build) console.log(` ${b.component}: artifacts built in ${b.env}`);
233
+ for (const r of plan.reused) console.log(` ${r.component}: artifacts unchanged, carried from ${r.from}`);
234
+ console.log(` digest ${plan.releaseDigest ?? "decided when the artifacts are built"}`);
235
+ console.log("What will be published where:");
236
+ if (plan.publishSteps.length === 0) console.log(" no component declares a publish step");
237
+ for (const p of plan.publishSteps) {
238
+ console.log(` ${p.component}: ${p.kind}${p.to ? ` to ${p.to}` : ""} in ${p.envs.join(", ")}, by that environment's deploy (chant components promote --release ${deployRef})`);
239
+ }
240
+ const publishCmd = `chant release publish ${ref ?? "<label|digest>"}`;
241
+ if (!Array.isArray(publishTargets)) console.log(` ${publishCmd}: the publish targets can't be read: ${publishTargets.message}`);
242
+ else if (publishTargets.length === 0) console.log(` ${publishCmd}: nothing, since this member declares no publish target (release.publish in chant.config.ts)`);
243
+ else for (const t of publishTargets) console.log(` ${publishCmd}: ${t}`);
244
+ if (!provenance.oidc) console.log(` a publish from here would carry no OIDC provenance: ${provenance.reason}`);
245
+ console.error(`Dry run: nothing was cut, signed or tagged. Run again with --yes to cut: ${doc.next[0]}`);
246
+ return 0;
247
+ }
248
+
249
+ const result = await cutRelease(request);
69
250
  const push = await pushLifecycleReport({ cwd }).catch(() => ({ pushed: false, pushWarning: undefined }));
251
+ // The tag names the release's label, which a re-cut with no label reads from the earlier cut.
252
+ let tagged: { name: string; created: boolean } | null = null;
253
+ let tagWarning: string | undefined;
254
+ if (result.label !== null && top) {
255
+ const name = ws.releaseTagName(member, result.label);
256
+ try {
257
+ const state = ws.checkReleaseTag(top, name, result.release.commit);
258
+ if (state === "absent") ws.createReleaseTag(top, name, result.release.commit, { releaseDigest: result.release.releaseDigest, label: result.label, member });
259
+ tagged = { name, created: state === "absent" };
260
+ } catch (err) {
261
+ tagWarning = `the release was cut, and the commit was not tagged: ${err instanceof Error ? err.message : String(err)}`;
262
+ }
263
+ }
264
+ const firstEnv = result.release.targets[0]?.env ?? null;
265
+ const next = nextCommands({
266
+ ref: result.label ?? result.release.releaseDigest,
267
+ tag: tagged?.name ?? null,
268
+ inCi: ci !== undefined,
269
+ firstEnv,
270
+ noComponents: result.release.targets.every((t) => t.templates.every((tpl) => tpl.component === null)) && result.release.artifactDigests.length === 0,
271
+ });
70
272
  if (args.json) {
71
- console.log(JSON.stringify({ release: result.release, label: result.label, cut: result.cut, stored: result.stored, reused: result.reused }, null, 2));
273
+ console.log(
274
+ JSON.stringify(
275
+ { dryRun: false, release: result.release, label: result.label, cut: result.cut, stored: result.stored, reused: result.reused, actor, actorSource: who.source, tag: tagged, next },
276
+ null,
277
+ 2,
278
+ ),
279
+ );
72
280
  } else {
73
281
  const name = result.label ? formatBold(result.label) : "unlabelled release";
74
282
  if (result.cut) console.error(formatSuccess(`Cut ${name}: ${result.release.releaseDigest}`));
75
283
  else console.error(formatSuccess(`Already cut${result.label ? ` as ${formatBold(result.label)}` : ""}: ${result.release.releaseDigest}`));
76
284
  console.error(` commit ${result.release.commit}`);
77
- if (result.cut) console.error(result.cut.signature ? ` signed by ${result.cut.actor}` : " not signed");
285
+ if (result.cut) console.error(` ${result.cut.signature ? `signed by ${result.cut.actor}` : `not signed; actor ${result.cut.actor}`}, run ${result.cut.runId}`);
78
286
  console.error(` artifacts ${result.release.artifactDigests.length}, targets ${result.release.targets.map((t) => `${t.env} (${t.templates.length} template${t.templates.length === 1 ? "" : "s"})`).join(", ")}`);
79
287
  for (const r of result.reused) console.error(` ${r.component}: artifacts unchanged, carried from ${r.from}`);
288
+ if (tagged) console.error(` tag ${tagged.name}${tagged.created ? "" : " (already there)"}, not pushed`);
289
+ console.error(`Next: ${next[0]}`);
290
+ for (const n of next.slice(1)) console.error(` or: ${n}`);
80
291
  console.log(result.release.releaseDigest);
81
292
  }
82
293
  if (push.pushWarning) console.error(formatWarning({ message: push.pushWarning }));
294
+ if (tagWarning) console.error(formatWarning({ message: tagWarning }));
83
295
  return 0;
84
296
  } catch (err) {
85
- if (err instanceof ReleaseCutError) {
86
- console.error(formatError({ message: `${err.message} [${err.code}]`, ...(err.fix ? { hint: err.fix } : {}) }));
87
- return 1;
88
- }
89
- if (err instanceof ReleaseLabelError) {
90
- console.error(formatError({ message: `${err.message} [${err.code}]` }));
91
- return 1;
297
+ if (err instanceof ws.WorkstationCheckError || err instanceof ReleaseCutError) {
298
+ return fail(err.code, err.message, err.fix);
92
299
  }
300
+ if (err instanceof ReleaseLabelError) return fail(err.code, err.message);
93
301
  if (err instanceof ReleaseCompileError) {
94
302
  console.error(formatError({ message: `no release was cut: ${err.message}` }));
95
303
  return 1;
@@ -98,6 +306,180 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
98
306
  }
99
307
  }
100
308
 
309
+ /**
310
+ * `release publish <label|digest> [--yes]` (#4027, #4047): publish a cut
311
+ * release to the targets the member declares under `release.publish` in
312
+ * its `chant.config.ts`. It refuses a release no cut names, one cut from a
313
+ * revoked commit, one with no signature that verifies against the signers
314
+ * file at base unless `--allow-unsigned` (#4049; never under
315
+ * identity.gates.release), and, when there is a target, a checkout that is
316
+ * not the release's commit with a clean tree. It warns before publishing
317
+ * when the run has no OIDC token, so what it publishes carries no
318
+ * provenance, and refuses under `--require-provenance`. Each target attaches
319
+ * an SBOM and the release's provenance statement (#4049). Every
320
+ * target is planned (its files read and checked, its credentials found)
321
+ * before any publishes. Without `--yes` it is a dry run that prints the plan
322
+ * and uploads nothing. With `--yes` it stages every target, then reveals them
323
+ * (#4048, `runPublish`), recording each step in `_releases/publishes.jsonl`,
324
+ * so running it again after a failure resumes. Every line is redacted.
325
+ */
326
+ export async function runReleasePublish(ctx: CommandContext): Promise<number> {
327
+ const { args } = ctx;
328
+ const ref = args.extraPositional;
329
+ if (!ref) {
330
+ console.error(formatError({ message: "release publish needs a release", hint: USAGE }));
331
+ return 1;
332
+ }
333
+ const dryRun = args.yes !== true;
334
+ const cwd = resolve(".");
335
+ const fail = (code: string, message: string, fix?: string): number => {
336
+ console.error(formatError({ message: `${message} [${code}]`, ...(fix ? { hint: fix } : {}) }));
337
+ return 1;
338
+ };
339
+ const { fetchLifecycle } = await import("../../lifecycle/git");
340
+ const { preparePublish, publishContext, runPublish, PublishTargetError, PublishRunError } = await import("../../lifecycle/release-publish");
341
+ const { publishProgress, progressOf, readPublishRecords } = await import("../../lifecycle/publish-record");
342
+ await fetchLifecycle({ cwd }).catch(() => false);
343
+ const prepared = await preparePublish({ cwd, ref, allowUnsigned: args.allowUnsigned === true, requireProvenance: args.requireProvenance === true });
344
+ if ("refusal" in prepared) return fail(prepared.refusal.code, prepared.refusal.message, prepared.refusal.fix);
345
+ const { release, label, signature, provenance, warning, signatureWarning, targets, redactor } = prepared;
346
+ const name = label ?? release.releaseDigest;
347
+ const deploySteps = release.targets.map((t) => t.env);
348
+ const say = (line: string) => console.error(redactor.redact(line));
349
+ const failed = (code: string, message: string, fix?: string) => fail(code, redactor.redact(message), fix);
350
+ const errorOf = (err: unknown): { code: string; message: string; fix?: string } =>
351
+ err instanceof PublishTargetError ? { code: err.code, message: err.message, ...(err.fix ? { fix: err.fix } : {}) } : { code: "publish-failed", message: err instanceof Error ? err.message : String(err) };
352
+
353
+ if (!args.json) {
354
+ say(`${dryRun ? "Dry run: publish" : "Publishing"} ${formatBold(name)} (${release.releaseDigest}) from commit ${release.commit.slice(0, 12)}`);
355
+ say(` ${signature.status === "verified" ? `signed by ${signature.actor}` : signature.status === "unsigned" ? "not signed" : `signature unverified: ${signature.reason}`}`);
356
+ }
357
+ // The warnings go to stderr with --json too, before any target runs.
358
+ if (signatureWarning) console.error(formatWarning({ message: signatureWarning }));
359
+ if (warning) console.error(formatWarning({ message: warning }));
360
+ const pctx = publishContext(prepared, { cwd, log: (line) => (args.json ? undefined : console.error(` ${line}`)) });
361
+
362
+ // Every target is planned before any publishes, so a missing file or credential stops the publish with nothing sent.
363
+ const progress = publishProgress(await readPublishRecords(release.releaseDigest, { cwd }).catch(() => []));
364
+ const planned: Array<{ kind: string; name: string; destination: string; progress: "none" | "staged" | "revealed"; items: import("../../lifecycle/release-publish").PublishItem[] }> = [];
365
+ for (const target of targets) {
366
+ try {
367
+ planned.push({ kind: target.kind, name: target.name, destination: target.destination, progress: progressOf(progress, target), items: await target.plan(pctx) });
368
+ } catch (err) {
369
+ const e = errorOf(err);
370
+ return failed(e.code, `nothing was published: ${target.name}: ${e.message}`, e.fix);
371
+ }
372
+ }
373
+ const promoteHint = (): string | null => {
374
+ if (deploySteps.length === 0) return null;
375
+ const noComponents = release.artifactDigests.length === 0 && release.targets.every((t) => t.templates.every((tpl) => tpl.component === null));
376
+ return noComponents
377
+ ? ` Each environment deploys the files the release recorded: chant release export ${name} --env ${deploySteps[0]}`
378
+ : ` A component's publish steps run in each environment's deploy of the release: chant components promote --release ${name} --to ${deploySteps[0]}`;
379
+ };
380
+ const base = { releaseDigest: release.releaseDigest, label, commit: release.commit, signature, provenance, warnings: [signatureWarning, warning].filter((w): w is string => w !== null) };
381
+
382
+ if (dryRun) {
383
+ const next = [sameWithYes(process.argv.slice(2))];
384
+ if (args.json) {
385
+ console.log(redactor.redact(JSON.stringify({ dryRun: true, ...base, targets: planned, published: [], environments: deploySteps, next }, null, 2)));
386
+ return 0;
387
+ }
388
+ const out = (line: string) => console.log(redactor.redact(line));
389
+ out("What would be published where:");
390
+ if (planned.length === 0) out(" nothing: this member declares no publish target (release.publish in chant.config.ts)");
391
+ for (const p of planned) {
392
+ const earlier = p.progress === "revealed" ? " (published by an earlier run; skipped)" : p.progress === "staged" ? " (staged by an earlier run; only revealed)" : "";
393
+ out(` ${p.name}${provenance.oidc && p.kind === "npm" ? ", with --provenance" : ""}${earlier}:`);
394
+ for (const i of p.items) out(` ${describeItem(i)}`);
395
+ }
396
+ say(`Dry run: nothing was published. Run again with --yes to publish: ${next[0]}`);
397
+ const hint = planned.length === 0 ? promoteHint() : null;
398
+ if (hint) say(hint);
399
+ return 0;
400
+ }
401
+
402
+ if (targets.length > 0 && !args.json) say(" every target stages first, and none is visible until all have staged");
403
+ let run: import("../../lifecycle/release-publish").PublishRun;
404
+ try {
405
+ run = await runPublish({ cwd, release, label, targets, ctx: pctx, say: (line) => (args.json ? undefined : say(line)) });
406
+ } catch (err) {
407
+ if (err instanceof PublishRunError) {
408
+ const again = sameWithYes(process.argv.slice(2));
409
+ const where =
410
+ err.step === "stage"
411
+ ? `nothing is visible yet${err.staged.length > 0 ? `; staged: ${err.staged.join(", ")}` : ""}`
412
+ : `revealed: ${err.revealed.length > 0 ? err.revealed.join(", ") : "none"}; staged and not yet revealed: ${err.staged.filter((t) => !err.revealed.includes(t)).join(", ")}`;
413
+ return failed(err.code, `${err.message}; ${where}`, `${err.fix ? `${err.fix} ` : ""}Run the same command again to resume; what went out is not published again: ${again}`);
414
+ }
415
+ const e = errorOf(err);
416
+ return failed(e.code, e.message, e.fix);
417
+ }
418
+ const { published } = run;
419
+ if (run.recordWarning && targets.length > 0) console.error(formatWarning({ message: redactor.redact(`${run.recordWarning}. A resume asks the registries what is already there.`) }));
420
+ if (args.json) {
421
+ console.log(redactor.redact(JSON.stringify({ dryRun: false, ...base, targets: planned, published, resumed: run.resumed, environments: deploySteps }, null, 2)));
422
+ return 0;
423
+ }
424
+ if (targets.length === 0) {
425
+ say("Nothing was published: this member declares no publish target (release.publish in chant.config.ts).");
426
+ const hint = promoteHint();
427
+ if (hint) say(hint);
428
+ } else {
429
+ for (const p of published) for (const i of p.items) say(` ${i.present ? "already there" : "published"}: ${describeItem(i)}`);
430
+ console.error(formatSuccess(`Published ${name} to ${published.length} target(s)`));
431
+ }
432
+ return 0;
433
+ }
434
+
435
+ /** One item a target publishes, for a line: what, where, and the SBOM or attachment it is (#4049). */
436
+ function describeItem(i: import("../../lifecycle/release-publish").PublishItem): string {
437
+ const what = i.attachment === "sbom" ? `${i.name} (SBOM)` : i.attachment === "provenance" ? `${i.name} (provenance)` : i.name;
438
+ return `${what} to ${i.destination}${i.sbom ? `, carrying ${i.sbom}` : ""}`;
439
+ }
440
+
441
+ /**
442
+ * `release verify <label|digest> [--json]` (#4049): check, offline, that the
443
+ * release was signed by a key `.chant/allowed_signers` lists at the release's
444
+ * commit, and that its file is the digest of what it holds. Exits 0 when it
445
+ * verifies and 1 when it does not or can't be checked.
446
+ */
447
+ export async function runReleaseVerify(ctx: CommandContext): Promise<number> {
448
+ const { args } = ctx;
449
+ const ref = args.extraPositional;
450
+ if (!ref) {
451
+ console.error(formatError({ message: "release verify needs a release", hint: USAGE }));
452
+ return 1;
453
+ }
454
+ const { verifyRelease, verificationCode } = await import("../../lifecycle/release-verify");
455
+ const result = await verifyRelease({ cwd: resolve("."), ref });
456
+ if ("error" in result) {
457
+ if (args.json) console.log(JSON.stringify({ ref, error: result.error }, null, 2));
458
+ else console.error(formatError({ message: `${result.error.message} [${result.error.code}]`, ...(result.error.fix ? { hint: result.error.fix } : {}) }));
459
+ return 1;
460
+ }
461
+ const code = verificationCode(result);
462
+ if (args.json) {
463
+ console.log(JSON.stringify({ ...result, ...(code ? { error: { code, message: result.reason } } : {}) }, null, 2));
464
+ return code ? 1 : 0;
465
+ }
466
+ const name = result.label ? `${formatBold(result.label)} (${result.releaseDigest})` : result.releaseDigest;
467
+ for (const c of result.cuts) {
468
+ console.log(` cut by ${c.actor} at ${c.timestamp}: ${c.signatureCheck.status}${c.signatureCheck.key ? ` (${c.signatureCheck.key})` : ""}`);
469
+ }
470
+ if (code) {
471
+ console.error(
472
+ formatError({
473
+ message: `${name} does not verify: ${result.reason} [${code}]`,
474
+ hint: code === "release-digest-mismatch" ? "Do not publish or deploy it; cut the release again." : `Cut it again signed with a key ${result.signers.path} lists: chant release cut ${result.label ?? "<label>"} --sign --yes`,
475
+ }),
476
+ );
477
+ return 1;
478
+ }
479
+ console.error(formatSuccess(`Verified ${name}: ${result.reason}`));
480
+ return 0;
481
+ }
482
+
101
483
  export async function runReleaseList(ctx: CommandContext): Promise<number> {
102
484
  const { releasesDocument } = await import("../../workspace/releases");
103
485
  const doc = await releasesDocument({ cwd: resolve(".") });
@@ -119,8 +501,13 @@ export async function runReleaseList(ctx: CommandContext): Promise<number> {
119
501
  // #4031: the newest cut's signature, as the policy at base judges it.
120
502
  const newest = r.cuts.at(-1);
121
503
  const signature = newest ? (newest.signatureCheck.status === "verified" ? `signed by ${newest.actor}` : newest.signatureCheck.status) : "-";
122
- console.log(`${(r.label ?? "-").padEnd(16)} ${r.releaseDigest} ${r.commit.slice(0, 12)} ${signature} ${where}`);
504
+ // #4033: a release cut from a revoked commit says so at the end of its line, and why on stderr.
505
+ console.log(`${(r.label ?? "-").padEnd(16)} ${r.releaseDigest} ${r.commit.slice(0, 12)} ${signature} ${where}${r.revoked ? " revoked" : ""}`);
123
506
  if (newest?.signatureCheck.status === "unverified") console.error(` ${r.label ?? r.releaseDigest}: ${newest.signatureCheck.reason}`);
507
+ if (r.revoked) {
508
+ const { describeRevocation } = await import("../../lifecycle/release-revocation");
509
+ console.error(` ${r.label ?? r.releaseDigest}: revoked by ${r.revoked.tag}, ${describeRevocation(r.revoked)}`);
510
+ }
124
511
  }
125
512
  return 0;
126
513
  }
@@ -46,6 +46,15 @@ describe("recordAutoReleasesForRun", () => {
46
46
  expect(plain).not.toHaveProperty("admission");
47
47
  });
48
48
 
49
+ test("a run under --release records the release digest on each component, one without it records none (#4043)", async () => {
50
+ const release = `jcs1-sha256:${"c".repeat(64)}`;
51
+ await recordAutoReleasesForRun([plainResult], "dev", "9001", false, { forge: "local" }, release);
52
+ expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).toMatchObject({ releaseDigest: release });
53
+ maybeRecordAutoReleaseMock.mockClear();
54
+ await recordAutoReleasesForRun([plainResult], "dev", "9001", false, { forge: "local" });
55
+ expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).not.toHaveProperty("releaseDigest");
56
+ });
57
+
49
58
  test("the origin defaults to local", async () => {
50
59
  await recordAutoReleasesForRun([plainResult], "prod", "local-1", false);
51
60
  expect(maybeRecordAutoReleaseMock.mock.calls[0][0]).toMatchObject({ runId: "local-1", runOrigin: { forge: "local" } });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `chant run --components --release <label|digest>` (#4043): the run builds
3
+ * from the checked-out commit, so the release must be cut from it and have a
4
+ * target for the environment. Checked before anything deploys.
5
+ */
6
+ import { describe, test, expect, vi } from "vitest";
7
+ import { makeRelease } from "../../lifecycle/release-store";
8
+
9
+ const COMMIT = "a1b2c3d4".repeat(5);
10
+ const release = makeRelease({
11
+ member: null,
12
+ commit: COMMIT,
13
+ artifactDigests: [],
14
+ targets: [{ env: "dev", templates: [], params: [] }],
15
+ });
16
+ let head = COMMIT;
17
+
18
+ vi.mock("../../lifecycle/git", () => ({
19
+ getHeadCommit: async () => head,
20
+ fetchLifecycle: async () => true,
21
+ pushLifecycle: async () => false,
22
+ }));
23
+ vi.mock("../../lifecycle/release-store", async () => {
24
+ const actual = await vi.importActual<typeof import("../../lifecycle/release-store")>("../../lifecycle/release-store");
25
+ return {
26
+ ...actual,
27
+ resolveRelease: async (ref: string) => (ref === "v1" || ref === release.releaseDigest ? { release, label: "v1" } : null),
28
+ };
29
+ });
30
+
31
+ const { resolveRunRelease } = await import("./run");
32
+
33
+ describe("resolveRunRelease", () => {
34
+ test("a release cut from HEAD with a target for the environment is the run's release, by label or digest", async () => {
35
+ head = COMMIT;
36
+ expect(await resolveRunRelease("v1", "dev", ".")).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1" });
37
+ expect(await resolveRunRelease(release.releaseDigest, "dev", ".")).toMatchObject({ releaseDigest: release.releaseDigest });
38
+ });
39
+
40
+ test("a release it can't find is refused", async () => {
41
+ expect(await resolveRunRelease("v9", "dev", ".")).toMatch(/no release is labelled or digested "v9"/);
42
+ });
43
+
44
+ test("a release cut from another commit is refused, and the message says why", async () => {
45
+ head = "f".repeat(40);
46
+ const why = await resolveRunRelease("v1", "dev", ".");
47
+ expect(why).toMatch(/was cut from a1b2c3d4a1b2 and this checkout is at ffffffffffff/);
48
+ expect(why).toMatch(/promote --release/);
49
+ });
50
+
51
+ test("a release with no target for the environment is refused", async () => {
52
+ head = COMMIT;
53
+ expect(await resolveRunRelease("v1", "prod", ".")).toMatch(/has no target for "prod"/);
54
+ });
55
+ });