@intentius/chant 0.124.31 → 0.124.33

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 (509) hide show
  1. package/bin/chant +7 -2
  2. package/dist/ci-jobs.d.ts +4 -0
  3. package/dist/ci-jobs.d.ts.map +1 -1
  4. package/dist/ci-jobs.js +9 -2
  5. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  6. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  8. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  9. package/dist/cli/commands/check-lexicon.js +18 -12
  10. package/dist/cli/commands/import.d.ts +10 -0
  11. package/dist/cli/commands/import.d.ts.map +1 -1
  12. package/dist/cli/commands/import.js +37 -0
  13. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  14. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  15. package/dist/cli/commands/onboard.d.ts.map +1 -1
  16. package/dist/cli/commands/onboard.js +8 -4
  17. package/dist/cli/commands/update.d.ts.map +1 -1
  18. package/dist/cli/commands/update.js +2 -1
  19. package/dist/cli/dependencies.d.ts +18 -0
  20. package/dist/cli/dependencies.d.ts.map +1 -0
  21. package/dist/cli/dependencies.js +120 -0
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/build.js +38 -4
  24. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  25. package/dist/cli/handlers/fan-out.js +12 -4
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +16 -6
  28. package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
  29. package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
  30. package/dist/cli/handlers/lifecycle-health.js +150 -0
  31. package/dist/cli/handlers/lifecycle.d.ts +26 -0
  32. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  33. package/dist/cli/handlers/lifecycle.js +1 -1
  34. package/dist/cli/handlers/lint.d.ts.map +1 -1
  35. package/dist/cli/handlers/lint.js +6 -0
  36. package/dist/cli/handlers/operator.d.ts +2 -2
  37. package/dist/cli/handlers/operator.js +3 -3
  38. package/dist/cli/handlers/pr.d.ts +16 -0
  39. package/dist/cli/handlers/pr.d.ts.map +1 -1
  40. package/dist/cli/handlers/pr.js +610 -32
  41. package/dist/cli/handlers/promote.d.ts.map +1 -1
  42. package/dist/cli/handlers/promote.js +19 -2
  43. package/dist/cli/handlers/release.d.ts +16 -1
  44. package/dist/cli/handlers/release.d.ts.map +1 -1
  45. package/dist/cli/handlers/release.js +83 -11
  46. package/dist/cli/handlers/run.d.ts +13 -1
  47. package/dist/cli/handlers/run.d.ts.map +1 -1
  48. package/dist/cli/handlers/run.js +38 -15
  49. package/dist/cli/main.d.ts.map +1 -1
  50. package/dist/cli/main.js +121 -33
  51. package/dist/cli/parse-args.d.ts.map +1 -1
  52. package/dist/cli/parse-args.js +37 -0
  53. package/dist/cli/registry.d.ts +29 -6
  54. package/dist/cli/registry.d.ts.map +1 -1
  55. package/dist/cli/registry.js +6 -2
  56. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  57. package/dist/codegen/lexicon-regen.d.ts +4 -0
  58. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  59. package/dist/codegen/lexicon-regen.js +1 -1
  60. package/dist/codegen/pinned-upgrade.js +2 -2
  61. package/dist/components/capability.d.ts +44 -3
  62. package/dist/components/capability.d.ts.map +1 -1
  63. package/dist/components/capability.js +21 -0
  64. package/dist/components/changed-files.d.ts +7 -1
  65. package/dist/components/changed-files.d.ts.map +1 -1
  66. package/dist/components/changed-files.js +9 -1
  67. package/dist/components/deploy-inputs.d.ts +91 -0
  68. package/dist/components/deploy-inputs.d.ts.map +1 -0
  69. package/dist/components/deploy-inputs.js +184 -0
  70. package/dist/components/driver.d.ts +38 -0
  71. package/dist/components/driver.d.ts.map +1 -1
  72. package/dist/components/driver.js +56 -6
  73. package/dist/components/fan-out-record.d.ts +6 -1
  74. package/dist/components/fan-out-record.d.ts.map +1 -1
  75. package/dist/components/fan-out-record.js +24 -0
  76. package/dist/components/fan-out-run.d.ts +20 -0
  77. package/dist/components/fan-out-run.d.ts.map +1 -1
  78. package/dist/components/fan-out-run.js +21 -1
  79. package/dist/components/gate-plan.d.ts +8 -0
  80. package/dist/components/gate-plan.d.ts.map +1 -1
  81. package/dist/components/gate-plan.js +6 -0
  82. package/dist/components/pr-pipeline.d.ts +98 -12
  83. package/dist/components/pr-pipeline.d.ts.map +1 -1
  84. package/dist/components/pr-pipeline.js +146 -23
  85. package/dist/components/pr-run.d.ts +128 -30
  86. package/dist/components/pr-run.d.ts.map +1 -1
  87. package/dist/components/pr-run.js +290 -70
  88. package/dist/components/release-compile.d.ts +37 -2
  89. package/dist/components/release-compile.d.ts.map +1 -1
  90. package/dist/components/release-compile.js +64 -8
  91. package/dist/components/release-promote.d.ts +17 -3
  92. package/dist/components/release-promote.d.ts.map +1 -1
  93. package/dist/components/release-promote.js +79 -18
  94. package/dist/components/verbs/bom-writer.d.ts +1 -1
  95. package/dist/components/verbs/bom-writer.d.ts.map +1 -1
  96. package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
  97. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
  98. package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
  99. package/dist/components/wave-plan.d.ts +15 -2
  100. package/dist/components/wave-plan.d.ts.map +1 -1
  101. package/dist/components/wave-plan.js +44 -5
  102. package/dist/discovery/fold-import.d.ts.map +1 -1
  103. package/dist/discovery/fold-import.js +16 -6
  104. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  105. package/dist/discovery/sandbox/bundle.js +6 -0
  106. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  107. package/dist/discovery/specifier-resolve.js +8 -2
  108. package/dist/fold/fold.d.ts.map +1 -1
  109. package/dist/fold/fold.js +68 -1
  110. package/dist/fold/subset.d.ts +37 -0
  111. package/dist/fold/subset.d.ts.map +1 -1
  112. package/dist/fold/subset.js +88 -0
  113. package/dist/graph-detail.d.ts.map +1 -1
  114. package/dist/graph-detail.js +32 -7
  115. package/dist/health.d.ts +110 -0
  116. package/dist/health.d.ts.map +1 -0
  117. package/dist/health.js +105 -0
  118. package/dist/health.schema.json +191 -0
  119. package/dist/lexicon-module.d.ts +10 -0
  120. package/dist/lexicon-module.d.ts.map +1 -1
  121. package/dist/lexicon-module.js +53 -8
  122. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  123. package/dist/lexicon-plugin-helpers.js +6 -4
  124. package/dist/lexicon.d.ts +29 -0
  125. package/dist/lexicon.d.ts.map +1 -1
  126. package/dist/lifecycle/affected.d.ts.map +1 -1
  127. package/dist/lifecycle/affected.js +7 -1
  128. package/dist/lifecycle/merge-record.d.ts +104 -0
  129. package/dist/lifecycle/merge-record.d.ts.map +1 -0
  130. package/dist/lifecycle/merge-record.js +102 -0
  131. package/dist/lifecycle/publish-attachments.d.ts +8 -5
  132. package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
  133. package/dist/lifecycle/publish-attachments.js +27 -13
  134. package/dist/lifecycle/publish-npm.d.ts +3 -2
  135. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  136. package/dist/lifecycle/publish-npm.js +4 -3
  137. package/dist/lifecycle/release-at.d.ts +40 -0
  138. package/dist/lifecycle/release-at.d.ts.map +1 -0
  139. package/dist/lifecycle/release-at.js +83 -0
  140. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  141. package/dist/lifecycle/release-cut.js +23 -12
  142. package/dist/lifecycle/release-ledger.d.ts +21 -0
  143. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  144. package/dist/lifecycle/release-ledger.js +24 -0
  145. package/dist/lifecycle/release-publish.d.ts +28 -12
  146. package/dist/lifecycle/release-publish.d.ts.map +1 -1
  147. package/dist/lifecycle/release-publish.js +29 -17
  148. package/dist/lifecycle/release-store.d.ts +25 -2
  149. package/dist/lifecycle/release-store.d.ts.map +1 -1
  150. package/dist/lifecycle/release-store.js +40 -13
  151. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  152. package/dist/lifecycle/run-ledger.js +1 -0
  153. package/dist/lifecycle/work-dir.d.ts +45 -0
  154. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  155. package/dist/lifecycle/work-dir.js +78 -0
  156. package/dist/lint/discover.d.ts +17 -9
  157. package/dist/lint/discover.d.ts.map +1 -1
  158. package/dist/lint/discover.js +62 -54
  159. package/dist/lint/rules/op/index.d.ts +2 -1
  160. package/dist/lint/rules/op/index.d.ts.map +1 -1
  161. package/dist/lint/rules/op/index.js +4 -2
  162. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
  163. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
  164. package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
  165. package/dist/module-source.d.ts +57 -0
  166. package/dist/module-source.d.ts.map +1 -0
  167. package/dist/module-source.js +104 -0
  168. package/dist/op/activities/activity-contracts.d.ts +5 -0
  169. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  170. package/dist/op/activities/activity-contracts.js +3 -0
  171. package/dist/op/activities/factory.js +1 -1
  172. package/dist/op/activities/shell.d.ts +38 -0
  173. package/dist/op/activities/shell.d.ts.map +1 -1
  174. package/dist/op/activities/shell.js +89 -3
  175. package/dist/op/builders.d.ts +7 -0
  176. package/dist/op/builders.d.ts.map +1 -1
  177. package/dist/op/builders.js +7 -0
  178. package/dist/op/discover.d.ts.map +1 -1
  179. package/dist/op/discover.js +13 -3
  180. package/dist/op/gate.d.ts.map +1 -1
  181. package/dist/op/gate.js +3 -0
  182. package/dist/op/op-waves-run.d.ts.map +1 -1
  183. package/dist/op/op-waves-run.js +20 -5
  184. package/dist/op/operator.d.ts +2 -2
  185. package/dist/op/operator.js +2 -2
  186. package/dist/op/runtime.d.ts +2 -0
  187. package/dist/op/runtime.d.ts.map +1 -1
  188. package/dist/op/steward.d.ts +6 -4
  189. package/dist/op/steward.d.ts.map +1 -1
  190. package/dist/op/steward.js +5 -3
  191. package/dist/op/work-lease-run.d.ts +20 -7
  192. package/dist/op/work-lease-run.d.ts.map +1 -1
  193. package/dist/op/work-lease-run.js +56 -12
  194. package/dist/pr-forge.d.ts +10 -0
  195. package/dist/pr-forge.d.ts.map +1 -1
  196. package/dist/pr-forge.js +39 -0
  197. package/dist/pr-loop.d.ts +155 -7
  198. package/dist/pr-loop.d.ts.map +1 -1
  199. package/dist/pr-loop.js +137 -11
  200. package/dist/pr-report-url.d.ts +4 -4
  201. package/dist/pr-report-url.d.ts.map +1 -1
  202. package/dist/pr-report-url.js +5 -5
  203. package/dist/runner-start.d.ts +179 -0
  204. package/dist/runner-start.d.ts.map +1 -0
  205. package/dist/runner-start.js +277 -0
  206. package/dist/ts-loader.d.ts +38 -4
  207. package/dist/ts-loader.d.ts.map +1 -1
  208. package/dist/ts-loader.js +190 -16
  209. package/dist/wave-runtime.mjs +238 -231
  210. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  211. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  212. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  213. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  214. package/dist/workspace/chant-migrations.js +2 -0
  215. package/dist/workspace/check.schema.json +70 -2
  216. package/dist/workspace/checks/boxes.d.ts +6 -6
  217. package/dist/workspace/checks/boxes.js +9 -9
  218. package/dist/workspace/checks/security.d.ts +35 -0
  219. package/dist/workspace/checks/security.d.ts.map +1 -0
  220. package/dist/workspace/checks/security.js +305 -0
  221. package/dist/workspace/checks.d.ts +2 -0
  222. package/dist/workspace/checks.d.ts.map +1 -1
  223. package/dist/workspace/checks.js +5 -0
  224. package/dist/workspace/ci-builds.d.ts +163 -0
  225. package/dist/workspace/ci-builds.d.ts.map +1 -0
  226. package/dist/workspace/ci-builds.js +315 -0
  227. package/dist/workspace/ci-green-cli.d.ts +2 -1
  228. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  229. package/dist/workspace/ci-green-cli.js +25 -5
  230. package/dist/workspace/ci-green-forge.d.ts +18 -4
  231. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  232. package/dist/workspace/ci-green-forge.js +12 -0
  233. package/dist/workspace/ci-green-workflow.d.ts +55 -1
  234. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  235. package/dist/workspace/ci-green-workflow.js +128 -1
  236. package/dist/workspace/ci-green.d.ts +117 -16
  237. package/dist/workspace/ci-green.d.ts.map +1 -1
  238. package/dist/workspace/ci-green.js +358 -43
  239. package/dist/workspace/ci-last-green.schema.json +49 -0
  240. package/dist/workspace/ci-list.schema.json +47 -7
  241. package/dist/workspace/ci-report-url.d.ts +18 -0
  242. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  243. package/dist/workspace/ci-report-url.js +33 -0
  244. package/dist/workspace/ci-run-index.d.ts +13 -1
  245. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  246. package/dist/workspace/ci-run-index.js +6 -4
  247. package/dist/workspace/ci-run-index.schema.json +6 -4
  248. package/dist/workspace/ci-run.d.ts +85 -6
  249. package/dist/workspace/ci-run.d.ts.map +1 -1
  250. package/dist/workspace/ci-run.js +262 -64
  251. package/dist/workspace/ci-runner-key.d.ts +16 -3
  252. package/dist/workspace/ci-runner-key.d.ts.map +1 -1
  253. package/dist/workspace/ci-runner-key.js +30 -10
  254. package/dist/workspace/ci-runner-start.d.ts +77 -0
  255. package/dist/workspace/ci-runner-start.d.ts.map +1 -0
  256. package/dist/workspace/ci-runner-start.js +185 -0
  257. package/dist/workspace/ci-serve.d.ts +130 -11
  258. package/dist/workspace/ci-serve.d.ts.map +1 -1
  259. package/dist/workspace/ci-serve.js +323 -40
  260. package/dist/workspace/declaration.d.ts +25 -0
  261. package/dist/workspace/declaration.d.ts.map +1 -1
  262. package/dist/workspace/declaration.js +14 -2
  263. package/dist/workspace/declaration.schema.json +30 -2
  264. package/dist/workspace/export.d.ts +1 -1
  265. package/dist/workspace/export.d.ts.map +1 -1
  266. package/dist/workspace/export.js +7 -3
  267. package/dist/workspace/ls.d.ts +8 -1
  268. package/dist/workspace/ls.d.ts.map +1 -1
  269. package/dist/workspace/ls.js +2 -1
  270. package/dist/workspace/ls.schema.json +9 -2
  271. package/dist/workspace/member-pipeline.d.ts +1 -0
  272. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  273. package/dist/workspace/member-pipeline.js +2 -0
  274. package/dist/workspace/merge-record.schema.json +49 -0
  275. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  276. package/dist/workspace/pin-integrity.js +3 -2
  277. package/dist/workspace/pr-report.schema.json +75 -8
  278. package/dist/workspace/reason-codes.d.ts +18 -1
  279. package/dist/workspace/reason-codes.d.ts.map +1 -1
  280. package/dist/workspace/reason-codes.js +20 -2
  281. package/dist/workspace/release-candidates.d.ts +129 -0
  282. package/dist/workspace/release-candidates.d.ts.map +1 -0
  283. package/dist/workspace/release-candidates.js +233 -0
  284. package/dist/workspace/release-candidates.schema.json +353 -0
  285. package/dist/workspace/run-event.schema.json +3 -1
  286. package/dist/workspace/security.d.ts +40 -0
  287. package/dist/workspace/security.d.ts.map +1 -0
  288. package/dist/workspace/security.js +99 -0
  289. package/dist/workspace/status.schema.json +3 -1
  290. package/dist/workspace/write-scope.d.ts +9 -0
  291. package/dist/workspace/write-scope.d.ts.map +1 -1
  292. package/dist/workspace/write-scope.js +11 -1
  293. package/package.json +8 -1
  294. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  295. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
  296. package/src/ci-jobs.ts +11 -2
  297. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  298. package/src/cli/commands/audit.test.ts +2 -2
  299. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  300. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  301. package/src/cli/commands/check-lexicon.test.ts +1 -1
  302. package/src/cli/commands/check-lexicon.ts +16 -11
  303. package/src/cli/commands/import.ts +40 -0
  304. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  305. package/src/cli/commands/init-lexicon.test.ts +8 -5
  306. package/src/cli/commands/onboard.test.ts +45 -0
  307. package/src/cli/commands/onboard.ts +8 -4
  308. package/src/cli/commands/update.test.ts +6 -0
  309. package/src/cli/commands/update.ts +4 -1
  310. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  311. package/src/cli/dependencies.e2e.test.ts +101 -0
  312. package/src/cli/dependencies.test.ts +93 -0
  313. package/src/cli/dependencies.ts +122 -0
  314. package/src/cli/handlers/build.test.ts +105 -0
  315. package/src/cli/handlers/build.ts +35 -4
  316. package/src/cli/handlers/fan-out.test.ts +73 -0
  317. package/src/cli/handlers/fan-out.ts +11 -4
  318. package/src/cli/handlers/graph.test.ts +76 -0
  319. package/src/cli/handlers/graph.ts +19 -12
  320. package/src/cli/handlers/lifecycle-health.test.ts +123 -0
  321. package/src/cli/handlers/lifecycle-health.ts +157 -0
  322. package/src/cli/handlers/lifecycle.ts +1 -1
  323. package/src/cli/handlers/lint.test.ts +42 -0
  324. package/src/cli/handlers/lint.ts +6 -0
  325. package/src/cli/handlers/operator.ts +3 -3
  326. package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
  327. package/src/cli/handlers/pr-record.test.ts +201 -0
  328. package/src/cli/handlers/pr-waves.test.ts +449 -0
  329. package/src/cli/handlers/pr.test.ts +156 -0
  330. package/src/cli/handlers/pr.ts +648 -33
  331. package/src/cli/handlers/promote.test.ts +43 -1
  332. package/src/cli/handlers/promote.ts +19 -3
  333. package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
  334. package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
  335. package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
  336. package/src/cli/handlers/release.e2e.test.ts +19 -28
  337. package/src/cli/handlers/release.ts +81 -11
  338. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  339. package/src/cli/handlers/run-signer.test.ts +79 -0
  340. package/src/cli/handlers/run.ts +34 -16
  341. package/src/cli/main.test.ts +18 -0
  342. package/src/cli/main.ts +120 -33
  343. package/src/cli/parse-args.ts +28 -0
  344. package/src/cli/registry.ts +35 -8
  345. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  346. package/src/codegen/lexicon-regen.ts +1 -1
  347. package/src/codegen/pinned-upgrade.ts +2 -2
  348. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  349. package/src/components/capability.ts +51 -3
  350. package/src/components/changed-files.test.ts +16 -1
  351. package/src/components/changed-files.ts +9 -1
  352. package/src/components/deploy-inputs.test.ts +108 -0
  353. package/src/components/deploy-inputs.ts +225 -0
  354. package/src/components/driver.test.ts +36 -1
  355. package/src/components/driver.ts +87 -4
  356. package/src/components/fan-out-record.ts +29 -1
  357. package/src/components/fan-out-run.test.ts +35 -0
  358. package/src/components/fan-out-run.ts +40 -1
  359. package/src/components/gate-plan.ts +9 -0
  360. package/src/components/pr-pipeline.test.ts +100 -0
  361. package/src/components/pr-pipeline.ts +184 -28
  362. package/src/components/pr-run.test.ts +323 -18
  363. package/src/components/pr-run.ts +355 -76
  364. package/src/components/release-compile.test.ts +34 -0
  365. package/src/components/release-compile.ts +89 -8
  366. package/src/components/release-promote.test.ts +55 -2
  367. package/src/components/release-promote.ts +93 -23
  368. package/src/components/verbs/bom-writer.ts +1 -1
  369. package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
  370. package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
  371. package/src/components/wave-plan.ts +48 -8
  372. package/src/discovery/fold-host-method.test.ts +87 -0
  373. package/src/discovery/fold-import.ts +15 -5
  374. package/src/discovery/fold-package-composite.test.ts +27 -0
  375. package/src/discovery/fold-specifiers.test.ts +8 -0
  376. package/src/discovery/sandbox/bundle.ts +6 -0
  377. package/src/discovery/specifier-resolve.ts +8 -2
  378. package/src/fold/fold.test.ts +72 -0
  379. package/src/fold/fold.ts +71 -0
  380. package/src/fold/subset.test.ts +31 -0
  381. package/src/fold/subset.ts +99 -0
  382. package/src/graph-detail.test.ts +57 -0
  383. package/src/graph-detail.ts +33 -7
  384. package/src/health.schema.json +191 -0
  385. package/src/health.test.ts +62 -0
  386. package/src/health.ts +178 -0
  387. package/src/lexicon-dist.e2e.test.ts +254 -0
  388. package/src/lexicon-module.test.ts +47 -0
  389. package/src/lexicon-module.ts +49 -7
  390. package/src/lexicon-plugin-helpers.ts +6 -4
  391. package/src/lexicon.ts +43 -1
  392. package/src/lifecycle/affected.test.ts +7 -0
  393. package/src/lifecycle/affected.ts +6 -1
  394. package/src/lifecycle/merge-record.test.ts +111 -0
  395. package/src/lifecycle/merge-record.ts +154 -0
  396. package/src/lifecycle/publish-attachments.ts +25 -13
  397. package/src/lifecycle/publish-npm.ts +4 -3
  398. package/src/lifecycle/publish-targets.test.ts +34 -4
  399. package/src/lifecycle/release-at.test.ts +48 -0
  400. package/src/lifecycle/release-at.ts +100 -0
  401. package/src/lifecycle/release-cut.test.ts +41 -0
  402. package/src/lifecycle/release-cut.ts +22 -11
  403. package/src/lifecycle/release-ledger.ts +34 -0
  404. package/src/lifecycle/release-publish.test.ts +43 -13
  405. package/src/lifecycle/release-publish.ts +38 -18
  406. package/src/lifecycle/release-store.ts +57 -13
  407. package/src/lifecycle/run-ledger.test.ts +13 -0
  408. package/src/lifecycle/run-ledger.ts +1 -0
  409. package/src/lifecycle/work-dir.test.ts +77 -0
  410. package/src/lifecycle/work-dir.ts +82 -0
  411. package/src/lint/discover.test.ts +46 -2
  412. package/src/lint/discover.ts +55 -49
  413. package/src/lint/rules/op/index.ts +4 -2
  414. package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
  415. package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
  416. package/src/module-source.test.ts +85 -0
  417. package/src/module-source.ts +112 -0
  418. package/src/op/activities/activity-contracts.ts +3 -0
  419. package/src/op/activities/factory.ts +1 -1
  420. package/src/op/activities/shell-container.e2e.test.ts +67 -0
  421. package/src/op/activities/shell.test.ts +44 -1
  422. package/src/op/activities/shell.ts +110 -3
  423. package/src/op/builders.ts +7 -0
  424. package/src/op/discover-root.test.ts +31 -2
  425. package/src/op/discover.ts +14 -4
  426. package/src/op/gate.test.ts +30 -0
  427. package/src/op/gate.ts +3 -0
  428. package/src/op/op-waves-run.ts +20 -5
  429. package/src/op/op-waves.test.ts +49 -0
  430. package/src/op/operator.ts +2 -2
  431. package/src/op/runtime.ts +2 -0
  432. package/src/op/step-console.test.ts +4 -2
  433. package/src/op/steward.ts +6 -4
  434. package/src/op/work-lease-run.test.ts +88 -2
  435. package/src/op/work-lease-run.ts +52 -12
  436. package/src/pr-forge-bundle.test.ts +2 -2
  437. package/src/pr-forge.test.ts +42 -0
  438. package/src/pr-forge.ts +40 -0
  439. package/src/pr-loop-bundle.test.ts +2 -1
  440. package/src/pr-loop.test.ts +146 -1
  441. package/src/pr-loop.ts +278 -17
  442. package/src/pr-report-url.ts +7 -7
  443. package/src/runner-start-bundle.test.ts +29 -0
  444. package/src/runner-start.test.ts +179 -0
  445. package/src/runner-start.ts +329 -0
  446. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  447. package/src/ts-loader.test.ts +89 -1
  448. package/src/ts-loader.ts +180 -13
  449. package/src/workspace/changes.test.ts +1 -1
  450. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  451. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  452. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  453. package/src/workspace/chant-migrations.ts +2 -0
  454. package/src/workspace/check-contract.test.ts +3 -2
  455. package/src/workspace/check.schema.json +70 -2
  456. package/src/workspace/checks/boxes.ts +9 -9
  457. package/src/workspace/checks/security.test.ts +138 -0
  458. package/src/workspace/checks/security.ts +305 -0
  459. package/src/workspace/checks.ts +5 -0
  460. package/src/workspace/ci-builds.e2e.test.ts +208 -0
  461. package/src/workspace/ci-builds.test.ts +174 -0
  462. package/src/workspace/ci-builds.ts +385 -0
  463. package/src/workspace/ci-green-builds.test.ts +180 -0
  464. package/src/workspace/ci-green-cli.test.ts +54 -0
  465. package/src/workspace/ci-green-cli.ts +26 -5
  466. package/src/workspace/ci-green-forge.ts +30 -4
  467. package/src/workspace/ci-green-runs.test.ts +53 -11
  468. package/src/workspace/ci-green-workflow.test.ts +84 -1
  469. package/src/workspace/ci-green-workflow.ts +139 -2
  470. package/src/workspace/ci-green.test.ts +6 -3
  471. package/src/workspace/ci-green.ts +408 -48
  472. package/src/workspace/ci-last-green.schema.json +49 -0
  473. package/src/workspace/ci-list.schema.json +47 -7
  474. package/src/workspace/ci-report-url.test.ts +220 -0
  475. package/src/workspace/ci-report-url.ts +33 -0
  476. package/src/workspace/ci-run-index.schema.json +6 -4
  477. package/src/workspace/ci-run-index.test.ts +42 -3
  478. package/src/workspace/ci-run-index.ts +19 -5
  479. package/src/workspace/ci-run.test.ts +203 -8
  480. package/src/workspace/ci-run.ts +295 -63
  481. package/src/workspace/ci-runner-key.test.ts +55 -17
  482. package/src/workspace/ci-runner-key.ts +36 -11
  483. package/src/workspace/ci-runner-start.test.ts +151 -0
  484. package/src/workspace/ci-runner-start.ts +193 -0
  485. package/src/workspace/ci-serve-builds.test.ts +86 -0
  486. package/src/workspace/ci-serve.test.ts +209 -6
  487. package/src/workspace/ci-serve.ts +341 -42
  488. package/src/workspace/declaration.schema.json +30 -2
  489. package/src/workspace/declaration.test.ts +40 -0
  490. package/src/workspace/declaration.ts +40 -3
  491. package/src/workspace/export.ts +6 -3
  492. package/src/workspace/ls.schema.json +9 -2
  493. package/src/workspace/ls.ts +10 -4
  494. package/src/workspace/member-pipeline.ts +2 -1
  495. package/src/workspace/merge-record.schema.json +49 -0
  496. package/src/workspace/pin-integrity.test.ts +5 -0
  497. package/src/workspace/pin-integrity.ts +3 -2
  498. package/src/workspace/pr-report.schema.json +75 -8
  499. package/src/workspace/reason-codes.test.ts +2 -0
  500. package/src/workspace/reason-codes.ts +20 -2
  501. package/src/workspace/release-candidates.schema.json +353 -0
  502. package/src/workspace/release-candidates.test.ts +106 -0
  503. package/src/workspace/release-candidates.ts +315 -0
  504. package/src/workspace/run-event.schema.json +3 -1
  505. package/src/workspace/security.test.ts +70 -0
  506. package/src/workspace/security.ts +118 -0
  507. package/src/workspace/status.schema.json +3 -1
  508. package/src/workspace/write-scope.test.ts +29 -1
  509. package/src/workspace/write-scope.ts +12 -1
