@intentius/chant 0.124.28 → 0.124.30

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 (449) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +18 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +67 -8
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +91 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +73 -2
  41. package/dist/cli/registry.d.ts +32 -4
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +49 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +38 -4
  67. package/dist/components/pr-run.d.ts +20 -2
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +26 -13
  70. package/dist/components/promote.d.ts +23 -0
  71. package/dist/components/promote.d.ts.map +1 -1
  72. package/dist/components/promote.js +60 -2
  73. package/dist/components/release-compile.d.ts +9 -0
  74. package/dist/components/release-compile.d.ts.map +1 -1
  75. package/dist/components/release-compile.js +9 -3
  76. package/dist/components/release-gate.d.ts +11 -1
  77. package/dist/components/release-gate.d.ts.map +1 -1
  78. package/dist/components/release-gate.js +10 -4
  79. package/dist/components/wave-plan.d.ts +16 -0
  80. package/dist/components/wave-plan.d.ts.map +1 -1
  81. package/dist/components/wave-plan.js +36 -2
  82. package/dist/config.d.ts +38 -0
  83. package/dist/config.d.ts.map +1 -1
  84. package/dist/config.js +2 -0
  85. package/dist/flag-params.d.ts +54 -0
  86. package/dist/flag-params.d.ts.map +1 -0
  87. package/dist/flag-params.js +130 -0
  88. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  89. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  90. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  91. package/dist/lifecycle/change-set-store.d.ts +2 -1
  92. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  93. package/dist/lifecycle/change-set-store.js +2 -1
  94. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  95. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  96. package/dist/lifecycle/delivery-metrics.js +145 -0
  97. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  98. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  99. package/dist/lifecycle/deploy-outcome.js +101 -0
  100. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  101. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  102. package/dist/lifecycle/gate-change-set.js +35 -3
  103. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +30 -1
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/git.js +31 -5
  108. package/dist/lifecycle/index.d.ts +4 -0
  109. package/dist/lifecycle/index.d.ts.map +1 -1
  110. package/dist/lifecycle/index.js +4 -0
  111. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  112. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  113. package/dist/lifecycle/mixed-environment.js +154 -0
  114. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  115. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-attachments.js +179 -0
  117. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  118. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-credentials.js +69 -0
  120. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  121. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-declaration.js +83 -0
  123. package/dist/lifecycle/publish-forge.d.ts +53 -0
  124. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-forge.js +298 -0
  126. package/dist/lifecycle/publish-npm.d.ts +61 -0
  127. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-npm.js +242 -0
  129. package/dist/lifecycle/publish-oci.d.ts +64 -0
  130. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-oci.js +383 -0
  132. package/dist/lifecycle/publish-record.d.ts +66 -0
  133. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-record.js +84 -0
  135. package/dist/lifecycle/publish-targets.d.ts +45 -0
  136. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  137. package/dist/lifecycle/publish-targets.js +85 -0
  138. package/dist/lifecycle/release-cut.d.ts +48 -1
  139. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  140. package/dist/lifecycle/release-cut.js +116 -23
  141. package/dist/lifecycle/release-join.d.ts +95 -0
  142. package/dist/lifecycle/release-join.d.ts.map +1 -0
  143. package/dist/lifecycle/release-join.js +139 -0
  144. package/dist/lifecycle/release-ledger.d.ts +8 -3
  145. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  146. package/dist/lifecycle/release-ledger.js +2 -0
  147. package/dist/lifecycle/release-publish.d.ts +312 -0
  148. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  149. package/dist/lifecycle/release-publish.js +335 -0
  150. package/dist/lifecycle/release-revocation.d.ts +80 -0
  151. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  152. package/dist/lifecycle/release-revocation.js +123 -0
  153. package/dist/lifecycle/release-store.d.ts +12 -2
  154. package/dist/lifecycle/release-store.d.ts.map +1 -1
  155. package/dist/lifecycle/release-store.js +4 -2
  156. package/dist/lifecycle/release-verify.d.ts +61 -0
  157. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  158. package/dist/lifecycle/release-verify.js +92 -0
  159. package/dist/lifecycle/release-workstation.d.ts +91 -0
  160. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  161. package/dist/lifecycle/release-workstation.js +180 -0
  162. package/dist/lifecycle/retention-policy.d.ts +45 -0
  163. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  164. package/dist/lifecycle/retention-policy.js +43 -0
  165. package/dist/lifecycle/retention.d.ts +185 -0
  166. package/dist/lifecycle/retention.d.ts.map +1 -0
  167. package/dist/lifecycle/retention.js +400 -0
  168. package/dist/lifecycle/run-ledger.d.ts +1 -1
  169. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  170. package/dist/lifecycle/run-ledger.js +9 -0
  171. package/dist/op/activities/activity-contracts.d.ts +1 -0
  172. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  173. package/dist/op/activities/activity-contracts.js +1 -0
  174. package/dist/op/activities/shell.d.ts +6 -0
  175. package/dist/op/activities/shell.d.ts.map +1 -1
  176. package/dist/op/activities/shell.js +1 -1
  177. package/dist/op/local-executor.d.ts +23 -1
  178. package/dist/op/local-executor.d.ts.map +1 -1
  179. package/dist/op/local-executor.js +9 -3
  180. package/dist/op/runtime.d.ts +27 -0
  181. package/dist/op/runtime.d.ts.map +1 -1
  182. package/dist/op/runtime.js +20 -0
  183. package/dist/plan-summary.d.ts.map +1 -1
  184. package/dist/plan-summary.js +8 -2
  185. package/dist/pr-forge.d.ts +23 -0
  186. package/dist/pr-forge.d.ts.map +1 -1
  187. package/dist/pr-forge.js +44 -0
  188. package/dist/pr-loop.d.ts +17 -0
  189. package/dist/pr-loop.d.ts.map +1 -1
  190. package/dist/pr-loop.js +30 -4
  191. package/dist/pr-report-url.d.ts +82 -0
  192. package/dist/pr-report-url.d.ts.map +1 -0
  193. package/dist/pr-report-url.js +174 -0
  194. package/dist/provenance.d.ts +2 -2
  195. package/dist/provenance.d.ts.map +1 -1
  196. package/dist/run-event-store.d.ts +29 -2
  197. package/dist/run-event-store.d.ts.map +1 -1
  198. package/dist/run-event-store.js +50 -3
  199. package/dist/run-event-stream.d.ts +4 -3
  200. package/dist/run-event-stream.d.ts.map +1 -1
  201. package/dist/run-event-stream.js +7 -3
  202. package/dist/run-event.d.ts +34 -1
  203. package/dist/run-event.d.ts.map +1 -1
  204. package/dist/run-event.js +4 -0
  205. package/dist/wave-runtime.mjs +260 -229
  206. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  207. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  208. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  209. package/dist/workspace/change-set.schema.json +5 -1
  210. package/dist/workspace/check.schema.json +4 -2
  211. package/dist/workspace/checks/records.d.ts +9 -1
  212. package/dist/workspace/checks/records.d.ts.map +1 -1
  213. package/dist/workspace/checks/records.js +35 -1
  214. package/dist/workspace/checks.d.ts +1 -0
  215. package/dist/workspace/checks.d.ts.map +1 -1
  216. package/dist/workspace/checks.js +2 -1
  217. package/dist/workspace/ci-green-forge.d.ts +2 -0
  218. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  219. package/dist/workspace/ci-green.d.ts +10 -0
  220. package/dist/workspace/ci-green.d.ts.map +1 -1
  221. package/dist/workspace/ci-green.js +14 -5
  222. package/dist/workspace/ci-run.d.ts +222 -0
  223. package/dist/workspace/ci-run.d.ts.map +1 -0
  224. package/dist/workspace/ci-run.js +464 -0
  225. package/dist/workspace/ci-serve.d.ts +214 -0
  226. package/dist/workspace/ci-serve.d.ts.map +1 -0
  227. package/dist/workspace/ci-serve.js +412 -0
  228. package/dist/workspace/declaration.d.ts +16 -0
  229. package/dist/workspace/declaration.d.ts.map +1 -1
  230. package/dist/workspace/declaration.js +26 -0
  231. package/dist/workspace/declaration.schema.json +64 -0
  232. package/dist/workspace/declared-kinds.d.ts +16 -0
  233. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  234. package/dist/workspace/declared-kinds.js +25 -0
  235. package/dist/workspace/delivery-metrics.d.ts +91 -0
  236. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  237. package/dist/workspace/delivery-metrics.js +217 -0
  238. package/dist/workspace/delivery-metrics.schema.json +519 -0
  239. package/dist/workspace/flags-read.d.ts +37 -0
  240. package/dist/workspace/flags-read.d.ts.map +1 -0
  241. package/dist/workspace/flags-read.js +103 -0
  242. package/dist/workspace/flags.d.ts +100 -0
  243. package/dist/workspace/flags.d.ts.map +1 -0
  244. package/dist/workspace/flags.js +98 -0
  245. package/dist/workspace/graph-cli.js +1 -1
  246. package/dist/workspace/intent-cli.d.ts +11 -2
  247. package/dist/workspace/intent-cli.d.ts.map +1 -1
  248. package/dist/workspace/intent-cli.js +54 -5
  249. package/dist/workspace/intent-trace.d.ts +219 -0
  250. package/dist/workspace/intent-trace.d.ts.map +1 -0
  251. package/dist/workspace/intent-trace.js +314 -0
  252. package/dist/workspace/intent.d.ts +30 -6
  253. package/dist/workspace/intent.d.ts.map +1 -1
  254. package/dist/workspace/intent.js +141 -21
  255. package/dist/workspace/intent.schema.json +702 -6
  256. package/dist/workspace/overlap.d.ts +146 -0
  257. package/dist/workspace/overlap.d.ts.map +1 -0
  258. package/dist/workspace/overlap.js +276 -0
  259. package/dist/workspace/pr-report.schema.json +5 -0
  260. package/dist/workspace/reason-codes.d.ts +2 -0
  261. package/dist/workspace/reason-codes.d.ts.map +1 -1
  262. package/dist/workspace/reason-codes.js +3 -0
  263. package/dist/workspace/records.d.ts +15 -2
  264. package/dist/workspace/records.d.ts.map +1 -1
  265. package/dist/workspace/records.js +32 -0
  266. package/dist/workspace/records.schema.json +21 -2
  267. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  268. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  269. package/dist/workspace/releases.d.ts +9 -0
  270. package/dist/workspace/releases.d.ts.map +1 -1
  271. package/dist/workspace/releases.js +9 -0
  272. package/dist/workspace/releases.schema.json +123 -1
  273. package/dist/workspace/run-event.schema.json +24 -6
  274. package/dist/workspace/status-gates.d.ts +8 -0
  275. package/dist/workspace/status-gates.d.ts.map +1 -1
  276. package/dist/workspace/status-stewards.d.ts +2 -0
  277. package/dist/workspace/status-stewards.d.ts.map +1 -1
  278. package/dist/workspace/status-stewards.js +1 -0
  279. package/dist/workspace/status.d.ts +78 -0
  280. package/dist/workspace/status.d.ts.map +1 -1
  281. package/dist/workspace/status.js +170 -3
  282. package/dist/workspace/status.schema.json +210 -2
  283. package/dist/workspace/wip.d.ts.map +1 -1
  284. package/dist/workspace/wip.js +11 -3
  285. package/package.json +1 -1
  286. package/src/build-params.test.ts +46 -0
  287. package/src/build-params.ts +55 -1
  288. package/src/change-set-graph.ts +10 -2
  289. package/src/change-set.ts +14 -3
  290. package/src/cli/build-params-cli.ts +22 -9
  291. package/src/cli/commands/build.ts +6 -3
  292. package/src/cli/handlers/approve-release.test.ts +5 -1
  293. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  294. package/src/cli/handlers/approve.ts +47 -0
  295. package/src/cli/handlers/build.test.ts +30 -0
  296. package/src/cli/handlers/build.ts +20 -4
  297. package/src/cli/handlers/fan-out.test.ts +262 -1
  298. package/src/cli/handlers/fan-out.ts +72 -8
  299. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  300. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  301. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  302. package/src/cli/handlers/pr.test.ts +148 -1
  303. package/src/cli/handlers/pr.ts +144 -28
  304. package/src/cli/handlers/promote.test.ts +280 -2
  305. package/src/cli/handlers/promote.ts +210 -34
  306. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  307. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  308. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  309. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  310. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  311. package/src/cli/handlers/release.e2e.test.ts +146 -32
  312. package/src/cli/handlers/release.ts +415 -28
  313. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  314. package/src/cli/handlers/run-release.test.ts +55 -0
  315. package/src/cli/handlers/run.ts +69 -5
  316. package/src/cli/main.test.ts +1 -0
  317. package/src/cli/main.ts +91 -10
  318. package/src/cli/parse-args.ts +54 -2
  319. package/src/cli/registry.ts +32 -4
  320. package/src/cli/run-events.ts +5 -2
  321. package/src/components/auto-release.test.ts +9 -0
  322. package/src/components/auto-release.ts +3 -0
  323. package/src/components/capability.ts +9 -0
  324. package/src/components/driver.test.ts +24 -0
  325. package/src/components/driver.ts +36 -0
  326. package/src/components/fan-out-inputs.test.ts +85 -0
  327. package/src/components/fan-out-inputs.ts +192 -0
  328. package/src/components/fan-out-record.test.ts +10 -0
  329. package/src/components/fan-out-record.ts +19 -0
  330. package/src/components/fan-out-run.ts +48 -0
  331. package/src/components/fan-out.ts +4 -0
  332. package/src/components/pr-pipeline.ts +67 -4
  333. package/src/components/pr-run.test.ts +127 -0
  334. package/src/components/pr-run.ts +32 -12
  335. package/src/components/promote.ts +68 -2
  336. package/src/components/release-compile.ts +17 -3
  337. package/src/components/release-gate.ts +18 -4
  338. package/src/components/wave-plan.ts +45 -2
  339. package/src/config.ts +8 -0
  340. package/src/flag-params.test.ts +144 -0
  341. package/src/flag-params.ts +134 -0
  342. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  343. package/src/lifecycle/change-set-store.ts +2 -1
  344. package/src/lifecycle/delivery-metrics.ts +263 -0
  345. package/src/lifecycle/deploy-outcome.ts +147 -0
  346. package/src/lifecycle/gate-change-set.ts +37 -6
  347. package/src/lifecycle/gate-ledger.ts +2 -2
  348. package/src/lifecycle/git.ts +61 -8
  349. package/src/lifecycle/index.ts +4 -0
  350. package/src/lifecycle/mixed-environment.test.ts +97 -0
  351. package/src/lifecycle/mixed-environment.ts +204 -0
  352. package/src/lifecycle/publish-attachments.ts +196 -0
  353. package/src/lifecycle/publish-credentials.ts +84 -0
  354. package/src/lifecycle/publish-declaration.ts +95 -0
  355. package/src/lifecycle/publish-forge.ts +325 -0
  356. package/src/lifecycle/publish-npm.ts +259 -0
  357. package/src/lifecycle/publish-oci.ts +396 -0
  358. package/src/lifecycle/publish-record.ts +113 -0
  359. package/src/lifecycle/publish-resume.test.ts +168 -0
  360. package/src/lifecycle/publish-targets.test.ts +494 -0
  361. package/src/lifecycle/publish-targets.ts +100 -0
  362. package/src/lifecycle/release-cut.test.ts +74 -2
  363. package/src/lifecycle/release-cut.ts +158 -22
  364. package/src/lifecycle/release-flag.test.ts +112 -0
  365. package/src/lifecycle/release-join.test.ts +68 -0
  366. package/src/lifecycle/release-join.ts +182 -0
  367. package/src/lifecycle/release-ledger.ts +9 -3
  368. package/src/lifecycle/release-publish.test.ts +225 -0
  369. package/src/lifecycle/release-publish.ts +568 -0
  370. package/src/lifecycle/release-revocation.ts +147 -0
  371. package/src/lifecycle/release-store.ts +15 -4
  372. package/src/lifecycle/release-verify.test.ts +110 -0
  373. package/src/lifecycle/release-verify.ts +120 -0
  374. package/src/lifecycle/release-workstation.test.ts +204 -0
  375. package/src/lifecycle/release-workstation.ts +216 -0
  376. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  377. package/src/lifecycle/retention-policy.ts +63 -0
  378. package/src/lifecycle/retention.test.ts +363 -0
  379. package/src/lifecycle/retention.ts +508 -0
  380. package/src/lifecycle/run-ledger.ts +7 -1
  381. package/src/op/activities/activity-contracts.ts +1 -0
  382. package/src/op/activities/shell.ts +7 -1
  383. package/src/op/local-executor.ts +27 -4
  384. package/src/op/runtime.ts +42 -0
  385. package/src/plan-summary.test.ts +20 -0
  386. package/src/plan-summary.ts +8 -1
  387. package/src/pr-forge.test.ts +25 -0
  388. package/src/pr-forge.ts +62 -0
  389. package/src/pr-loop.test.ts +13 -1
  390. package/src/pr-loop.ts +41 -4
  391. package/src/pr-report-url.test.ts +131 -0
  392. package/src/pr-report-url.ts +209 -0
  393. package/src/provenance.ts +2 -2
  394. package/src/run-event-store.ts +63 -3
  395. package/src/run-event-stream.ts +8 -4
  396. package/src/run-event.ts +38 -1
  397. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  398. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  399. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  400. package/src/workspace/change-set.schema.json +5 -1
  401. package/src/workspace/check.schema.json +4 -2
  402. package/src/workspace/checks/records.ts +35 -1
  403. package/src/workspace/checks.ts +2 -1
  404. package/src/workspace/ci-green-forge.ts +2 -0
  405. package/src/workspace/ci-green.ts +20 -5
  406. package/src/workspace/ci-run.test.ts +416 -0
  407. package/src/workspace/ci-run.ts +571 -0
  408. package/src/workspace/ci-serve.test.ts +268 -0
  409. package/src/workspace/ci-serve.ts +521 -0
  410. package/src/workspace/declaration.schema.json +64 -0
  411. package/src/workspace/declaration.test.ts +28 -0
  412. package/src/workspace/declaration.ts +38 -0
  413. package/src/workspace/declared-kinds.ts +40 -1
  414. package/src/workspace/delivery-metrics.schema.json +519 -0
  415. package/src/workspace/delivery-metrics.test.ts +236 -0
  416. package/src/workspace/delivery-metrics.ts +286 -0
  417. package/src/workspace/flags-read.ts +121 -0
  418. package/src/workspace/flags.test.ts +141 -0
  419. package/src/workspace/flags.ts +157 -0
  420. package/src/workspace/graph-cli.ts +1 -1
  421. package/src/workspace/intent-cli.ts +46 -5
  422. package/src/workspace/intent-trace.test.ts +173 -0
  423. package/src/workspace/intent-trace.ts +484 -0
  424. package/src/workspace/intent.schema.json +702 -6
  425. package/src/workspace/intent.ts +148 -25
  426. package/src/workspace/older-records.test.ts +412 -0
  427. package/src/workspace/overlap.test.ts +165 -0
  428. package/src/workspace/overlap.ts +340 -0
  429. package/src/workspace/pr-report.schema.json +5 -0
  430. package/src/workspace/read-contract.e2e.test.ts +2 -1
  431. package/src/workspace/reason-codes.test.ts +5 -0
  432. package/src/workspace/reason-codes.ts +3 -0
  433. package/src/workspace/record-assets.test.ts +2 -1
  434. package/src/workspace/records-contract.e2e.test.ts +2 -1
  435. package/src/workspace/records.schema.json +21 -2
  436. package/src/workspace/records.ts +36 -2
  437. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  438. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  439. package/src/workspace/releases.schema.json +123 -1
  440. package/src/workspace/releases.ts +11 -0
  441. package/src/workspace/run-event.schema.json +24 -6
  442. package/src/workspace/status-contract.test.ts +29 -0
  443. package/src/workspace/status-gates.ts +8 -0
  444. package/src/workspace/status-mixed.test.ts +152 -0
  445. package/src/workspace/status-revoked.test.ts +153 -0
  446. package/src/workspace/status-stewards.ts +3 -0
  447. package/src/workspace/status.schema.json +210 -2
  448. package/src/workspace/status.ts +254 -3
  449. package/src/workspace/wip.ts +11 -2
