@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
package/src/op/runtime.ts CHANGED
@@ -183,6 +183,48 @@ export interface OpRunRecord {
183
183
  * written before 0.124.28.
184
184
  */
185
185
  statement?: { payloadType: string; payload: string; signatures: { keyid: string; sig: string }[] };
186
+ /**
187
+ * Why a failed run failed (#4094), for a reader that acts on it without
188
+ * reading the log: a short `code`, a one-line `reason`, the phase that
189
+ * failed and the exit code of the command that failed. `chant ci run`
190
+ * writes it on every failed job, from the job's pull-request report when
191
+ * it wrote one (`not-a-reviewer`, `plan-failed`, `failed` and the other
192
+ * refusals and statuses), else `command-failed`, `setup-failed` or
193
+ * `build-failed`. Optional within contract 1, written by chant
194
+ * {@link OP_RUN_FAILURE_FLOOR} and newer; absent on a run that passed and
195
+ * on a run nothing explained.
196
+ */
197
+ failure?: OpRunFailureReason;
198
+ }
199
+
200
+ /** Why a run failed, on {@link OpRunRecord.failure} and on the run's `run-settle` event (#4094). */
201
+ export interface OpRunFailureReason {
202
+ /** A short code: a pull-request report's refusal or status, or `command-failed`, `setup-failed`, `build-failed`. */
203
+ code: string;
204
+ /** One line a person reads. */
205
+ reason: string;
206
+ /** The phase that failed, when one did. */
207
+ phase?: string;
208
+ /** The exit code of the command that failed, when it exited with one. */
209
+ exitCode?: number;
210
+ }
211
+
212
+ /** The first chant that writes {@link OpRunRecord.failure} and `failure` on `run-settle` (#4094). */
213
+ export const OP_RUN_FAILURE_FLOOR = "0.124.30";
214
+
215
+ /** `value` as an {@link OpRunFailureReason}, or undefined when it is not one, so a reader drops a malformed field and keeps the record. */
216
+ export function opRunFailureOf(value: unknown): OpRunFailureReason | undefined {
217
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
218
+ const v = value as Record<string, unknown>;
219
+ if (typeof v.code !== "string" || v.code === "" || typeof v.reason !== "string") return undefined;
220
+ if (v.phase !== undefined && typeof v.phase !== "string") return undefined;
221
+ if (v.exitCode !== undefined && !Number.isInteger(v.exitCode)) return undefined;
222
+ return {
223
+ code: v.code,
224
+ reason: v.reason,
225
+ ...(typeof v.phase === "string" ? { phase: v.phase } : {}),
226
+ ...(typeof v.exitCode === "number" ? { exitCode: v.exitCode } : {}),
227
+ };
186
228
  }
187
229
 
188
230
  /** An {@link OpRunRecord} before the ledger stamps its version and mints an id. */
@@ -261,3 +261,23 @@ describe("addresses", () => {
261
261
  expect(escapeMarkerAddress('aws_s3_bucket.logs["a*b"]')).toBe("aws_s3_bucket.logs:a+00002Ab");
262
262
  });
263
263
  });
