@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
@@ -0,0 +1,146 @@
1
+ /**
2
+ * `chant lifecycle gc [--yes] [--push] [--json] [--verbose]` (#4036): prune
3
+ * stored change sets, releases and their templates from `chant/lifecycle`, and
4
+ * run event files from this host, by the retention policy.
5
+ *
6
+ * Without `--yes` it is a dry run: it prints what it would remove and changes
7
+ * nothing. With `--yes` it writes one commit to the local `chant/lifecycle`
8
+ * that removes those files, and deletes the run event files. It pushes only
9
+ * with `--push`. History is never rewritten, so a pruned file stays in the
10
+ * branch's earlier commits. The rules are in ../../lifecycle/retention.ts.
11
+ *
12
+ * The policy is the defaults, overridden by the `retention` block of the
13
+ * workspace declaration at base when the checkout is a workspace. The prune
14
+ * covers the whole branch, every member, from wherever in the repository it
15
+ * runs.
16
+ */
17
+
18
+ import { execFileSync } from "node:child_process";
19
+ import { existsSync } from "node:fs";
20
+ import { dirname, join, resolve } from "node:path";
21
+ import { formatError, formatSuccess, formatWarning } from "../format";
22
+ import type { CommandContext } from "../registry";
23
+ import type { RetentionPolicy } from "../../lifecycle/retention-policy";
24
+
25
+ const USAGE = "chant lifecycle gc [--yes] [--push] [--json] [--verbose]";
26
+
27
+ function tryGit(cwd: string, args: string[]): string | null {
28
+ try {
29
+ return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** Whether a workspace declaration sits in `cwd` or a directory above it, up to the repository's top. */
36
+ function inWorkspace(cwd: string, top: string): boolean {
37
+ for (let dir = cwd; ; dir = dirname(dir)) {
38
+ if (existsSync(join(dir, "chant.workspace.json")) || existsSync(join(dir, "chant.workspace.jsonc"))) return true;
39
+ if (dir === top || dirname(dir) === dir) return false;
40
+ }
41
+ }
42
+
43
+ /** Every commit with a `ci/revoked/<sha>` tag, counted as `chant release cut` counts it. */
44
+ async function revokedCommits(cwd: string, top: string, green: import("../../workspace/declaration").CiGreen | null, workspace: boolean): Promise<Set<string>> {
45
+ if (workspace) {
46
+ const { readCiTags } = await import("../../workspace/ci-green");
47
+ const base = green
48
+ ? (tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${green.branch}^{commit}`]) ??
49
+ tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/heads/${green.branch}^{commit}`]) ??
50
+ tryGit(top, ["rev-parse", "HEAD"]))
51
+ : null;
52
+ return new Set(readCiTags(top, green, base).revoked.keys());
53
+ }
54
+ // With no declaration every tag counts, as readCiTags counts them without `ci.green`.
55
+ const out = tryGit(cwd, ["for-each-ref", "--format=%(refname:lstrip=4)", "refs/tags/ci/revoked/"]) ?? "";
56
+ return new Set(out.split("\n").filter((s) => /^[0-9a-f]{40,64}$/.test(s)));
57
+ }
58
+
59
+ const kb = (bytes: number): string => (bytes >= 1024 * 1024 ? `${(bytes / 1024 / 1024).toFixed(1)} MB` : `${(bytes / 1024).toFixed(1)} KB`);
60
+
61
+ export async function runLifecycleGc(ctx: CommandContext): Promise<number> {
62
+ const { args } = ctx;
63
+ if (args.extraPositional) {
64
+ console.error(formatError({ message: `lifecycle gc takes no argument, and was given "${args.extraPositional}"`, hint: USAGE }));
65
+ return 1;
66
+ }
67
+ const cwd = resolve(".");
68
+ const top = tryGit(cwd, ["rev-parse", "--show-toplevel"]);
69
+ if (!top) {
70
+ console.error(formatError({ message: "lifecycle gc prunes the chant/lifecycle branch, and this directory is not in a git repository", hint: USAGE }));
71
+ return 1;
72
+ }
73
+ const dryRun = args.yes !== true;
74
+
75
+ const { resolveRetention } = await import("../../lifecycle/retention-policy");
76
+ let policy: RetentionPolicy = resolveRetention(null);
77
+ let green: import("../../workspace/declaration").CiGreen | null = null;
78
+ const workspace = inWorkspace(cwd, top);
79
+ if (workspace) {
80
+ const { scopeSource } = await import("../../workspace/write-scope");
81
+ const source = scopeSource(cwd);
82
+ if (!source.declaration) {
83
+ console.error(formatError({ message: `the workspace declaration can't be read, so its retention policy is unknown and nothing is pruned: ${source.failure?.message ?? "no declaration"}` }));
84
+ return 1;
85
+ }
86
+ policy = resolveRetention(source.declaration.retention);
87
+ green = source.declaration.ci?.green ?? null;
88
+ }
89
+
90
+ // Read the remote's facts first: a prune planned on a stale branch could
91
+ // remove what a newer gate binds, and a forked branch must not be pushed over.
92
+ const { fetchLifecycleStatus } = await import("../../lifecycle/git");
93
+ const fetched = await fetchLifecycleStatus({ cwd });
94
+ if (fetched.status === "diverged" || fetched.status === "unreachable") {
95
+ const why = fetched.status === "diverged" ? `the local chant/lifecycle and ${fetched.remote}'s have forked` : `${fetched.remote} did not answer: ${fetched.stderr}`;
96
+ if (!dryRun) {
97
+ console.error(formatError({ message: `nothing was pruned: ${why}`, hint: "Bring chant/lifecycle up to date with its remote, then run again." }));
98
+ return 1;
99
+ }
100
+ if (!args.json) console.error(formatWarning({ message: `planned on the local branch only: ${why}` }));
101
+ }
102
+
103
+ const { runLifecycleGc: run } = await import("../../lifecycle/retention");
104
+ const report = await run({
105
+ cwd,
106
+ policy,
107
+ now: new Date(),
108
+ revokedCommits: await revokedCommits(cwd, top, green, workspace),
109
+ dryRun,
110
+ push: args.push === true,
111
+ });
112
+
113
+ if (args.json) {
114
+ console.log(JSON.stringify(report, null, 2));
115
+ return 0;
116
+ }
117
+
118
+ const lc = report.lifecycle;
119
+ const count = (list: ReadonlyArray<{ kind: string }>, kind: string) => list.filter((f) => f.kind === kind).length;
120
+ const bytes = lc.remove.reduce((n, f) => n + f.bytes, 0);
121
+ const verb = dryRun ? "would remove" : "removed";
122
+ if (!lc.tip) {
123
+ console.error("No chant/lifecycle branch: nothing stored to prune.");
124
+ } else {
125
+ console.log(
126
+ `chant/lifecycle at ${lc.tip.slice(0, 12)}: ${verb} ${count(lc.remove, "change-set")} change set(s), ${count(lc.remove, "release")} release(s) and ${count(lc.remove, "template")} template(s), ${kb(bytes)}; ` +
127
+ `keeps ${count(lc.keep, "change-set")}, ${count(lc.keep, "release")} and ${count(lc.keep, "template")}`,
128
+ );
129
+ console.log(` tree at the tip: ${lc.before.files} file(s), ${kb(lc.before.bytes)} before; ${lc.after.files} file(s), ${kb(lc.after.bytes)} after. The ledgers (${kb(lc.before.ledgers.bytes)}) are never pruned, and history keeps every removed file.`);
130
+ if (args.verbose) for (const f of lc.remove) console.log(` - ${f.path}`);
131
+ }
132
+ const ev = report.runEvents;
133
+ if (ev.dir) {
134
+ console.log(` run events in ${ev.dir}: ${verb} ${ev.remove.length} file(s), ${kb(ev.remove.reduce((n, f) => n + f.bytes, 0))}; keeps ${ev.kept.files}`);
135
+ if (args.verbose) for (const f of ev.remove) console.log(` - ${f.file}`);
136
+ }
137
+ if (dryRun) {
138
+ console.error("Dry run: nothing was removed. Run again with --yes to prune.");
139
+ } else if (lc.commit) {
140
+ console.error(formatSuccess(`Pruned in commit ${lc.commit.slice(0, 12)} on chant/lifecycle`));
141
+ if (lc.pushed) console.error(" pushed");
142
+ else if (lc.pushWarning) console.error(formatWarning({ message: lc.pushWarning }));
143
+ else console.error(" not pushed: run with --push, or push chant/lifecycle yourself");
144
+ }
145
+ return 0;
146
+ }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { mkdtemp, rm } from "node:fs/promises";
10
10
  import { tmpdir } from "node:os";