@@ -26,6 +26,7 @@ import {
26
26
  resolveWiring,
27
27
  runComponentDeploy,
28
28
  runInterpretDriver,
29
+ unresolvedWiringSources,
29
30
  type DriverComponent,
30
31
  type DriverStepRecord,
31
32
  type RunProgressEvent,
@@ -1149,3 +1150,26 @@ describe("the release a deploy step sees (#3061, ws-081)", () => {
1149
1150
  expect(deploy.calls[0]!.ctx.release).toEqual({ revision: "r0", version: "sha256:recorded" });
1150
1151
  });
1151
1152
  });
1153
+
1154
+ describe("unresolvedWiringSources (#4084)", () => {
1155
+ it("names each component whose cross-component output has no value yet, sorted and once", () => {
1156
+ const input = {
1157
+ template: "dist/a.json",
1158
+ inputs: {
1159
+ topic: { stackOutput: { stack: "network", name: "TopicArn" } },
1160
+ queue: { stackOutput: { stack: "network", name: "QueueUrl" } },
1161
+ table: { stackOutput: { stack: "cluster-a", name: "TableName" } },
1162
+ image: "@api-image.publish.digest",
1163
+ local: "@Publish.digest",
1164
+ },
1165
+ };
1166
+ expect(unresolvedWiringSources(input, {})).toEqual(["api-image", "cluster-a", "network"]);
1167
+ expect(
1168
+ unresolvedWiringSources(input, {
1169
+ network: { TopicArn: "t", QueueUrl: "q" },
1170
+ "cluster-a": { TableName: "x" },
1171
+ "api-image": { publish: { digest: "sha256:1" } },
1172
+ }),
1173
+ ).toEqual([]);
1174
+ });
1175
+ });
@@ -345,6 +345,42 @@ export function resolveStepInput(
345
345
  return walk(input) as Record<string, unknown>;
346
346
  }