264
+
265
+ describe("a no-change group with holes (#4084)", () => {
266
+ const part = (member: string, holes: ChangeSetPart["member"]["holes"] = []): ChangeSetPart => ({
267
+ member: { member, lexicon: "aws", planner: "chant", status: "planned", planDigest: `jcs1-sha256:${member}`, holes },
268
+ entries: [],
269
+ });
270
+ const hole = { address: "cluster-a", type: "AWS::CloudFormation::Stack", reason: "inputs from network; re-checked on apply" };
271
+
272
+ test("does not say no changes for a member whose plan has holes, and keeps the group and its id", () => {
273
+ const withHole = groupChangeSet(composeChangeSet([part("app-01"), part("app-02"), part("cluster-a", [hole])]));
274
+ const without = groupChangeSet(composeChangeSet([part("app-01"), part("app-02"), part("cluster-a")]));
275
+ expect(withHole.groups.map((g) => [g.id, g.units, g.noChanges])).toEqual(without.groups.map((g) => [g.id, g.units, g.noChanges]));
276
+ const id = withHole.groups[0].id;
277
+ const text = renderPlanSummaryText(withHole);
278
+ expect(text).toContain(`Group ${id}: 3 members, no changes known: 1 with holes, listed above:`);
279
+ expect(text).toContain("cluster-a: cluster-a: inputs from network; re-checked on apply");
280
+ expect(renderPlanSummaryMarkdown(withHole)).toContain(`#### Group ${id}: 3 members, no changes known: 1 with holes, listed above`);
281
+ expect(renderPlanSummaryText(without)).toContain(`Group ${id}: 3 members, no changes:`);
282
+ });
283
+ });
@@ -689,7 +689,14 @@ function groupTitle(s: PlanSummary, g: PlanSummaryGroup): string {
689
689
  if (g.resource) t += ` of ${g.resource}`;
690
690
  if (g.outlier) t += " (outlier)";
691
691
  if (g.provisional) t += " (provisional)";
692
- if (g.noChanges) t += ", no changes";
692
+ if (g.noChanges) {
693
+ // A member with holes planned no entries, but its plan says nothing about
694
+ // what the holes cover, so the group cannot say "no changes" for it
695
+ // (#4084). Only the heading says so: the group and its id are unchanged.
696
+ const holed = s.unit === "member" ? new Set(s.holes.map((h) => h.member)) : new Set<string>();
697
+ const unknown = g.units.filter((u) => holed.has(u)).length;
698
+ t += unknown > 0 ? `, no changes known: ${unknown} with holes, listed above` : ", no changes";
699
+ }
693
700
  else if (g.extends) t += `, group ${g.extends}'s change plus`;
694
701
  else if (g.units.length > 1) t += ", identical change";
695
702
  else t += ", change";
@@ -782,6 +782,31 @@ describe("pull requests over REST (#3798)", () => {
782
782
  expect((calls[1].body as { target_branch: string }).target_branch).toBe("main");
783
783
  });
784
784
 
785
+ test("openPullRequests lists the heads into a base and marks forks; branchHead reads a branch's commit (#4095)", async () => {
786
+ const f = fakeFetch({
787
+ "GET /api/v1/repos/acme/infra/pulls": [
788
+ { number: 12, head: { ref: "x", sha: "c".repeat(40), repo: { full_name: "mallory/infra" } }, base: { ref: "main", sha: "b".repeat(40) }, user: { login: "mallory" } },
789
+ { number: 4, head: { ref: "y", sha: "d".repeat(40), repo: { full_name: "acme/infra" } }, base: { ref: "main", sha: "b".repeat(40) }, user: { login: "driver" } },
790
+ { number: 5, head: { ref: "z", sha: "e".repeat(40) }, base: { ref: "release" } },
791
+ ],
792
+ "GET /api/v1/repos/acme/infra/branches/main": { name: "main", commit: { id: "f".repeat(40) } },
793
+ });
794
+ const forge = forgejoForge({ ...fjOpts, fetch: f.fetch });
795
+ expect(await forge.openPullRequests!("main")).toEqual([
796
+ { number: 4, head: "y", headSha: "d".repeat(40), base: "main", baseSha: "b".repeat(40), author: "driver", fork: false },
797
+ { number: 12, head: "x", headSha: "c".repeat(40), base: "main", baseSha: "b".repeat(40), author: "mallory", fork: true },
798
+ ]);
799
+ expect(f.calls[0].url).toBe("https://forge.example/api/v1/repos/acme/infra/pulls?state=open&limit=50&page=1");
800
+ expect(await forge.branchHead!("main")).toBe("f".repeat(40));
801
+ expect(await forge.branchHead!("gone")).toBeUndefined();
802
+
803
+ const g = fakeFetch({ "GET /repos/acme/infra/pulls": [], "GET /repos/acme/infra/branches/main": { commit: { sha: SHA } } });
804
+ const github = githubForge({ ...gh, fetch: g.fetch });
805
+ expect(await github.openPullRequests!("main")).toEqual([]);
806
+ expect(g.calls[0].url).toBe("https://api.github.com/repos/acme/infra/pulls?state=open&base=main&per_page=100&page=1");
807
+ expect(await github.branchHead!("main")).toBe(SHA);
808
+ });
809
+
785
810
  test("findPullRequest takes the oldest open one from this repository, not a fork's", async () => {
786
811
  const f = fakeFetch({
787
812
  "GET /api/v1/repos/acme/infra/pulls": [
package/src/pr-forge.ts CHANGED
@@ -212,6 +212,23 @@ export interface PullRequest {
212
212
  base?: string;
213
213
  }
214
214
 
215
+ /** An open pull request as `chant ci serve` polls it (#4095). */
216
+ export interface OpenPullRequest {
217
+ number: number;
218
+ /** Its head branch. */
219
+ head: string;
220
+ /** The commit its head branch points at. */
221
+ headSha: string;
222
+ /** The branch it merges into. */
223
+ base: string;
224
+ /** The base branch's commit as the forge last measured the pull request against it. */
225
+ baseSha?: string;
226
+ /** Who opened it. */
227
+ author?: string;
228
+ /** Its head branch lives in another repository, a fork. */
229
+ fork: boolean;
230
+ }
231
+
215
232
  /** What {@link PrForge.mergePullRequest} merges, and how. */
216
233
  export interface MergeOptions {
217
234
  /** The head commit the caller reviewed. The merge happens only while the pull request's head is still this commit. */
@@ -339,6 +356,13 @@ export interface PrForge {
339
356
  * still throw.
340
357
  */
341
358
  mergePullRequest(pr: number, opts: MergeOptions): Promise<MergeResult>;
359
+ /**
360
+ * GitHub and Forgejo (#4095): every open pull request into `base`, with
361
+ * its head commit, oldest first. `chant ci serve` polls it.
362
+ */
363
+ openPullRequests?(base: string): Promise<OpenPullRequest[]>;
364
+ /** GitHub and Forgejo (#4095): the commit `branch` points at, or undefined when there is no such branch. */
365
+ branchHead?(branch: string): Promise<string | undefined>;
342
366
  }
343
367
 
344
368
  /** A GitHub or Forgejo client. */
@@ -667,6 +691,44 @@ function githubShaped(kind: "github" | "forgejo", opts: GithubForgeOptions): Git
667
691
  const open = listed.filter((p) => fromHere(p, head));
668
692
  return open.map(pull).filter((p): p is PullRequest => p !== undefined).sort((a, b) => a.number - b.number)[0];
669
693
  },
694
+ async openPullRequests(base) {
695
+ // GitHub filters by base server-side; Forgejo's list is matched here.
696
+ const query = kind === "github" ? `?state=open&base=${encodeURIComponent(base)}` : "?state=open";
697
+ let listed: unknown[];
698
+ try {
699
+ listed = await api.list(`${repo}/pulls${query}`, pageParams, pageSize);
700
+ } catch (err) {
701
+ // Forgejo answers 404 while it counts the repository as empty, as findPullRequest notes.
702
+ if (kind !== "forgejo" || !(err instanceof ForgeApiError) || err.status !== 404) throw err;
703
+ await api.call("GET", repo);
704
+ return [];
705
+ }
706
+ const out: OpenPullRequest[] = [];
707
+ for (const v of listed) {
708
+ if (!isObject(v) || !isObject(v.head) || !isObject(v.base)) continue;
709
+ const number = num(v.number);
710
+ const head = str(v.head.ref);
711
+ const headSha = str(v.head.sha);
712
+ const into = str(v.base.ref);
713
+ if (number === undefined || !head || !headSha || into !== base) continue;
714
+ const baseSha = str(v.base.sha);
715
+ const author = isObject(v.user) ? str(v.user.login) : undefined;
716
+ out.push({ number, head, headSha, base: into, ...(baseSha ? { baseSha } : {}), ...(author ? { author } : {}), fork: !fromHere(v, head) });
717
+ }
718
+ return out.sort((a, b) => a.number - b.number);
719
+ },
720
+ async branchHead(branch) {
721
+ let b: unknown;
722
+ try {
723
+ b = await api.call("GET", `${repo}/branches/${branch.split("/").map(encodeURIComponent).join("/")}`);
724
+ } catch (err) {
725
+ if (err instanceof ForgeApiError && err.status === 404) return undefined;
726
+ throw err;
727
+ }
728
+ // GitHub names the commit `sha`, Forgejo `id`.
729
+ const commit = isObject(b) && isObject(b.commit) ? b.commit : undefined;
730
+ return commit ? str(commit.sha) ?? str(commit.id) : undefined;
731
+ },
670
732
  async editPullRequest(pr, change) {
671
733
  return pull(await api.call("PATCH", `${repo}/pulls/${pr}`, { ...(change.title !== undefined ? { title: change.title } : {}), ...(change.body !== undefined ? { body: change.body } : {}) }));
672
734
  },
@@ -13,6 +13,7 @@ import { composeChangeSet, type ChangeSetPart } from "./change-set";
13
13
  import {
14
14
  describePlanChanged,
15
15
  memberCounts,
16
+ PR_REPORT_DEPLOYED_OUTPUTS_FLOOR,
16
17
  PR_REPORT_SCHEMA_ID,
17
18
  prApproveCommand,
18
19
  prNoteMarker,
@@ -109,6 +110,9 @@ const resumedReport: PrReport = {
109
110
  resumed: { applied: ["a"] },
110
111
  };
111
112
 
113
+ /** A plan whose members read `net` and `a` from the deployed estate (#4104). */
114
+ const deployedReport: PrReport = { ...planReport, deployedOutputs: { a: ["id"], net: ["cidr"], dns: [] } };
115
+
112
116
  describe("the pr-report schema", () => {
113
117
  test("is a valid schema and names this module's id", () => {
114
118
  expect(validSchema(schema)).toBe(true);
@@ -116,7 +120,7 @@ describe("the pr-report schema", () => {
116
120
  });
117
121
 
118
122
  test("every stage's report validates, and its change set validates against the change-set schema", () => {
119
- for (const report of [planReport, refusedReport, appliedReport, resumedReport]) {
123
+ for (const report of [planReport, refusedReport, appliedReport, resumedReport, deployedReport]) {
120
124
  expectValid(report);
121
125
  expectValidChangeSet(report.changeSet);
122
126
  expect(report.changeSet.digest).toBe(report.digest);
@@ -158,6 +162,14 @@ describe("the note", () => {
158
162
  expect(renderPrNote(appliedReport)).not.toContain("Resumed");
159
163
  });
160
164
 
165
+ test("a plan that read dependencies from the deployed estate names each and its outputs (#4104)", () => {
166
+ const note = renderPrNote(deployedReport);
167
+ expect(note).toContain("Planned against outputs read from the deployed estate: `a` (`id`); `net` (`cidr`); `dns` (none yet).");
168
+ expect(renderPrNote(planReport)).not.toContain("deployed estate");
169
+ expect(PR_REPORT_DEPLOYED_OUTPUTS_FLOOR).toBe("0.124.30");
170
+ expect(schema.required).not.toContain("deployedOutputs");
171
+ });
172
+
161
173
  test("a small limit leaves the summary out rather than cutting the table", () => {
162
174
  const note = renderPrNote(planReport, { limit: 800 });
163
175
  expect(note).toContain("| `app` |");
package/src/pr-loop.ts CHANGED
@@ -46,6 +46,9 @@ export const PR_REPORT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace
46
46
  /** The read-contract version the report follows. */
47
47
  export const PR_REPORT_CONTRACT = 1;
48
48
 
49
+ /** The first chant whose report can carry `deployedOutputs` (#4104). */
50
+ export const PR_REPORT_DEPLOYED_OUTPUTS_FLOOR = "0.124.30";
51
+
49
52
  /** The gate a pull request's apply waits on, unless the pipeline names another. */
50
53
  export const PR_APPLY_GATE = "pr-apply";
51
54
 
@@ -183,6 +186,14 @@ export interface PrReport {
183
186
  * the rest under the same approval and did not run them again.
184
187
  */
185
188
  resumed?: { applied: string[] };
189
+ /**
190
+ * Per dependency a member was planned against, the names of the outputs
191
+ * read from the deployed estate (#4104), sorted. A dependency outside the
192
+ * change is here too. Seeded outputs (`--seed-outputs`, a resume record)
193
+ * are not. Absent when nothing was read. Added within contract 1 by chant
194
+ * {@link PR_REPORT_DEPLOYED_OUTPUTS_FLOOR}.
195
+ */
196
+ deployedOutputs?: Record<string, string[]>;
186
197
  approval: PrApproval;
187
198
  /** Component names: which changed, which run because they depend on one, and what the change touched that no component deploys. */
188
199
  selection: {
@@ -204,6 +215,17 @@ export function memberCounts(doc: Pick<ChangeSetDocument, "summary">, member: st
204
215
  return { create: n("create"), update: n("update"), replace: n("replace"), delete: n("delete") };
205
216
  }
206
217
 
218
+ /**
219
+ * Why a member's plan cannot say everything it will change: its holes'
220
+ * reasons, each once, in order (#4084). A `cfn-deploy` member whose inputs
221
+ * come from a stack the same run applies first says `inputs from <stack>;
222
+ * re-checked on apply`.
223
+ */
224
+ export function memberHoleReasons(report: Pick<PrReport, "changeSet">, member: string): string[] {
225
+ const m = report.changeSet.members.find((x) => x.member === member);
226
+ return [...new Set((m?.holes ?? []).map((h) => h.reason))];
227
+ }
228
+
207
229
  /** The approve command a reviewer runs for this report's digest. */
208
230
  export function prApproveCommand(report: Pick<PrReport, "op" | "gate" | "digest">, approver = "<you>"): string {
209
231
  return `chant approve ${report.op} ${report.gate} --plan ${report.digest} --approver ${approver} --sign`;
@@ -295,8 +317,16 @@ export function renderPrNote(report: PrReport, options: { limit?: number; approv
295
317
  if (report.members.length > 0) {
296
318
  out.push("", "| Member | Component | Status | Create | Update | Replace | Delete |", "|---|---|---|---:|---:|---:|---:|");
297
319
  for (const m of report.members) {
298
- const status = STATUS_WORDS[m.status] + (m.inputsMoved?.length ? `, inputs from ${m.inputsMoved.join(", ")} moved` : "");
299
- out.push(`| ${code(m.member)} | ${m.component} | ${status} | ${m.counts.create} | ${m.counts.update} | ${m.counts.replace} | ${m.counts.delete} |`);
320
+ const holes = memberHoleReasons(report, m.member);
321
+ const status =
322
+ STATUS_WORDS[m.status] +
323
+ (m.inputsMoved?.length ? `, inputs from ${m.inputsMoved.join(", ")} moved` : "") +
324
+ (holes.length > 0 ? `; ${holes.length > 2 ? `${holes[0]}; and ${holes.length - 1} more, listed under Holes` : holes.join("; ")}` : "");
325
+ // A member whose plan has holes and no entries says nothing about what
326
+ // it changes, so its counts are unknown, not zero (#4084).
327
+ const unknown = holes.length > 0 && !report.changeSet.entries.some((e) => e.member === m.member);
328
+ const n = (k: keyof PrMemberCounts): string => (unknown ? "?" : String(m.counts[k]));
329
+ out.push(`| ${code(m.member)} | ${m.component} | ${status} | ${n("create")} | ${n("update")} | ${n("replace")} | ${n("delete")} |`);
300
330
  }
301
331
  if (report.members.some((m) => m.inputsMoved?.length)) {
302
332
  out.push("", "A member whose inputs moved applied the plan it was approved with. The next run plans it against the new values.");
@@ -305,6 +335,11 @@ export function renderPrNote(report: PrReport, options: { limit?: number; approv
305
335
  if (report.selection.unclaimed.length > 0) {
306
336
  out.push("", `Changed, and deployed by no component: ${report.selection.unclaimed.map(code).join(", ")}.`);
307
337
  }
338
+ const deployed = Object.entries(report.deployedOutputs ?? {});
339
+ if (deployed.length > 0) {
340
+ const each = deployed.map(([dep, names]) => `${code(dep)} (${names.length > 0 ? names.map(code).join(", ") : "none yet"})`);
341
+ out.push("", `Planned against outputs read from the deployed estate: ${each.join("; ")}.`);
342
+ }
308
343
  if (report.status !== "nothing") {
309
344
  out.push("", `Plan digest: ${code(report.digest)}`, "", ...approvalLines(report, options.approver ?? "<you>"));
310
345
  }
@@ -328,9 +363,11 @@ export function prStatusDescription(report: PrReport): string {
328
363
  case "nothing":
329
364
  text = "nothing to plan";
330
365
  break;
331
- case "planned":
332
- text = `${n} member${n === 1 ? "" : "s"}: ${sum.change} to change, ${sum.destroy} to destroy or replace${digest}`;
366
+ case "planned": {
367
+ const unknown = report.members.filter((m) => memberHoleReasons(report, m.member).length > 0).length;
368
+ text = `${n} member${n === 1 ? "" : "s"}: ${sum.change} to change, ${sum.destroy} to destroy or replace${unknown > 0 ? `, ${unknown} with holes` : ""}${digest}`;
333
369
  break;
370
+ }
334
371
  case "plan-failed":
335
372
  text = `a member failed to plan${digest}`;
336
373
  break;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `--report-url` (#4007): the URL forms, the key, and the upload of a run's
3
+ * events as it goes: one upload in flight at a time, a failure that warns
4
+ * and never stops the run, and a last upload that carries everything. The
5
+ * s3 scheme runs a fake AWS CLI; nothing here reaches a network.
6
+ */
7
+
8
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
9
+ import { tmpdir } from "node:os";
10
+ import { join } from "node:path";
11
+ import { pathToFileURL } from "node:url";
12
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
13
+ import { openPrReportUpload, parseReportUrl, reportKey, reportPut, type ReportPut } from "./pr-report-url";
14
+
15
+ const line = (type: string, extra: Record<string, unknown> = {}): string => JSON.stringify({ type, ...extra }) + "\n";
16
+
17
+ let dir: string;
18
+ beforeEach(async () => {
19
+ dir = await mkdtemp(join(tmpdir(), "pr-report-url-"));
20
+ });
21
+ afterEach(async () => {
22
+ await rm(dir, { recursive: true, force: true });
23
+ });
24
+
25
+ describe("parseReportUrl", () => {
26
+ test("reads s3 and file URLs and refuses anything else", () => {
27
+ expect(parseReportUrl("s3://bucket/a/b/")).toEqual({ scheme: "s3", url: "s3://bucket/a/b", bucket: "bucket", prefix: "a/b" });
28
+ expect(parseReportUrl("s3://bucket")).toEqual({ scheme: "s3", url: "s3://bucket", bucket: "bucket", prefix: "" });
29
+ expect(parseReportUrl(pathToFileURL(dir).href)).toMatchObject({ scheme: "file", dir });
30
+ expect(parseReportUrl("https://example.invalid/x")).toHaveProperty("error");
31
+ });
32
+
33
+ test("keys by pull request and commit, and by member after them", () => {
34
+ expect(reportKey(12, "abc")).toBe("12/abc");
35
+ expect(reportKey(null, "abc")).toBe("unknown/abc");
36
+ expect(reportKey(12, "abc", "api")).toBe("12/abc/api");
37
+ });
38
+ });
39
+
40
+ describe("openPrReportUpload", () => {
41
+ /** A put whose uploads finish only when the test says, recording what each carried. */
42
+ function heldPut() {
43
+ const calls: Array<{ key: string; body: string; finish: (err?: Error) => void }> = [];
44
+ let inFlight = 0;
45
+ let most = 0;
46
+ const put: ReportPut = (key, body) =>
47
+ new Promise<void>((resolve, reject) => {
48
+ inFlight++;
49
+ most = Math.max(most, inFlight);
50
+ calls.push({ key, body, finish: (err) => { inFlight--; if (err) reject(err); else resolve(); } });
51
+ });
52
+ return { put, calls, most: () => most };
53
+ }
54
+
55
+ test("uploads the events on each settle, never two at once, and the settles that came meanwhile in one more", async () => {
56
+ const held = heldPut();
57
+ const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: () => {}, put: held.put });
58
+ upload.line(line("run-start"));
59
+ expect(held.calls).toHaveLength(0);
60
+ upload.line(line("wave-start", { wave: 1 }));
61
+ upload.line(line("component-settle", { wave: 1 }));
62
+ expect(held.calls).toHaveLength(1);
63
+ upload.line(line("wave-settle", { wave: 1 }));
64
+ upload.line(line("wave-start", { wave: 2 }));
65
+ upload.line(line("component-settle", { wave: 2 }));
66
+ // Both settles wait behind the one in flight, and go up as one.
67
+ expect(held.calls).toHaveLength(1);
68
+ held.calls[0]!.finish();
69
+ await new Promise((r) => setImmediate(r));
70
+ expect(held.calls).toHaveLength(2);
71
+ expect(held.calls[1]!.body.split("\n").filter(Boolean)).toHaveLength(6);
72
+ expect(held.calls[1]!.key).toBe("12/abc/pr-apply.events.jsonl");
73
+
74
+ upload.file("pr-apply.json", "{}\n");
75
+ const closing = upload.close();
76
+ await new Promise((r) => setImmediate(r));
77
+ // The last upload waits for the one in flight.
78
+ expect(held.calls).toHaveLength(2);
79
+ held.calls[1]!.finish();
80
+ await new Promise((r) => setImmediate(r));
81
+ held.calls[2]!.finish();
82
+ await new Promise((r) => setImmediate(r));
83
+ held.calls[3]!.finish();
84
+ await closing;
85
+ expect(held.calls.map((c) => c.key)).toEqual(["12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.events.jsonl", "12/abc/pr-apply.json"]);
86
+ expect(held.most()).toBe(1);
87
+ });
88
+
89
+ test("a failed upload as the run goes warns once and the run goes on; the last one is tried again", async () => {
90
+ const warnings: string[] = [];
91
+ let fail = true;
92
+ const put: ReportPut = async (key) => {
93
+ if (fail) throw new Error(`no bucket for ${key}`);
94
+ };
95
+ const upload = openPrReportUpload({ target: { scheme: "s3", url: "s3://b", bucket: "b", prefix: "" }, key: "12/abc", stage: "apply", warn: (m) => warnings.push(m), put });
96
+ upload.line(line("component-settle"));
97
+ await new Promise((r) => setImmediate(r));
98
+ upload.line(line("wave-settle"));
99
+ await new Promise((r) => setImmediate(r));
100
+ expect(warnings).toHaveLength(1);
101
+ expect(warnings[0]).toMatch(/as the run went: no bucket .*The run goes on/);
102
+ fail = false;
103
+ await upload.close();
104
+ expect(warnings).toHaveLength(1);
105
+ });
106
+
107
+ test("file://: each settle replaces the events file whole, so it holds a wave's settle before the next wave starts", async () => {
108
+ const target = parseReportUrl(pathToFileURL(dir).href);
109
+ if ("error" in target) throw new Error(target.error);
110
+ const upload = openPrReportUpload({ target, key: "12/abc", stage: "apply", warn: () => {} });
111
+ upload.line(line("run-start"));
112
+ upload.line(line("wave-settle", { wave: 1 }));
113
+ await new Promise((r) => setImmediate(r));
114
+ const events = join(dir, "12", "abc", "pr-apply.events.jsonl");
115
+ expect(await readFile(events, "utf8")).toBe(line("run-start") + line("wave-settle", { wave: 1 }));
116
+ upload.line(line("run-settle"));
117
+ upload.file("change-set.json", "{}\n");
118
+ await upload.close();
119
+ expect((await readFile(events, "utf8")).split("\n").filter(Boolean)).toHaveLength(3);
120
+ expect(await readFile(join(dir, "12", "abc", "change-set.json"), "utf8")).toBe("{}\n");
121
+ });
122
+
123
+ test("s3://: the AWS CLI copies each object from its stdin, with the content type, and chant hands it no credential", async () => {
124
+ const runs: Array<{ args: string[]; stdin: string }> = [];
125
+ const put = reportPut({ scheme: "s3", url: "s3://b/p", bucket: "b", prefix: "p" }, async (args, stdin) => {
126
+ runs.push({ args, stdin });
127
+ });
128
+ await put("12/abc/pr-plan.json", "{}\n", "application/json");
129
+ expect(runs).toEqual([{ args: ["s3", "cp", "-", "s3://b/p/12/abc/pr-plan.json", "--content-type", "application/json", "--only-show-errors"], stdin: "{}\n" }]);
130
+ });
131
+ });