@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
@@ -205,6 +205,25 @@ export interface FanOutRunOptions {
205
205
  * the returned result cannot do.
206
206
  */
207
207
  onComponentSettled?: (result: DriverComponentResult, outputs: Record<string, unknown> | undefined) => void;
208
+ /**
209
+ * Asked as each component's wave starts, with the outputs standing then
210
+ * (#3999). A reason holds the component back: it does not run, and nothing
211
+ * downstream of it runs either; each is listed in `held` and settles
212
+ * `waiting`. A pull request's apply holds a member whose plan read an
213
+ * upstream output that applied as another value, or that was known only
214
+ * after apply. Not with `waveGate`, which plans each wave after the one
215
+ * before it applied.
216
+ */
217
+ hold?: (component: string, componentOutputs: Record<string, Record<string, unknown>>) => string | undefined;
218
+ }
219
+
220
+ /** A component {@link FanOutRunOptions.hold} held back, or one downstream of it. */
221
+ export interface FanOutHeld {
222
+ component: string;
223
+ /** Why the hook held it, or, downstream, why it held the component named in `heldBy`. */
224
+ reason: string;
225
+ /** Downstream of a held component: that component. */
226
+ heldBy?: string;
208
227
  }
209
228
 
210
229
  export interface FanOutRunResult {
@@ -223,6 +242,8 @@ export interface FanOutRunResult {
223
242
  gate?: PendingGateRecord;
224
243
  /** With `waveGate`: each wave this attempt planned, in order, the one it stopped at included. */
225
244
  waves?: WaveRecord[];
245
+ /** What {@link FanOutRunOptions.hold} held back, in plan order. Absent when nothing was. */
246
+ held?: FanOutHeld[];
226
247
  }
227
248
 
228
249
  /**
@@ -296,9 +317,21 @@ export async function runFanOut(
296
317
  const completed: string[] = [];
297
318
  const failed: string[] = [];
298
319
  const blockedBy = new Map<string, string>();
320
+ const held = new Map<string, FanOutHeld>();
299
321
  let gatedFact: PendingGateRecord | undefined;
300
322
 
301
323
  const inPlan = new Set(active.order);
324
+ const waveOf = waveNumbers(active.waves);
325
+ /** Hold `root` and everything downstream of it that has not settled otherwise. */
326
+ const markHeld = (root: string, reason: string): void => {
327
+ held.set(root, { component: root, reason });
328
+ emit?.({ type: "component-settle", wave: waveOf.get(root)!, component: root, status: "waiting" });
329
+ for (const [component] of downstreamWithin(components, inPlan, [root])) {
330
+ if (held.has(component) || blockedBy.has(component) || failed.includes(component)) continue;
331
+ held.set(component, { component, reason, heldBy: root });
332
+ emit?.({ type: "component-settle", wave: waveOf.get(component)!, component, status: "waiting" });
333
+ }
334
+ };
302
335
  /** Skip everything downstream of `root`, blaming `root` rather than the nearest edge. */
303
336
  const markBlocked = (root: string): void => {
304
337
  for (const [component, by] of downstreamWithin(components, inPlan, [root])) {
@@ -310,7 +343,12 @@ export async function runFanOut(
310
343
  for (const [waveIndex, wave] of active.waves.entries()) {
311
344
  const waveNum = waveIndex + 1;
312
345
  // A component whose upstream failed in an earlier wave is not dispatched.
313
- const runnable = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name));
346
+ const candidates = wave.filter((name) => inPlan.has(name) && !blockedBy.has(name) && !held.has(name));
347
+ for (const name of candidates) {
348
+ const reason = options.hold?.(name, componentOutputs);
349
+ if (reason !== undefined) markHeld(name, reason);
350
+ }
351
+ const runnable = candidates.filter((name) => !held.has(name));
314
352
  if (runnable.length === 0) continue;
315
353
 
316
354
  emit?.({ type: "wave-start", wave: waveNum, components: runnable });
@@ -384,6 +422,7 @@ export async function runFanOut(
384
422
  blocked,
385
423
  componentOutputs,
386
424
  ...(gatedFact ? { gate: gatedFact } : {}),
425
+ ...(held.size > 0 ? { held: active.order.filter((name) => held.has(name)).map((name) => held.get(name)!) } : {}),
387
426
  };
388
427
  }
389
428
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The apply job's base fallback (#4004), run in sh against a real
3
+ * repository. GitHub and Forgejo set `github.event.before`, and GitLab
4
+ * `CI_COMMIT_BEFORE_SHA`, to forty zeros on a branch's first push.
5
+ */
6
+
7
+ import { execFileSync } from "node:child_process";
8
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { afterAll, describe, expect, test } from "vitest";
12
+ import { EMPTY_TREE_SHA } from "./changed-files";
13
+ import { githubBaseFallback, gitlabBaseFallback, prLoopJobs } from "./pr-pipeline";
14
+
15
+ const ZEROS = "0".repeat(40);
16
+ const dir = mkdtempSync(join(tmpdir(), "chant-pr-base-"));
17
+ afterAll(() => rmSync(dir, { recursive: true, force: true }));
18
+ const ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@example.com", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@example.com" };
19
+
20
+ const git = (cwd: string, ...args: string[]): string => execFileSync("git", args, { cwd, encoding: "utf8", env: ENV }).trim();
21
+
22
+ /** A bare origin and a clone of it, `main` holding `commits` commits. */
23
+ function repo(name: string, commits: number): string {
24
+ const origin = join(dir, `${name}.git`);
25
+ const work = join(dir, name);
26
+ execFileSync("git", ["init", "-q", "--bare", "-b", "main", origin]);
27
+ execFileSync("git", ["init", "-q", "-b", "main", work]);
28
+ git(work, "remote", "add", "origin", origin);
29
+ for (let i = 0; i < commits; i++) {
30
+ writeFileSync(join(work, "f.txt"), `${i}\n`);
31
+ git(work, "add", "-A");
32
+ git(work, "commit", "-q", "-m", `c${i}`);
33
+ }
34
+ git(work, "push", "-q", "origin", "main");
35
+ return work;
36
+ }
37
+
38
+ /** BASE_SHA after `line` runs in `cwd` with `env`. */
39
+ function settle(cwd: string, line: string, env: Record<string, string>): string {
40
+ return execFileSync("sh", ["-ec", `${line}\nprintf %s "$BASE_SHA"`], { cwd, encoding: "utf8", env: { ...ENV, ...env } });
41
+ }
42
+
43
+ describe("githubBaseFallback (#4004)", () => {
44
+ test("a set base is kept", () => {
45
+ const work = repo("kept", 2);
46
+ const parent = git(work, "rev-parse", "HEAD^");
47
+ expect(settle(work, githubBaseFallback(), { BASE_SHA: parent, GITHUB_SHA: git(work, "rev-parse", "HEAD") })).toBe(parent);
48
+ });
49
+
50
+ test("forty zeros or empty on the target branch's first push: the pushed commit's parent", () => {
51
+ const work = repo("first", 2);
52
+ const head = git(work, "rev-parse", "HEAD");
53
+ const parent = git(work, "rev-parse", "HEAD^");
54
+ expect(settle(work, githubBaseFallback(), { BASE_SHA: ZEROS, GITHUB_SHA: head })).toBe(parent);
55
+ expect(settle(work, githubBaseFallback(), { BASE_SHA: "", GITHUB_SHA: head })).toBe(parent);
56
+ });
57
+
58
+ test("a branch's first push: the merge base with the target branch", () => {
59
+ const work = repo("branch", 1);
60
+ const fork = git(work, "rev-parse", "HEAD");
61
+ git(work, "checkout", "-q", "-b", "feature");
62
+ writeFileSync(join(work, "g.txt"), "g\n");
63
+ git(work, "add", "-A");
64
+ git(work, "commit", "-q", "-m", "feature");
65
+ expect(settle(work, githubBaseFallback(), { BASE_SHA: ZEROS, GITHUB_SHA: git(work, "rev-parse", "HEAD") })).toBe(fork);
66
+ });
67
+
68
+ test("a root commit: the empty tree", () => {
69
+ const work = repo("root", 1);
70
+ expect(settle(work, githubBaseFallback(), { BASE_SHA: ZEROS, GITHUB_SHA: git(work, "rev-parse", "HEAD") })).toBe(EMPTY_TREE_SHA);
71
+ });
72
+
73
+ test("a named branch is the one fetched", () => {
74
+ expect(githubBaseFallback()).toContain('git fetch origin "main"');
75
+ expect(githubBaseFallback("trunk")).toContain('git fetch origin "trunk"');
76
+ });
77
+ });
78
+
79
+ describe("gitlabBaseFallback", () => {
80
+ test("a root commit's first push: the empty tree, as on GitHub (#4004)", () => {
81
+ const work = repo("gitlab-root", 1);
82
+ expect(settle(work, gitlabBaseFallback("main"), { BASE_SHA: ZEROS, CI_COMMIT_SHA: git(work, "rev-parse", "HEAD") })).toBe(EMPTY_TREE_SHA);
83
+ });
84
+
85
+ test("the merge request's diff base comes first", () => {
86
+ const work = repo("gitlab-mr", 2);
87
+ const parent = git(work, "rev-parse", "HEAD^");
88
+ expect(settle(work, gitlabBaseFallback("main"), { BASE_SHA: ZEROS, CI_MERGE_REQUEST_DIFF_BASE_SHA: parent, CI_COMMIT_SHA: git(work, "rev-parse", "HEAD") })).toBe(parent);
89
+ });
90
+ });
91
+
92
+ describe("prLoopJobs", () => {
93
+ test("every forge's apply settles its base first; no plan does", () => {
94
+ for (const forge of ["github", "forgejo", "gitlab"] as const) {
95
+ const [plan, apply] = prLoopJobs(forge, "prod", { branch: "trunk" });
96
+ expect(plan!.setup).toBeUndefined();
97
+ expect(apply!.setup).toEqual([forge === "gitlab" ? gitlabBaseFallback("trunk") : githubBaseFallback("trunk")]);
98
+ }
99
+ });
100
+ });
@@ -42,6 +42,25 @@
42
42
  * build is `chant build --env <env>`, or `buildCommand` when the templates
43
43
  * come from several builds (`npm run build`).
44
44
  *
45
+ * With `waveGate` (#4003) the apply runs a wave at a time: `pr-apply
46
+ * --wave-gate <gate>`, each wave planned after the one before it applied. A
47
+ * generator writes one apply job per wave of the component graph
48
+ * (`prLoopWaveJobs`), job `apply-wave-<k>` passing `--wave <k>` and needing
49
+ * the job before it, and the attempt record carries the approved set from
50
+ * one job to the next. A wave whose plan the approved change set covers
51
+ * applies under the pull request's approval; one whose plan moved stops at
52
+ * `<gate>-wave-<k>` (exit 3), and re-running its job after the approval
53
+ * carries on. `chant ci serve` runs the one apply job, which runs the waves
54
+ * in order until one stops.
55
+ *
56
+ * Each generated job runs its command through `chant ci run plan|apply
57
+ * --in-place` (#4156, `PrLoopJob.run`), so a forge job writes the same run
58
+ * record the chant runner does: Op `pr-plan` or `pr-apply`, signed with
59
+ * the runner key a forge secret puts in `CHANT_RUNNER_KEY`, and naming the
60
+ * forge's run as its `runOrigin`. `ci run` runs `command` in the job's own
61
+ * checkout with the job's environment, less the key. The plan passes
62
+ * `--no-push`, so it still writes nothing to the remote `chant/lifecycle`.
63
+ *
45
64
  * In a workspace member (#3465) both commands run in the member's directory
46
65
  * and pass `--member <name>`, so the member's gate (`pr-<number>-<member>`),
47
66
  * note and statuses stay apart from every other member's on the same pull
@@ -52,6 +71,13 @@
52
71
  import type { ForgeKind } from "../pr-forge";
53
72
  import { PR_APPLY_GATE } from "../pr-loop";
54
73
  import { CI_IMAGE, GITHUB_JOB_TOKEN } from "../ci-jobs";
74
+ import { EMPTY_TREE_SHA } from "./changed-files";
75
+ import { layerWaves } from "../gated-waves";
76
+ import type { DriverComponent } from "./driver";
77
+ import type { GatedWavePipelineOptions } from "./gated-wave-pipeline";
78
+
79
+ /** The variable `chant ci run` reads its runner key from (`RUNNER_KEY_ENV` in ../workspace/ci-green.ts). */
80
+ const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
55
81
 
56
82
  /** Where both jobs write their reports, kept as a CI artifact. */
57
83
  export const PR_LOOP_REPORT_DIR = ".chant/pr";
@@ -125,6 +151,12 @@ export interface PrLoopPipelineOptions {
125
151
  * `npm run build`. Setting it turns `build` on.
126
152
  */
127
153
  buildCommand?: string;
154
+ /**
155
+ * A gate per wave on merge (#4003): the apply runs `pr-apply --wave-gate
156
+ * <waveGate>`, a wave at a time, and a generator writes one apply job per
157
+ * wave. Set from `--wave-gate` beside `--pr-loop` ({@link prLoopWithWaves}).
158
+ */
159
+ waveGate?: string;
128
160
  /**
129
161
  * Which GitHub-shaped forge the workflow is for. Set by the forgejo
130
162
  * generator, which reuses the github one; a caller never needs to.
@@ -148,6 +180,17 @@ export interface PrLoopJob {
148
180
  setup?: string[];
149
181
  /** With `build` (#4088): the build line and the lint line, run in this order before the command. */
150
182
  build?: string[];
183
+ /** With `waveGate` (#4003): the wave a generated apply job runs, 1-based. */
184
+ wave?: number;
185
+ /**
186
+ * What the generated job runs in place of `command` (#4156): `chant ci run
187
+ * <job> --in-place` with the same environment, gate, forge, events and
188
+ * report URL, which runs `command` and records the run. `env` is the
189
+ * job's `env` and, on GitHub and Forgejo, `CHANT_RUNNER_KEY` from the
190
+ * repository secret of that name. GitLab hands its CI/CD variables to a
191
+ * job itself.
192
+ */
193
+ run: { command: string; env: Record<string, string> };
151
194
  }
152
195
 
153
196
  /** The lines `build` (#4088) runs before a job's command: the build, then the lint. Empty without the option. */
@@ -176,29 +219,82 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
176
219
  ...(github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}),
177
220
  ...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
178
221
  };
222
+ // #4156: the flags `chant ci run` rebuilds `command` from. It finds the member from the directory it runs in.
223
+ const ciRun = (job: "plan" | "apply"): string[] => [
224
+ "chant", "ci", "run", job, "--in-place", ...(job === "plan" ? ["--no-push"] : []),
225
+ "--base", '"$BASE_SHA"', ...(job === "plan" ? ["--pr", '"$PR_NUMBER"'] : []),
226
+ "--env", env, "--gate", gate, "--forge", forge,
227
+ ...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
228
+ ...(job === "apply" && options.branch ? ["--branch", JSON.stringify(options.branch)] : []),
229
+ ...(job === "apply" && options.requireReview === false ? ["--no-require-review"] : []),
230
+ ...(options.events ? ["--events-json"] : []),
231
+ ...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
232
+ ];
233
+ const runEnv = (env: Record<string, string>): Record<string, string> => (github ? { ...env, [RUNNER_KEY_ENV]: `\${{ secrets.${RUNNER_KEY_ENV} }}` } : env);
234
+ const planEnv = github
235
+ ? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...jobEnv }
236
+ : { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID", ...jobEnv };
237
+ const applyEnv = github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv };
179
238
  return [
180
239
  {
181
240
  jobName: "plan",
182
- command: ["chant", "components", "pr-plan", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common].join(" "),
183
- env: github
184
- ? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...jobEnv }
185
- : { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID", ...jobEnv },
241
+ // With `waveGate` the plan's note says the later waves may stop at their own gates (#4003).
242
+ command: ["chant", "components", "pr-plan", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common, ...(options.waveGate ? ["--wave-gate", options.waveGate] : [])].join(" "),
243
+ env: planEnv,
186
244
  ...build,
245
+ run: { command: ciRun("plan").join(" "), env: runEnv(planEnv) },
187
246
  },
188
247
  {
189
248
  jobName: "apply",
190
249
  command: [
191
250
  "chant", "components", "pr-apply", "--base", '"$BASE_SHA"', ...common,
192
251
  ...(options.requireReview === false ? [] : ["--require-review"]),
252
+ ...(options.waveGate ? ["--wave-gate", options.waveGate] : []),
193
253
  "--resume", PR_APPLY_RECORD,
194
254
  ].join(" "),
195
- env: github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv },
196
- ...(github ? {} : { setup: [gitlabBaseFallback(options.branch)] }),
255
+ env: applyEnv,
256
+ setup: [github ? githubBaseFallback(options.branch) : gitlabBaseFallback(options.branch)],
197
257
  ...build,
258
+ run: { command: ciRun("apply").join(" "), env: runEnv(applyEnv) },
198
259
  },
199
260
  ];
200
261
  }
201
262
 
263
+ /**
264
+ * The pull-request options with `--wave-gate` beside `--pr-loop` (#4003):
265
+ * the apply gets a gate per wave. A canary or a fixed base is refused, since
266
+ * the pull request's waves are its derivation's and each push is measured
267
+ * from the commit before it.
268
+ */
269
+ export function prLoopWithWaves(prLoop: PrLoopPipelineOptions, gatedWaves?: GatedWavePipelineOptions): PrLoopPipelineOptions {
270
+ if (!gatedWaves) return prLoop;
271
+ if (gatedWaves.canary?.length) throw new Error("a pull-request pipeline's waves are the change's derivation, so it takes no --canary; drop --canary");
272
+ if (gatedWaves.base !== undefined) throw new Error("a pull-request pipeline measures each push from the commit before it, so it takes no --base; drop --base");
273
+ return { ...prLoop, waveGate: gatedWaves.gate };
274
+ }
275
+
276
+ /** The name of the apply job for wave `wave` (#4003). */
277
+ export function prApplyWaveJobName(wave: number): string {
278
+ return `apply-wave-${wave}`;
279
+ }
280
+
281
+ /**
282
+ * One apply job per wave of the component graph (#4003), each `apply`'s
283
+ * command with `--wave <k>`. A change selects a subset, which never layers
284
+ * deeper than the graph, so a job past the change's last wave finds nothing
285
+ * left and exits 0.
286
+ */
287
+ export function prLoopWaveJobs(components: DriverComponent[], apply: PrLoopJob): PrLoopJob[] {
288
+ const count = Math.max(1, layerWaves(components.map((c) => ({ name: c.name, dependsOn: c.dependsOn ?? [] }))).length);
289
+ return Array.from({ length: count }, (_, i) => ({
290
+ ...apply,
291
+ command: `${apply.command} --wave ${i + 1}`,
292
+ wave: i + 1,
293
+ // #4156: the job runs it through chant ci run, which appends the same --wave to pr-apply.
294
+ run: { ...apply.run, command: `${apply.run.command} --wave ${i + 1}` },
295
+ }));
296
+ }
297
+
202
298
  /**
203
299
  * The concurrency group (GitHub, Forgejo) or resource group (GitLab) that
204
300
  * keeps one apply at a time per environment, and per member in a workspace.
@@ -227,19 +323,38 @@ export function prApplyRecordKey(env: string, commit: string, member?: string):
227
323
  }
228
324
 
229
325
  /**
230
- * GitLab sets `CI_COMMIT_BEFORE_SHA` to forty zeros on a branch's first push.
231
- * The line replaces it with the merge request's diff base when the pipeline
232
- * has one, and otherwise with the merge base of the target branch and the
233
- * pushed commit, or the pushed commit's parent when that is the commit itself.
326
+ * The shell that settles an unusable push base. `before` is empty or forty
327
+ * zeros on a branch's first push; then the base is `diffBase` when the forge
328
+ * gives one, else the merge base of `target` and `commit`, else `commit`'s
329
+ * parent when the merge base is `commit` itself, else, for a root commit, the
330
+ * empty tree.
234
331
  */
235
- export function gitlabBaseFallback(branch?: string): string {
236
- const target = branch ? JSON.stringify(branch) : "$CI_DEFAULT_BRANCH";
332
+ function pushBaseFallback(target: string, commit: string, diffBase?: string): string {
333
+ const fromBranch =
334
+ `git fetch origin ${target} && BASE_SHA="$(git merge-base FETCH_HEAD "${commit}")"; ` +
335
+ `if [ "$BASE_SHA" = "${commit}" ]; then BASE_SHA="$(git rev-parse --verify --quiet "${commit}^" || echo ${EMPTY_TREE_SHA})"; fi; `;
237
336
  return (
238
337
  'if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then ' +
239
- 'BASE_SHA="${CI_MERGE_REQUEST_DIFF_BASE_SHA:-}"; ' +
240
- 'if [ -z "$BASE_SHA" ]; then ' +
241
- `git fetch origin ${target} && BASE_SHA="$(git merge-base FETCH_HEAD "$CI_COMMIT_SHA")"; ` +
242
- 'if [ "$BASE_SHA" = "$CI_COMMIT_SHA" ]; then BASE_SHA="$(git rev-parse "$CI_COMMIT_SHA^")"; fi; ' +
243
- "fi; fi; export BASE_SHA"
338
+ (diffBase ? `BASE_SHA="\${${diffBase}:-}"; if [ -z "$BASE_SHA" ]; then ${fromBranch}fi; ` : fromBranch) +
339
+ "fi; export BASE_SHA"
244
340
  );
245
341
  }
342
+
343
+ /**
344
+ * GitLab sets `CI_COMMIT_BEFORE_SHA` to forty zeros on a branch's first push.
345
+ * The line replaces it with the merge request's diff base when the pipeline
346
+ * has one, and otherwise as {@link pushBaseFallback} does.
347
+ */
348
+ export function gitlabBaseFallback(branch?: string): string {
349
+ return pushBaseFallback(branch ? JSON.stringify(branch) : "$CI_DEFAULT_BRANCH", "$CI_COMMIT_SHA", "CI_MERGE_REQUEST_DIFF_BASE_SHA");
350
+ }
351
+
352
+ /**
353
+ * GitHub and Forgejo set `github.event.before` to forty zeros on a branch's
354
+ * first push (#4004). The line replaces it as {@link pushBaseFallback} does,
355
+ * with the target branch `main` unless the pipeline names another, the
356
+ * generators' default.
357
+ */
358
+ export function githubBaseFallback(branch?: string): string {
359
+ return pushBaseFallback(JSON.stringify(branch ?? "main"), "$GITHUB_SHA");
360
+ }