11
+ import { readFileSync } from "node:fs";
11
12
  import { join } from "node:path";
12
13
  import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
13
14
  import type { DriverComponent } from "../../components/driver";
@@ -58,6 +59,8 @@ vi.mock("../../op/gate", async () => {
58
59
  });
59
60
 
60
61
  const failing = new Set<string>();
62
+ /** Called as a component's apply starts, before it can fail. */
63
+ let onRun: (root: string) => void = () => {};
61
64
  vi.mock("../../components/fan-out-support", async () => {
62
65
  const { CapabilityRegistry } = await import("../../components/capability");
63
66
  const { planFanOut } = await import("../../components/fan-out");
@@ -78,6 +81,7 @@ vi.mock("../../components/fan-out-support", async () => {
78
81
  return {};
79
82
  },
80
83
  async run(_ctx: DeployContext, input: { root: string }) {
84
+ onRun(input.root);
81
85
  if (failing.has(input.root)) throw new Error(`${input.root} failed`);
82
86
  return { outputs: {} };
83
87
  },
@@ -106,6 +110,7 @@ const { checkRunEventLines, stepTrail } = await import("../../workspace/__fixtur
106
110
 
107
111
  let dir: string;
108
112
  let out: string[];
113
+ let errors: string[];
109
114
 
110
115
  const ctx = (a: Partial<ParsedArgs>) =>
111
116
  ({ args: { command: "components", path: "pr-apply", format: "", fix: false, watch: false, verbose: false, help: false, live: false, base: "main", env: "prod", output: dir, eventsJson: true, ...a } as ParsedArgs, plugins: [] }) as never;
@@ -113,10 +118,12 @@ const ctx = (a: Partial<ParsedArgs>) =>
113
118
  beforeEach(async () => {
114
119
  dir = await mkdtemp(join(tmpdir(), "pr-events-"));
115
120
  out = [];
121
+ errors = [];
116
122
  failing.clear();
123
+ onRun = () => {};
117
124
  gateLedger = memoryGateLedgerPort();
118
125
  vi.spyOn(process.stdout, "write").mockImplementation((chunk: string | Uint8Array) => { out.push(String(chunk)); return true; });
119
- vi.spyOn(console, "error").mockImplementation(() => {});
126
+ vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.map(String).join(" ")); });
120
127
  vi.spyOn(console, "log").mockImplementation(() => {});
121
128
  });
122
129
 
@@ -182,6 +189,146 @@ describe("pr-apply --events-json", () => {
182
189
  });
183
190
  });