347
347
 
348
+ /**
349
+ * The components (or stacks) whose outputs `input` reads through
350
+ * cross-component wiring, `stackOutput()` or `@<component>.publish.*`, and
351
+ * which have no value for it in `componentOutputs`, sorted and each once
352
+ * (#4084). {@link resolveStepInput} turns each such reference into
353
+ * `undefined`; a plan uses this to say whose outputs it is waiting on.
354
+ */
355
+ export function unresolvedWiringSources(
356
+ input: Record<string, unknown>,
357
+ componentOutputs: Record<string, Record<string, unknown>>,
358
+ ): string[] {
359
+ const found = new Set<string>();
360
+ const walk = (value: unknown): void => {
361
+ if (typeof value === "string") {
362
+ if (PRIOR_STEP_REF.test(value)) return;
363
+ const ref = value.match(COMPONENT_ARTIFACT_REF);
364
+ if (ref && resolvePath(componentOutputs[ref[1]]?.publish, ref[2]) === undefined) found.add(ref[1]);
365
+ return;
366
+ }
367
+ if (Array.isArray(value)) {
368
+ value.forEach(walk);
369
+ return;
370
+ }
371
+ if (value && typeof value === "object") {
372
+ if ("stackOutput" in value) {
373
+ const { stack, name } = (value as { stackOutput: { stack: string; name: string } }).stackOutput;
374
+ if (resolvePath(componentOutputs[stack], name) === undefined) found.add(stack);
375
+ return;
376
+ }
377
+ Object.values(value as Record<string, unknown>).forEach(walk);
378
+ }
379
+ };
380
+ walk(input);
381
+ return [...found].sort();
382
+ }
383
+
348
384
  // ── Step + phase execution ───────────────────────────────────────────────
