@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,110 @@
1
+ /**
2
+ * #4049: `chant release verify` checks a release's signature offline against
3
+ * `.chant/allowed_signers` at the release's commit. A release signed by a
4
+ * listed key verifies; an unsigned one and one signed by a key the file does
5
+ * not list do not; and a key listed only after the release's commit does not
6
+ * make it verify. Throwaway keys in temp repos only.
7
+ */
8
+
9
+ import { afterAll, describe, expect, test } from "vitest";
10
+ import { rmSync } from "node:fs";
11
+ import { dirname } from "node:path";
12
+ import { cutRelease } from "./release-cut";
13
+ import { verificationCode, verifyRelease, type ReleaseVerification } from "./release-verify";
14
+ import type { ReleaseArtifact } from "./release-store";
15
+ import type { CompiledTarget, TargetCompiler } from "../components/release-compile";
16
+ import { hasSshKeygen, TestRepo, type Key } from "../workspace/trust/test-repo";
17
+
18
+ const repos: TestRepo[] = [];
19
+ afterAll(() => {
20
+ for (const r of repos) rmSync(dirname(r.dir), { recursive: true, force: true });
21
+ });
22
+
23
+ const compiler: TargetCompiler = {
24
+ async compile(env): Promise<CompiledTarget> {
25
+ return { env, params: [], components: ["api"], buildSteps: {}, buildInputs: {}, componentFiles: { api: "api.component.ts" }, templates: [{ component: "api", path: "dist/api.json", content: `{"env":"${env}"}\n` }] };
26
+ },
27
+ async buildArtifacts(): Promise<ReleaseArtifact[]> {
28
+ return [];
29
+ },
30
+ };
31
+
32
+ /** A plain project whose signers file lists alice, with one green commit. */
33
+ function setup(): { r: TestRepo; alice: Key; bob: Key } {
34
+ const r = new TestRepo("release-verify");
35
+ repos.push(r);
36
+ const alice = r.key("alice");
37
+ const bob = r.key("bob");
38
+ r.write(".chant/allowed_signers", `alice ${alice.pub}\n`);
39
+ r.write("chant.config.ts", `export default { environments: ["dev"] };\n`);
40
+ r.write("api.component.ts", "export const api = {};\n");
41
+ green(r, "init");
42
+ return { r, alice, bob };
43
+ }
44
+
45
+ function green(r: TestRepo, message: string): string {
46
+ const head = r.commit(message);
47
+ r.git(["tag", `ci/green/${head}`]);
48
+ return head;
49
+ }
50
+
51
+ const cut = (r: TestRepo, label: string, actor: string, sign?: Key) =>
52
+ cutRelease({ cwd: r.dir, compiler, label, actor, env: {}, now: () => new Date("2026-10-17T10:00:00.000Z"), ...(sign ? { sign: sign.file } : {}) });
53
+ const checked = async (r: TestRepo, ref: string): Promise<ReleaseVerification> => {
54
+ const v = await verifyRelease({ cwd: r.dir, ref });
55
+ if ("error" in v) throw new Error(v.error.message);
56
+ return v;
57
+ };
58
+
59
+ describe("chant release verify", () => {
60
+ test.skipIf(!hasSshKeygen)("passes for a release signed by a key the signers file lists at its commit", async () => {
61
+ const { r, alice } = setup();
62
+ const { release } = await cut(r, "v1", "alice", alice);
63
+ const v = await checked(r, "v1");
64
+ expect(v).toMatchObject({ releaseDigest: release.releaseDigest, label: "v1", commit: release.commit, digestMatches: true, status: "verified", verifiedBy: { actor: "alice", key: expect.stringMatching(/^SHA256:/) } });
65
+ expect(v.signers).toEqual({ path: ".chant/allowed_signers", at: release.commit, active: true });
66
+ expect(v.reason).toBe(`signed by alice, checked against .chant/allowed_signers at ${release.commit.slice(0, 12)}`);
67
+ expect(verificationCode(v)).toBeNull();
68
+ // By digest too.
69
+ expect((await checked(r, release.releaseDigest)).status).toBe("verified");
70
+ });
71
+
72
+ test("fails for an unsigned release", async () => {
73
+ const { r } = setup();
74
+ await cut(r, "v1", "alice");
75
+ const v = await checked(r, "v1");
76
+ expect(v.status).toBe("unsigned");
77
+ expect(v.verifiedBy).toBeNull();
78
+ expect(v.cuts).toEqual([expect.objectContaining({ actor: "alice", signatureCheck: { status: "unsigned", key: null, reason: "the cut carries no signature" } })]);
79
+ expect(verificationCode(v)).toBe("release-unsigned");
80
+ });
81
+
82
+ test.skipIf(!hasSshKeygen)("fails for a release signed by a key the signers file does not list, also once a later commit lists it", async () => {
83
+ const { r, bob } = setup();
84
+ await cut(r, "v1", "bob", bob);
85
+ const v = await checked(r, "v1");
86
+ expect(v.status).toBe("unverified");
87
+ expect(v.reason).toBe(`bob has no key in .chant/allowed_signers at ${v.commit.slice(0, 12)}, so the cut's signature can't count`);
88
+ expect(verificationCode(v)).toBe("release-signature-unverified");
89
+
90
+ // bob's key is listed after the release's commit: the release still reads the file at its own commit.
91
+ r.write(".chant/allowed_signers", `alice ${r.key("alice2").pub}\nbob ${bob.pub}\n`);
92
+ green(r, "list bob");
93
+ expect((await checked(r, "v1")).status).toBe("unverified");
94
+ });
95
+
96
+ test.skipIf(!hasSshKeygen)("fails for a cut signed with one key in another signer's name", async () => {
97
+ const { r, bob } = setup();
98
+ // bob's key, in alice's name.
99
+ await cut(r, "v1", "alice", bob);
100
+ const v = await checked(r, "v1");
101
+ expect(v.status).toBe("unverified");
102
+ expect(v.reason).toMatch(/the cut's signature does not verify for alice/);
103
+ });
104
+
105
+ test("names a release it can't find", async () => {
106
+ const { r } = setup();
107
+ const v = await verifyRelease({ cwd: r.dir, ref: "v9" });
108
+ expect("error" in v && v.error.code).toBe("release-not-found");
109
+ });
110
+ });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * `chant release verify <label|digest>` (#4049): whether a release was
3
+ * signed by a key `.chant/allowed_signers` lists, checked offline.
4
+ *
5
+ * It reads the release and its cuts from the local `chant/lifecycle` branch,
6
+ * and the signers file, its signed history (#2553) and the declaration at the
7
+ * release's own commit, as `chant workspace evidence verify` reads the policy
8
+ * at the revision it is given. It fetches nothing and needs no key. A release
9
+ * verifies when its file's digest is the digest of what it holds and one of
10
+ * its cuts carries a signature in the `chant-release` namespace that verifies
11
+ * for the cut's actor, over the release digest, the label, the actor and the
12
+ * time (`releaseSealPayload`), and, when the declaration at that commit has an
13
+ * `identity.gates.release` entry naming a class, the actor is in it.
14
+ *
15
+ * Reading the signers at the release's commit means a key added later does
16
+ * not make an old release verify, and one removed later does not unmake it.
17
+ */
18
+
19
+ import type { ReleaseSignatureCheck } from "../workspace/identity-gate-rule";
20
+ import type { Release, ReleaseCut } from "./release-store";
21
+
22
+ /** What `release verify --json` prints. */
23
+ export interface ReleaseVerification {
24
+ releaseDigest: string;
25
+ label: string | null;
26
+ member: string | null;
27
+ commit: string;
28
+ /** Where the signers were read: the signers file's path and the commit. */
29
+ signers: { path: string; at: string; active: boolean };
30
+ /** True when the release file's digest is the digest of what it holds. */
31
+ digestMatches: boolean;
32
+ /** `verified` when a cut's signature verifies; `unsigned` when no cut carries one; `unverified` otherwise. */
33
+ status: "verified" | "unsigned" | "unverified";
34
+ /** The cut whose signature verified, or null. */
35
+ verifiedBy: { actor: string; key: string | null } | null;
36
+ reason: string;
37
+ cuts: Array<Pick<ReleaseCut, "label" | "actor" | "timestamp" | "runId"> & { signatureCheck: ReleaseSignatureCheck }>;
38
+ }
39
+
40
+ /** Why a release could not be checked at all. */
41
+ export interface ReleaseVerifyError {
42
+ code: "release-not-found" | "release-commit-missing" | "trust-policy-unreadable" | "not-a-git-repository";
43
+ message: string;
44
+ fix?: string;
45
+ }
46
+
47
+ /** The exit-1 codes of a release that was checked and did not verify. */
48
+ export function verificationCode(v: ReleaseVerification): "release-digest-mismatch" | "release-unsigned" | "release-signature-unverified" | null {
49
+ if (!v.digestMatches) return "release-digest-mismatch";
50
+ if (v.status === "verified") return null;
51
+ return v.status === "unsigned" ? "release-unsigned" : "release-signature-unverified";
52
+ }
53
+
54
+ /** Check the release `ref` names in the member at `cwd`. Reads only the local repository. */
55
+ export async function verifyRelease(req: { cwd: string; ref: string }): Promise<ReleaseVerification | { error: ReleaseVerifyError }> {
56
+ const { resolveRelease, readCuts, verifyReleaseDigest } = await import("./release-store");
57
+ const { gitRoot } = await import("../workspace/record-source");
58
+ const { resolveBase } = await import("../workspace/trust/provenance");
59
+ const { scopeSource } = await import("../workspace/write-scope");
60
+ const { releaseRuleFrom } = await import("../workspace/identity");
61
+ const { checkReleaseSignature } = await import("../workspace/identity-gate-rule");
62
+ const { cwd, ref } = req;
63
+
64
+ const top = gitRoot(cwd);
65
+ if (!top) return { error: { code: "not-a-git-repository", message: "a release is checked against the signers file at its commit, and this directory is not in a git repository" } };
66
+ const found = await resolveRelease(ref, { cwd });
67
+ if (!found) return { error: { code: "release-not-found", message: `no release is labelled or digested "${ref}" in this member`, fix: "Fetch chant/lifecycle (git fetch origin chant/lifecycle:chant/lifecycle), then list them with: chant release list" } };
68
+ const release: Release = found.release;
69
+ const at = resolveBase(top, release.commit).commit;
70
+ if (!at) {
71
+ return {
72
+ error: {
73
+ code: "release-commit-missing",
74
+ message: `the release's commit ${release.commit} is not in this clone, so the signers file at it can't be read`,
75
+ fix: `Fetch it: git fetch origin ${release.commit}`,
76
+ },
77
+ };
78
+ }
79
+ const source = scopeSource(cwd, at);
80
+ if (source.policy.problems.length > 0) {
81
+ return { error: { code: "trust-policy-unreadable", message: `the signers at ${at.slice(0, 12)} can't be read: ${source.policy.problems.join("; ")}` } };
82
+ }
83
+ const read = releaseRuleFrom(source);
84
+ // A declaration read from the working tree is not the commit's, so its class does not apply.
85
+ const rule = source.from === "base" ? read.rule : { ...read.rule, class: null };
86
+
87
+ const own = (await readCuts({ cwd })).cuts.filter((c) => c.releaseDigest === release.releaseDigest);
88
+ // The check words where it read the signers as "at base"; here that is the release's commit.
89
+ const atCommit = (check: ReleaseSignatureCheck): ReleaseSignatureCheck => ({ ...check, reason: check.reason.split(`${rule.signers.signersPath} at base`).join(`${rule.signers.signersPath} at ${at.slice(0, 12)}`) });
90
+ const cuts = own.map((c) => ({
91
+ label: c.label,
92
+ actor: c.actor,
93
+ timestamp: c.timestamp,
94
+ runId: c.runId ?? null,
95
+ signatureCheck: atCommit(checkReleaseSignature(rule, { releaseDigest: c.releaseDigest, label: c.label, actor: c.actor, timestamp: c.timestamp, signature: c.signature })),
96
+ }));
97
+ const verified = cuts.find((c) => c.signatureCheck.status === "verified");
98
+ const digestMatches = verifyReleaseDigest(release);
99
+ const status: ReleaseVerification["status"] = verified ? "verified" : cuts.every((c) => c.signatureCheck.status === "unsigned") ? "unsigned" : "unverified";
100
+ const where = `${rule.signers.signersPath} at ${at.slice(0, 12)}`;
101
+ const reason = !digestMatches
102
+ ? `the release file's digest ${release.releaseDigest} is not the digest of what it holds, so it was changed after the cut`
103
+ : verified
104
+ ? `${verified.signatureCheck.reason}, checked against ${where}`
105
+ : status === "unsigned"
106
+ ? `no cut of the release carries a signature`
107
+ : cuts.map((c) => c.signatureCheck.reason).join("; ");
108
+ return {
109
+ releaseDigest: release.releaseDigest,
110
+ label: found.label,
111
+ member: release.member,
112
+ commit: release.commit,
113
+ signers: { path: rule.signers.signersPath, at, active: rule.signers.active },
114
+ digestMatches,
115
+ status: digestMatches ? status : "unverified",
116
+ verifiedBy: digestMatches && verified ? { actor: verified.actor, key: verified.signatureCheck.key } : null,
117
+ reason,
118
+ cuts,
119
+ };
120
+ }
@@ -0,0 +1,204 @@
1
+ /**
2
+ * #4027: what a cut reads from the checkout and the environment before it
3
+ * compiles, against temp repos with temp bare remotes. Nothing is pushed
4
+ * anywhere but those remotes, and no tag is pushed at all.
5
+ */
6
+
7
+ import { describe, test, expect } from "vitest";
8
+ import { withTestDir } from "@intentius/chant-test-utils";
9
+ import { execFileSync, spawnSync } from "node:child_process";
10
+ import { mkdirSync, writeFileSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import {
13
+ checkCleanTree,
14
+ checkOnTrunk,
15
+ checkReleaseTag,
16
+ createReleaseTag,
17
+ publicKeyOf,
18
+ releaseTagName,
19
+ resolveCutActor,
20
+ resolveTrunk,
21
+ WorkstationCheckError,
22
+ } from "./release-workstation";
23
+ import { nextCommands, sameWithYes } from "../cli/handlers/release";
24
+ import { hasSshKeygen } from "../workspace/trust/test-repo";
25
+
26
+ function git(dir: string, ...args: string[]): string {
27
+ const r = spawnSync("git", args, { cwd: dir, encoding: "utf-8" });
28
+ if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
29
+ return r.stdout.trim();
30
+ }
31
+
32
+ /** A repo at `<root>/work` with one commit on main, pushed to the bare `<root>/origin.git`. */
33
+ function pushedRepo(root: string): { dir: string; sha: string } {
34
+ const dir = join(root, "work");
35
+ mkdirSync(dir);
36
+ git(root, "init", "-q", "--bare", "-b", "main", "origin.git");
37
+ git(dir, "init", "-q", "-b", "main");
38
+ git(dir, "config", "user.email", "dev@chant.dev");
39
+ git(dir, "config", "user.name", "Dev");
40
+ writeFileSync(join(dir, "README.md"), "fixture\n");
41
+ writeFileSync(join(dir, ".gitignore"), "dist/\n");
42
+ git(dir, "add", ".");
43
+ git(dir, "commit", "-q", "-m", "init");
44
+ git(dir, "remote", "add", "origin", join(root, "origin.git"));
45
+ git(dir, "push", "-q", "origin", "main");
46
+ git(dir, "fetch", "-q", "origin");
47
+ return { dir, sha: git(dir, "rev-parse", "HEAD") };
48
+ }
49
+
50
+ const codeOf = (fn: () => unknown): string | undefined => {
51
+ try {
52
+ fn();
53
+ return undefined;
54
+ } catch (err) {
55
+ if (err instanceof WorkstationCheckError) return err.code;
56
+ throw err;
57
+ }
58
+ };
59
+
60
+ describe("trunk and pushed", () => {
61
+ test("trunk is ci.green.branch when declared, else what origin/HEAD names, else main", () =>
62
+ withTestDir(async (root) => {
63
+ const { dir } = pushedRepo(root);
64
+ expect(resolveTrunk(dir, "release")).toEqual({ remote: "origin", branch: "release", from: "ci.green.branch" });
65
+ // A fetch may set origin/HEAD, depending on the git version.
66
+ git(dir, "remote", "set-head", "origin", "-d");
67
+ expect(resolveTrunk(dir, null)).toEqual({ remote: "origin", branch: "main", from: "default" });
68
+ git(dir, "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/trunk");
69
+ expect(resolveTrunk(dir, null)).toEqual({ remote: "origin", branch: "trunk", from: "origin/HEAD" });
70
+ }));
71
+
72
+ test("a commit reachable from origin/<trunk> passes; one only on the local branch is not pushed; one off trunk is refused", () =>
73
+ withTestDir(async (root) => {
74
+ const { dir, sha } = pushedRepo(root);
75
+ const trunk = resolveTrunk(dir, null);
76
+ expect(codeOf(() => checkOnTrunk(dir, sha, trunk))).toBeUndefined();
77
+
78
+ writeFileSync(join(dir, "a.txt"), "a\n");
79
+ git(dir, "add", ".");
80
+ git(dir, "commit", "-q", "-m", "local only");
81
+ const local = git(dir, "rev-parse", "HEAD");
82
+ expect(codeOf(() => checkOnTrunk(dir, local, trunk))).toBe("release-commit-not-pushed");
83
+ // An older commit stays pushed whatever the local branch holds.
84
+ expect(codeOf(() => checkOnTrunk(dir, sha, trunk))).toBeUndefined();
85
+
86
+ git(dir, "checkout", "-q", "-b", "feature", sha);
87
+ writeFileSync(join(dir, "b.txt"), "b\n");
88
+ git(dir, "add", ".");
89
+ git(dir, "commit", "-q", "-m", "feature");
90
+ const feature = git(dir, "rev-parse", "HEAD");
91
+ expect(codeOf(() => checkOnTrunk(dir, feature, trunk))).toBe("release-commit-not-on-trunk");
92
+ // The branch ci.green names is trunk even when the commit is pushed elsewhere.
93
+ git(dir, "push", "-q", "origin", "feature");
94
+ git(dir, "fetch", "-q", "origin");
95
+ expect(codeOf(() => checkOnTrunk(dir, feature, trunk))).toBe("release-commit-not-on-trunk");
96
+ expect(codeOf(() => checkOnTrunk(dir, feature, resolveTrunk(dir, "feature")))).toBeUndefined();
97
+ }));
98
+
99
+ test("a clone with no origin/<trunk> can't show the commit pushed", () =>
100
+ withTestDir(async (root) => {
101
+ git(root, "init", "-q", "-b", "main", "solo");
102
+ const dir = join(root, "solo");
103
+ git(dir, "-c", "user.email=a@b", "-c", "user.name=a", "commit", "-q", "--allow-empty", "-m", "init");
104
+ const err = (() => {
105
+ try {
106
+ checkOnTrunk(dir, git(dir, "rev-parse", "HEAD"), resolveTrunk(dir, null));
107
+ } catch (e) {
108
+ return e as WorkstationCheckError;
109
+ }
110
+ })();
111
+ expect(err?.code).toBe("release-commit-not-pushed");
112
+ expect(err?.message).toMatch(/this clone knows no origin\/main/);
113
+ }));
114
+ });
115
+
116
+ describe("a clean tree", () => {
117
+ test("refuses a tracked change and an untracked file, and ignores what .gitignore covers", () =>
118
+ withTestDir(async (root) => {
119
+ const { dir, sha } = pushedRepo(root);
120
+ expect(codeOf(() => checkCleanTree(dir, sha))).toBeUndefined();
121
+ mkdirSync(join(dir, "dist"));
122
+ writeFileSync(join(dir, "dist", "out.json"), "{}\n");
123
+ expect(codeOf(() => checkCleanTree(dir, sha))).toBeUndefined();
124
+ writeFileSync(join(dir, "extra.component.ts"), "export const x = {};\n");
125
+ expect(() => checkCleanTree(dir, sha)).toThrow(/1 untracked file\(s\) such as extra\.component\.ts/);
126
+ git(dir, "add", "extra.component.ts");
127
+ writeFileSync(join(dir, "README.md"), "changed\n");
128
+ expect(() => checkCleanTree(dir, sha)).toThrow(/2 tracked file\(s\) with uncommitted changes/);
129
+ }));
130
+ });
131
+
132
+ describe("the actor", () => {
133
+ test("is --actor, else the CI's actor, else git's user.email", () =>
134
+ withTestDir(async (root) => {
135
+ const { dir } = pushedRepo(root);
136
+ expect(resolveCutActor({ cwd: dir, explicit: "carol", env: { GITHUB_ACTOR: "ci" } })).toEqual({ actor: "carol", source: "flag" });
137
+ expect(resolveCutActor({ cwd: dir, env: { GITHUB_ACTOR: "octo", USER: "u" } })).toEqual({ actor: "octo", source: "ci" });
138
+ expect(resolveCutActor({ cwd: dir, env: { GITLAB_USER_LOGIN: "gl", USER: "u" } })).toEqual({ actor: "gl", source: "ci" });
139
+ expect(resolveCutActor({ cwd: dir, env: { USER: "u" } })).toEqual({ actor: "dev@chant.dev", source: "git" });
140
+ }));
141
+
142
+ test.skipIf(!hasSshKeygen)("is the principal the signers file lists for the signing key, ahead of the CI and git", () =>
143
+ withTestDir(async (root) => {
144
+ const { dir } = pushedRepo(root);
145
+ const key = join(root, "key-alice");
146
+ execFileSync("ssh-keygen", ["-q", "-t", "ed25519", "-N", "", "-C", "alice", "-f", key]);
147
+ const pub = publicKeyOf(key);
148
+ expect(pub).toMatch(/^ssh-ed25519 [A-Za-z0-9+/=]+$/);
149
+ expect(publicKeyOf(`${key}.pub`)).toBe(pub);
150
+ const signers = [
151
+ { principal: "github:alice", key: pub!, line: 1 },
152
+ { principal: "bob", key: "ssh-ed25519 AAAAother", line: 2 },
153
+ ];
154
+ expect(resolveCutActor({ cwd: dir, publicKey: pub, signers, env: { GITHUB_ACTOR: "octo" } })).toEqual({ actor: "github:alice", source: "signer" });
155
+ // A key listed for two principals names no one, so the next source answers.
156
+ const twice = [...signers, { principal: "alice2", key: pub!, line: 3 }];
157
+ expect(resolveCutActor({ cwd: dir, publicKey: pub, signers: twice, env: { GITHUB_ACTOR: "octo" } })).toEqual({ actor: "octo", source: "ci" });
158
+ expect(resolveCutActor({ cwd: dir, explicit: "carol", publicKey: pub, signers })).toEqual({ actor: "carol", source: "flag" });
159
+ }));
160
+ });
161
+
162
+ describe("the release tag", () => {
163
+ test("is release/<label>, or release/<member>/<label> in a member, made locally as an annotated tag naming the digest", () =>
164
+ withTestDir(async (root) => {
165
+ const { dir, sha } = pushedRepo(root);
166
+ expect(releaseTagName(null, "v1.2.0")).toBe("release/v1.2.0");
167
+ expect(releaseTagName("services/api", "v1.2.0")).toBe("release/services/api/v1.2.0");
168
+ expect(checkReleaseTag(dir, "release/v1", sha)).toBe("absent");
169
+ createReleaseTag(dir, "release/v1", sha, { releaseDigest: "jcs1-sha256:abc", label: "v1", member: null });
170
+ expect(git(dir, "cat-file", "-t", "release/v1")).toBe("tag");
171
+ expect(git(dir, "tag", "-l", "--format=%(contents)", "release/v1")).toBe("release v1\n\nRelease-Digest: jcs1-sha256:abc");
172
+ expect(checkReleaseTag(dir, "release/v1", sha)).toBe("present");
173
+ expect(git(root, "ls-remote", "--tags", join(root, "origin.git"))).toBe("");
174
+
175
+ writeFileSync(join(dir, "c.txt"), "c\n");
176
+ git(dir, "add", ".");
177
+ git(dir, "commit", "-q", "-m", "next");
178
+ expect(codeOf(() => checkReleaseTag(dir, "release/v1", git(dir, "rev-parse", "HEAD")))).toBe("release-tag-taken");
179
+ expect(codeOf(() => checkReleaseTag(dir, "release/v1..2", sha))).toBe("release-tag-invalid");
180
+ }));
181
+ });
182
+
183
+ describe("the next command", () => {
184
+ test("in CI is the publish; on a workstation it pushes the tag first, then publish or deploy", () => {
185
+ expect(nextCommands({ ref: "v1", tag: "release/v1", inCi: true, firstEnv: "dev", noComponents: false })).toEqual([
186
+ "chant release publish v1",
187
+ "chant components promote --release v1 --to dev",
188
+ ]);
189
+ expect(nextCommands({ ref: "v1", tag: "release/v1", inCi: false, firstEnv: "dev", noComponents: false })).toEqual([
190
+ "git push origin release/v1",
191
+ "chant release publish v1",
192
+ "chant components promote --release v1 --to dev",
193
+ ]);
194
+ expect(nextCommands({ ref: "jcs1-sha256:ab", tag: null, inCi: false, firstEnv: "dev", noComponents: true })).toEqual([
195
+ "chant release publish jcs1-sha256:ab",
196
+ "chant release export jcs1-sha256:ab --env dev",
197
+ ]);
198
+ });
199
+
200
+ test("after a dry run is the same command line with --yes, without --json", () => {
201
+ expect(sameWithYes(["release", "cut", "v1", "--build", "npm run build", "--json"])).toBe("chant release cut v1 --build 'npm run build' --yes");
202
+ expect(sameWithYes(["release", "cut", "v1", "--build", "echo 'x'"])).toBe(`chant release cut v1 --build 'echo '\\''x'\\''' --yes`);
203
+ });
204
+ });
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Cutting a release from a workstation as from CI (#4027).
3
+ *
4
+ * `chant release cut` runs the same steps wherever it runs. What differs
5
+ * between a laptop and a CI job is read here, from git and the process
6
+ * environment, so the CLI and its tests resolve it the same way:
7
+ *
8
+ * - trunk: the branch `ci.green.branch` names in the declaration at base,
9
+ * else the branch `origin/HEAD` points at, else `main`. The commit must be
10
+ * reachable from `origin/<branch>` as this clone last fetched it, so a
11
+ * commit that is on the local branch only is refused as not pushed. Nothing
12
+ * is fetched: a stale `origin/<branch>` can only refuse a commit that was
13
+ * pushed since, never admit one that wasn't.
14
+ * - a clean tree: no tracked change and no untracked file that git does not
15
+ * ignore, since the compile reads the directory and a new
16
+ * `*.component.ts` would be compiled into the release.
17
+ * - the actor: `--actor`, else the principal the signers file at base lists
18
+ * for the signing key, else the CI's actor, else git's `user.email`, else
19
+ * `$USER`.
20
+ * - the tag: `release/<label>`, or `release/<member>/<label>` in a workspace
21
+ * member, an annotated tag on the commit that is created locally and never
22
+ * pushed by chant.
23
+ */
24
+
25
+ import { execFileSync, spawnSync } from "node:child_process";
26
+ import { existsSync, readFileSync } from "node:fs";
27
+ import type { Signer } from "../workspace/trust/trust-files";
28
+
29
+ export type WorkstationCheckCode = "release-commit-not-on-trunk" | "release-commit-not-pushed" | "release-worktree-dirty" | "release-tag-taken" | "release-tag-invalid";
30
+
31
+ /** A refusal before anything is compiled or written. */
32
+ export class WorkstationCheckError extends Error {
33
+ constructor(
34
+ readonly code: WorkstationCheckCode,
35
+ message: string,
36
+ readonly fix?: string,
37
+ ) {
38
+ super(message);
39
+ this.name = "WorkstationCheckError";
40
+ }
41
+ }
42
+
43
+ function tryGit(cwd: string, args: string[]): string | null {
44
+ try {
45
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ const isAncestor = (cwd: string, a: string, b: string): boolean =>
52
+ spawnSync("git", ["merge-base", "--is-ancestor", a, b], { cwd, stdio: "ignore" }).status === 0;
53
+
54
+ /** The remote a release's commit must be pushed to. */
55
+ export const RELEASE_REMOTE = "origin";
56
+
57
+ /** The trunk branch, and where its name came from. */
58
+ export interface Trunk {
59
+ remote: string;
60
+ branch: string;
61
+ from: "ci.green.branch" | "origin/HEAD" | "default";
62
+ }
63
+
64
+ /** The trunk of the repository at `cwd`: `ci.green.branch` when declared, else what `origin/HEAD` names, else `main`. */
65
+ export function resolveTrunk(cwd: string, greenBranch: string | null): Trunk {
66
+ if (greenBranch) return { remote: RELEASE_REMOTE, branch: greenBranch, from: "ci.green.branch" };
67
+ const head = tryGit(cwd, ["symbolic-ref", "--quiet", `refs/remotes/${RELEASE_REMOTE}/HEAD`]);
68
+ const prefix = `refs/remotes/${RELEASE_REMOTE}/`;
69
+ if (head?.startsWith(prefix)) return { remote: RELEASE_REMOTE, branch: head.slice(prefix.length), from: "origin/HEAD" };
70
+ return { remote: RELEASE_REMOTE, branch: "main", from: "default" };
71
+ }
72
+
73
+ /**
74
+ * Refuse `commit` unless it is reachable from `<remote>/<branch>` as this
75
+ * clone knows it. A commit on the local branch only is `release-commit-not-pushed`;
76
+ * any other is `release-commit-not-on-trunk`.
77
+ */
78
+ export function checkOnTrunk(cwd: string, commit: string, trunk: Trunk): void {
79
+ const remoteRef = `refs/remotes/${trunk.remote}/${trunk.branch}`;
80
+ const short = commit.slice(0, 12);
81
+ const named = `${trunk.remote}/${trunk.branch}`;
82
+ const why = trunk.from === "ci.green.branch" ? " (the branch ci.green names)" : trunk.from === "origin/HEAD" ? ` (the branch ${trunk.remote}/HEAD names)` : "";
83
+ const remoteTip = tryGit(cwd, ["rev-parse", "--verify", "--quiet", `${remoteRef}^{commit}`]);
84
+ if (remoteTip && isAncestor(cwd, commit, remoteTip)) return;
85
+ const localTip = tryGit(cwd, ["rev-parse", "--verify", "--quiet", `refs/heads/${trunk.branch}^{commit}`]);
86
+ if (localTip && isAncestor(cwd, commit, localTip)) {
87
+ throw new WorkstationCheckError(
88
+ "release-commit-not-pushed",
89
+ remoteTip
90
+ ? `commit ${short} is on ${trunk.branch}${why} but not on ${named}, so it has not been pushed`
91
+ : `commit ${short} is on ${trunk.branch}${why}, and this clone knows no ${named}, so it can't be shown pushed`,
92
+ `Push it, or fetch if it was pushed elsewhere: git push ${trunk.remote} ${trunk.branch}, or git fetch ${trunk.remote} ${trunk.branch}`,
93
+ );
94
+ }
95
+ throw new WorkstationCheckError(
96
+ "release-commit-not-on-trunk",
97
+ remoteTip
98
+ ? `commit ${short} is not on ${named}${why}, so a release cut from it would ship what trunk never had`
99
+ : `commit ${short} is not on ${trunk.branch}${why}, and this clone knows no ${named}`,
100
+ `Check out a commit of ${named} after fetching it: git fetch ${trunk.remote} ${trunk.branch}`,
101
+ );
102
+ }
103
+
104
+ /** Refuse a tree with a tracked change or an untracked file git does not ignore. */
105
+ export function checkCleanTree(cwd: string, commit: string): void {
106
+ const status = tryGit(cwd, ["status", "--porcelain", "--untracked-files=normal"]);
107
+ if (status === null || status === "") return;
108
+ const lines = status.split("\n");
109
+ const untracked = lines.filter((l) => l.startsWith("??")).map((l) => l.slice(3));
110
+ const tracked = lines.length - untracked.length;
111
+ const parts = [
112
+ ...(tracked > 0 ? [`${tracked} tracked file(s) with uncommitted changes`] : []),
113
+ ...(untracked.length > 0 ? [`${untracked.length} untracked file(s) such as ${untracked[0]}`] : []),
114
+ ];
115
+ throw new WorkstationCheckError(
116
+ "release-worktree-dirty",
117
+ `the working tree has ${parts.join(" and ")}, so a compile here would not be commit ${commit.slice(0, 12)}'s`,
118
+ "Commit, stash or remove them (or add them to .gitignore), or cut from a clean checkout of the commit.",
119
+ );
120
+ }
121
+
122
+ /** Where the actor came from. */
123
+ export type ActorSource = "flag" | "signer" | "ci" | "git" | "user";
124
+
125
+ /** The public key `keyFile` holds or names, as `<type> <base64>`, or null when it can't be read without a passphrase. */
126
+ export function publicKeyOf(keyFile: string): string | null {
127
+ const parse = (text: string): string | null => {
128
+ const [type, blob] = text.trim().split(/\s+/);
129
+ return type && blob && /^(ssh-|ecdsa-|sk-)/.test(type) ? `${type} ${blob}` : null;
130
+ };
131
+ try {
132
+ if (existsSync(keyFile)) {
133
+ const own = parse(readFileSync(keyFile, "utf-8"));
134
+ if (own) return own;
135
+ }
136
+ if (existsSync(`${keyFile}.pub`)) {
137
+ const pub = parse(readFileSync(`${keyFile}.pub`, "utf-8"));
138
+ if (pub) return pub;
139
+ }
140
+ } catch {
141
+ return null;
142
+ }
143
+ const derived = spawnSync("ssh-keygen", ["-y", "-P", "", "-f", keyFile], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
144
+ return derived.status === 0 ? parse(derived.stdout) : null;
145
+ }
146
+
147
+ /** The one principal `signers` lists for `publicKey`, or null for none or several. */
148
+ export function signerPrincipal(publicKey: string, signers: readonly Signer[]): string | null {
149
+ const principals = [...new Set(signers.filter((s) => s.key === publicKey).map((s) => s.principal))];
150
+ return principals.length === 1 ? principals[0] : null;
151
+ }
152
+
153
+ /**
154
+ * Who cut the release, and where the name came from: `--actor`, else the
155
+ * signer the signers file at base lists for the signing key, else the CI's
156
+ * actor (`GITHUB_ACTOR`, `GITLAB_USER_LOGIN`), else git's `user.email`, else
157
+ * `$USER`. Undefined when none is set.
158
+ */
159
+ export function resolveCutActor(req: {
160
+ cwd: string;
161
+ explicit?: string;
162
+ /** The signing key's public half, when the cut is signed and it could be read. */
163
+ publicKey?: string | null;
164
+ signers?: readonly Signer[];
165
+ env?: NodeJS.ProcessEnv;
166
+ }): { actor: string; source: ActorSource } | undefined {
167
+ const env = req.env ?? process.env;
168
+ if (req.explicit) return { actor: req.explicit, source: "flag" };
169
+ if (req.publicKey && req.signers) {
170
+ const principal = signerPrincipal(req.publicKey, req.signers);
171
+ if (principal) return { actor: principal, source: "signer" };
172
+ }
173
+ const ci = env.GITHUB_ACTOR || env.GITLAB_USER_LOGIN;
174
+ if (ci) return { actor: ci, source: "ci" };
175
+ const email = tryGit(req.cwd, ["config", "--get", "user.email"]);
176
+ if (email) return { actor: email, source: "git" };
177
+ if (env.USER) return { actor: env.USER, source: "user" };
178
+ return undefined;
179
+ }
180
+
181
+ /** The tag a labelled release gets: `release/<label>`, or `release/<member>/<label>` for a workspace member. */
182
+ export function releaseTagName(member: string | null, label: string): string {
183
+ return member ? `release/${member}/${label}` : `release/${label}`;
184
+ }
185
+
186
+ /**
187
+ * Whether `tag` can name `commit`: `absent` (the cut creates it), `present`
188
+ * (it already names the commit), or a {@link WorkstationCheckError} for a
189
+ * tag that names another commit or is not a valid tag name.
190
+ */
191
+ export function checkReleaseTag(cwd: string, tag: string, commit: string): "absent" | "present" {
192
+ if (spawnSync("git", ["check-ref-format", `refs/tags/${tag}`], { cwd, stdio: "ignore" }).status !== 0) {
193
+ throw new WorkstationCheckError("release-tag-invalid", `"${tag}" is not a valid git tag name, so the release can't be tagged`, "Pick a label git accepts in a ref name.");
194
+ }
195
+ const at = tryGit(cwd, ["rev-parse", "--verify", "--quiet", `refs/tags/${tag}^{commit}`]);
196
+ if (at === null) return "absent";
197
+ if (at === commit) return "present";
198
+ throw new WorkstationCheckError(
199
+ "release-tag-taken",
200
+ `the tag ${tag} already names commit ${at.slice(0, 12)}, not ${commit.slice(0, 12)}`,
201
+ "Pick another label, or delete the tag if it was made by mistake: git tag -d " + tag,
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Create `tag` on `commit` as an annotated tag whose message names the
207
+ * release. Local only. A clone with no git identity tags as chant, as the ci
208
+ * tags are made.
209
+ */
210
+ export function createReleaseTag(cwd: string, tag: string, commit: string, release: { releaseDigest: string; label: string; member: string | null }): void {
211
+ const ident: string[] = [];
212
+ if (tryGit(cwd, ["config", "user.name"]) === null) ident.push("-c", "user.name=chant");
213
+ if (tryGit(cwd, ["config", "user.email"]) === null) ident.push("-c", "user.email=chant@localhost");
214
+ const message = [`release ${release.label}`, "", `Release-Digest: ${release.releaseDigest}`, ...(release.member ? [`Member: ${release.member}`] : [])].join("\n");
215
+ execFileSync("git", [...ident, "tag", "-a", "-m", message, tag, commit], { cwd, stdio: ["ignore", "ignore", "pipe"] });
216
+ }