@@ -8,7 +8,8 @@
8
8
  * tag the commits that turned green, revoke the ones that turned red
9
9
  * ci workflow [--workflow <name>]... [--output <file>] [--chant <command>] [--install <command>]
10
10
  * [--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>] [--runner-key-secret <NAME>]
11
- * write the root CI file that runs the tick
11
+ * write the root CI file that runs the tick, and the triggers and
12
+ * concurrency of the workflows holding a phase that sets build (#4169)
12
13
  *
13
14
  * The tick signs its tags with a runner key (#4031) from `--key <file>` or
14
15
  * the PEM in `CHANT_RUNNER_KEY`, which the generated workflow sets from the
@@ -30,7 +31,7 @@ import type { CommandContext } from "../cli/registry";
30
31
  import { findWorkspaceRoot } from "../project-root";
31
32
  import { ciList, ciTick, CiGreenError, formatList, formatTick, lastGreen, RUNNER_KEY_ENV } from "./ci-green";
32
33
  import { ciForgeFromEnv } from "./ci-green-forge";
33
- import { ACTIONS_NAME, CI_GREEN_WORKFLOW_FILE, ciGreenHeader, type PushToken, readWorkflowSources, renderCiGreenWorkflow, workflowsHolding, writeWorkflowFile } from "./ci-green-workflow";
34
+ import { ACTIONS_NAME, buildTriggerEdits, CI_GREEN_WORKFLOW_FILE, ciGreenHeader, type PushToken, readWorkflowSources, renderCiGreenWorkflow, workflowsHolding, writeWorkflowFile } from "./ci-green-workflow";
34
35
  import { readDeclaration, readerVersion, WorkspaceReadError, type CiSource } from "./declaration";
35
36
  import { recordGeneratedFiles, resolveMemberContext, shellArg } from "./member-pipeline";
36
37
  import { gitTop, workingTree } from "./tree";
@@ -105,6 +106,13 @@ export async function runCiTick(ctx: CommandContext): Promise<number> {
105
106
  // The forge is made only when the source is check runs, so a tick from chant runs needs no forge token.
106
107
  const forge = () => ciForgeFromEnv(ctx.args.forge ?? "github");
107
108
  const runnerKey = ctx.args.key !== undefined ? readFileSync(resolve(ctx.args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || undefined;
109
+ // #4168: under who-ran-it every tag the tick makes is signed, so a tick without a key makes none.
110
+ if (runnerKey === undefined && ctx.args.dryRun !== true) {
111
+ const { securityGroupsAt } = await import("./security");
112
+ if (securityGroupsAt(process.cwd()).has("who-ran-it")) {
113
+ throw new Error(`the workspace opts into who-ran-it, so the tick signs every tag it makes and needs a runner key; pass --key <runner key.pem> or set ${RUNNER_KEY_ENV}`);
114
+ }
115
+ }
108
116
  const result = await ciTick({ cwd: process.cwd(), forge, dryRun: ctx.args.dryRun === true, ...(source ? { source } : {}), ...(runnerKey !== undefined ? { runnerKey } : {}) });
109
117
  console.log(formatTick(result, ctx.args.dryRun === true));
110
118
  return 0;
@@ -143,7 +151,10 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
143
151
  const target = ctx.args.output ? resolve(realpathSync(cwd), ctx.args.output) : join(repoRoot, CI_GREEN_WORKFLOW_FILE);
144
152
  const fromRepo = relative(repoRoot, target).split(sep).join("/");
145
153
 
146
- const scan = workflowsHolding(green, readWorkflowSources(repoRoot, fromRepo));
154
+ const sources = readWorkflowSources(repoRoot, fromRepo);
155
+ const scan = workflowsHolding(green, sources);
156
+ // #4169: the triggers and concurrency of the workflows holding a phase that sets build. Refused before anything is written.
157
+ const edits = buildTriggerEdits(green, sources);
147
158
  const extra = ctx.args.ciWorkflows ?? [];
148
159
  const workflows = [...new Set([...scan.workflows, ...extra])].sort();
149
160
  for (const f of scan.unreadable) console.error(`warning: ${f} is not valid YAML, so its jobs were not read`);
@@ -161,8 +172,9 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
161
172
  if (push && "appIdVar" in push) parts.push("--app-id-var", push.appIdVar, "--app-key-secret", push.appKeySecret);
162
173
  const runnerKeySecret = ctx.args.ciRunnerKeySecret;
163
174
  if (runnerKeySecret) parts.push("--runner-key-secret", runnerKeySecret);
164
- if (green.signedSince && !runnerKeySecret) {
165
- console.error(`warning: ci.green.signedSince is set, so the tick needs a runner key; pass --runner-key-secret <NAME> naming the repository secret that holds it, or every tick will fail`);
175
+ const { securityGroupsAt } = await import("./security");
176
+ if ((green.signedSince || securityGroupsAt(root).has("who-ran-it")) && !runnerKeySecret) {
177
+ console.error(`warning: ci.green.signedSince is set or the workspace opts into who-ran-it, so the tick needs a runner key; pass --runner-key-secret <NAME> naming the repository secret that holds it, or every tick will fail`);
166
178
  }
167
179
  const command = parts.map(shellArg).join(" ");
168
180
  const workspaceRoot = relative(repoRoot, root).split(sep).join("/") || ".";
@@ -172,6 +184,15 @@ export async function runCiWorkflow(ctx: CommandContext): Promise<number> {
172
184
  const written = writeWorkflowFile(target, ciGreenHeader(command, member ? "member" : "root") + yaml);
173
185
  if (member) recordGeneratedFiles(member.memberRoot, [{ path: fromRepo, command }]);
174
186
  console.log(`Wrote ${written}: ticks when ${workflows.join(", ")} complete${workflows.length === 1 ? "s" : ""} on ${green.branch}, and every 15 minutes.`);
187
+ for (const e of edits) {
188
+ if (!e.changed) continue;
189
+ const path = writeWorkflowFile(join(repoRoot, e.path), e.text);
190
+ console.log(
191
+ e.build === "newest"
192
+ ? `Wrote ${path}'s triggers and concurrency: ${e.phases.join(", ")} build${e.phases.length === 1 ? "s" : ""} the newest commit on ${green.branch} when a run ends, and a ci/build/<sha> tag by hand.`
193
+ : `Wrote ${path}'s concurrency: ${e.phases.join(", ")} build${e.phases.length === 1 ? "s" : ""} every commit on ${green.branch}, none cancelled.`,
194
+ );
195
+ }
175
196
  return 0;
176
197
  } catch (err) {
177
198
  console.error(formatError({ message: `chant ci workflow: ${describe(err)}`, hint: USAGE }));
@@ -32,8 +32,8 @@ export interface CheckRun {
32
32
  url: string | null;
33
33
  /**
34
34
  * Set when the "check run" is a chant Op run (#4044, `ci.green.source:
35
- * "runs"`): the run's id and env, the runner whose statement it carries,
36
- * and that statement. `name` is then the Op's name and `id` the run's end
35
+ * "runs"`): the run's id and env, and the runner whose statement it
36
+ * carries with that statement, or a null signer for an unsigned run. `name` is then the Op's name and `id` the run's end
37
37
  * time in milliseconds, which orders a re-run after the run it replaced.
38
38
  */
39
39
  opRun?: OpRunRef;
@@ -45,14 +45,28 @@ export interface CheckRun {
45
45
  export interface OpRunRef {
46
46
  id: string;
47
47
  env: string;
48
- signer: string;
49
- statement: NonNullable<OpRunRecord["statement"]>;
48
+ /** The runner whose statement the run carries, or null for an unsigned run, which counts unless `ci.green.signedSince` asks for one (#4156). */
49
+ signer: string | null;
50
+ /** The run's statement, when it has one. */
51
+ statement?: NonNullable<OpRunRecord["statement"]>;
50
52
  }
51
53
 
52
54
  export interface CiForge {
53
55
  kind: ForgeKind;
54
56
  /** Every check run on `sha`, every attempt. */
55
57
  checkRuns(sha: string): Promise<CheckRun[]>;
58
+ /**
59
+ * Run a failed check run's job again (#4169), so a flake costs one more
60
+ * run before the commit counts red. Absent on a forge chant can't ask.
61
+ */
62
+ rerun?(run: CheckRun): Promise<void>;
63
+ /**
64
+ * Start a workflow at a ref (#4169), for a build the tick asks for: a
65
+ * bisect step, or `chant ci run --at`. `workflow` is the file's name in
66
+ * `.github/workflows/`, and `ref` a branch or tag. Absent on a forge chant
67
+ * can't ask.
68
+ */
69
+ dispatch?(workflow: string, ref: string): Promise<void>;
56
70
  }
57
71
 
58
72
  export interface GithubCiForgeOptions {
@@ -101,7 +115,19 @@ export function githubCiForge(opts: GithubCiForgeOptions): CiForge {
101
115
  }
102
116
  return out;
103
117
  },
118
+ // An Actions job's check run has the job's id, so the job is re-run by it. Both need `actions: write`.
119
+ async rerun(run) {
120
+ await post(`${root}/repos/${opts.repo}/actions/jobs/${run.id}/rerun`, {});
121
+ },
122
+ async dispatch(workflow, ref) {
123
+ await post(`${root}/repos/${opts.repo}/actions/workflows/${encodeURIComponent(workflow)}/dispatches`, { ref });
124
+ },
104
125
  };
126
+
127
+ async function post(url: string, body: unknown): Promise<void> {
128
+ const res = await fetchImpl(url, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify(body) });
129
+ if (!res.ok) throw new ForgeApiError("POST", url, res.status, await res.text());
130
+ }
105
131
  }
106
132
 
107
133
  /** Thrown for a forge `chant ci tick` has no check-run client for yet. */
@@ -1,11 +1,13 @@
1
1
  /**
2
2
  * #4044: chant run results as a status source for `ci tick`. With
3
3
  * `ci.green.source: "runs"` (or `--source runs`) the tick reads Op runs from
4
- * the run ledger on `chant/lifecycle` instead of a forge's check runs. A run
5
- * counts only when its record carries a statement signed by a runner key
6
- * `.chant/trust.json` lists for a principal holding the `runner` role; the
7
- * tick ignores and reports any other. Every tag it makes is signed, and names
8
- * the runs behind it.
4
+ * the run ledger on `chant/lifecycle` instead of a forge's check runs.
5
+ * Signing is opt-in (#4156): an unsigned run counts with no signer, and a run
6
+ * whose statement a runner key `.chant/trust.json` lists for a principal
7
+ * holding the `runner` role signed counts with that signer. A statement that
8
+ * does not verify is ignored and reported. With `ci.green.signedSince` an
9
+ * unsigned run on that commit or after it is ignored too. A tick given a
10
+ * runner key signs its tags, and every tag names the runs behind it.
9
11
  *
10
12
  * Each test works in its own temp repository with a bare origin, and every
11
13
  * key is a throwaway one. Nothing is written in process.cwd().
@@ -149,19 +151,61 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
149
151
  expect((await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem })).made).toEqual([]);
150
152
  });
151
153
 
152
- test("an unsigned run is refused: it neither greens nor revokes a commit, and is reported", async () => {
154
+ test("an unsigned run counts with no signer: with no key anywhere the tick tags the commit and ci list shows it (#4156)", async () => {
153
155
  const { root } = workspace();
154
156
  const a = commitAt(root, 3, "app/a.txt");
157
+ const lint = await runOp(root, "lint");
158
+ await runOp(root, "test");
159
+ expect(lint.statement).toBeUndefined();
160
+ push(root);
161
+
162
+ const list = (await ciList({ cwd: root, now: NOW })) as Extract<Awaited<ReturnType<typeof ciList>>, { commits: unknown }>;
163
+ readList.expectValid(list);
164
+ const listed = list.commits.find((c) => c.sha === a)!;
165
+ expect(listed.phases.map((p) => [p.phase, p.result])).toEqual([
166
+ ["lint", "pass"],
167
+ ["test", "pass"],
168
+ ]);
169
+ expect(listed.phases[0]!.runs[0]).toMatchObject({ name: "lint", id: null, opRun: { id: lint.id, env: "ci", signer: null } });
170
+ expect(listed.ignoredRuns).toEqual([]);
171
+
172
+ // No runner key: the tick still tags, unsigned, and the tag names the unsigned runs.
173
+ const tick = await ciTick({ cwd: root, now: NOW });
174
+ expect(tick.made).toEqual([`ci/green/${a}`]);
175
+ expect(tick.signer).toBeNull();
176
+ const record = recordOf(root, `ci/green/${a}`) as { statement?: unknown; phases: Record<string, { opRun: Record<string, unknown> }[]> };
177
+ expect(record.statement).toBeUndefined();
178
+ expect(record.phases.lint![0]!.opRun).toEqual({ id: lint.id, env: "ci", signer: null });
179
+
180
+ // A signed run beside them shows its signer.
181
+ const signed = await runOp(root, "lint", { pem: BOT.pem });
182
+ const source = await readOpRunSource(root, root, git(root, "rev-parse", "HEAD"));
183
+ expect(source.checkRuns(a).map((r) => [r.opRun!.id, r.opRun!.signer])).toEqual([
184
+ [lint.id, null],
185
+ [signed.id, "ci-bot"],
186
+ [expect.any(String), null],
187
+ ]);
188
+ expect(source.ignored.size).toBe(0);
189
+ });
190
+
191
+ test("with ci.green.signedSince an unsigned run neither greens nor revokes a commit from that one on, and is reported", async () => {
192
+ const { root } = workspace();
193
+ const a = commitAt(root, 3, "app/a.txt");
194
+ git(root, "push", "-q", "origin", "main");
195
+ // The opt-in: every run on a, and after it, must be signed.
196
+ writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { ...GREEN, signedSince: a } } }) });
155
197
  await runOp(root, "lint", { pem: BOT.pem });
156
198
  const unsigned = await runOp(root, "test");
157
199
  expect(unsigned.statement).toBeUndefined();
158
200
  push(root);
159
201
 
202
+ // The opt-in also asks for signed tags, so the tick needs the key.
203
+ await expect(ciTick({ cwd: root, now: NOW })).rejects.toThrow(/needs a statement signed by a runner key, and none was given/);
160
204
  const tick = await ciTick({ cwd: root, now: NOW, runnerKey: BOT.pem });
161
205
  expect(tick.made).toEqual([]);
162
206
  const at = tick.commits.find((c) => c.sha === a)!;
163
207
  expect(at.verdict?.verdict).toBe("pending");
164
- expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("carries no statement signed by a runner key") }]);
208
+ expect(at.ignoredRuns).toEqual([{ run: unsigned.id, op: "test", env: "ci", status: "unsigned", reason: expect.stringContaining("ci.green.signedSince is") }]);
165
209
  expect(formatTick(tick)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
166
210
  expect(git(root, "tag", "-l")).toBe("");
167
211
 
@@ -173,7 +217,6 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
173
217
  ["test", "pending"],
174
218
  ]);
175
219
  expect(listed.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned"]);
176
- expect(formatList(list)).toContain(`ignored run ${unsigned.id} of test in ci (unsigned)`);
177
220
 
178
221
  // Signed now, the commit goes green; a later unsigned failure does not revoke it.
179
222
  await runOp(root, "test", { pem: BOT.pem });
@@ -186,7 +229,7 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
186
229
  expect(after.commits.find((c) => c.sha === a)!.ignoredRuns?.map((r) => r.status)).toEqual(["unsigned", "unsigned"]);
187
230
  });
188
231
 
189
- test("a run signed by an unlisted key, or whose record changed after signing, does not count", async () => {
232
+ test("a run signed by an unlisted key, or whose record changed after signing, does not count, with or without the opt-in", async () => {
190
233
  const { root } = workspace();
191
234
  const a = commitAt(root, 3, "app/a.txt");
192
235
  // A key trust.json does not list can't sign a run here at all.
@@ -219,13 +262,12 @@ describe("chant runs as the ci tick's status source (#4044)", () => {
219
262
  expect(record).toMatchObject({ source: "runs", phase: "test", run: { name: "test", id: null, conclusion: "failure", opRun: { id: red.id, signer: "ci-bot" } } });
220
263
  });
221
264
 
222
- test("a tick from runs needs a runner key unless it is a dry run, --source overrides the declaration, and ci list refuses --forge", async () => {
265
+ test("a tick from runs without a runner key makes unsigned tags, --source overrides the declaration, and ci list refuses --forge", async () => {
223
266
  const { root } = workspace({ ...GREEN, source: "checks" });
224
267
  const a = commitAt(root, 3, "app/a.txt");
225
268
  await runOp(root, "lint", { pem: BOT.pem });
226
269
  await runOp(root, "test", { pem: BOT.pem });
227
270
  push(root);
228
- await expect(ciTick({ cwd: root, now: NOW, source: "runs" })).rejects.toThrow(/every tag it makes from them is signed by a runner key, and none was given/);
229
271
  expect((await ciTick({ cwd: root, now: NOW, source: "runs", dryRun: true })).made).toEqual([`ci/green/${a}`]);
230
272
  expect(git(root, "tag", "-l")).toBe("");
231
273
  // The declaration says checks, so without --source the tick wants a forge.
@@ -11,7 +11,7 @@ import { GENERATED_MARKER } from "../discovery/files";
11
11
  import type { CommandContext } from "../cli/registry";
12
12
  import { parseYAMLDocument } from "../yaml";
13
13
  import { runCiWorkflow } from "./ci-green-cli";
14
- import { CI_GREEN_WORKFLOW_FILE, jobCheckRuns, renderCiGreenWorkflow, workflowsHolding } from "./ci-green-workflow";
14
+ import { BUILD_BLOCK_MARKER, buildTriggerEdits, CI_GREEN_WORKFLOW_FILE, dispatchableWorkflows, jobCheckRuns, renderCiGreenWorkflow, workflowsHolding, writeTopLevel } from "./ci-green-workflow";
15
15
  import { parseDeclaration } from "./declaration";
16
16
  import { cleanScratch, declaration, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
17
17
 
@@ -302,3 +302,86 @@ describe("chant ci workflow (#3573)", () => {
302
302
  }
303
303
  });
304
304
  });
305
+
306
+ // ── A phase's triggers (#4169) ───────────────────────────────────────────────
307
+
308
+ const BUILD_GREEN = { branch: "main", phases: { lint: { runs: ["lint"], build: "every" }, suites: { runs: ["suites"], build: "newest" } }, require: ["lint", "suites"] };
309
+ const LINT_WF = `name: ci
310
+ on:
311
+ push:
312
+ branches: [main]
313
+ pull_request:
314
+
315
+ # Comments above a block stay.
316
+ concurrency:
317
+ group: ci-\${{ github.ref }}
318
+ cancel-in-progress: true
319
+
320
+ jobs:
321
+ lint:
322
+ runs-on: ubuntu-latest
323
+ `;
324
+ const SUITES_WF = `name: suites
325
+ on:
326
+ schedule:
327
+ - cron: "0 */3 * * *"
328
+ workflow_dispatch: {}
329
+ permissions:
330
+ contents: read
331
+ jobs:
332
+ suites:
333
+ runs-on: ubuntu-latest
334
+ `;
335
+
336
+ describe("the triggers of a phase that sets build (#4169)", () => {
337
+ const green = () => parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: BUILD_GREEN } }), "chant.workspace.json").ci!.green!;
338
+
339
+ test("newest: pushes to the branch and by hand, no schedule, one group per ref that never cancels; every: the commit's own group on the branch", () => {
340
+ const edits = buildTriggerEdits(green(), [
341
+ { path: ".github/workflows/ci.yml", text: LINT_WF },
342
+ { path: ".github/workflows/suites.yml", text: SUITES_WF },
343
+ ]);
344
+ expect(edits.map((e) => [e.path, e.build, e.phases, e.changed])).toEqual([
345
+ [".github/workflows/ci.yml", "every", ["lint"], true],
346
+ [".github/workflows/suites.yml", "newest", ["suites"], true],
347
+ ]);
348
+ const ci = parseYAMLDocument(edits[0].text) as Yaml;
349
+ // The triggers of an every phase stay, pull requests included.
350
+ expect(ci.on).toEqual({ push: { branches: ["main"] }, pull_request: null });
351
+ expect(ci.concurrency).toEqual({ group: "chant-build-lint-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }}", "cancel-in-progress": "${{ github.ref != 'refs/heads/main' }}" });
352
+ expect(edits[0].text).toContain(`# Comments above a block stay.\n${BUILD_BLOCK_MARKER}\nconcurrency:`);
353
+ const suites = parseYAMLDocument(edits[1].text) as Yaml;
354
+ expect(suites.on).toEqual({ push: { branches: ["main"] }, workflow_dispatch: {} });
355
+ expect(suites.concurrency).toEqual({ group: "chant-build-suites-${{ github.ref }}", "cancel-in-progress": false });
356
+ // A file without the block gets it before permissions:, and writing again changes nothing.
357
+ expect(edits[1].text.indexOf("concurrency:")).toBeLessThan(edits[1].text.indexOf("permissions:"));
358
+ const again = buildTriggerEdits(green(), edits.map((e) => ({ path: e.path, text: e.text })));
359
+ expect(again.map((e) => e.changed)).toEqual([false, false]);
360
+ });
361
+
362
+ test("a phase without build leaves its workflow alone, and one workflow holding both builds is refused", () => {
363
+ const plain = parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: { branch: "main", phases: { lint: ["lint"] }, require: ["lint"] } } }), "chant.workspace.json").ci!.green!;
364
+ expect(buildTriggerEdits(plain, [{ path: ".github/workflows/ci.yml", text: LINT_WF }])).toEqual([]);
365
+ const both = LINT_WF.replace("jobs:\n", "jobs:\n suites:\n runs-on: ubuntu-latest\n");
366
+ expect(() => buildTriggerEdits(green(), [{ path: ".github/workflows/ci.yml", text: both }])).toThrow(/holds runs of lint \(build: every\) and suites \(build: newest\)/);
367
+ });
368
+
369
+ test("writeTopLevel replaces a block up to the next top-level line, or adds one before jobs", () => {
370
+ expect(writeTopLevel("a: 1\nconcurrency:\n group: x\n\njobs: {}\n", "concurrency", { group: "y" })).toBe(`a: 1\n${BUILD_BLOCK_MARKER}\nconcurrency:\n group: y\n\njobs: {}\n`);
371
+ expect(writeTopLevel("a: 1\njobs: {}\n", "concurrency", { group: "y" })).toBe(`a: 1\n${BUILD_BLOCK_MARKER}\nconcurrency:\n group: y\n\njobs: {}\n`);
372
+ });
373
+
374
+ test("the tick may re-run jobs and start workflows, and ticks on a build tag's runs, only when a phase sets build", () => {
375
+ const tick = parseYAMLDocument(renderCiGreenWorkflow({ green: green(), workflows: ["ci"], chantVersion: "0.124.33", workspaceRoot: "." })) as Yaml;
376
+ expect(tick.permissions).toEqual({ contents: "write", checks: "read", actions: "write" });
377
+ expect(tick.on.workflow_run.branches).toEqual(["main", "ci/build/**"]);
378
+ const plain = parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [], ci: { green: { branch: "main", phases: { lint: ["lint"] }, require: ["lint"] } } }), "chant.workspace.json").ci!.green!;
379
+ const before = parseYAMLDocument(renderCiGreenWorkflow({ green: plain, workflows: ["ci"], chantVersion: "0.124.33", workspaceRoot: "." })) as Yaml;
380
+ expect(before.permissions).toEqual({ contents: "write", checks: "read" });
381
+ expect(before.on.workflow_run.branches).toEqual(["main"]);
382
+ });
383
+
384
+ test("the workflows a build request can start are the ones holding its phases with workflow_dispatch", () => {
385
+ expect(dispatchableWorkflows(green(), ["lint", "suites"], [{ path: ".github/workflows/ci.yml", text: LINT_WF }, { path: ".github/workflows/suites.yml", text: SUITES_WF }])).toEqual({ dispatchable: ["suites.yml"], without: ["ci.yml"] });
386
+ });
387
+ });
@@ -48,8 +48,8 @@
48
48
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
49
49
  import { dirname, join, relative, resolve, sep } from "node:path";
