@intentius/chant 0.124.30 → 0.124.32

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 (346) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +74 -12
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +65 -0
  41. package/dist/cli/registry.d.ts +29 -2
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/local-executor.d.ts +12 -0
  111. package/dist/op/local-executor.d.ts.map +1 -1
  112. package/dist/op/local-executor.js +22 -3
  113. package/dist/op/op-waves-run.d.ts.map +1 -1
  114. package/dist/op/op-waves-run.js +20 -5
  115. package/dist/op/operator.d.ts +2 -2
  116. package/dist/op/operator.js +2 -2
  117. package/dist/op/redact.d.ts +34 -0
  118. package/dist/op/redact.d.ts.map +1 -0
  119. package/dist/op/redact.js +94 -0
  120. package/dist/op/step-console.d.ts +2 -0
  121. package/dist/op/step-console.d.ts.map +1 -1
  122. package/dist/op/step-console.js +5 -2
  123. package/dist/op/steward.d.ts +6 -4
  124. package/dist/op/steward.d.ts.map +1 -1
  125. package/dist/op/steward.js +5 -3
  126. package/dist/op/work-lease-run.d.ts +20 -7
  127. package/dist/op/work-lease-run.d.ts.map +1 -1
  128. package/dist/op/work-lease-run.js +56 -12
  129. package/dist/pr-forge.d.ts +27 -0
  130. package/dist/pr-forge.d.ts.map +1 -1
  131. package/dist/pr-forge.js +51 -6
  132. package/dist/pr-loop.d.ts +141 -4
  133. package/dist/pr-loop.d.ts.map +1 -1
  134. package/dist/pr-loop.js +122 -6
  135. package/dist/pr-report-url.d.ts +10 -2
  136. package/dist/pr-report-url.d.ts.map +1 -1
  137. package/dist/pr-report-url.js +52 -6
  138. package/dist/ts-loader.d.ts +38 -4
  139. package/dist/ts-loader.d.ts.map +1 -1
  140. package/dist/ts-loader.js +190 -16
  141. package/dist/wave-runtime.mjs +208 -208
  142. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  143. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  144. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  145. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  146. package/dist/workspace/chant-migrations.js +2 -0
  147. package/dist/workspace/checks/boxes.d.ts +6 -6
  148. package/dist/workspace/checks/boxes.js +9 -9
  149. package/dist/workspace/ci-green-forge.d.ts +6 -4
  150. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  151. package/dist/workspace/ci-green.d.ts +28 -12
  152. package/dist/workspace/ci-green.d.ts.map +1 -1
  153. package/dist/workspace/ci-green.js +49 -21
  154. package/dist/workspace/ci-list.schema.json +12 -6
  155. package/dist/workspace/ci-report-url.d.ts +18 -0
  156. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  157. package/dist/workspace/ci-report-url.js +33 -0
  158. package/dist/workspace/ci-run-index.d.ts +108 -0
  159. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  160. package/dist/workspace/ci-run-index.js +164 -0
  161. package/dist/workspace/ci-run-index.schema.json +52 -0
  162. package/dist/workspace/ci-run.d.ts +159 -9
  163. package/dist/workspace/ci-run.d.ts.map +1 -1
  164. package/dist/workspace/ci-run.js +419 -50
  165. package/dist/workspace/ci-runner-key.d.ts +133 -0
  166. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  167. package/dist/workspace/ci-runner-key.js +237 -0
  168. package/dist/workspace/ci-secrets.d.ts +65 -0
  169. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  170. package/dist/workspace/ci-secrets.js +211 -0
  171. package/dist/workspace/ci-serve.d.ts +67 -4
  172. package/dist/workspace/ci-serve.d.ts.map +1 -1
  173. package/dist/workspace/ci-serve.js +240 -35
  174. package/dist/workspace/declaration.d.ts +8 -0
  175. package/dist/workspace/declaration.d.ts.map +1 -1
  176. package/dist/workspace/declaration.js +11 -2
  177. package/dist/workspace/declaration.schema.json +7 -2
  178. package/dist/workspace/export.d.ts +1 -1
  179. package/dist/workspace/export.d.ts.map +1 -1
  180. package/dist/workspace/export.js +7 -3
  181. package/dist/workspace/ls.d.ts +6 -1
  182. package/dist/workspace/ls.d.ts.map +1 -1
  183. package/dist/workspace/ls.js +1 -0
  184. package/dist/workspace/ls.schema.json +7 -1
  185. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  186. package/dist/workspace/pin-integrity.js +3 -2
  187. package/dist/workspace/pr-report.schema.json +74 -7
  188. package/dist/workspace/reason-codes.d.ts +7 -3
  189. package/dist/workspace/reason-codes.d.ts.map +1 -1
  190. package/dist/workspace/reason-codes.js +9 -4
  191. package/dist/workspace/run-event.schema.json +3 -1
  192. package/dist/workspace/status.schema.json +3 -1
  193. package/dist/workspace/trust/trust-write.d.ts +109 -0
  194. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  195. package/dist/workspace/trust/trust-write.js +320 -0
  196. package/dist/workspace/trust-write.schema.json +265 -0
  197. package/dist/workspace/write-scope.d.ts +9 -0
  198. package/dist/workspace/write-scope.d.ts.map +1 -1
  199. package/dist/workspace/write-scope.js +11 -1
  200. package/package.json +1 -1
  201. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  202. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  203. package/src/cli/commands/audit.test.ts +2 -2
  204. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  205. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  206. package/src/cli/commands/check-lexicon.test.ts +1 -1
  207. package/src/cli/commands/check-lexicon.ts +16 -11
  208. package/src/cli/commands/import.ts +40 -0
  209. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  210. package/src/cli/commands/init-lexicon.test.ts +8 -5
  211. package/src/cli/commands/onboard.test.ts +45 -0
  212. package/src/cli/commands/onboard.ts +8 -4
  213. package/src/cli/commands/update.test.ts +6 -0
  214. package/src/cli/commands/update.ts +4 -1
  215. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  216. package/src/cli/dependencies.e2e.test.ts +101 -0
  217. package/src/cli/dependencies.test.ts +93 -0
  218. package/src/cli/dependencies.ts +122 -0
  219. package/src/cli/handlers/build.test.ts +69 -0
  220. package/src/cli/handlers/build.ts +20 -4
  221. package/src/cli/handlers/fan-out.test.ts +73 -0
  222. package/src/cli/handlers/fan-out.ts +11 -4
  223. package/src/cli/handlers/graph.test.ts +76 -0
  224. package/src/cli/handlers/graph.ts +19 -12
  225. package/src/cli/handlers/lint.test.ts +42 -0
  226. package/src/cli/handlers/lint.ts +6 -0
  227. package/src/cli/handlers/operator.ts +3 -3
  228. package/src/cli/handlers/pr-waves.test.ts +449 -0
  229. package/src/cli/handlers/pr.test.ts +134 -0
  230. package/src/cli/handlers/pr.ts +523 -31
  231. package/src/cli/handlers/promote.test.ts +26 -1
  232. package/src/cli/handlers/promote.ts +3 -2
  233. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  234. package/src/cli/handlers/run-signer.test.ts +79 -0
  235. package/src/cli/handlers/run.ts +34 -16
  236. package/src/cli/main.test.ts +19 -0
  237. package/src/cli/main.ts +73 -12
  238. package/src/cli/parse-args.ts +48 -0
  239. package/src/cli/registry.ts +35 -4
  240. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  241. package/src/codegen/lexicon-regen.ts +1 -1
  242. package/src/codegen/pinned-upgrade.ts +2 -2
  243. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  244. package/src/components/capability.ts +51 -3
  245. package/src/components/changed-files.test.ts +16 -1
  246. package/src/components/changed-files.ts +9 -1
  247. package/src/components/driver.test.ts +36 -1
  248. package/src/components/driver.ts +76 -4
  249. package/src/components/fan-out-record.ts +29 -1
  250. package/src/components/fan-out-run.test.ts +35 -0
  251. package/src/components/fan-out-run.ts +40 -1
  252. package/src/components/pr-pipeline.test.ts +100 -0
  253. package/src/components/pr-pipeline.ts +132 -17
  254. package/src/components/pr-run.test.ts +323 -18
  255. package/src/components/pr-run.ts +354 -75
  256. package/src/components/wave-plan.ts +16 -4
  257. package/src/discovery/fold-host-method.test.ts +87 -0
  258. package/src/discovery/fold-import.ts +15 -5
  259. package/src/discovery/fold-package-composite.test.ts +27 -0
  260. package/src/discovery/fold-specifiers.test.ts +8 -0
  261. package/src/discovery/sandbox/bundle.ts +6 -0
  262. package/src/discovery/specifier-resolve.ts +8 -2
  263. package/src/fold/fold.test.ts +72 -0
  264. package/src/fold/fold.ts +71 -0
  265. package/src/fold/subset.test.ts +31 -0
  266. package/src/fold/subset.ts +99 -0
  267. package/src/lexicon-dist.e2e.test.ts +254 -0
  268. package/src/lexicon-module.test.ts +47 -0
  269. package/src/lexicon-module.ts +49 -7
  270. package/src/lexicon-plugin-helpers.ts +6 -4
  271. package/src/lexicon.ts +2 -0
  272. package/src/lifecycle/affected.test.ts +7 -0
  273. package/src/lifecycle/affected.ts +6 -1
  274. package/src/lifecycle/release-ledger.ts +34 -0
  275. package/src/lifecycle/work-dir.test.ts +77 -0
  276. package/src/lifecycle/work-dir.ts +82 -0
  277. package/src/lint/discover.test.ts +46 -2
  278. package/src/lint/discover.ts +55 -49
  279. package/src/module-source.test.ts +85 -0
  280. package/src/module-source.ts +112 -0
  281. package/src/op/activities/factory.ts +1 -1
  282. package/src/op/discover-root.test.ts +31 -2
  283. package/src/op/discover.ts +14 -4
  284. package/src/op/gate.test.ts +30 -0
  285. package/src/op/gate.ts +3 -0
  286. package/src/op/local-executor.ts +31 -3
  287. package/src/op/op-waves-run.ts +20 -5
  288. package/src/op/op-waves.test.ts +49 -0
  289. package/src/op/operator.ts +2 -2
  290. package/src/op/redact.ts +93 -0
  291. package/src/op/step-console.ts +7 -2
  292. package/src/op/steward.ts +6 -4
  293. package/src/op/work-lease-run.test.ts +88 -2
  294. package/src/op/work-lease-run.ts +52 -12
  295. package/src/pr-forge-bundle.test.ts +2 -2
  296. package/src/pr-forge.test.ts +71 -0
  297. package/src/pr-forge.ts +70 -6
  298. package/src/pr-loop-bundle.test.ts +2 -1
  299. package/src/pr-loop.test.ts +117 -1
  300. package/src/pr-loop.ts +252 -10
  301. package/src/pr-report-url.ts +58 -6
  302. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  303. package/src/ts-loader.test.ts +89 -1
  304. package/src/ts-loader.ts +180 -13
  305. package/src/workspace/changes.test.ts +1 -1
  306. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  307. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  308. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  309. package/src/workspace/chant-migrations.ts +2 -0
  310. package/src/workspace/checks/boxes.ts +9 -9
  311. package/src/workspace/ci-green-forge.ts +6 -4
  312. package/src/workspace/ci-green-runs.test.ts +53 -11
  313. package/src/workspace/ci-green.test.ts +1 -1
  314. package/src/workspace/ci-green.ts +50 -22
  315. package/src/workspace/ci-list.schema.json +12 -6
  316. package/src/workspace/ci-report-url.test.ts +220 -0
  317. package/src/workspace/ci-report-url.ts +33 -0
  318. package/src/workspace/ci-run-index.schema.json +52 -0
  319. package/src/workspace/ci-run-index.test.ts +392 -0
  320. package/src/workspace/ci-run-index.ts +206 -0
  321. package/src/workspace/ci-run.test.ts +520 -42
  322. package/src/workspace/ci-run.ts +483 -51
  323. package/src/workspace/ci-runner-key.test.ts +204 -0
  324. package/src/workspace/ci-runner-key.ts +275 -0
  325. package/src/workspace/ci-secrets.test.ts +106 -0
  326. package/src/workspace/ci-secrets.ts +213 -0
  327. package/src/workspace/ci-serve.test.ts +282 -39
  328. package/src/workspace/ci-serve.ts +231 -34
  329. package/src/workspace/declaration.schema.json +7 -2
  330. package/src/workspace/declaration.test.ts +23 -0
  331. package/src/workspace/declaration.ts +19 -3
  332. package/src/workspace/export.ts +6 -3
  333. package/src/workspace/ls.schema.json +7 -1
  334. package/src/workspace/ls.ts +7 -2
  335. package/src/workspace/pin-integrity.test.ts +5 -0
  336. package/src/workspace/pin-integrity.ts +3 -2
  337. package/src/workspace/pr-report.schema.json +74 -7
  338. package/src/workspace/reason-codes.test.ts +2 -0
  339. package/src/workspace/reason-codes.ts +9 -4
  340. package/src/workspace/run-event.schema.json +3 -1
  341. package/src/workspace/status.schema.json +3 -1
  342. package/src/workspace/trust/trust-write.test.ts +179 -0
  343. package/src/workspace/trust/trust-write.ts +377 -0
  344. package/src/workspace/trust-write.schema.json +265 -0
  345. package/src/workspace/write-scope.test.ts +29 -1
  346. package/src/workspace/write-scope.ts +12 -1