349
385
 
350
386
  /**
@@ -0,0 +1,85 @@
1
+ /**
2
+ * What each component would apply, as a digest (#4081), and which of an
3
+ * earlier attempt's progress holds against it.
4
+ */
5
+
6
+ import { describe, expect, test } from "vitest";
7
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { componentInputDigests, progressThatCarries } from "./fan-out-inputs";
11
+ import type { DriverComponent } from "./driver";
12
+
13
+ function project(): string {
14
+ const root = mkdtempSync(join(tmpdir(), "fan-out-inputs-"));
15
+ mkdirSync(join(root, "dist"));
16
+ for (const name of ["net", "cluster", "app"]) writeFileSync(join(root, "dist", `${name}.json`), `{"name":"${name}"}`);
17
+ return root;
18
+ }
19
+
20
+ const component = (name: string, dependsOn: string[] = [], extra: Record<string, unknown> = {}): DriverComponent => ({
21
+ name,
22
+ dependsOn,
23
+ deploy: [{ phase: "Apply", steps: [{ kind: "cfn-deploy", stack: name, template: `dist/${name}.json`, ...extra }] }],
24
+ });
25
+
26
+ const estate = [component("net"), component("cluster", ["net"]), component("app", ["cluster"])];
27
+
28
+ describe("componentInputDigests", () => {
29
+ test("is stable for the same project, environment and files", () => {
30
+ const root = project();
31
+ expect(componentInputDigests(estate, { projectRoot: root, env: "prod" })).toEqual(
32
+ componentInputDigests(estate, { projectRoot: root, env: "prod" }),
33
+ );
34
+ });
35
+
36
+ test("covers the bytes of a file a step input names, and everything downstream of it", () => {
37
+ const root = project();
38
+ const before = componentInputDigests(estate, { projectRoot: root, env: "prod" });
39
+ writeFileSync(join(root, "dist", "cluster.json"), `{"name":"cluster","queue":true}`);
40
+ const after = componentInputDigests(estate, { projectRoot: root, env: "prod" });
41
+ expect(after.net).toBe(before.net);
42
+ expect(after.cluster).not.toBe(before.cluster);
43
+ expect(after.app).not.toBe(before.app);
44
+ });
45
+
46
+ test("covers a directory a step names, by its files", () => {
47
+ const root = project();
48
+ const chart = [component("chart", [], { chart: "dist" })];
49
+ const before = componentInputDigests(chart, { projectRoot: root, env: "prod" });
50
+ writeFileSync(join(root, "dist", "app.json"), "{}");
51
+ expect(componentInputDigests(chart, { projectRoot: root, env: "prod" }).chart).not.toBe(before.chart);
52
+ });
53
+
54
+ test("covers the environment and the authored steps", () => {
55
+ const root = project();
56
+ const prod = componentInputDigests(estate, { projectRoot: root, env: "prod" });
57
+ expect(componentInputDigests(estate, { projectRoot: root, env: "staging" }).net).not.toBe(prod.net);
58
+ const moved = [component("net", [], { inputs: { size: 2 } }), ...estate.slice(1)];
59
+ expect(componentInputDigests(moved, { projectRoot: root, env: "prod" }).net).not.toBe(prod.net);
60
+ });
61
+
62
+ test("ignores a string that is not a file, a path outside the project, and the phase name", () => {
63
+ const root = project();
64
+ mkdirSync(join(root, "Apply"));
65
+ const before = componentInputDigests(estate, { projectRoot: root, env: "prod" });
66
+ writeFileSync(join(root, "Apply", "x"), "changes nothing");
67
+ expect(componentInputDigests(estate, { projectRoot: root, env: "prod" })).toEqual(before);
68
+ const outside = [component("net", [], { other: "../elsewhere.json" })];
69
+ expect(() => componentInputDigests(outside, { projectRoot: root, env: "prod" })).not.toThrow();
70
+ });
71
+ });
72
+
73
+ describe("progressThatCarries", () => {
74
+ test("carries a component only when its recorded input is the one it has now", () => {
75
+ const split = progressThatCarries(
76
+ { completed: ["net", "cluster", "app"], inputs: { net: "a", cluster: "b" } },
77
+ { net: "a", cluster: "B", app: "c" },
78
+ );
79
+ expect(split).toEqual({ carried: ["net"], changed: ["app", "cluster"], unrecorded: false });
80
+ });
81
+
82
+ test("a record with no input digests carries nothing", () => {
83
+ expect(progressThatCarries({ completed: ["net"] }, { net: "a" })).toEqual({ carried: [], changed: ["net"], unrecorded: true });
84
+ });
85
+ });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * What each component of a fan-out would apply, as one digest per component
3
+ * (#4081), and which of an earlier attempt's progress still holds against it.
4
+ *
5
+ * A fan-out's plan digest identifies the derivation: who runs, in what order,
6
+ * off which edges. Two different changes that touch the same components in
7
+ * the same shape derive the same plan, so the plan digest alone cannot say
8
+ * whether "cluster-a already applied" is about the change in front of us.
9
+ * The attempt record therefore keeps, for each component it applied, the
10
+ * input digest computed here, and a resumed attempt carries a component as
11
+ * `already-applied` only when that digest is the one the component has now.
12
+ *
13
+ * ## What a component's input digest covers
14
+ *
15
+ * - The environment. Progress against `local` says nothing about `prod`.
16
+ * - The composition: every phase and step as authored, `rollback` included.
17
+ * A `stackOutput` reference is hashed as the reference, not its value.
18
+ * - The bytes of every file a step input names. A string input that resolves,
19
+ * relative to the project root, to a file or directory inside the root is
20
+ * read and hashed, so `template: "dist/cluster-a.template.json"` covers the
21
+ * built template and a rebuild that changes it changes the digest. A
22
+ * directory is hashed by its files, skipping `.git`, `node_modules` and
23
+ * `.chant`, the last because the attempt record itself lives there.
24
+ * - The input digests of the components it depends on. A parameter filled
25
+ * from an upstream output gets its value from that upstream's apply, so a
26
+ * change upstream changes the digest of everything downstream of it, the
27
+ * same reach the fan-out's own walk has.
28
+ *
29
+ * Nothing decided during the run goes in: no run id, no time, no output an
30
+ * apply produced.
31
+ */
32
+
33
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
34
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
35
+ import { computePlanDigest } from "../lifecycle/plan-digest";
36
+ import { sha256Hex } from "../content-digest";
37
+ import type { DriverComponent, DriverPhase } from "./driver";
38
+
39
+ export interface ComponentInputOptions {
40
+ /** The root step inputs' file paths are read relative to, the project the fan-out runs in. */
41
+ projectRoot: string;
42
+ /** The environment the fan-out applies to. */
43
+ env: string;
44
+ /** Environment config resolved ahead of the run, when the caller has any. */
45
+ vars?: Record<string, unknown>;
46
+ }
47
+
48
+ const SKIPPED_DIRS = new Set([".git", "node_modules", ".chant"]);
49
+
50
+ /** Inside `root`, and not `root` itself: a bare "." names the whole project, which is no one step's input. */
51
+ function insideRoot(root: string, path: string): boolean {
52
+ const rel = relative(root, path);
53
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
54
+ }
55
+
56
+ /** Hash a file, or a directory by the relative path and bytes of each file under it. */
57
+ function hashPath(path: string): string | undefined {
58
+ let stat;
59
+ try {
60
+ stat = statSync(path);
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ if (stat.isFile()) return `sha256:${sha256Hex(readFileSync(path))}`;
65
+ if (!stat.isDirectory()) return undefined;
66
+ const files: Record<string, string> = {};
67
+ const walk = (dir: string): void => {
68
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
69
+ if (SKIPPED_DIRS.has(entry.name)) continue;
70
+ const full = join(dir, entry.name);
71
+ if (entry.isDirectory()) walk(full);
72
+ else if (entry.isFile()) files[relative(path, full).split(sep).join("/")] = sha256Hex(readFileSync(full));
73
+ }
74
+ };
75
+ walk(path);
76
+ return computePlanDigest("component-input-dir", files);
77
+ }
78
+
79
+ /** Every file or directory a component's steps name, keyed by the string the step wrote. */
80
+ function referencedFiles(component: DriverComponent, root: string): Record<string, string> {
81
+ const out: Record<string, string> = {};
82
+ const visit = (value: unknown): void => {
83
+ if (typeof value === "string") {
84
+ if (value === "" || value.includes("\n") || value in out) return;
85
+ const path = resolve(root, value);
86
+ if (!insideRoot(root, path) || !existsSync(path)) return;
87
+ const digest = hashPath(path);
88
+ if (digest !== undefined) out[value] = digest;
89
+ return;
90
+ }
91
+ if (Array.isArray(value)) value.forEach(visit);
92
+ else if (value && typeof value === "object") Object.values(value).forEach(visit);
93
+ };
94
+ // Only a step's inputs name files. A phase name or a step's `kind` that
95
+ // happens to match a directory in the project is not one.
96
+ const visitPhases = (phases: DriverPhase[] | undefined): void => {
97
+ for (const phase of phases ?? []) {
98
+ for (const step of phase.steps) {
99
+ if (isPhase(step)) {
100
+ visitPhases([step]);
101
+ continue;
102
+ }
103
+ if (step.kind === "gate") continue;
104
+ const { kind: _kind, ...inputs } = step;
105
+ visit(inputs);
106
+ }
107
+ visitPhases(phase.onFailure);
108
+ }
109
+ };
110
+ visitPhases(component.deploy);
111
+ visitPhases(component.rollback);
112
+ return out;
113
+ }
114
+
115
+ function isPhase(step: DriverPhase["steps"][number]): step is DriverPhase {
116
+ return typeof (step as { phase?: unknown }).phase === "string" && Array.isArray((step as DriverPhase).steps);
117
+ }
118
+
119
+ /**
120
+ * One input digest per component in `components`, keyed by name. Each folds
121
+ * in the digests of the components it depends on, so `components` should be
122
+ * the whole project: a dependency it does not contain is hashed by name only.
123
+ */
124
+ export function componentInputDigests(
125
+ components: DriverComponent[],
126
+ options: ComponentInputOptions,
127
+ ): Record<string, string> {
128
+ const root = resolve(options.projectRoot);
129
+ const byName = new Map(components.map((c) => [c.name, c]));
130
+ const digests = new Map<string, string>();
131
+ const visiting = new Set<string>();
132
+
133
+ const digestOf = (name: string): string | null => {
134
+ const known = digests.get(name);
135
+ if (known !== undefined) return known;
136
+ const component = byName.get(name);
137
+ // An unknown dependency or a cycle is refused by the fan-out's derivation
138
+ // before this runs; here it only has to not loop.
139
+ if (!component || visiting.has(name)) return null;
140
+ visiting.add(name);
141
+ const upstream = [...(component.dependsOn ?? [])].sort().map((dep) => ({ component: dep, digest: digestOf(dep) }));
142
+ visiting.delete(name);
143
+ const digest = computePlanDigest("component-input", {
144
+ environment: options.env,
145
+ component: name,
146
+ deploy: component.deploy,
147
+ rollback: component.rollback ?? null,
148
+ vars: options.vars ?? null,
149
+ files: referencedFiles(component, root),
150
+ upstream,
151
+ });
152
+ digests.set(name, digest);
153
+ return digest;
154
+ };
155
+
156
+ for (const c of components) digestOf(c.name);
157
+ return Object.fromEntries([...digests.entries()].sort(([a], [b]) => a.localeCompare(b)));
158
+ }
159
+
160
+ /** What of an earlier attempt's progress holds against the inputs the components have now. */
161
+ export interface CarriedProgress {
162
+ /** Components the attempt applied whose input is the same now. These stay `already-applied`. */
163
+ carried: string[];
164
+ /** Components the attempt applied whose input moved since. These run again. */
165
+ changed: string[];
166
+ /**
167
+ * True when the record has no input digests at all, as one written before
168
+ * #4081 has. Nothing carries from such a record, since nothing in it says
169
+ * what was applied.
170
+ */
171
+ unrecorded: boolean;
172
+ }
173
+
174
+ /**
175
+ * Split an attempt's `completed` by whether each component's input is the
176
+ * same as now. A component the record holds no digest for counts as changed:
177
+ * a claim that it applied is only worth keeping for the input it applied.
178
+ */
179
+ export function progressThatCarries(
180
+ attempt: { completed: string[]; inputs?: Record<string, string> },
181
+ now: Record<string, string>,
182
+ ): CarriedProgress {
183
+ const recorded = attempt.inputs;
184
+ const carried: string[] = [];
185
+ const changed: string[] = [];
186
+ for (const name of [...attempt.completed].sort()) {
187
+ const then = recorded?.[name];
188
+ if (then !== undefined && now[name] !== undefined && then === now[name]) carried.push(name);
189
+ else changed.push(name);
190
+ }
191
+ return { carried, changed, unrecorded: recorded === undefined };
192
+ }
@@ -70,3 +70,13 @@ describe("prApply (#3464)", () => {
70
70
  for (const name of ["a.json", "b.json", "c.json"]) expect(readFanOutAttempt(join(dir, name))?.prApply).toBeUndefined();
71
71
  });
72
72
  });
