@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
@@ -0,0 +1,416 @@
1
+ /**
2
+ * #4094: `chant ci run plan|apply` runs one pull-request job as a pipeline
3
+ * Op in a fresh worktree with a clean environment, records it as `pr-plan`
4
+ * or `pr-apply` on `chant/lifecycle` signed by a runner key, and `ci tick
5
+ * --source runs` then tags the commit.
6
+ *
7
+ * Every test works in its own temp repository with a bare origin, and the
8
+ * runner key is a throwaway one. The job's `chant` is a stub script outside
9
+ * the repository that records the arguments, environment and directory it
10
+ * was run with. Nothing is written in process.cwd().
11
+ */
12
+
13
+ import { execFileSync } from "node:child_process";
14
+ import { createPublicKey, generateKeyPairSync } from "node:crypto";
15
+ import { chmodSync, existsSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { afterAll, describe, expect, test } from "vitest";
18
+ import { cleanScratch, declaration, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
19
+ import { checkRunEventLines } from "./__fixtures__/run-event-check";
20
+ import { ciJobCommand, ciJobEnv, ciJobFailure, ciJobOp, ciRun, CiRunError, formatCiRun } from "./ci-run";
21
+ import { ciTick, evaluateCommit, readOpRunSource, revokedAnnotation } from "./ci-green";
22
+ import type { StepRecord } from "../op/local-executor";
23
+ import { sshFromEd25519 } from "./trust/dsse";
24
+ import { verifyOpRunStatement } from "./trust/op-run-statement";
25
+ import { readRunLedger } from "../lifecycle/run-ledger";
26
+ import { prLoopJobs, PR_LOOP_SETUP } from "../components/pr-pipeline";
27
+ import { parseArgs } from "../cli/parse-args";
28
+
29
+ afterAll(cleanScratch);
30
+
31
+ const NOW = new Date("2026-10-18T12:00:00Z");
32
+ const hoursAgo = (h: number) => new Date(NOW.getTime() - h * 3_600_000).toISOString();
33
+
34
+ function runnerKey(): { pem: string; ssh: string } {
35
+ const { privateKey } = generateKeyPairSync("ed25519");
36
+ const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
37
+ return { pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh: sshFromEd25519(Buffer.from(jwk.x, "base64url")) };
38
+ }
39
+
40
+ const BOT = runnerKey();
41
+ const RUNNERS = [{ principal: "chant-runner", class: "runner" as const, key: BOT.ssh }];
42
+ const GREEN = { branch: "main", phases: { plan: ["pr-plan"], apply: ["pr-apply"] }, require: ["plan", "apply"], source: "runs" };
43
+
44
+ function commitAt(root: string, hours: number, file: string): string {
45
+ writeFiles(root, { [file]: `${file} ${hours}\n` });
46
+ const date = hoursAgo(hours);
47
+ const env = { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date };
48
+ for (const args of [["add", "-A"], ["commit", "-q", "-m", file]]) {
49
+ execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: root, env, stdio: "ignore" });
50
+ }
51
+ return git(root, "rev-parse", "HEAD");
52
+ }
53
+
54
+ /** A checkout on main with a bare origin: member `app`, `ci.green` from runs, and BOT as the runner `chant-runner`. */
55
+ function workspace(): { root: string; app: string } {
56
+ const origin = scratchDir("chant-ci-run-origin-");
57
+ git(origin, "init", "-q", "--bare");
58
+ const root = scratchDir("chant-ci-run-");
59
+ git(root, "init", "-q");
60
+ git(root, "symbolic-ref", "HEAD", "refs/heads/main");
61
+ git(root, "config", "user.name", "t");
62
+ git(root, "config", "user.email", "t@example.com");
63
+ writeFiles(root, {
64
+ "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: GREEN } }),
65
+ ".chant/trust.json": JSON.stringify({ schema: 1, runners: [{ principal: "chant-runner", class: "runner", key: BOT.ssh }], roles: { runner: ["chant-runner"] } }),
66
+ ".gitignore": ".chant/pr/\n",
67
+ "app/a.txt": "a\n",
68
+ });
69
+ git(root, "remote", "add", "origin", origin);
70
+ commitAt(root, 23, "app/init.txt");
71
+ return { root, app: join(root, "app") };
72
+ }
73
+
74
+ /**
75
+ * A stand-in for `chant` outside the repository. It keeps the arguments,
76
+ * the environment and the directory it ran in as files under `.chant/pr`,
77
+ * where the real commands write their reports, and exits `$FAKE_EXIT`.
78
+ */
79
+ function fakeChant(): string {
80
+ const file = join(scratchDir("chant-ci-run-bin-"), "chant");
81
+ writeFileSync(
82
+ file,
83
+ [
84
+ "#!/bin/sh",
85
+ 'stage="$2"',
86
+ "mkdir -p .chant/pr",
87
+ 'echo "chant $1${2:+ $2}" >> .chant/pr/order',
88
+ 'printf "%s\\n" "$@" > ".chant/pr/$stage.args"',
89
+ 'env | sort > ".chant/pr/$stage.env"',
90
+ 'pwd -P > ".chant/pr/$stage.pwd"',
91
+ // FAKE_REPORT, when the job is given it, is the pr report the command writes.
92
+ 'if [ -n "$FAKE_REPORT" ]; then printf "%s" "$FAKE_REPORT" > ".chant/pr/$stage.json"; else echo "{\\"stage\\": \\"$stage\\"}" > ".chant/pr/$stage.json"; fi',
93
+ 'echo "fake $stage ran"',
94
+ 'exit "${FAKE_EXIT:-0}"',
95
+ "",
96
+ ].join("\n"),
97
+ );
98
+ chmodSync(file, 0o755);
99
+ return file;
100
+ }
101
+
102
+ // A job image of its own, so the jobs get no setup lines: the default image's
103
+ // OpenTofu install would download from the network on a runner without tofu.
104
+ const JOB_IMAGE = "chant-ci-run-test:local";
105
+
106
+ const HOST = {
107
+ PATH: process.env.PATH,
108
+ HOME: process.env.HOME,
109
+ GITHUB_TOKEN: "forge-token",
110
+ GITHUB_REPOSITORY: "acme/estate",
111
+ GITHUB_SERVER_URL: "http://forgejo.localhost",
112
+ GITHUB_API_URL: "http://forgejo.localhost/api/v1",
113
+ AWS_ACCESS_KEY_ID: "floci",
114
+ NOT_FOR_JOBS: "leak",
115
+ CHANT_RUNNER_KEY: BOT.pem,
116
+ GITHUB_RUN_ID: "999",
117
+ };
118
+
119
+ const envFile = (dir: string, stage: string): Record<string, string> =>
120
+ Object.fromEntries(
121
+ readFileSync(join(dir, `${stage}.env`), "utf-8")
122
+ .split("\n")
123
+ .filter(Boolean)
124
+ .map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]),
125
+ );
126
+
127
+ describe("the job as an Op (#4094)", () => {
128
+ test("the job's variables come from flags, literals and the runner's tokens, and nothing else of the runner's", () => {
129
+ const [plan, apply] = prLoopJobs("forgejo", "prod", { events: true });
130
+ const env = ciJobEnv(plan!, { forge: "forgejo", env: "prod", values: { base: "b".repeat(40), pr: 7, commit: "c".repeat(40) }, hostEnv: HOST, passEnv: ["AWS_ACCESS_KEY_ID"] });
131
+ expect(env).toMatchObject({
132
+ BASE_SHA: "b".repeat(40),
133
+ PR_NUMBER: "7",
134
+ GITHUB_TOKEN: "forge-token",
135
+ GITHUB_REPOSITORY: "acme/estate",
136
+ GITHUB_SHA: "c".repeat(40),
137
+ CHANT_ENV: "prod",
138
+ CHANT_RUN_EVENTS_DIR: ".chant/pr/run-events",
139
+ AWS_ACCESS_KEY_ID: "floci",
140
+ });
141
+ for (const name of ["NOT_FOR_JOBS", "CHANT_RUNNER_KEY", "GITHUB_RUN_ID"]) expect(env).not.toHaveProperty(name);
142
+ // No `${{ ... }}` expression reaches a job.
143
+ expect(Object.values(env).some((v) => v.includes("${{"))).toBe(false);
144
+
145
+ expect(() => ciJobEnv(plan!, { forge: "forgejo", env: "prod", values: { base: "b".repeat(40), commit: "c".repeat(40) }, hostEnv: {} })).toThrow(CiRunError);
146
+ expect(() => ciJobEnv(apply!, { forge: "forgejo", env: "prod", values: { commit: "c".repeat(40) }, hostEnv: {} })).toThrow(/pass --base <sha>/);
147
+
148
+ const [, gitlabApply] = prLoopJobs("gitlab", "prod");
149
+ const gl = ciJobEnv(gitlabApply!, { forge: "gitlab", env: "prod", values: { base: "b".repeat(40), commit: "c".repeat(40) }, hostEnv: {} });
150
+ expect(gl).toEqual({ BASE_SHA: "b".repeat(40), CHANT_ENV: "prod", CI_COMMIT_SHA: "c".repeat(40) });
151
+ });
152
+
153
+ test("the Op runs the generated job's command, after its setup and build lines, each a clean shell step", () => {
154
+ const [plan] = prLoopJobs("github", "prod", {}, "app");
155
+ const op = ciJobOp({ job: plan!, env: "prod", cwd: "/w/app", jobEnv: { BASE_SHA: "b" }, setup: PR_LOOP_SETUP, build: ["npm ci"], chant: "npx chant" });
156
+ expect(op.name).toBe("pr-plan");
157
+ expect(op.labels).toMatchObject({ Env: "prod" });
158
+ expect(op.phases.map((p) => [p.name, p.steps.length])).toEqual([["setup", 1], ["build", 1], ["plan", 1]]);
159
+ const last = op.phases[2]!.steps[0] as { fn: string; args: Record<string, unknown> };
160
+ expect(last.fn).toBe("shellCmd");
161
+ expect(last.args).toEqual({ cmd: `npx chant${plan!.command.slice("chant".length)}`, cwd: "/w/app", env: { BASE_SHA: "b" }, inheritEnv: false });
162
+ expect(plan!.command).toContain("--member app");
163
+
164
+ // No setup or build lines: the job's phase alone. GitLab's apply keeps its base fallback in the command's shell.
165
+ const [, gitlabApply] = prLoopJobs("gitlab", "prod");
166
+ const bare = ciJobOp({ job: gitlabApply!, env: "prod", cwd: "/w", jobEnv: {}, setup: [], build: [] });
167
+ expect(bare.phases.map((p) => p.name)).toEqual(["apply"]);
168
+ expect(ciJobCommand(gitlabApply!)).toBe(`${gitlabApply!.setup![0]}\n${gitlabApply!.command}`);
169
+
170
+ // #4088's build and lint lines follow --before-script, with chant replaced in each.
171
+ const [built] = prLoopJobs("forgejo", "prod", { build: true });
172
+ expect(built!.build).toEqual(["chant build --env prod", "chant lint"]);
173
+ const withBuild = ciJobOp({ job: built!, env: "prod", cwd: "/w", jobEnv: {}, setup: [], build: ["npm ci", ...built!.build!], chant: "npx chant" });
174
+ expect(withBuild.phases[0]!.steps.map((st) => ((st as unknown as { args: { cmd: string } }).args.cmd))).toEqual(["npm ci", "npx chant build --env prod", "npx chant lint"]);
175
+ });
176
+
177
+ test("the command line takes the job and the forge's values as flags", () => {
178
+ const args = parseArgs(["ci", "run", "apply", "--env", "prod", "--base", "abc", "--commit", "def", "--pass-env", "AWS_PROFILE", "--pass-env", "AWS_REGION", "--chant", "npx chant", "--key", "k.pem", "--pr-build", "--pr-build-command", "npm run build"]);
179
+ expect(args).toMatchObject({ prBuild: true, prBuildCommand: "npm run build" });
180
+ expect(args).toMatchObject({ command: "ci", path: "run", extraPositional: "apply", env: "prod", base: "abc", commit: "def", passEnv: ["AWS_PROFILE", "AWS_REGION"], ciChant: "npx chant", key: "k.pem" });
181
+ expect(() => parseArgs(["ci", "run", "plan", "--commit"])).toThrow(/--commit needs/);
182
+ });
183
+ });
184
+
185
+ describe("chant ci run, then ci tick from runs (#4094)", () => {
186
+ test("plan and apply run in a fresh worktree, are recorded signed, and the tick tags the commit green", async () => {
187
+ const before = { worktrees: undefined as string | undefined };
188
+ const { root, app } = workspace();
189
+ const base = git(root, "rev-parse", "HEAD");
190
+ const sha = commitAt(root, 2, "app/a.txt");
191
+ git(root, "push", "-q", "origin", "main");
192
+ before.worktrees = git(root, "worktree", "list", "--porcelain");
193
+ const chant = fakeChant();
194
+ const reports = scratchDir("chant-ci-run-reports-");
195
+
196
+ const events: string[] = [];
197
+ const plan = await ciRun({ image: JOB_IMAGE,
198
+ cwd: app,
199
+ job: "plan",
200
+ env: "prod",
201
+ forge: "forgejo",
202
+ base,
203
+ pr: 7,
204
+ chant,
205
+ hostEnv: HOST,
206
+ passEnv: ["AWS_ACCESS_KEY_ID"],
207
+ runnerKey: BOT.pem,
208
+ output: join(reports, "plan"),
209
+ runId: "chant-plan-1",
210
+ runEvents: (line) => events.push(line),
211
+ });
212
+ expect(plan.status).toBe("ok");
213
+ expect(plan.warnings).toEqual([]);
214
+ expect(plan.signer).toBe("chant-runner");
215
+ expect(plan.pushed).toBe(true);
216
+
217
+ // The job ran the generated command, in the member's directory of a checkout outside the repository.
218
+ const out = join(reports, "plan");
219
+ const [job] = prLoopJobs("forgejo", "prod", {}, "app");
220
+ const argv = readFileSync(join(out, "pr-plan.args"), "utf-8").trimEnd().split("\n");
221
+ expect(argv).toEqual(["components", "pr-plan", "--base", base, "--pr", "7", "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app"]);
222
+ expect(job!.command.split(" ").slice(1).map((a) => a.replace(/"/g, ""))).toEqual(argv.map((a) => (a === base ? "$BASE_SHA" : a === "7" ? "$PR_NUMBER" : a)));
223
+ const pwd = readFileSync(join(out, "pr-plan.pwd"), "utf-8").trim();
224
+ expect(pwd).toBe(join(plan.checkout, "app"));
225
+ expect(pwd.startsWith(realpathSync(root))).toBe(false);
226
+ expect(existsSync(plan.checkout)).toBe(false);
227
+ expect(git(root, "worktree", "list", "--porcelain")).toBe(before.worktrees);
228
+ expect(JSON.parse(readFileSync(join(out, "pr-plan.json"), "utf-8"))).toEqual({ stage: "pr-plan" });
229
+
230
+ // The environment is clean: the job's variables and the few the runner hands on.
231
+ const env = envFile(out, "pr-plan");
232
+ expect(env).toMatchObject({ BASE_SHA: base, PR_NUMBER: "7", GITHUB_SHA: sha, CHANT_ENV: "prod", GITHUB_TOKEN: "forge-token", AWS_ACCESS_KEY_ID: "floci" });
233
+ for (const name of ["NOT_FOR_JOBS", "CHANT_RUNNER_KEY", "GITHUB_RUN_ID"]) expect(env).not.toHaveProperty(name);
234
+
235
+ // The record: Op pr-plan at the commit, run by chant, with a statement that verifies.
236
+ expect(plan.record).toMatchObject({ op: "pr-plan", env: "prod", status: "ok", gitSha: sha, runOrigin: { forge: "chant" }, id: "chant-plan-1" });
237
+ const stored = (await readRunLedger("prod", "pr-plan", { cwd: app })).records.at(-1)!;
238
+ expect(stored.statement).toEqual(plan.record.statement);
239
+ expect(verifyOpRunStatement(stored, RUNNERS, { runner: ["chant-runner"] })).toMatchObject({ status: "signed", signer: "chant-runner" });
240
+ expect(stored.phases.map((p) => [p.name, p.status])).toEqual([["plan", "ok"]]);
241
+
242
+ // The events: a pipeline run, on the chant runner.
243
+ const stream = checkRunEventLines(events);
244
+ expect(stream[0]).toMatchObject({ kind: "pipeline", runtime: "ci", name: "pr-plan", runId: "chant-plan-1", gitSha: sha, runOrigin: { forge: "chant" } });
245
+ expect(stream.at(-1)).toMatchObject({ type: "run-settle", status: "ok" });
246
+
247
+ const apply = await ciRun({ image: JOB_IMAGE, cwd: app, job: "apply", env: "prod", forge: "forgejo", base, chant, hostEnv: HOST, runnerKey: BOT.pem, output: join(reports, "apply") });
248
+ expect(apply.status).toBe("ok");
249
+ const applyArgs = readFileSync(join(reports, "apply", "pr-apply.args"), "utf-8").trimEnd().split("\n");
250
+ expect(applyArgs).toEqual(["components", "pr-apply", "--base", base, "--env", "prod", "--gate", "pr-apply", "--output", ".chant/pr", "--forge", "forgejo", "--member", "app", "--require-review", "--resume", ".chant/pr-resume/pr-apply.json"]);
251
+ expect(formatCiRun(apply)).toContain("signed by chant-runner");
252
+
253
+ // The records reached origin, and a tick from runs tags the commit green with no forge.
254
+ expect(git(root, "ls-remote", "origin", "chant/lifecycle")).toContain(git(root, "rev-parse", "chant/lifecycle"));
255
+ const tick = await ciTick({ cwd: app, now: NOW, runnerKey: BOT.pem });
256
+ expect(tick.source).toBe("runs");
257
+ expect(tick.made).toEqual([`ci/green/${sha}`]);
258
+ const note = git(root, "tag", "-l", "--format=%(contents)", `ci/green/${sha}`);
259
+ const record = JSON.parse(note.slice(note.indexOf("{"))) as { phases: Record<string, { name: string; opRun: { id: string; signer: string } }[]> };
260
+ expect(record.phases.plan![0]).toMatchObject({ name: "pr-plan", opRun: { id: "chant-plan-1", signer: "chant-runner" } });
261
+ expect(record.phases.apply![0]).toMatchObject({ name: "pr-apply", opRun: { id: apply.record.id, signer: "chant-runner" } });
262
+ });
263
+
264
+ test("a failing job is recorded failed, and a key from the environment that trust.json does not list leaves it unsigned", async () => {
265
+ const { root, app } = workspace();
266
+ const base = git(root, "rev-parse", "HEAD");
267
+ commitAt(root, 2, "app/a.txt");
268
+ const stranger = runnerKey();
269
+ const lines: string[] = [];
270
+ const r = await ciRun({ image: JOB_IMAGE,
271
+ cwd: app,
272
+ job: "apply",
273
+ env: "prod",
274
+ forge: "github",
275
+ base,
276
+ chant: fakeChant(),
277
+ hostEnv: { ...HOST, FAKE_EXIT: "3" },
278
+ passEnv: ["FAKE_EXIT"],
279
+ runnerKey: stranger.pem,
280
+ runnerKeyFromEnv: true,
281
+ output: join(scratchDir("chant-ci-run-out-"), "pr"),
282
+ push: false,
283
+ stepOutput: (line) => lines.push(line),
284
+ });
285
+ expect(r.status).toBe("fail");
286
+ expect(r.record.status).toBe("fail");
287
+ expect(r.record.statement).toBeUndefined();
288
+ expect(r.signer).toBeNull();
289
+ expect(r.warnings[0]).toMatch(/CHANT_RUNNER_KEY is set, and the run will not be signed: .*lists no runner with this key/);
290
+ expect(r.failure).toMatchObject({ phase: "apply", error: expect.stringContaining("command exited 3") });
291
+ // The report says nothing about a failure, so the record names the command.
292
+ expect(r.record.failure).toMatchObject({ code: "command-failed", phase: "apply", exitCode: 3, reason: expect.stringContaining("command exited 3") });
293
+ expect(lines.some((l) => l.includes("fake pr-apply ran"))).toBe(true);
294
+ expect(r.report).not.toBeNull();
295
+ expect(formatCiRun(r)).toContain("unsigned, so ci tick from runs does not count it");
296
+
297
+ // The same key given with --key refuses the run before anything runs.
298
+ await expect(ciRun({ image: JOB_IMAGE, cwd: app, job: "apply", env: "prod", forge: "github", base, chant: fakeChant(), hostEnv: HOST, runnerKey: stranger.pem, push: false })).rejects.toThrow(/would not be signed/);
299
+ expect((await readRunLedger("prod", "pr-apply", { cwd: app })).records).toHaveLength(1);
300
+ });
301
+
302
+ test("--pr-build runs the job's build and lint lines after --before-script and before the command, and a failing build fails the job", async () => {
303
+ const { root, app } = workspace();
304
+ const base = git(root, "rev-parse", "HEAD");
305
+ const sha = commitAt(root, 2, "app/a.txt");
306
+ const chant = fakeChant();
307
+ const out = join(scratchDir("chant-ci-run-build-"), "pr");
308
+ const ok = await ciRun({ image: JOB_IMAGE,
309
+ cwd: app,
310
+ job: "plan",
311
+ env: "prod",
312
+ forge: "forgejo",
313
+ base,
314
+ pr: 3,
315
+ chant,
316
+ hostEnv: HOST,
317
+ beforeScript: ["mkdir -p .chant/pr && echo before-script >> .chant/pr/order"],
318
+ loop: { buildCommand: "echo build >> .chant/pr/order" },
319
+ push: false,
320
+ output: out,
321
+ });
322
+ expect(ok.status).toBe("ok");
323
+ expect(readFileSync(join(out, "order"), "utf-8").trimEnd().split("\n")).toEqual(["before-script", "build", "chant lint", "chant components pr-plan"]);
324
+ expect(ok.record.phases.map((p) => [p.name, p.status, p.steps.length])).toEqual([["build", "ok", 3], ["plan", "ok", 1]]);
325
+
326
+ const failed = await ciRun({ image: JOB_IMAGE,
327
+ cwd: app,
328
+ job: "apply",
329
+ env: "prod",
330
+ forge: "forgejo",
331
+ base,
332
+ chant,
333
+ hostEnv: HOST,
334
+ loop: { buildCommand: "mkdir -p .chant/pr && echo build >> .chant/pr/order && exit 4" },
335
+ runnerKey: BOT.pem,
336
+ push: false,
337
+ output: join(scratchDir("chant-ci-run-build-"), "pr"),
338
+ });
339
+ expect(failed.status).toBe("fail");
340
+ expect(failed.failure).toMatchObject({ phase: "build", error: expect.stringContaining("command exited 4") });
341
+ expect(failed.record).toMatchObject({ op: "pr-apply", status: "fail", gitSha: sha, runOrigin: { forge: "chant" } });
342
+ expect(failed.record.phases.map((p) => [p.name, p.status])).toEqual([["build", "fail"]]);
343
+ // The command never ran: the report holds the build's line alone.
344
+ expect(readFileSync(join(failed.report!, "order"), "utf-8").trimEnd().split("\n")).toEqual(["build"]);
345
+ const stored = (await readRunLedger("prod", "pr-apply", { cwd: app })).records.at(-1)!;
346
+ expect(stored.status).toBe("fail");
347
+ expect(stored.failure).toEqual({ code: "build-failed", phase: "build", exitCode: 4, reason: "mkdir -p .chant/pr && echo build >> .chant/pr/order && exit 4 exited 4" });
348
+ expect(verifyOpRunStatement(stored, RUNNERS, { runner: ["chant-runner"] })).toMatchObject({ status: "signed" });
349
+ });
350
+
351
+ test("a failed job's record and settle event say why from its pr report, signed, and a revoke note names the code", async () => {
352
+ const { root, app } = workspace();
353
+ const base = git(root, "rev-parse", "HEAD");
354
+ const sha = commitAt(root, 2, "app/a.txt");
355
+ const refusedReport = { stage: "apply", status: "refused", refusal: "not-a-reviewer", message: "ana approved the plan, and ana has not approved the pull request on the forge" };
356
+ const extra = { FAKE_EXIT: "3", FAKE_REPORT: JSON.stringify(refusedReport) };
357
+ const events: string[] = [];
358
+ const refused = await ciRun({
359
+ image: JOB_IMAGE,
360
+ cwd: app,
361
+ job: "apply",
362
+ env: "prod",
363
+ forge: "forgejo",
364
+ base,
365
+ chant: fakeChant(),
366
+ hostEnv: { ...HOST, ...extra },
367
+ passEnv: Object.keys(extra),
368
+ runnerKey: BOT.pem,
369
+ push: false,
370
+ output: join(scratchDir("chant-ci-run-why-"), "pr"),
371
+ runEvents: (line) => events.push(line),
372
+ });
373
+ expect(refused.status).toBe("fail");
374
+ const why = { code: "not-a-reviewer", reason: refusedReport.message, phase: "apply", exitCode: 3 };
375
+ expect(refused.record.failure).toEqual(why);
376
+ expect(formatCiRun(refused)).toContain("failure: not-a-reviewer in apply, exit 3: ana approved the plan");
377
+ const stored = (await readRunLedger("prod", "pr-apply", { cwd: app })).records.at(-1)!;
378
+ expect(stored.failure).toEqual(why);
379
+ // The failure is signed with the rest of the record.
380
+ expect(verifyOpRunStatement(stored, RUNNERS, { runner: ["chant-runner"] })).toMatchObject({ status: "signed" });
381
+ // The settle event carries it, and validates against run-event.schema.json.
382
+ expect(checkRunEventLines(events).at(-1)).toMatchObject({ type: "run-settle", status: "failed", kind: "pipeline", failure: why });
383
+
384
+ // A tick from runs reads the code, and the revoke note names it.
385
+ const runs = (await readOpRunSource(app, root, git(root, "rev-parse", "HEAD"))).checkRuns(sha);
386
+ expect(runs).toEqual([expect.objectContaining({ name: "pr-apply", conclusion: "failure", failureCode: "not-a-reviewer" })]);
387
+ const green = { branch: "main", window: "7d", phases: [{ name: "apply", runs: ["pr-apply"] }], require: ["apply"], source: "runs" } as unknown as Parameters<typeof evaluateCommit>[0];
388
+ const verdict = evaluateCommit(green, runs);
389
+ expect(revokedAnnotation(green, sha, verdict.failure!, NOW).split("\n")[0]).toBe("ci: phase apply failed in pr-apply (failure, not-a-reviewer)");
390
+ });
391
+
392
+ test("the failure code comes from the report's refusal, its status and failed members, or the command", () => {
393
+ const op = ciJobOp({ job: prLoopJobs("forgejo", "prod")[1]!, env: "prod", cwd: "/w", jobEnv: {}, setup: [], build: ["npm ci"] });
394
+ const dir = scratchDir("chant-ci-run-report-");
395
+ const fail = (phase: string, code: number) => ({ phase, fn: "shellCmd", status: "fail", durationMs: 1, error: `command exited ${code} (expected 0): Command failed: x\nnoise` }) as unknown as StepRecord;
396
+ const ok = (phase: string) => ({ phase, fn: "shellCmd", status: "ok", durationMs: 1 }) as unknown as StepRecord;
397
+ const at = (records: StepRecord[]) => ciJobFailure({ job: "apply", op, records, reportDir: dir });
398
+
399
+ expect(at([ok("build"), ok("apply")])).toBeUndefined();
400
+ expect(at([fail("build", 2)])).toEqual({ code: "build-failed", reason: "npm ci exited 2", phase: "build", exitCode: 2 });
401
+ expect(at([ok("build"), fail("apply", 1)])).toEqual({ code: "command-failed", reason: "command exited 1 (expected 0): Command failed: x", phase: "apply", exitCode: 1 });
402
+
403
+ writeFileSync(join(dir, "pr-apply.json"), JSON.stringify({ status: "failed", members: [
404
+ { member: "network", status: "applied" },
405
+ { member: "cluster-a", status: "failed", error: "stack CREATE_FAILED\nmore" },
406
+ { member: "cluster-b", status: "failed" },
407
+ { member: "app-01", status: "blocked" },
408
+ ] }));
409
+ expect(at([ok("build"), fail("apply", 1)])).toEqual({ code: "failed", reason: "members cluster-a failed (stack CREATE_FAILED), cluster-b failed", phase: "apply", exitCode: 1 });
410
+ writeFileSync(join(dir, "pr-apply.json"), JSON.stringify({ status: "refused", refusal: "plan-changed", message: "the plan changed after review\nsecond line" }));
411
+ expect(at([ok("build"), fail("apply", 3)])).toEqual({ code: "plan-changed", reason: "the plan changed after review", phase: "apply", exitCode: 3 });
412
+ // A report that says the job did its work leaves the command's failure.
413
+ writeFileSync(join(dir, "pr-apply.json"), JSON.stringify({ status: "applied", members: [] }));
414
+ expect(at([ok("build"), fail("apply", 1)])!.code).toBe("command-failed");
415
+ });
416
+ });