@@ -6,6 +6,7 @@
6
6
  * chant components pr-plan --base <ref> --pr <n> --env <env> [--forge github|gitlab|forgejo]
7
7
  * chant approve pr-<n> pr-apply --plan <digest> --approver github:<login> --sign
8
8
  * chant components pr-apply --base <ref> --env <env> [--pr <n>] [--forge <kind>] [--require-review] [--resume <file>]
9
+ * [--wave-gate <gate> [--wave <k>]]
9
10
  * ```
10
11
  *
11
12
  * Both measure the change from the merge base of `--base` and `HEAD`, select
@@ -27,6 +28,13 @@
27
28
  * approval still stands, applies the rest without a fresh approval. When
28
29
  * they are not covered it says why and runs as it would without a record.
29
30
  *
31
+ * `--wave-gate <gate>` (#4003) applies the approved set a wave at a time,
32
+ * each wave planned after the one before it applied (`prApplyWaves`). A
33
+ * wave whose plan the approved change set covers applies under the pull
34
+ * request's approval; one whose plan moved waits at `<gate>-wave-<k>` under
35
+ * the pull request's op (exit 3). `--wave <k>` runs one wave, reading the
36
+ * approved set and the waves that applied from the `--resume` record.
37
+ *
30
38
  * Every run writes `pr-plan.json` or `pr-apply.json` (`pr-report.schema.json`),
31
39
  * `change-set.json` and `pr-note.md` under `--output` (default `.chant/pr`). *
32
40
  * `--events-json` (#4041, #4007) streams the stage's run events to stdout,
@@ -48,13 +56,21 @@
48
56
  * recorded under `pr-<n>-<member>`, and the note and the statuses carry the
49
57
  * member's name, so each member's loop on one pull request stands apart.
50
58
  * The name must be the member that owns the current directory.
59
+ *
60
+ * `--no-status` (#4161) posts neither the stage's status nor its note: the
61
+ * report and the note are written under `--output` only. `chant ci run
62
+ * --local` passes it. pr-plan then builds no forge client, since posting is
63
+ * all it uses one for; pr-apply still reads the pull request and its reviews
64
+ * through `--forge`.
51
65
  */
52
66
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
53
67
  import { join, resolve } from "node:path";
54
68
  import { loadChantConfig, resolveAutoReleaseDisabled } from "../../config.js";
55
69
  import { resolveCommit, resolveMergeBase } from "../../components/changed-files.js";
56
70
  import { deriveFanOut, fanOutRegistry } from "../../components/fan-out-support.js";
57
- import { applyPrSet, approvalOf, approversWithoutReview, decidePrGate, planPrSet, prReport, readOnlyLedger, resumePrSet, } from "../../components/pr-run.js";
71
+ import { applyPrSet, approvalOf, approversWhoAuthored, approversWithoutReview, decidePrGate, planPrSet, prReport, readOnlyLedger, prWavePlan, resumePrSet, waveNotCovered, } from "../../components/pr-run.js";
72
+ import { composeChangeSet, verifyChangeSetDigest } from "../../change-set.js";
73
+ import { waveGateName } from "../../gated-waves.js";
58
74
  import { readFanOutAttempt, writeFanOutAttempt } from "../../components/fan-out-record.js";
59
75
  import { componentInputDigests, progressThatCarries } from "../../components/fan-out-inputs.js";
60
76
  import { samePlanDigest } from "../../lifecycle/plan-digest.js";
@@ -67,9 +83,11 @@ import { FORGE_KINDS, forgeFromEnv, forgePrincipalOf } from "../../pr-forge.js";
67
83
  import { GITHUB_COMMENT_LIMIT, GITLAB_NOTE_LIMIT } from "../../plan-summary.js";
68
84
  import { resolveCliBuildParams, parseParamFlags } from "../build-params-cli.js";
69
85
  import { formatError, formatInfo, formatSuccess, formatWarning } from "../format.js";
86
+ import { reportUninstalledDependencies } from "../dependencies.js";
70
87
  import { findWorkspaceRoot } from "../../project-root.js";
71
88
  import { resolveChangedUnits } from "./fan-out.js";
72
89
  import { GATED_EXIT_CODE, autoReleaseRun, recordAutoReleasesForRun } from "./run.js";
90
+ import { failedRunExitCode } from "../../components/driver.js";
73
91
  /** Where the reports land unless `--output` says. */
74
92
  export const PR_REPORT_DIR = ".chant/pr";
75
93
  const fail = (message, hint) => {
@@ -107,7 +125,7 @@ async function prepare(ctx, stage) {
107
125
  return fail(`--forge ${args.forge} is not one of ${FORGE_KINDS.join(", ")}`);
108
126
  }
109
127
  let forge;
110
- if (args.forge) {
128
+ if (args.forge && !(stage === "plan" && args.noStatus)) {
111
129
  try {
112
130
  forge = forgeFromEnv(args.forge);
113
131
  }
@@ -138,10 +156,16 @@ async function prepare(ctx, stage) {
138
156
  });
139
157
  }
140
158
  catch (err) {
141
- return fail(err instanceof Error ? err.message : String(err));
159
+ const message = err instanceof Error ? err.message : String(err);
160
+ reportUninstalledDependencies([message], projectPath); // #4116
161
+ return fail(message);
142
162
  }
143
- if (!derived.success)
163
+ if (!derived.success) {
164
+ // #4116: component files that import packages the project has not installed.
165
+ if (derived.error)
166
+ reportUninstalledDependencies([derived.error], projectPath);
144
167
  return fail(derived.error ?? "Could not discover components");
168
+ }
145
169
  const seededOutputs = {};
146
170
  for (const file of args.seedOutputs ?? []) {
147
171
  try {
@@ -218,6 +242,7 @@ function settleOf(status) {
218
242
  case "applied":
219
243
  return "ok";
220
244
  case "refused":
245
+ case "waiting":
221
246
  return "gated";
222
247
  default:
223
248
  return "failed";
@@ -281,7 +306,8 @@ function printMembers(report) {
281
306
  async function finish(ctx, p, report, upload) {
282
307
  const note = noteFor(report, p.forge);
283
308
  writeOutputs(ctx, report, note, upload);
284
- await publish(p.forge, report, note);
309
+ if (!ctx.args.noStatus)
310
+ await publish(p.forge, report, note);
285
311
  if (ctx.args.json)
286
312
  console.log(JSON.stringify(report, null, 2));
287
313
  }
@@ -369,6 +395,7 @@ async function prPlan(ctx, stage) {
369
395
  set,
370
396
  approval,
371
397
  status,
398
+ ...(ctx.args.waveGate ? { waveGate: ctx.args.waveGate } : {}),
372
399
  });
373
400
  await finish(ctx, p, report, upload);
374
401
  // The plan stage settles on its planning: an approval still to come is what it reports, not a stop.
@@ -391,6 +418,10 @@ async function prApply(ctx, stage) {
391
418
  const { args } = ctx;
392
419
  if (args.requireReview && !args.forge)
393
420
  return fail("--require-review reads the pull request's reviews, so it needs --forge <kind>");
421
+ if (args.wave !== undefined && !args.waveGate)
422
+ return fail("--wave runs one wave of a --wave-gate apply, so it needs --wave-gate <name>");
423
+ if (args.wave !== undefined && args.wave < 1)
424
+ return fail(`--wave counts from 1, not ${args.wave}`);
394
425
  const p = await prepare(ctx, "apply");
395
426
  if (typeof p === "number")
396
427
  return p;
@@ -411,6 +442,8 @@ async function prApply(ctx, stage) {
411
442
  const events = await openCliRunEvents(args, { kind: "component", name: `pr-apply ${op}`, runId: release.runId, cwd: resolve(".") }, upload?.line);
412
443
  stage.stream = events;
413
444
  events?.emit({ type: "run-start", waves: p.plan.waves });
445
+ if (args.waveGate)
446
+ return prApplyWaves(ctx, p, { pr, op, release, upload, events });
414
447
  // `--resume` (#3464): an earlier attempt at this change on this commit
415
448
  // applied part of an approved set. When the members left are covered by
416
449
  // it and its approval still stands, finish it under that approval.
@@ -441,15 +474,7 @@ async function prApply(ctx, stage) {
441
474
  }
442
475
  const set = resumed?.set ?? (await planAll(p, events?.emit));
443
476
  const base = { stage: "apply", 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 };
444
- const stop = async (report, code, settle) => {
445
- events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
446
- await finish(ctx, p, report, upload);
447
- console.error((code === 0 ? formatInfo : (m) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
448
- if (report.message)
449
- console.error(report.message);
450
- printMembers(report);
451
- return code;
452
- };
477
+ const stop = stopper(ctx, p, events, upload);
453
478
  if (p.plan.order.length === 0) {
454
479
  return stop(prReport({ ...base, approval: { status: "pending" }, status: "nothing" }), 0);
455
480
  }
@@ -503,22 +528,13 @@ async function prApply(ctx, stage) {
503
528
  return stop(report, GATED_EXIT_CODE);
504
529
  }
505
530
  }
506
- if (args.requireReview && p.forge) {
507
- let reviewers;
508
- try {
509
- reviewers = (await p.forge.approvers(pr)).map((login) => p.forge.principalOf(login));
510
- }
511
- catch (err) {
512
- return fail(`could not read the reviews of #${pr}: ${err instanceof Error ? err.message : String(err)}`);
513
- }
514
- const strangers = approversWithoutReview(approval.approvedBy ?? [], reviewers);
515
- if (strangers.length > 0) {
516
- const message = `The plan is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
517
- ` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
518
- `An approval counts when it names the reviewer the way the forge does: ${p.forge.principalOf("<login>")}.`;
519
- emitReviewStop(events, message);
520
- return stop(prReport({ ...base, approval, status: "refused", refusal: "not-a-reviewer", message }), GATED_EXIT_CODE);
521
- }
531
+ const reviewOf = reviewCheck(args, p, pr);
532
+ const review = await reviewOf(approval.approvedBy ?? [], "The plan");
533
+ if (review && "error" in review)
534
+ return fail(review.error);
535
+ if (review) {
536
+ emitReviewStop(events, review.message);
537
+ return stop(prReport({ ...base, approval, status: "refused", refusal: review.refusal, message: review.message }), GATED_EXIT_CODE);
522
538
  }