73
+
74
+ describe("inputs (#4081)", () => {
75
+ test("round-trips, and a record without them reads as none", () => {
76
+ const dir = mkdtempSync(join(tmpdir(), "fan-out-record-"));
77
+ writeFanOutAttempt(join(dir, "a.json"), { digest: "d", completed: ["net"], failed: [], outputs: {}, inputs: { net: "jcs1-sha256:x" } });
78
+ expect(readFanOutAttempt(join(dir, "a.json"))?.inputs).toEqual({ net: "jcs1-sha256:x" });
79
+ writeFileSync(join(dir, "b.json"), JSON.stringify({ digest: "d", completed: ["net"], failed: [], outputs: {} }));
80
+ expect(readFanOutAttempt(join(dir, "b.json"))?.inputs).toBeUndefined();
81
+ });
82
+ });
@@ -25,6 +25,14 @@
25
25
  * `carried` holds what a step asked to keep for its member (#3459). It rides
26
26
  * in the record rather than beside it because the record is the one file a
27
27
  * per-wave CI job hands to the next.
28
+ *
29
+ * `inputs` holds, for each component in `completed`, the input digest it
30
+ * applied (#4081, ./fan-out-inputs.ts). The plan digest only identifies the
31
+ * derivation, and two different changes with the same shape derive the same
32
+ * one, so the digest alone would carry "cluster-a already applied" into a
33
+ * change that never applied it. A resumed attempt carries a component only
34
+ * when its input digest is the same now. A record written before #4081 has
35
+ * no `inputs`, and none of its progress carries.
28
36
  */
29
37
 
30
38
  import { existsSync, readFileSync } from "node:fs";
@@ -68,6 +76,11 @@ export interface FanOutAttempt {
68
76
  carried?: Record<string, unknown>;
69
77
  /** A pull request's apply only (#3464). */
70
78
  prApply?: PrApplyRecord;
79
+ /**
80
+ * The input digest each completed component applied (#4081), keyed by
81
+ * name. Absent on a record written before it, which then carries nothing.
82
+ */
83
+ inputs?: Record<string, string>;
71
84
  }
72
85
 
73
86
  const isRecord = (value: unknown): value is Record<string, unknown> =>
@@ -109,6 +122,11 @@ export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
109
122
  }
110
123
  const waves = readWaveRecords(parsed.waves);
111
124
  const prApply = readPrApplyRecord(parsed.prApply);
125
+ let inputs: Record<string, string> | undefined;
126
+ if (isRecord(parsed.inputs)) {
127
+ inputs = {};
128
+ for (const [name, value] of Object.entries(parsed.inputs)) if (typeof value === "string") inputs[name] = value;
129
+ }
112
130
  return {
113
131
  digest: typeof parsed.digest === "string" ? parsed.digest : "",
114
132
  completed: Array.isArray(parsed.completed) ? parsed.completed.filter((n): n is string => typeof n === "string") : [],
@@ -117,6 +135,7 @@ export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
117
135
  ...(waves.length > 0 ? { waves } : {}),
118
136
  ...(isRecord(parsed.carried) && Object.keys(parsed.carried).length > 0 ? { carried: parsed.carried } : {}),
119
137
  ...(prApply ? { prApply } : {}),
138
+ ...(inputs ? { inputs } : {}),
120
139
  };
121
140
  }