50
50
  import { GENERATED_MARKER } from "../discovery/files";
51
- import { parseYAMLDocument } from "../yaml";
52
- import type { CiGreen } from "./declaration";
51
+ import { emitYAMLEntry, parseYAMLDocument } from "../yaml";
52
+ import type { CiBuild, CiGreen } from "./declaration";
53
53
  import { matchesCheckRun } from "./ci-green";
54
54
  import { githubSecretOrJobToken } from "../ci-jobs";
55
55
 
@@ -129,6 +129,31 @@ export function workflowsHolding(green: CiGreen, sources: readonly WorkflowSourc
129
129
  };
130
130
  }
131
131
 
132
+ /**
133
+ * The workflow files that hold a check run of one of `phases` and can be
134
+ * started by hand (`workflow_dispatch`), by file name, for a build the tick
135
+ * asks for (#4169). Files that can't be parsed are left out.
136
+ */
137
+ export function dispatchableWorkflows(green: CiGreen, phases: readonly string[], sources: readonly WorkflowSource[]): { dispatchable: string[]; without: string[] } {
138
+ const patterns = phases.flatMap((name) => green.phases.find((p) => p.name === name)?.runs ?? []);
139
+ const dispatchable: string[] = [];
140
+ const without: string[] = [];
141
+ for (const s of sources) {
142
+ let doc: unknown;
143
+ try {
144
+ doc = parseYAMLDocument(s.text);
145
+ } catch {
146
+ continue;
147
+ }
148
+ const runs = jobCheckRuns(doc);
149
+ if (!patterns.some((p) => runs.some((r) => meet(p, r)))) continue;
150
+ const on = isObject(doc) ? doc.on : undefined;
151
+ const file = s.path.slice(s.path.lastIndexOf("/") + 1);
152
+ (isObject(on) && "workflow_dispatch" in on ? dispatchable : without).push(file);
153
+ }
154
+ return { dispatchable: dispatchable.sort(), without: without.sort() };
155
+ }
156
+
132
157
  /** The repository's GitHub workflow files, other than `skip`. */