523
539
  // The record is written before the first component runs and again as each
524
540
  // one settles, so a run killed partway still knows what applied.
@@ -550,12 +566,36 @@ async function prApply(ctx, stage) {
550
566
  });
551
567
  };
552
568
  recordAttempt();
569
+ // #3999: a round planned after an upstream applied waits on a gate of its
570
+ // own, under the same identity.gates rule and the same review checks.
571
+ const roundLedger = gitGateLedgerPort();
572
+ const decideRound = async ({ round, gate, set: planned }) => {
573
+ console.error(formatInfo(`round ${round}: ${planned.members.map((m) => m.component).filter((c, i, all) => all.indexOf(c) === i).join(", ")} planned again after an upstream applied, ${planned.doc.digest}`));
574
+ await storeChangeSetForGate(ctx, p, planned.doc);
575
+ const port = {
576
+ ...roundLedger,
577
+ ...(roundLedger.approvalRule ? { approvalRule: (g, at) => roundLedger.approvalRule(g === gate ? p.gate : g, at) } : {}),
578
+ };
579
+ const check = await emitGateStep(events?.emit, { op, gate }, () => decidePrGate(port, { op, gate, digest: planned.doc.digest, description: `pull request #${pr}, round ${round}: planned again after an upstream applied` }));
580
+ const roundApproval = approvalOf(check);
581
+ if (!check.satisfied)
582
+ return { satisfied: false, approval: roundApproval };
583
+ const refused = await reviewOf(roundApproval.approvedBy ?? [], `Round ${round}'s plan`);
584
+ if (refused) {
585
+ const message = "error" in refused ? refused.error : refused.message;
586
+ emitReviewStop(events, message);
587
+ return { satisfied: false, approval: roundApproval, message };
588
+ }
589
+ return { satisfied: true, approval: roundApproval };
590
+ };
553
591
  const applied = await applyPrSet({
554
592
  components: p.components,
555
593
  plan: resumed?.plan ?? p.plan,
556
594
  registry: p.registry,
557
595
  env: p.env,
558
596
  set,
597
+ gate: p.gate,
598
+ decideRound,
559
599
  ...(resumed ? { resumed: prior } : {}),
560
600
  // The apply is this run's waves, so the fan-out's own run framing is
561
601
  // dropped. The log's lines go to stderr with or without --events-json.
@@ -589,13 +629,86 @@ async function prApply(ctx, stage) {
589
629
  status: applied.status,
590
630
  members: applied.members,
591
631
  ...(resumed ? { resumed: resumed.completed } : {}),
632
+ ...(applied.rounds ? { rounds: applied.rounds } : {}),
633
+ ...(applied.status === "waiting" ? { message: waitingMessage(op, applied.rounds) } : {}),
592
634
  });
593
- // A component that stopped at a gate of its own leaves the apply waiting on a person.
594
- const code = await stop(report, applied.status === "applied" ? 0 : 1, applied.run.status === "gated" ? "gated" : undefined);
635
+ // A component that stopped at a gate of its own leaves the apply waiting on
636
+ // a person. A step that refused its plan because the target moved since
637
+ // the approval exits 3, as a moved digest does before anything applies (#3997).
638
+ const exit = applied.status === "applied" ? 0 : applied.status === "waiting" ? GATED_EXIT_CODE : failedRunExitCode(applied.run.results);
639
+ const code = await stop(report, exit, applied.run.status === "gated" ? "gated" : undefined);
595
640
  if (applied.status === "applied")
596
641
  console.error(formatSuccess(`applied the plan ${report.digest} approved by ${(approval.approvedBy ?? []).join(", ")}`));
597
642
  return code;
598
643
  }