122
141
 
@@ -61,6 +61,46 @@ function fanOutSink(options: FanOutRunOptions): RunEventSink | undefined {
61
61
  return joinRunEventSinks(options.onRunEvent, progressSinkOf(options.onProgress));
62
62
  }
63
63
 
64
+ /** The 1-based wave of each component in `waves`. */
65
+ function waveNumbers(waves: string[][]): Map<string, number> {
66
+ const out = new Map<string, number>();
67
+ waves.forEach((wave, i) => wave.forEach((name) => out.set(name, i + 1)));
68
+ return out;
69
+ }
70
+
71
+ /**
72
+ * The settles of components a fan-out does not start (#4007): `blocked`
73
+ * when a dependency failed or stopped at a gate, `already-applied` when an
74
+ * earlier attempt applied it. Each is said once, with no `component-start`
75
+ * before it, so a reader of the stream sees every component of the plan.
76
+ */
77
+ function skipSettles(emit: RunEventSink | undefined, waveOf: Map<string, number>) {
78
+ const said = new Set<string>();
79
+ const say = (component: string, wave: number | undefined, body: { status: "blocked"; blockedBy: string } | { status: "already-applied" }): void => {
80
+ if (!emit || wave === undefined || said.has(component)) return;
81
+ said.add(component);
82
+ emit({ type: "component-settle", wave, component, ...body });
83
+ };
84
+ return {
85
+ /** Each component in `blockedBy` not yet said, in wave order then by name. */
86
+ blocked(blockedBy: Map<string, string>): void {
87
+ const order = [...blockedBy.keys()].sort((a, b) => (waveOf.get(a) ?? 0) - (waveOf.get(b) ?? 0) || a.localeCompare(b));
88
+ for (const component of order) say(component, waveOf.get(component), { status: "blocked", blockedBy: blockedBy.get(component)! });
89
+ },
90
+ /**
91
+ * What an earlier attempt left (`remainingFanOut`'s skips): the components
92
+ * it applied, and the ones its failures blocked. Their wave is the one
93
+ * they have in the derivation, `derived`, which a resumed run re-layers.
94
+ */
95
+ carried(skipped: FanOutSkip[], derived: Map<string, number>): void {
96
+ for (const skip of skipped) {
97
+ if (skip.reason === "already-applied") say(skip.component, derived.get(skip.component), { status: "already-applied" });
98
+ else if (skip.reason === "blocked" && skip.blockedBy) say(skip.component, derived.get(skip.component), { status: "blocked", blockedBy: skip.blockedBy });
99
+ }
100
+ },
101
+ };
102
+ }
103
+
64
104
  /**
65
105
  * One gate per wave (#3049), each bound to its wave's set digest. The other
66
106
  * mode beside {@link FanOutGate}: that one approves the whole set once, this
@@ -220,6 +260,8 @@ export async function runFanOut(
220
260
 
221
261
  const emit = fanOutSink(options);
222
262
  emit?.({ type: "run-start", waves: active.waves });
263
+ const skips = skipSettles(emit, waveNumbers(active.waves));
264
+ skips.carried(active.skipped, waveNumbers(plan.waves));
223
265
 
224
266
  if (options.gate) {
225
267
  const gate = options.gate;
@@ -320,6 +362,8 @@ export async function runFanOut(
320
362
  wave: waveNum,
321
363
  status: waveFailed ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
322
364
  });
365
+ // What this wave's failures and gates hold back settles now, before the next wave starts.
366
+ skips.blocked(blockedBy);
323
367
  }
324
368
 
325
369
  const blocked: FanOutSkip[] = [...blockedBy.entries()]
@@ -411,6 +455,8 @@ async function runGatedWaves(
411
455
 
412
456
  const emit = fanOutSink(options);
413
457
  emit?.({ type: "run-start", waves: plan.waves });
458
+ const skips = skipSettles(emit, waveNumbers(plan.waves));
459
+ skips.carried(active.skipped, waveNumbers(plan.waves));
414
460
 
415
461
  /** What planning one wave gave. */