133
158
  export function readWorkflowSources(repoRoot: string, skip: string): WorkflowSource[] {
134
159
  const dir = join(repoRoot, ".github", "workflows");
@@ -213,6 +238,8 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
213
238
  " - completed",
214
239
  " branches:",
215
240
  ` - ${q(green.branch)}`,
241
+ // #4169: a build the tick asked for runs at a ci/build/<sha> tag, and its completion ticks too.
242
+ ...(newestPhases(green) ? [` - ${q("ci/build/**")}`] : []),
216
243
  " schedule:",
217
244
  " - cron: \"*/15 * * * *\"",
218
245
  " workflow_dispatch: {}",
@@ -225,6 +252,8 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
225
252
  "permissions:",
226
253
  " contents: write",
227
254
  " checks: read",
255
+ // #4169: re-running a failed job and starting a workflow at a ci/build/<sha> tag.
256
+ ...(declaresBuild(green) ? [" actions: write"] : []),
228
257
  "",
229
258
  "jobs:",
230
259
  " tick:",
@@ -254,6 +283,114 @@ export function renderCiGreenWorkflow(input: WorkflowInput): string {
254
283
  return lines.join("\n") + "\n";
255
284
  }
256
285
 
286
+ /** Whether any phase sets `build` (#4169): then the tick may re-run jobs and start workflows. */
287
+ export const declaresBuild = (green: CiGreen) => green.phases.some((p) => p.build !== undefined);
288
+ /** Whether a required phase builds only the newest commit (#4169). */
289
+ export const newestPhases = (green: CiGreen) => green.phases.some((p) => p.build === "newest" && green.require.includes(p.name));
290
+
291
+ // ── A phase's triggers (#4169) ───────────────────────────────────────────────
292
+
293
+ /** The comment line above each top-level block `ci workflow` writes into a phase's workflow. */
294
+ export const BUILD_BLOCK_MARKER = "# Written by chant ci workflow from ci.green's build (#4169); edit ci.green and run it again.";
295
+
296
+ /** What `ci workflow` writes into one workflow holding a phase that sets `build`. */
297
+ export interface BuildTriggerEdit {
298
+ /** Relative to the repository root. */
299
+ path: string;
300
+ build: CiBuild;
301
+ phases: string[];
302
+ /** The file with its triggers and concurrency written. */
303
+ text: string;
304
+ changed: boolean;
305
+ }
306
+
307
+ /**
308
+ * The workflows holding the check runs of phases that set `build`, each with
309
+ * its triggers and concurrency written for that build (#4169):
310
+ *
311
+ * - `newest`: on pushes to the branch, and by hand (`workflow_dispatch`),
312
+ * which is how the tick starts a bisect step or a requested build at a
313
+ * `ci/build/<sha>` tag. A `schedule` goes: a runner freeing up, not a
314
+ * clock, picks the commit. Other triggers stay. One concurrency group per
315
+ * ref that never cancels a run in progress, so GitHub keeps the newest
316
+ * push waiting and drops the ones between, and each tag's build runs in a
317
+ * group of its own.
318
+ * - `every`: the triggers stay, and the group is the commit itself on the
319
+ * branch, so no push cancels or replaces another's run; elsewhere, such as
320
+ * a pull request, a new push cancels the run it supersedes.
321
+ *
322
+ * Only the top-level `on:` (for `newest`) and `concurrency:` blocks change,
323
+ * each under {@link BUILD_BLOCK_MARKER}; the rest of the file is kept as it
324
+ * is. Throws when one workflow holds a phase of each build, which no one
325
+ * concurrency group can serve.
326
+ */
327
+ export function buildTriggerEdits(green: CiGreen, sources: readonly WorkflowSource[]): BuildTriggerEdit[] {
328
+ const out: BuildTriggerEdit[] = [];
329
+ for (const s of sources) {
330
+ let doc: unknown;
331
+ try {
332
+ doc = parseYAMLDocument(s.text);
333
+ } catch {
334
+ continue;
335
+ }
336
+ const runs = jobCheckRuns(doc);
337
+ const held = green.phases.filter((p) => p.build !== undefined && p.runs.some((pattern) => runs.some((r) => meet(pattern, r))));
338
+ if (held.length === 0) continue;
339
+ const builds = [...new Set(held.map((p) => p.build!))];
340
+ if (builds.length > 1) {
341
+ throw new Error(`${s.path} holds runs of ${held.map((p) => `${p.name} (build: ${p.build})`).join(" and ")}, and one workflow can't run on every commit and only the newest; move the jobs into a workflow per build`);
342
+ }
343
+ const build = builds[0];
344
+ const phases = held.map((p) => p.name);
345
+ const tag = phases.map((p) => p.replace(/[^A-Za-z0-9_-]/g, "-")).join("-");
346
+ let text = s.text;
347
+ if (build === "newest") {
348
+ const on = isObject(doc) && isObject(doc.on) ? { ...doc.on } : {};
349
+ delete on.schedule;
350
+ const push = isObject(on.push) ? { ...on.push } : {};
351
+ const branches = Array.isArray(push.branches) ? push.branches.filter((b): b is string => typeof b === "string") : [];
352
+ push.branches = branches.includes(green.branch) ? branches : [...branches, green.branch];
353
+ const { push: _push, workflow_dispatch: dispatch, ...rest } = on;
354
+ text = writeTopLevel(text, "on", { push, workflow_dispatch: dispatch ?? {}, ...rest });
355
+ text = writeTopLevel(text, "concurrency", { group: `chant-build-${tag}-\${{ github.ref }}`, "cancel-in-progress": false });
356
+ } else {
357
+ const main = `refs/heads/${green.branch}`;
358
+ // Written as lines: the emitter would quote the expression, and GitHub reads it the same either way.
359
+ text = writeTopLevel(text, "concurrency", [
360
+ "concurrency:",
361
+ ` group: chant-build-${tag}-\${{ github.ref == '${main}' && github.sha || github.ref }}`,
362
+ ` cancel-in-progress: \${{ github.ref != '${main}' }}`,
363
+ ]);
364
+ }
365
+ out.push({ path: s.path, build, phases, text, changed: text !== s.text });
366
+ }
367
+ return out;
368
+ }
369
+
370
+ /**
371
+ * Replace the top-level `key:` block of a YAML file with `value` (or with
372
+ * `value`'s lines, the key's own line first), under
373
+ * {@link BUILD_BLOCK_MARKER}, or add it before `permissions:` or `jobs:`
374
+ * when the file has none. The block runs from its key's line to the next
375
+ * line at column 0 that is not blank; comments above the key stay.
376
+ */
377
+ export function writeTopLevel(text: string, key: string, value: Record<string, unknown> | string[]): string {
378
+ const lines = text.split("\n");
379
+ const block = [BUILD_BLOCK_MARKER, ...(Array.isArray(value) ? value : emitYAMLEntry(key, value).split("\n"))];
380
+ const at = lines.findIndex((l) => l === `${key}:` || l.startsWith(`${key}: `) || l.startsWith(`${key}:#`));
381
+ if (at < 0) {
382
+ const before = lines.findIndex((l) => /^(permissions|jobs):/.test(l));
383
+ const where = before < 0 ? lines.length : before;
384
+ return [...lines.slice(0, where), ...block, "", ...lines.slice(where)].join("\n");
385
+ }
386
+ let end = at + 1;
387
+ while (end < lines.length && (lines[end] === "" || /^\s/.test(lines[end]))) end++;
388
+ // A blank line ending the block belongs to what follows it.
389
+ while (end > at + 1 && lines[end - 1] === "") end--;
390
+ const start = at > 0 && lines[at - 1] === BUILD_BLOCK_MARKER ? at - 1 : at;
391
+ return [...lines.slice(0, start), ...block, ...lines.slice(end)].join("\n");
392
+ }
393
+
257
394
  /** The header line naming the command that regenerates the file, and where to run it. */
258
395
  export function ciGreenHeader(command: string, where: "member" | "root"): string {
259
396
  return `# ${GENERATED_MARKER}. Regenerate with: ${command} (in the ${where === "member" ? "member's directory" : "workspace root"})\n`;
@@ -51,7 +51,7 @@ describe("the ci.green block (#3573)", () => {
51
51
  });
52
52
  expect(parseDeclaration(decl({ green: { ...GREEN, signedSince: "a".repeat(40) } }), "chant.workspace.json").ci!.green!.signedSince).toBe("a".repeat(40));
53
53
  expect(parseDeclaration(decl({ green: { ...GREEN, window: "90m" } }), "chant.workspace.json").ci!.green!.window).toBe("90m");
54
- expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null });
54
+ expect(parseDeclaration(decl({}), "chant.workspace.json").ci).toEqual({ green: null, reportUrl: null });
55
55
  expect(parseDeclaration(JSON.stringify({ name: "w", schema: 1, members: [] }), "chant.workspace.json").ci).toBeNull();