644
+ /** How a stage stops: settle its events, write and publish the report, say how it ended, and return `code`. */
645
+ function stopper(ctx, p, events, upload) {
646
+ return async (report, code, settle) => {
647
+ events?.settle(settle ?? settleOf(report.status), report.status === "plan-failed" || report.status === "failed" ? report.message ?? `pr-apply ${report.status}` : undefined);
648
+ await finish(ctx, p, report, upload);
649
+ console.error((code === 0 ? formatInfo : (m) => formatWarning({ message: m }))(`pr-apply: ${report.status}${report.refusal ? ` (${report.refusal})` : ""}`));
650
+ if (report.message)
651
+ console.error(report.message);
652
+ printMembers(report);
653
+ return code;
654
+ };
655
+ }
656
+ /**
657
+ * The `--require-review` check (#3183, #4000) of an approval's approvers,
658
+ * `what` naming the plan they approved. The pull request's reviews and
659
+ * authors are read once, at the first call. Without `--require-review` every
660
+ * approval counts.
661
+ */
662
+ function reviewCheck(args, p, pr) {
663
+ let read;
664
+ return async (approvedBy, what) => {
665
+ const forge = p.forge;
666
+ if (!args.requireReview || !forge)
667
+ return undefined;
668
+ read ?? (read = (async () => ({
669
+ reviewers: (await forge.approvers(pr)).map((login) => forge.principalOf(login)),
670
+ authors: (await forge.authors(pr)).map((login) => forge.principalOf(login)),
671
+ }))());
672
+ let reviewers;
673
+ let authors;
674
+ try {
675
+ ({ reviewers, authors } = await read);
676
+ }
677
+ catch (err) {
678
+ return { error: `could not read the reviews and authors of #${pr}: ${err instanceof Error ? err.message : String(err)}` };
679
+ }
680
+ // #4000: whoever wrote the change never counts toward its gate, through a forge review or a ledger line.
681
+ const writers = approversWhoAuthored(approvedBy, authors);
682
+ if (writers.length > 0) {
683
+ return {
684
+ refusal: "author-approved",
685
+ message: `${what} is approved by ${writers.join(", ")}, who wrote #${pr} ` +
686
+ `(its author and the authors and committers of its commits: ${authors.join(", ")}). ` +
687
+ `An approval from someone who wrote the change never counts, so nothing was applied. ` +
688
+ `Have someone else approve the pull request and the plan, then run the apply again.`,
689
+ };
690
+ }
691
+ const strangers = approversWithoutReview(approvedBy, reviewers);
692
+ if (strangers.length > 0) {
693
+ return {
694
+ refusal: "not-a-reviewer",
695
+ message: `${what} is approved by ${strangers.join(", ")}, who ${strangers.length === 1 ? "has" : "have"} no approving review on #${pr}` +
696
+ ` (approving reviews: ${reviewers.join(", ") || "none"}). Nothing was applied. ` +
697
+ `An approval counts when it names the reviewer the way the forge does: ${forge.principalOf("<login>")}.`,
698
+ };
699
+ }
700
+ return undefined;
701
+ };
702
+ }
703
+ /** What a `waiting` apply prints (#3999): the round's gate and digest to approve, and how to carry on. */
704
+ function waitingMessage(op, rounds) {
705
+ const last = rounds?.at(-1);
706
+ if (!last)
707
+ return "Some members were held back after an upstream applied.";
708
+ return (`${last.components.join(", ")} ${last.components.length === 1 ? "was" : "were"} planned again after an upstream applied, ` +
709
+ `and no approval stands for that plan (${last.digest}), so ${last.components.length === 1 ? "it" : "they"} did not apply. ` +
710
+ `Approve it: ${prApproveCommand({ op, gate: last.gate, digest: last.digest })}; then run the apply again with the same --resume record.`);
711
+ }
599
712
  /** What a component's settle says in the job log. */