416
462
  interface Planned {
@@ -478,6 +524,7 @@ async function runGatedWaves(
478
524
  results.push(planFailure(f.component, f.error ?? "plan failed"));
479
525
  markBlocked(f.component);
480
526
  }
527
+ skips.blocked(blockedBy);
481
528
 
482
529
  const toRun = runnable.filter((name) => plansFor.has(name));
483
530
  const record: WaveRecord = {
@@ -569,6 +616,7 @@ async function runGatedWaves(
569
616
  wave: waveNum,
570
617
  status: waveFailures.length > 0 ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
571
618
  });
619
+ skips.blocked(blockedBy);
572
620
  return "next";
573
621
  },
574
622
  });
@@ -89,6 +89,10 @@ export interface FanOutRequest {
89
89
  * {@link FanOutPlan.digest} so an approval is bound to *what* would be
90
90
  * applied and not only to who would run. Absent, the digest still identifies
91
91
  * the derivation — the selection, the order and the edges it came from.
92
+ *
93
+ * `chant components fan-out` does not pass it: its approval stays bound to
94
+ * the derivation, and it binds a resumed attempt's progress to each
95
+ * component's input instead (./fan-out-inputs.ts, #4081).
92
96
  */
93
97
  inputDigests?: Record<string, string>;
94
98
  /**
@@ -29,6 +29,19 @@
29
29
  * squash merges and rebase merges alike, and every job measures from the
30
30
  * merge base of that commit and its own head (`resolveMergeBase`).
31
31
  *
32
+ * With `events` (#4007) both commands pass `--events-json`, so the job log
33
+ * carries the run-event stream, and keep a copy of it under the report
34
+ * directory through `CHANT_RUN_EVENTS_DIR`, so the report artifact carries
35
+ * it too. With `reportUrl` both pass `--report-url`, which copies the report,
36
+ * the change set and the events where a reader outside CI can fetch them;
37
+ * the job's own environment brings the credentials an `s3://` URL needs.
38
+ *
39
+ * With `build` (#4088) both jobs run the project's build and `chant lint`
40
+ * before their command, and fail on an error. A `cfn-deploy` member's plan
41
+ * and apply read the built template, so a project with one needs it. The
42
+ * build is `chant build --env <env>`, or `buildCommand` when the templates
43
+ * come from several builds (`npm run build`).
44
+ *
32
45
  * In a workspace member (#3465) both commands run in the member's directory
33
46
  * and pass `--member <name>`, so the member's gate (`pr-<number>-<member>`),
34
47
  * note and statuses stay apart from every other member's on the same pull
@@ -68,6 +81,13 @@ export function prLoopSetup(image: string): string[] {
68
81
  return image === PR_LOOP_IMAGE ? PR_LOOP_SETUP : [];
69
82
  }
70
83
 
84
+ /**
85
+ * Where the jobs keep their run-event files with `events` (#4007), as
86
+ * `CHANT_RUN_EVENTS_DIR`: inside the report directory, so the report
87
+ * artifact carries them.
88
+ */
89
+ export const PR_LOOP_EVENTS_DIR = `${PR_LOOP_REPORT_DIR}/run-events`;
90
+
71
91
  /** The `prLoop` generator option (`ComponentPipelineOptions.prLoop`). */
72
92
  export interface PrLoopPipelineOptions {
73
93
  /** The gate the apply waits on. Default `pr-apply`. */
@@ -79,6 +99,32 @@ export interface PrLoopPipelineOptions {
79
99
  branch?: string;
80
100
  /** Count an approval only from someone who approved the pull request on the forge. Default true. */
81
101
  requireReview?: boolean;
102
+ /**
103
+ * Stream each job's run events (#4007): `--events-json` on both commands,
104
+ * and `CHANT_RUN_EVENTS_DIR` set to {@link PR_LOOP_EVENTS_DIR}. Default false.
105
+ */
106
+ events?: boolean;
107
+ /**
108
+ * Copy each job's report, change set and events to this `s3://` or
109
+ * `file://` URL (#4007, `--report-url`), under `<pr>/<commit>/`. It is
110
+ * passed in double quotes, so it may name a variable the job expands,
111
+ * such as `$CHANT_REPORT_URL`.
112
+ */
113
+ reportUrl?: string;
114
+ /**
115
+ * Build and lint before `pr-plan` and before `pr-apply` (#4088): the build
116
+ * line, then `chant lint`, each failing the job on an error. A `cfn-deploy`
117
+ * member reads the built template, so a project with one sets this. The
118
+ * build line is `chant build --env <env>` unless `buildCommand` names
119
+ * another. Default false.
120
+ */
121
+ build?: boolean;
122
+ /**
123
+ * The shell line that builds the project in place of `chant build --env
124
+ * <env>`, for a project whose templates come from several builds, such as
125
+ * `npm run build`. Setting it turns `build` on.
126
+ */
127
+ buildCommand?: string;
82
128
  /**
83
129
  * Which GitHub-shaped forge the workflow is for. Set by the forgejo
84
130
  * generator, which reuses the github one; a caller never needs to.
@@ -100,6 +146,14 @@ export interface PrLoopJob {
100
146
  env: Record<string, string>;
101
147
  /** Shell lines to run before the command, to settle values the forge may leave unusable. */
102
148
  setup?: string[];
149
+ /** With `build` (#4088): the build line and the lint line, run in this order before the command. */
150
+ build?: string[];
151
+ }
152
+
153
+ /** The lines `build` (#4088) runs before a job's command: the build, then the lint. Empty without the option. */
154
+ export function prLoopBuildLines(env: string, options: PrLoopPipelineOptions = {}): string[] {
155
+ if (!options.build && !options.buildCommand) return [];
156
+ return [options.buildCommand ?? `chant build --env ${env}`, "chant lint"];
103
157
  }
104
158
 
105
159
  /**
@@ -112,16 +166,24 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
112
166
  const common = [
113
167
  "--env", env, "--gate", gate, "--output", PR_LOOP_REPORT_DIR, "--forge", forge,
114
168
  ...(member ? ["--member", member] : []),
169
+ ...(options.events ? ["--events-json"] : []),
170
+ ...(options.reportUrl ? ["--report-url", JSON.stringify(options.reportUrl)] : []),
115
171
  ];
116
172
  const github = forge !== "gitlab";
117
- const token: Record<string, string> = github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {};
173
+ const buildLines = prLoopBuildLines(env, options);
174
+ const build = buildLines.length > 0 ? { build: buildLines } : {};
175
+ const jobEnv: Record<string, string> = {
176
+ ...(github ? { GITHUB_TOKEN: GITHUB_JOB_TOKEN } : {}),
177
+ ...(options.events ? { CHANT_RUN_EVENTS_DIR: PR_LOOP_EVENTS_DIR } : {}),
178
+ };
118
179
  return [
119
180
  {
120
181
  jobName: "plan",
121
182
  command: ["chant", "components", "pr-plan", "--base", '"$BASE_SHA"', "--pr", '"$PR_NUMBER"', ...common].join(" "),
122
183
  env: github
123
- ? { BASE_SHA: "${{ github.event.pull_request.base.sha }}", PR_NUMBER: "${{ github.event.pull_request.number }}", ...token }
124
- : { BASE_SHA: "$CI_MERGE_REQUEST_DIFF_BASE_SHA", PR_NUMBER: "$CI_MERGE_REQUEST_IID" },
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 },
186
+ ...build,
125
187
  },
126
188
  {
127
189
  jobName: "apply",
@@ -130,8 +192,9 @@ export function prLoopJobs(forge: ForgeKind, env: string, options: PrLoopPipelin
130
192
  ...(options.requireReview === false ? [] : ["--require-review"]),
131
193
  "--resume", PR_APPLY_RECORD,
132
194
  ].join(" "),
133
- env: github ? { BASE_SHA: "${{ github.event.before }}", ...token } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA" },
195
+ env: github ? { BASE_SHA: "${{ github.event.before }}", ...jobEnv } : { BASE_SHA: "$CI_COMMIT_BEFORE_SHA", ...jobEnv },
134
196
  ...(github ? {} : { setup: [gitlabBaseFallback(options.branch)] }),
197
+ ...build,
135
198
  },
136
199
  ];
137
200
  }