56
56
  });
57
57
 
@@ -117,10 +117,13 @@ describe("judging a phase from check runs (#3573)", () => {
117
117
  const runs = [run(1, "macos (test)", "skipped")];
118
118
  expect(evaluatePhase(phase(["macos (test)"]), runs).verdict).toBe("fail");
119
119
  expect(evaluatePhase(phase(["macos (test)"], "pass"), runs).verdict).toBe("pass");
120
- // Neither counts any other conclusion as a pass.
121
- for (const c of ["neutral", "cancelled", "timed_out", "action_required", "stale"]) {
120
+ // Neither counts any other conclusion as a pass, and a stopped run is no failure either (#4169).
121
+ for (const c of ["neutral", "timed_out", "action_required"]) {
122
122
  expect(evaluatePhase(phase(["x"], "pass"), [run(1, "x", c)]).verdict, c).toBe("fail");
123
123
  }
124
+ for (const c of ["cancelled", "stale"]) {
125
+ expect(evaluatePhase(phase(["x"], "pass"), [run(1, "x", c)]), c).toMatchObject({ verdict: "unbuilt", reason: "cancelled" });
126
+ }
124
127
  });
125
128
 
126
129
  test("a run in progress, or a pattern nothing matches yet, leaves the phase undecided; a failure decides it", () => {