184
191
 
192
+ /** Plan pull request 12 and approve its digest, as a reviewer would. */
193
+ async function planAndApprove(): Promise<void> {
194
+ await runComponentsPrPlan(ctx({ pr: 12 }));
195
+ const digest = await plannedDigest();
196
+ gateLedger = memoryGateLedgerPort({
197
+ resolutions: [{ version: 1, kind: "resolution", op: "pr-12", gate: "pr-apply", resolvedBy: "github:alice", timestamp: new Date().toISOString(), planDigest: digest }],
198
+ });
199
+ out = [];
200
+ errors = [];
201
+ }
202
+
203
+ /** The job log's wave and component lines, without colour. */
204
+ const logLines = (): string[] =>
205
+ errors.map((l) => l.replace(/\u001b\[[0-9;]*m/g, "")).filter((l) => /^wave \d|^ {2}\S+: /.test(l));
206
+
207
+ describe("pr-apply's job log (#4007)", () => {
208
+ test("without --events-json, a line as each wave starts and as each component settles, in order", async () => {
209
+ await planAndApprove();
210
+ expect(await runComponentsPrApply(ctx({ pr: 12, eventsJson: false }))).toBe(0);
211
+ expect(out).toEqual([]);
212
+ expect(logLines()).toEqual(["wave 1/2: net", " net: applied", "wave 2/2: app", " app: applied"]);
213
+ });
214
+
215
+ test("a failed dependency: the component behind it settles blocked, naming it, in the log and the stream", async () => {
216
+ await planAndApprove();
217
+ failing.add("net");
218
+ expect(await runComponentsPrApply(ctx({ pr: 12 }))).toBe(1);
219
+ expect(logLines()).toEqual(["wave 1/2: net", " net: failed", " app: blocked by net, not run"]);
220
+ const events = checkRunEventLines(out);
221
+ const blocked = events.find((e) => e.type === "component-settle" && e.component === "app");
222
+ expect(blocked).toMatchObject({ type: "component-settle", wave: 2, component: "app", status: "blocked", blockedBy: "net" });
223
+ expect(events.some((e) => e.type === "component-start" && e.component === "app")).toBe(false);
224
+ expect(events.some((e) => e.type === "wave-start" && e.wave === 2)).toBe(false);
225
+ expect(events.at(-1)).toMatchObject({ type: "run-settle", status: "failed" });
226
+ });
227
+
228
+ test("a resumed apply settles what the earlier attempt applied as already-applied, before the gate", async () => {
229
+ await planAndApprove();
230
+ const resume = join(dir, "attempt.json");
231
+ failing.add("app");
232
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
233
+ failing.clear();
234
+ out = [];
235
+ errors = [];
236
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume }))).toBe(0);
237
+ const events = checkRunEventLines(out);
238
+ const settles = events.filter((e) => e.type === "component-settle").map((e) => [e.component, (e as { status: string }).status, (e as { wave: number }).wave]);
239
+ expect(settles).toEqual([["net", "already-applied", 1], ["app", "ok", 1]]);
240
+ const gate = events.findIndex((e) => e.type === "phase-start" && e.phase === "gate");
241
+ expect(events.findIndex((e) => e.type === "component-settle" && e.component === "net")).toBeLessThan(gate);
242
+ expect(logLines()).toEqual([" net: already applied in an earlier attempt", "wave 1/1: app", " app: applied"]);
243
+ });
244
+ });
245
+
246
+ describe("--resume carries only an unchanged input (#4081)", () => {
247
+ test("a component whose input moved since the attempt runs again, and says why", async () => {
248
+ await planAndApprove();
249
+ const resume = join(dir, "attempt.json");
250
+ failing.add("app");
251
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
252
+ expect(JSON.parse(readFileSync(resume, "utf8")).inputs).toEqual({ net: expect.stringMatching(/^jcs1-sha256:/) });
253
+ failing.clear();
254
+ errors = [];
255
+ const step = ESTATE[0]!.deploy[0]!.steps[0] as Record<string, unknown>;
256
+ step.note = "a different change of the same shape";
257
+ const ran: string[] = [];
258
+ onRun = (name) => ran.push(name);
259
+ try {
260
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(0);
261
+ } finally {
262
+ delete step.note;
263
+ }
264
+ expect(ran).toEqual(["net", "app"]);
265
+ expect(errors.join("\n")).toContain("net: changed since the earlier attempt, so it runs again");
266
+ });
267
+
268
+ test("a record without input digests does not carry", async () => {
269
+ await planAndApprove();
270
+ const resume = join(dir, "attempt.json");
271
+ failing.add("app");
272
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(1);
273
+ const { writeFileSync } = await import("node:fs");
274
+ const record = JSON.parse(readFileSync(resume, "utf8"));
275
+ delete record.inputs;
276
+ writeFileSync(resume, JSON.stringify(record));
277
+ failing.clear();
278
+ errors = [];
279
+ const ran: string[] = [];
280
+ onRun = (name) => ran.push(name);
281
+ expect(await runComponentsPrApply(ctx({ pr: 12, resume, eventsJson: false }))).toBe(0);
282
+ expect(ran).toEqual(["net", "app"]);
283
+ expect(errors.join("\n")).toMatch(/does not carry: it does not record what each component applied/);
284
+ });
285
+ });
286
+
287
+ describe("--report-url (#4007)", () => {
288
+ test("refuses a URL it can't upload to, before anything plans", async () => {
289
+ expect(await runComponentsPrPlan(ctx({ pr: 12, reportUrl: "https://example.invalid/reports" }))).toBe(1);
290
+ expect(errors.join("\n")).toMatch(/takes an s3:\/\/<bucket>\/<prefix> or file:\/\/<directory> URL/);
291
+ expect(out).toEqual([]);
292
+ });
293
+
294
+ test("file://: the report, the change set and the events land under <pr>/<commit>/, the events as each wave settles", async () => {
295
+ const { pathToFileURL } = await import("node:url");
296
+ const { readFile } = await import("node:fs/promises");
297
+ const reports = join(dir, "reports");
298
+ const reportUrl = pathToFileURL(reports).href;
299
+ await runComponentsPrPlan(ctx({ pr: 12, reportUrl }));
300
+ const at = join(reports, "12", HEAD);
301
+ expect(JSON.parse(await readFile(join(at, "pr-plan.json"), "utf8"))).toMatchObject({ stage: "plan", pr: 12, status: "planned" });
302
+ expect(checkRunEventLines((await readFile(join(at, "pr-plan.events.jsonl"), "utf8")).split(/(?<=\n)/))[0]).toMatchObject({ type: "run-start" });
303
+
304
+ await planAndApprove();
305
+ // What a reader of the URL sees while wave 2 applies: wave 1 settled.
306
+ let seenMidRun: string[] = [];
307
+ onRun = (root) => {
308
+ if (root === "app") seenMidRun = readFileSync(join(at, "pr-apply.events.jsonl"), "utf8").split(/(?<=\n)/);
309
+ };
310
+ expect(await runComponentsPrApply(ctx({ pr: 12, reportUrl }))).toBe(0);
311
+ const mid = seenMidRun.map((l) => JSON.parse(l) as { type: string; wave?: number; status?: string });
312
+ expect(mid.some((e) => e.type === "wave-settle" && e.wave === 1 && e.status === "ok")).toBe(true);
313
+ expect(mid.some((e) => e.type === "run-settle")).toBe(false);
314
+
315
+ const final = checkRunEventLines((await readFile(join(at, "pr-apply.events.jsonl"), "utf8")).split(/(?<=\n)/));
316
+ expect(final.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
317
+ expect(final.map((e) => e.type).join("\n")).toBe(out.map((l) => JSON.parse(l).type).join("\n"));
318
+ expect(JSON.parse(await readFile(join(at, "pr-apply.json"), "utf8"))).toMatchObject({ stage: "apply", status: "applied" });
319
+ expect(JSON.parse(await readFile(join(at, "change-set.json"), "utf8"))).toHaveProperty("digest");
320
+ expect(errors.join("\n")).toContain(`report : ${reportUrl}/12/${HEAD}/`);
321
+ });
322
+
323
+ test("without --events-json, only the report and the change set are copied", async () => {
324
+ const { pathToFileURL } = await import("node:url");
325
+ const { readdir } = await import("node:fs/promises");
326
+ const reports = join(dir, "reports");
327
+ await runComponentsPrPlan(ctx({ pr: 12, reportUrl: pathToFileURL(reports).href, eventsJson: false }));
328
+ expect((await readdir(join(reports, "12", HEAD))).sort()).toEqual(["change-set.json", "pr-plan.json"]);
329
+ });
330
+ });
331
+
185
332
  describe("--events-json beside --json", () => {
186
333
  test("is refused on pr-plan and pr-apply before anything plans, since both print to stdout", async () => {
187
334
  const errors: string[] = [];
@@ -33,7 +33,15 @@
33
33
  * one `run-event.schema.json` line each: the planning as a `plan` phase with
34
34
  * a step per component, the gate's decision as a `gate` phase, and on
35
35
  * pr-apply the apply's waves and components as `chant run --components`
36
- * streams them.
36
+ * streams them. A component the apply does not start settles too, `blocked`
37
+ * behind a failed dependency or `already-applied` on a resumed apply.
38
+ * With or without it, pr-apply prints a line on stderr as each wave starts
39
+ * and as each component settles, so a job log shows the waves in order.
40
+ *
41
+ * `--report-url <s3 or file URL>` (#4007) copies `pr-<stage>.json`,
42
+ * `change-set.json` and, with `--events-json`, the events to
43
+ * `<url>/<pr>/<commit>/`, the events again as each wave and component
44
+ * settles (`../../pr-report-url.ts`).
37
45
  *
38
46
  * `--member <name>` (#3465) runs a workspace member's loop: the member's
39
47
  * generated pipeline passes it from the member's directory. The gate is
@@ -59,11 +67,13 @@ import {
59
67
  type PrPlanSet,
60
68
  } from "../../components/pr-run";
61
69
  import { readFanOutAttempt, writeFanOutAttempt, type FanOutAttempt, type PrApplyRecord } from "../../components/fan-out-record";
70
+ import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs";
62
71
  import { samePlanDigest } from "../../lifecycle/plan-digest";
63
72
  import { gitGateLedgerPort, type GateCheck } from "../../op/gate";
64
73
  import { emitGateStep, gateEventsOf, GATE_PHASE, type RunEventSink, type RunEventStream } from "../../run-event-stream";
65
- import type { RunEventSettleStatus } from "../../run-event";
74
+ import type { RunEventBody, RunEventComponentSettle, RunEventSettleStatus } from "../../run-event";
66
75
  import { openCliRunEvents, refusesEventsWithJson } from "../run-events";
76
+ import { openPrReportUpload, parseReportUrl, reportKey, type PrReportUpload, type ReportTarget } from "../../pr-report-url";
67
77
  import {
68
78
  describePlanChanged,
69
79
  PR_APPLY_GATE,
@@ -262,12 +272,18 @@ function settleOf(status: PrReport["status"]): RunEventSettleStatus {
262
272
  }
263
273
  }
264
274
 
265
- /** Write the report, the change set and the note; print the note's path. */
266
- function writeOutputs(ctx: CommandContext, report: PrReport, note: string): void {
275
+ /** Write the report, the change set and the note, and hand the first two to `--report-url`'s upload. */
276
+ function writeOutputs(ctx: CommandContext, report: PrReport, note: string, upload?: PrReportUpload): void {
267
277
  const dir = resolve(ctx.args.output ?? PR_REPORT_DIR);
268
278
  mkdirSync(dir, { recursive: true });
269
- writeFileSync(join(dir, `pr-${report.stage}.json`), JSON.stringify(report, null, 2) + "\n");
270
- writeFileSync(join(dir, "change-set.json"), JSON.stringify(report.changeSet, null, 2) + "\n");
279
+ const files: Array<[string, string]> = [
280
+ [`pr-${report.stage}.json`, JSON.stringify(report, null, 2) + "\n"],
281
+ ["change-set.json", JSON.stringify(report.changeSet, null, 2) + "\n"],
282
+ ];
283
+ for (const [name, body] of files) {
284
+ writeFileSync(join(dir, name), body);
285
+ upload?.file(name, body);
286
+ }
271
287
  writeFileSync(join(dir, "pr-note.md"), note);
272
288
  }
273
289
 
@@ -310,21 +326,32 @@ function printMembers(report: PrReport): void {
310
326
  }
311
327
  }
312
328
 
313
- async function finish(ctx: CommandContext, p: Prepared, report: PrReport): Promise<void> {
329
+ async function finish(ctx: CommandContext, p: Prepared, report: PrReport, upload?: PrReportUpload): Promise<void> {
314
330
  const note = noteFor(report, p.forge);
315
- writeOutputs(ctx, report, note);
331
+ writeOutputs(ctx, report, note, upload);
316
332
  await publish(p.forge, report, note);
317
333
  if (ctx.args.json) console.log(JSON.stringify(report, null, 2));
318
334
  }
319
335
 
320
- /** A stage's stream, held so that a stage that returns or throws before it settles still ends it. */
336
+ /**
337
+ * A stage's stream and its `--report-url` upload, held so that a stage that
338
+ * returns or throws before it settles still ends the stream and uploads.
339
+ */
321
340
  interface StageEvents {
322
341
  stream?: RunEventStream;
342
+ /** Where `--report-url` points, read before the stage starts. */
343
+ target?: ReportTarget;
344
+ upload?: PrReportUpload;
323
345
  }
324
346
 
325
- /** Run a stage, and end its stream as `failed` when the stage did not settle it. */
326
- async function withStageEvents(stage: string, run: (events: StageEvents) => Promise<number>): Promise<number> {
347
+ /** Run a stage, end its stream as `failed` when the stage did not settle it, and finish its upload. */
348
+ async function withStageEvents(stage: string, ctx: CommandContext, run: (events: StageEvents) => Promise<number>): Promise<number> {
327
349
  const events: StageEvents = {};
350
+ if (ctx.args.reportUrl !== undefined) {
351
+ const target = parseReportUrl(ctx.args.reportUrl);
352
+ if ("error" in target) return fail(target.error);
353
+ events.target = target;
354
+ }
328
355
  try {
329
356
  return await run(events);
330
357
  } catch (err) {
@@ -332,13 +359,32 @@ async function withStageEvents(stage: string, run: (events: StageEvents) => Prom
332
359
  throw err;
333
360
  } finally {
334
361
  events.stream?.settle("failed", `${stage} stopped before it finished`);
362
+ if (events.upload) {
363
+ await events.upload.close();
364
+ console.error(formatInfo(`report : ${events.upload.where}/`));
365
+ }
335
366
  }
336
367
  }
337
368
 
338
- /** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] */
369
+ /**
370
+ * Open the stage's `--report-url` upload under its pull request and commit,
371
+ * when the flag was given. Its events are the lines `openCliRunEvents` writes.
372
+ */
373
+ function openUpload(events: StageEvents, stage: "plan" | "apply", pr: number | null, p: Prepared): PrReportUpload | undefined {
374
+ if (!events.target) return undefined;
375
+ events.upload = openPrReportUpload({
376
+ target: events.target,
377
+ key: reportKey(pr, p.head, p.member),
378
+ stage,
379
+ warn: (message) => console.error(formatWarning({ message })),
380
+ });
381
+ return events.upload;
382
+ }
383
+
384
+ /** chant components pr-plan --base <ref> --pr <n> [--env <env>] [--gate <name>] [--forge <kind>] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
339
385
  export function runComponentsPrPlan(ctx: CommandContext): Promise<number> {
340
386
  if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
341
- return withStageEvents("pr-plan", (events) => prPlan(ctx, events));
387
+ return withStageEvents("pr-plan", ctx, (events) => prPlan(ctx, events));
342
388
  }
343
389
 
344
390
  async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number> {
@@ -348,8 +394,9 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
348
394
  const p = await prepare(ctx, "plan");
349
395
  if (typeof p === "number") return p;
350
396
  const op = prOp(ctx.args.pr, p.member);
397
+ const upload = openUpload(stage, "plan", ctx.args.pr, p);
351
398
  // `--events-json` (#4041): planning, then where the approval stands.
352
- const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") });
399
+ const events = await openCliRunEvents(ctx.args, { kind: "component", name: `pr-plan ${op}`, cwd: resolve(".") }, upload?.line);
353
400
  stage.stream = events;
354
401
  events?.emit({ type: "run-start", waves: p.plan.waves });
355
402
  const set = await planAll(p, events?.emit);
@@ -381,7 +428,7 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
381
428
  approval,
382
429
  status,
383
430
  });
384
- await finish(ctx, p, report);
431
+ await finish(ctx, p, report, upload);
385
432
  // The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
386
433
  events?.settle(settleOf(status), status === "plan-failed" ? "a member failed to plan" : undefined);
387
434
 
@@ -394,10 +441,10 @@ async function prPlan(ctx: CommandContext, stage: StageEvents): Promise<number>
394
441
  return status === "plan-failed" ? 1 : 0;
395
442
  }
396
443
 
397
- /** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] */
444
+ /** chant components pr-apply --base <ref> [--pr <n>] [--env <env>] [--gate <name>] [--forge <kind>] [--require-review] [--output <dir>] [--json] [--events-json] [--report-url <url>] */
398
445
  export function runComponentsPrApply(ctx: CommandContext): Promise<number> {
399
446
  if (refusesEventsWithJson(ctx.args, "the stage's report")) return Promise.resolve(1);
400
- return withStageEvents("pr-apply", (events) => prApply(ctx, events));
447
+ return withStageEvents("pr-apply", ctx, (events) => prApply(ctx, events));
401
448
  }
402
449
 
403
450
  async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number> {
@@ -418,7 +465,8 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
418
465
  // The run id the release records carry, chosen before the run so that
419
466
  // `--events-json` (#4041, #4007) names the run by it too.
420
467
  const release = autoReleaseRun(`${op}-${Date.now()}`);
421
- const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") });
468
+ const upload = openUpload(stage, "apply", pr, p);
469
+ const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
422
470
  stage.stream = events;
423
471
  events?.emit({ type: "run-start", waves: p.plan.waves });
424
472
 
@@ -434,15 +482,28 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
434
482
  return fail(`--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}`);
435
483
  }
436
484
  }
437
- const resumed = pr !== null && p.plan.order.length > 0 && attempt ? await tryResume(p, attempt, args.resume!, op) : undefined;
485
+ // What each component would apply now (#4081): progress carries only where it is unchanged.
486
+ const inputsNow = resumePath ? componentInputDigests(p.components, { projectRoot: resolve("."), env: p.env }) : {};
487
+ const resumed = pr !== null && p.plan.order.length > 0 && attempt ? await tryResume(p, attempt, args.resume!, op, inputsNow) : undefined;
438
488
 
439
- if (resumed) emitResumedGate(events, op, p.gate, resumed.check);
489
+ // The components an earlier attempt applied settle first (#4007), each in
490
+ // the wave the derivation gave it, which is the run-start's.
491
+ const log = applyLog();
492
+ if (resumed) {
493
+ const waveOf = new Map(p.plan.waves.flatMap((wave, i) => wave.map((name) => [name, i + 1] as const)));
494
+ for (const component of p.plan.order.filter((name) => resumed.completed.includes(name))) {
495
+ const body: RunEventComponentSettle = { type: "component-settle", wave: waveOf.get(component)!, component, status: "already-applied" };
496
+ events?.emit(body);
497
+ log(body);
498
+ }
499
+ emitResumedGate(events, op, p.gate, resumed.check);
500
+ }
440
501
  const set = resumed?.set ?? (await planAll(p, events?.emit));
441
502
  const base = { stage: "apply" as const, pr, env: p.env, ...(p.member ? { member: p.member } : {}), base: p.base, head: p.head, op, gate: p.gate, plan: p.plan, signal: p.signal, set };
442
503
 
443
504
  const stop = async (report: PrReport, code: number, settle?: RunEventSettleStatus): Promise<number> => {
444
505
  events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
445
- await finish(ctx, p, report);
506
+ await finish(ctx, p, report, upload);
446
507
  console.error((code === 0 ? formatInfo : (m: string) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
447
508
  if (report.message) console.error(report.message);
448
509
  printMembers(report);
@@ -538,6 +599,9 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
538
599
  // one settles, so a run killed partway still knows what applied.
539
600
  const prior = resumed ? { completed: resumed.completed, outputs: resumed.outputs } : { completed: [], outputs: {} };
540
601
  const completedSoFar = new Set<string>(prior.completed);
602
+ const inputsSoFar: Record<string, string> = Object.fromEntries(
603
+ prior.completed.filter((name) => inputsNow[name] !== undefined).map((name) => [name, inputsNow[name]!]),
604
+ );
541
605
  const failedSoFar: string[] = [];
542
606
  const outputsSoFar: Record<string, Record<string, unknown>> = { ...prior.outputs };
543
607
  const record: PrApplyRecord = {
@@ -557,6 +621,7 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
557
621
  completed: [...completedSoFar].sort(),
558
622
  failed: [...failedSoFar].sort(),
559
623
  outputs: outputsSoFar,
624
+ inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
560
625
  prApply: record,
561
626
  });
562
627
  };
@@ -569,13 +634,18 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
569
634
  env: p.env,
570
635
  set,
571
636
  ...(resumed ? { resumed: prior } : {}),
572
- // The apply is this run's waves, so the fan-out's own run framing is dropped.
573
- ...(events ? { onRunEvent: events.nested } : {}),
637
+ // The apply is this run's waves, so the fan-out's own run framing is
638
+ // dropped. The log's lines go to stderr with or without --events-json.
639
+ onRunEvent: (body: RunEventBody) => {
640
+ events?.nested(body);
641
+ log(body);
642
+ },
574
643
  ...(resumePath
575
644
  ? {
576
645
  onComponentSettled: (settled, outputs) => {
577
646
  if (settled.status === "ok") {
578
647
  completedSoFar.add(settled.component);
648
+ if (inputsNow[settled.component] !== undefined) inputsSoFar[settled.component] = inputsNow[settled.component]!;
579
649
  if (outputs) outputsSoFar[settled.component] = outputs;
580
650
  } else if (settled.status === "fail") {
581
651
  failedSoFar.push(settled.component);
@@ -600,6 +670,37 @@ async function prApply(ctx: CommandContext, stage: StageEvents): Promise<number>
600
670
  return code;
601
671
  }
602
672
 
673
+ /** What a component's settle says in the job log. */
674
+ function settleWords(body: RunEventComponentSettle): string {
675
+ switch (body.status) {
676
+ case "ok":
677
+ return "applied";
678
+ case "failed":
679
+ return "failed";
680
+ case "gated":
681
+ return "stopped at its own gate";
682
+ case "waiting":
683
+ return "waiting on a decision";
684
+ case "blocked":
685
+ return `blocked by ${body.blockedBy ?? "a dependency"}, not run`;
686
+ case "already-applied":
687
+ return "already applied in an earlier attempt";
688
+ }
689
+ }
690
+
691
+ /**
692
+ * pr-apply's job log (#4007): a line on stderr as each wave starts and as
693
+ * each component settles, so the log shows the waves in order while they run.
694
+ */
695
+ function applyLog(): (body: RunEventBody) => void {
696
+ let waves = 0;
697
+ return (body) => {
698
+ if (body.type === "run-start") waves = body.waves?.length ?? 0;
699
+ else if (body.type === "wave-start") console.error(formatInfo(`wave ${body.wave}${waves ? `/${waves}` : ""}: ${body.components.join(", ")}`));
700
+ else if (body.type === "component-settle") console.error(formatInfo(` ${body.component}: ${settleWords(body)}`));
701
+ };
702
+ }
703
+
603
704
  /** A resumed apply's gate (#3464), decided against the approved digest, as the run's `gate` phase. */
604
705
  function emitResumedGate(events: RunEventStream | undefined, op: string, gate: string, check: GateCheck): void {
605
706
  if (!events) return;
@@ -658,8 +759,13 @@ interface Resumed {
658
759
  * derivation; when its approval still stands; and when every member left is
659
760
  * covered by the approved set. Otherwise it says why on stderr and the apply
660
761
  * runs as it would with no record.
762
+ *
763
+ * Of the components it applied, only those whose input digest is the same
764
+ * now carry (#4081), as in `components fan-out --resume`. One that changed
765
+ * runs again, under the same approval when the approved set covers its new
766
+ * plan. A record without input digests carries nothing.
661
767
  */
662
- async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op: string): Promise<Resumed | undefined> {
768
+ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op: string, inputsNow: Record<string, string>): Promise<Resumed | undefined> {
663
769
  const skip = (why: string): undefined => {
664
770
  console.error(formatWarning({ message: `--resume: "${file}" does not carry: ${why}`, hint: "Every member is planned again, and the plan needs an approval of its own." }));
665
771
  return undefined;
@@ -670,6 +776,11 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
670
776
  if (record.head !== p.head) return skip(`it records an apply of ${record.head.slice(0, 12)}, not ${p.head.slice(0, 12)}`);
671
777
  if (!samePlanDigest(attempt.digest, p.plan.digest)) return skip("the change selects other components than it did");
672
778
  if (attempt.completed.length === 0) return undefined;
779
+ const split = progressThatCarries(attempt, inputsNow);
780
+ if (split.unrecorded) return skip("it does not record what each component applied, since it was written before chant recorded component inputs");
781
+ for (const name of split.changed) console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
782
+ if (split.carried.length === 0) return undefined;
783
+ const completed = split.carried;
673
784
 
674
785
  let check;
675
786
  try {
@@ -679,7 +790,7 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
679
790
  }
680
791
  if (!check.satisfied) return skip(`no approval stands for the plan it applied (${record.digest}) any more`);
681
792
 
682
- const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => attempt.completed.includes(name)));
793
+ const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => completed.includes(name)));
683
794
  const resumed = await resumePrSet({
684
795
  components: p.components,
685
796
  plan: p.plan,
@@ -687,9 +798,9 @@ async function tryResume(p: Prepared, attempt: FanOutAttempt, file: string, op:
687
798
  env: p.env,
688
799
  seededOutputs: p.seededOutputs,
689
800
  record,
690
- completed: attempt.completed,
801
+ completed,
691
802
  });
692
803
  if (!resumed.ok) return skip(`what is left is not what was approved. ${resumed.reasons.join("; ")}`);
693
- console.error(formatInfo(`resume : ${attempt.completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
694
- return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed: attempt.completed, outputs };
804
+ console.error(formatInfo(`resume : ${completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
805
+ return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed, outputs };
695
806
  }