600
713
  function settleWords(body) {
601
714
  switch (body.status) {
@@ -733,3 +846,346 @@ async function tryResume(p, attempt, file, op, inputsNow) {
733
846
  console.error(formatInfo(`resume : ${completed.join(", ")} applied in an earlier attempt; applying ${resumed.plan.order.join(", ") || "nothing more"} under the approval of ${record.digest}`));
734
847
  return { plan: resumed.plan, set: resumed.set, check, approval: approvalOf(check), completed, outputs };
735
848
  }
849
+ /**
850
+ * Whether `attempt` carries into this wave's job. It does when it was made
851
+ * by an apply of this pull request's gate, on this commit, for this
852
+ * derivation; when its change set is the one its digest names; and when the
853
+ * pull request's approval still stands for that digest. Of the components it
854
+ * applied, those whose input is the same now carry (#4081). Otherwise it says
855
+ * why on stderr, and the job runs as it would with no record.
856
+ */
857
+ async function carryWaves(p, attempt, file, op, inputsNow) {
858
+ const skip = (why) => {
859
+ console.error(formatWarning({
860
+ message: `--resume: "${file}" does not carry: ${why}`,
861
+ hint: "Wave 1 plans every member again and needs the pull request's approval of that plan; a later wave refuses until wave 1 applied.",
862
+ }));
863
+ return undefined;
864
+ };
865
+ const record = attempt.prApply;
866
+ if (!record)
867
+ return skip("it was not written by pr-apply");
868
+ if (record.op !== op || record.gate !== p.gate)
869
+ return skip(`it records ${record.op} ${record.gate}, not ${op} ${p.gate}`);
870
+ if (record.head !== p.head)
871
+ return skip(`it records an apply of ${record.head.slice(0, 12)}, not ${p.head.slice(0, 12)}`);
872
+ if (!samePlanDigest(attempt.digest, p.plan.digest))
873
+ return skip("the change selects other components than it did");
874
+ if (record.changeSet.digest !== record.digest || !verifyChangeSetDigest(record.changeSet))
875
+ return skip(`its change set is not the one its digest ${record.digest} names`);
876
+ const split = progressThatCarries(attempt, inputsNow);
877
+ if (split.unrecorded && attempt.completed.length > 0)
878
+ return skip("it does not record what each component applied, since it was written before chant recorded component inputs");
879
+ for (const name of split.changed)
880
+ console.error(formatInfo(`${name}: changed since the earlier attempt, so it runs again`));
881
+ let check;
882
+ try {
883
+ check = await decidePrGate(readOnlyLedger(gitGateLedgerPort()), { op, gate: p.gate, digest: record.digest });
884
+ }
885
+ catch (err) {
886
+ return skip(`could not read the gate ledger: ${err instanceof Error ? err.message : String(err)}`);
887
+ }
888
+ if (!check.satisfied)
889
+ return skip(`no approval stands for the plan it applied (${record.digest}) any more`);
890
+ const completed = split.carried;
891
+ const outputs = Object.fromEntries(Object.entries(attempt.outputs).filter(([name]) => completed.includes(name)));
892
+ return { record, check, approval: approvalOf(check), completed, outputs };
893
+ }
894
+ /** A run event with its wave renumbered to `wave`: the fan-out that applies one wave numbers it 1. */
895
+ function inWave(body, wave) {
896
+ return "wave" in body && typeof body.wave === "number" ? { ...body, wave } : body;
897
+ }
898
+ /**
899
+ * `pr-apply --wave-gate <gate>` (#4003): apply the pull request's approved
900
+ * set a wave at a time, each wave planned after the one before it applied.
901
+ *
902
+ * The first job plans every member, as pr-apply does, and decides the pull
903
+ * request's gate against that digest. Approved, wave 1 applies with those
904
+ * plans. Each later wave is planned again against the outputs the waves
905
+ * before it left. When the approved change set covers its plan
906
+ * (`planCoveredBy`, as on a resume) it applies under the pull request's
907
+ * approval; when it does not, it waits at `<gate>-wave-<k>` under the pull
908
+ * request's op for an approval of its new plan, and exits 3.
909
+ *
910
+ * With `--wave <k>` the job runs wave k and no other, reading the approved
911
+ * set and the progress from the `--resume` record the job before it left.
912
+ * Without it the waves run in order until one stops.
913
+ */
914
+ async function prApplyWaves(ctx, p, at) {
915
+ const { args } = ctx;
916
+ const { pr, op, events, upload } = at;
917
+ const waveGate = args.waveGate;
918
+ const only = args.wave;
919
+ const total = p.plan.waves.length;
920
+ const stop = stopper(ctx, p, events, upload);
921
+ const log = applyLog();
922
+ log({ type: "run-start", waves: p.plan.waves });
923
+ const reportBase = { stage: "apply", 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, waveGate };
924
+ if (p.plan.order.length === 0 || pr === null) {
925
+ const set = await planAll(p, events?.emit);
926
+ if (p.plan.order.length === 0)
927
+ return stop(prReport({ ...reportBase, set, approval: { status: "pending" }, status: "nothing" }), 0);
928
+ return stop(prReport({
929
+ ...reportBase,
930
+ set,
931
+ approval: { status: "pending" },
932
+ status: "refused",
933
+ refusal: "no-pull-request",
934
+ message: `No pull request merged ${p.head}, so no approval answers for this change. Nothing was applied. Pass --pr <n>, or land the change through a pull request.`,
935
+ }), GATED_EXIT_CODE);
936
+ }
937
+ const resumePath = args.resume ? resolve(args.resume) : undefined;
938
+ let attempt;
939
+ if (resumePath) {
940
+ try {
941
+ attempt = readFanOutAttempt(resumePath);
942
+ }
943
+ catch (err) {
944
+ return fail(`--resume: could not read "${args.resume}": ${err instanceof Error ? err.message : String(err)}`);
945
+ }
946
+ }
947
+ const inputsNow = resumePath ? componentInputDigests(p.components, { projectRoot: resolve("."), env: p.env }) : {};
948
+ const carry = attempt ? await carryWaves(p, attempt, args.resume, op, inputsNow) : undefined;
949
+ const completed = new Set(carry?.completed ?? []);
950
+ const outputsSoFar = { ...(carry?.outputs ?? {}) };
951
+ const inputsSoFar = Object.fromEntries([...completed].filter((n) => inputsNow[n] !== undefined).map((n) => [n, inputsNow[n]]));
952
+ const failedSoFar = new Set();
953
+ let waves = carry?.record.waves ?? [];
954
+ const after = new Map();
955
+ const leftIn = (k) => (p.plan.waves[k - 1] ?? []).filter((name) => !completed.has(name));
956
+ const firstLeft = () => p.plan.waves.findIndex((_, i) => leftIn(i + 1).length > 0) + 1;
957
+ // The approved set: carried from the record, or planned here and decided against the pull request's gate.
958
+ let approved = carry
959
+ ? { doc: carry.record.changeSet, members: carry.record.members, plans: {}, outputs: carry.record.planOutputs, failed: false }
960
+ : undefined;
961
+ let approval = carry?.approval ?? { status: "pending" };
962
+ const report = (input) => {
963
+ const set = approved ?? { doc: composeChangeSet([]), members: [], plans: {}, outputs: {}, failed: false };
964
+ const members = set.members.map((m) => {
965
+ const now = after.get(m.member);
966
+ if (now)
967
+ return { ...m, status: now.status, ...(now.error ? { error: now.error } : {}) };
968
+ return completed.has(m.component) ? { ...m, status: "applied" } : m;
969
+ });
970
+ return prReport({ ...reportBase, set, approval, members, waves, ...input });
971
+ };
972
+ // `--wave <k>` reached before an earlier wave applied: nothing runs, as `fan-out --wave` refuses.
973
+ const left = firstLeft();
974
+ if (only !== undefined && left > 0 && left < only) {
975
+ return stop(report({
976
+ status: "refused",
977
+ refusal: "earlier-wave",
978
+ message: `Wave ${only} cannot run yet: wave ${left} has not applied (${leftIn(left).join(", ")})${carry ? "" : ", and no --resume record of this apply says otherwise"}. ` +
979
+ `Nothing was applied. Run wave ${left}'s job first, with the same --resume record.`,
980
+ }), 1);
981
+ }
982
+ let planned;
983
+ if (carry) {
984
+ // What the earlier waves applied settles first (#4007), each in its own wave, as a resumed apply's does.
985
+ p.plan.waves.forEach((names, i) => {
986
+ for (const component of names.filter((name) => completed.has(name))) {
987
+ const body = { type: "component-settle", wave: i + 1, component, status: "already-applied" };
988
+ events?.emit(body);
989
+ log(body);
990
+ }
991
+ });
992
+ emitResumedGate(events, op, p.gate, carry.check);
993
+ }
994
+ else {
995
+ const set = await planAll(p, events?.emit);
996
+ approved = set;
997
+ if (set.failed) {
998
+ return stop(report({ status: "plan-failed", message: "A member failed to plan, so there is no plan to compare with the approved one. Nothing was applied." }), 1);
999
+ }
1000
+ await storeChangeSetForGate(ctx, p, set.doc);
1001
+ const check = await emitGateStep(events?.emit, { op, gate: p.gate }, () => decidePrGate(gitGateLedgerPort(), { op, gate: p.gate, digest: set.doc.digest, description: `pull request #${pr}: ${p.plan.order.join(", ")}` }), (unapproved) => {
1002
+ const a = approvalOf(unapproved);
1003
+ return a.status === "changed"
1004
+ ? describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval: a })
1005
+ : `No approval stands for this plan. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}`;
1006
+ });
1007
+ approval = approvalOf(check);
1008
+ if (!check.satisfied) {
1009
+ return stop(approval.status === "changed"
1010
+ ? report({ status: "refused", refusal: "plan-changed", message: describePlanChanged({ op, gate: p.gate, digest: set.doc.digest, approval }) })
1011
+ : report({
1012
+ status: "refused",
1013
+ refusal: "not-approved",
1014
+ message: `No approval stands for this plan, so nothing was applied. Approve it: ${prApproveCommand({ op, gate: p.gate, digest: set.doc.digest })}; then run the apply again.`,
1015
+ }), GATED_EXIT_CODE);
1016
+ }
1017
+ planned = set;
1018
+ }
1019
+ const approvedSet = approved;
1020
+ const review = reviewCheck(args, p, pr);
1021
+ const prReview = await review(approval.approvedBy ?? [], "The plan");
1022
+ if (prReview && "error" in prReview)
1023
+ return fail(prReview.error);
1024
+ if (prReview) {
1025
+ emitReviewStop(events, prReview.message);
1026
+ return stop(report({ status: "refused", refusal: prReview.refusal, message: prReview.message }), GATED_EXIT_CODE);
1027
+ }
1028
+ const recordAttempt = () => {
1029
+ if (!resumePath)
1030
+ return;
1031
+ writeFanOutAttempt(resumePath, {
1032
+ digest: p.plan.digest,
1033
+ completed: [...completed].sort(),
1034
+ failed: [...failedSoFar].sort(),
1035
+ outputs: outputsSoFar,
1036
+ inputs: Object.fromEntries(Object.entries(inputsSoFar).sort(([a], [b]) => a.localeCompare(b))),
1037
+ prApply: {
1038
+ op,
1039
+ gate: p.gate,
1040
+ head: p.head,
1041
+ digest: approvedSet.doc.digest,
1042
+ approvedBy: approval.approvedBy ?? [],
1043
+ members: approvedSet.members,
1044
+ changeSet: approvedSet.doc,
1045
+ planOutputs: approvedSet.outputs,
1046
+ ...(waves.length > 0 ? { waves } : {}),
1047
+ },
1048
+ });
1049
+ };
1050
+ const setWave = (wave) => {
1051
+ waves = [...waves.filter((w) => w.wave !== wave.wave), wave].sort((a, b) => a.wave - b.wave);
1052
+ recordAttempt();
1053
+ };
1054
+ recordAttempt();
1055
+ const from = only ?? (left || total + 1);
1056
+ const to = only ?? total;
1057
+ for (let k = from; k <= to; k++) {
1058
+ const wavePlan = prWavePlan(p.plan, p.components, k, [...completed]);
1059
+ if (wavePlan.order.length === 0)
1060
+ continue;
1061
+ const inThisWave = new Set(wavePlan.order);
1062
+ // Wave 1 of the apply that just planned every member applies those plans:
1063
+ // they are the approved set's. Every other wave is planned again, against
1064
+ // the outputs the waves before it left.
1065
+ let set;
1066
+ let entry;
1067
+ if (planned && k === 1) {
1068
+ set = {
1069
+ ...planned,
1070
+ members: planned.members.filter((m) => inThisWave.has(m.component)),
1071
+ plans: Object.fromEntries(Object.entries(planned.plans).filter(([name]) => inThisWave.has(name))),
1072
+ };
1073
+ entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
1074
+ }
1075
+ else {
1076
+ const notApplied = Object.fromEntries(Object.entries(approvedSet.outputs).filter(([name]) => !completed.has(name)));
1077
+ const fresh = await planPrSet({
1078
+ components: p.components,
1079
+ plan: wavePlan,
1080
+ registry: p.registry,
1081
+ env: p.env,
1082
+ seededOutputs: { ...p.seededOutputs, ...notApplied, ...outputsSoFar },
1083
+ ...(events ? { onRunEvent: (body) => events.emit(body) } : {}),
1084
+ });
1085
+ if (fresh.failed) {
1086
+ for (const m of fresh.members)
1087
+ after.set(m.member, m);
1088
+ setWave({ wave: k, components: wavePlan.order, status: "plan-failed", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest });
1089
+ return stop(report({ status: "plan-failed", message: `A member of wave ${k} failed to plan, so nothing in wave ${k} was applied.` }), 1);
1090
+ }
1091
+ const moved = waveNotCovered(approvedSet.doc, fresh.doc, approvedSet.members);
1092
+ set = fresh;
1093
+ if (moved.length === 0) {
1094
+ console.error(formatInfo(`wave ${k}: the approved plan covers what it plans now, so it applies under the pull request's approval`));
1095
+ entry = { wave: k, components: wavePlan.order, status: "applied", approval: "pull-request", gate: p.gate, digest: approvedSet.doc.digest, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
1096
+ }
1097
+ else {
1098
+ const gate = waveGateName(waveGate, k);
1099
+ const digest = fresh.doc.digest;
1100
+ console.error(formatInfo(`wave ${k}: its plan moved since the pull request's (${moved.join("; ")}), so it needs an approval of ${gate}`));
1101
+ await storeChangeSetForGate(ctx, p, fresh.doc);
1102
+ writeWaveChangeSet(ctx, k, fresh.doc, upload);
1103
+ const approveLine = prApproveCommand({ op, gate, digest });
1104
+ const check = await emitGateStep(events?.emit, { op, gate }, () => decidePrGate(gitGateLedgerPort(), { op, gate, digest, description: `pull request #${pr}, wave ${k} of ${total}: ${wavePlan.order.join(", ")}` }), (unapproved) => unapproved.mismatch
1105
+ ? `wave ${k}'s plan changed after its approval. Approve the plan now: ${approveLine}`
1106
+ : `wave ${k}'s plan is not the one the pull request approved. Approve it: ${approveLine}`);
1107
+ const waveApproval = approvalOf(check);
1108
+ const waiting = {
1109
+ wave: k,
1110
+ components: wavePlan.order,
1111
+ status: "waiting",
1112
+ approval: "wave",
1113
+ gate,
1114
+ digest,
1115
+ moved,
1116
+ ...(waveApproval.approved ? { approved: waveApproval.approved } : {}),
1117
+ };
1118
+ if (!check.satisfied) {
1119
+ setWave(waiting);
1120
+ const message = waveApproval.status === "changed"
1121
+ ? `Wave ${k}'s plan changed after its approval, so nothing in it was applied. approved: ${waveApproval.approved ?? "(none)"}; planned now: ${digest}. ` +
1122
+ `Read the new plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`
1123
+ : `Wave ${k} was planned after the waves before it applied, and the pull request's approved plan does not cover it: ${moved.join("; ")}. ` +
1124
+ `Nothing in wave ${k} was applied. Read its plan, and if it is right, approve it: ${approveLine}; then run the wave's apply again.`;
1125
+ return stop(report({ status: "refused", refusal: waveApproval.status === "changed" ? "wave-plan-changed" : "wave-not-approved", message }), GATED_EXIT_CODE);
1126
+ }
1127
+ const waveReview = await review(waveApproval.approvedBy ?? [], `Wave ${k}'s plan`);
1128
+ if (waveReview && "error" in waveReview)
1129
+ return fail(waveReview.error);
1130
+ if (waveReview) {
1131
+ setWave(waiting);
1132
+ emitReviewStop(events, waveReview.message);
1133
+ return stop(report({ status: "refused", refusal: waveReview.refusal, message: waveReview.message }), GATED_EXIT_CODE);
1134
+ }
1135
+ entry = { wave: k, components: wavePlan.order, status: "applied", approval: "wave", gate, digest, moved, ...(waveApproval.approvedBy ? { approvedBy: waveApproval.approvedBy } : {}) };
1136
+ }
1137
+ }
1138
+ const applied = await applyPrSet({
1139
+ components: p.components,
1140
+ plan: wavePlan,
1141
+ registry: p.registry,
1142
+ env: p.env,
1143
+ set,
1144
+ // One wave's fan-out numbers its wave 1; the events and the log carry the derivation's number.
1145
+ onRunEvent: (body) => {
1146
+ const numbered = inWave(body, k);
1147
+ events?.nested(numbered);
1148
+ if (numbered.type !== "run-start")
1149
+ log(numbered);
1150
+ },
1151
+ onComponentSettled: (settled, outputs) => {
1152
+ if (settled.status === "ok") {
1153
+ completed.add(settled.component);
1154
+ if (inputsNow[settled.component] !== undefined)
1155
+ inputsSoFar[settled.component] = inputsNow[settled.component];
1156
+ if (outputs)
1157
+ outputsSoFar[settled.component] = outputs;
1158
+ }
1159
+ else if (settled.status === "fail") {
1160
+ failedSoFar.add(settled.component);
1161
+ }
1162
+ recordAttempt();
1163
+ },
1164
+ });
1165
+ for (const m of applied.members)
1166
+ after.set(m.member, m);
1167
+ for (const name of applied.run.completed) {
1168
+ completed.add(name);
1169
+ const outputs = applied.run.componentOutputs[name];
1170
+ if (outputs)
1171
+ outputsSoFar[name] = outputs;
1172
+ }
1173
+ setWave({ ...entry, status: applied.status === "applied" ? "applied" : "failed" });
1174
+ await recordAutoReleasesForRun(applied.run.results, p.env, at.release.runId, resolveAutoReleaseDisabled(p.config, args.noReleaseRecord), at.release.runOrigin);
1175
+ if (applied.status !== "applied") {
1176
+ return stop(report({ status: "failed" }), failedRunExitCode(applied.run.results), applied.run.status === "gated" ? "gated" : undefined);
1177
+ }
1178
+ console.error(formatSuccess(`applied wave ${k} of ${total} under ${entry.approval === "pull-request" ? `the pull request's approval of ${approvedSet.doc.digest}` : `${entry.gate}, approved for ${entry.digest}`} by ${(entry.approvedBy ?? []).join(", ")}`));
1179
+ }
1180
+ const done = p.plan.order.every((name) => completed.has(name));
1181
+ return stop(report({ status: done ? "applied" : "wave-applied" }), 0);
1182
+ }
1183
+ /** A wave's new plan (#4003), beside the report as `change-set-wave-<k>.json`, for whoever approves its gate. */
1184
+ function writeWaveChangeSet(ctx, wave, doc, upload) {
1185
+ const dir = resolve(ctx.args.output ?? PR_REPORT_DIR);
1186
+ mkdirSync(dir, { recursive: true });
1187
+ const name = `change-set-wave-${wave}.json`;
1188
+ const body = JSON.stringify(doc, null, 2) + "\n";
1189
+ writeFileSync(join(dir, name), body);
1190
+ upload?.file(name, body);
1191
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"promote.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/promote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA+CH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAukBlD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA2E/E;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsChF;AAsCD,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BhF"}
1
+ {"version":3,"file":"promote.d.ts","sourceRoot":"","sources":["../../../src/cli/handlers/promote.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AA+CH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAwkBlD,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA2E/E;AAED,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAsChF;AAsCD,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CA6BhF"}