@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
@@ -3,11 +3,15 @@
3
3
  * host. It polls the forge for the open pull requests into the target branch
4
4
  * and for the branch's head, and runs the pull-request jobs with
5
5
  * {@link ciRun} (./ci-run.ts): `plan` for each new pull request head, and
6
- * `apply` for each new commit on the target branch, measured from the head
7
- * the previous poll saw (the push's `before`).
6
+ * `record` for each new commit on the target branch (#4168), measured from
7
+ * the head the previous poll saw (the push's `before`). The record applies
8
+ * nothing; with `applyOnMerge` (`--apply-on-merge`) the commit gets `apply`
9
+ * in its place, as before #4168. After each job settles it runs `ci tick`
10
+ * from the runs the jobs recorded, when the declaration has `ci.green`.
11
+ * What the list below says of the apply holds for it under `applyOnMerge`.
8
12
  *
9
13
  * - Dedupe. A state file in the state directory keeps every job it started,
10
- * keyed by job and commit (`plan:<pr>:<sha>`, `apply:<sha>`), with how it
14
+ * keyed by job and commit (`plan:<pr>:<sha>`, `record:<sha>`, `apply:<sha>`), with how it
11
15
  * ended, and the target head the last apply was measured to. A job already
12
16
  * in it is not started again, unless it failed and `rerunFailed` asks for
13
17
  * it: the forge's "re-run failed jobs".
@@ -23,8 +27,34 @@
23
27
  * as its `--resume` record, so a re-run of a failed apply at the same
24
28
  * commit finishes under the approval it has. This replaces the forge's CI
25
29
  * cache.
30
+ * - A gate per wave (#4003). With `--wave-gate` the apply job runs
31
+ * `pr-apply --wave-gate`, which applies the waves in order and stops at
32
+ * the first whose moved plan waits on its own gate. The job fails, and the
33
+ * resume record keeps the waves that applied; once the wave's plan is
34
+ * approved, `rerunFailed` runs the apply again and it carries on from
35
+ * that wave.
26
36
  * - Capacity. At most `maxJobs` jobs run at once. A job over the limit is not
27
37
  * recorded, so a later poll starts it.
38
+ * - Fast-forwards only (#4122). An apply runs only when the target's new head
39
+ * descends from the head seen before it. Any other move, such as a force
40
+ * push back to an older commit, is recorded as seen and not applied.
41
+ * - `[skip ci]` (#4122). A target head or pull request head whose commit
42
+ * message says `[skip ci]` or `[ci skip]` is recorded as seen and not run,
43
+ * as Actions does.
44
+ * - Build requests (#4169). With `buildCommand`, each `ci/build/<sha>`
45
+ * tag the tick or `ci run --at` pushed is served once: the command runs
46
+ * in a fresh detached worktree of the commit, with `CHANT_BUILD_COMMIT`,
47
+ * `CHANT_BUILD_PHASES` (comma-separated) and `CHANT_BUILD_REASON` set,
48
+ * and is expected to run the phases' Ops (`chant run <op>`), whose
49
+ * records the tick reads. A build counts against `maxJobs`, and the state
50
+ * file keeps it under `build:<sha>`. Without `buildCommand` the tags are
51
+ * left alone.
52
+ * - The forge's ledger (#4121). Before each job, when no other job of this
53
+ * serve runs, the clone's `chant/lifecycle` follows the forge's: it is
54
+ * deleted when the forge has none, and reset to the forge's when neither
55
+ * contains the other. Each is a warning. A local branch ahead of the
56
+ * forge's keeps its facts, and the job's push sends them. Nothing here
57
+ * pushes the ledger.
28
58
  *
29
59
  * Host mode only: each job runs in a fresh worktree on this host as this
30
60
  * user, with the clean environment `ciRun` gives it. A pull request from a
@@ -36,17 +66,20 @@
36
66
  * turn a checked payload into the same triggers and schedule them.
37
67
  */
38
68
 
39
- import { execFileSync } from "node:child_process";
40
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
69
+ import { execFileSync, spawn, spawnSync } from "node:child_process";
70
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
71
+ import { tmpdir } from "node:os";
41
72
  import { join, resolve } from "node:path";
73
+ import { CI_BUILD_TAG_PREFIX, readBuildRequests, type BuildRequest } from "./ci-builds";
42
74
  import { prApplyGroup, prApplyRecordKey } from "../components/pr-pipeline";
43
75
  import { forgeFromEnv, type OpenPullRequest } from "../pr-forge";
44
76
  import type { CommandContext } from "../cli/registry";
45
77
  import { formatError, formatWarning } from "../cli/format";
46
78
  import { acquireLease, currentHolderId, releaseLease, DEFAULT_LEASE_TTL_MS, type LeaseRecord } from "../lifecycle/lease";
47
79
  import { parseDuration } from "../op/duration";
48
- import { CI_JOB_OPS, ciJobPlace, ciSecretsOf, ciRun, ciStatusFromEnv, CiRunError, type CiJobName, type CiRunRequest, type CiRunResult } from "./ci-run";
49
- import { RUNNER_KEY_ENV } from "./ci-green";
80
+ import { reportUrlOf } from "./ci-report-url";
81
+ import { CI_JOB_OPS, ciJobPlace, ciLoopOptions, ciSecretsOf, ciRun, ciStatusFromEnv, CiRunError, type CiJobName, type CiRunRequest, type CiRunResult } from "./ci-run";
82
+ import { CiGreenError, ciTick, RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green";
50
83
  import { gitTop } from "./tree";
51
84
  import { ciServeRunnerKey, formatRunnerKeySetup, setupRunnerKey } from "./ci-runner-key";
52
85
 
@@ -61,10 +94,14 @@ export interface CiServeForge {
61
94
  pullRequestFor?(sha: string): Promise<number | null>;
62
95
  }
63
96
 
64
- /** A job the forge's state calls for: the plan of a pull request's head, or the apply of a target-branch commit. */
97
+ /**
98
+ * A job the forge's state calls for: the plan of a pull request's head, or
99
+ * for a target-branch commit its record (#4168) or, with `applyOnMerge`,
100
+ * its apply.
101
+ */
65
102
  export type CiServeTrigger =
66
103
  | { job: "plan"; commit: string; base: string; pr: number; rerun?: boolean }
67
- | { job: "apply"; commit: string; base: string; rerun?: boolean };
104
+ | { job: "apply" | "record"; commit: string; base: string; rerun?: boolean };
68
105
 
69
106
  /** One job in the state file. */
70
107
  export interface CiServeJobState {
@@ -92,9 +129,13 @@ export interface CiServeState {
92
129
  env: string;
93
130
  branch: string;
94
131
  member?: string;
95
- /** The target branch's head the last apply was measured to, or the head the first poll found. */
132
+ /** The target branch's head the last apply was measured to, or the last head seen and not applied. */
96
133
  target?: string;
97
134
  jobs: Record<string, CiServeJobState>;
135
+ /** Plans seen and not run, by state key, with why (#4122). */
136
+ skipped?: Record<string, "skip-ci">;
137
+ /** The build requests served (#4169), by `build:<sha>`, with how each ended. */
138
+ builds?: Record<string, { tag: string; status: "running" | "ok" | "fail"; startedAt: string; settledAt?: string }>;
98
139
  }
99
140
 
100
141
  /** What a pass did, in order, for the log. */
@@ -105,6 +146,11 @@ export type CiServeEvent =
105
146
  | { type: "held"; job: CiJobName; commit: string; pr?: number; reason: "capacity" | "lease"; heldBy?: string }
106
147
  | { type: "started"; job: CiJobName; commit: string; pr?: number; base: string; runId: string; attempt: number; resume?: string }
107
148
  | { type: "settled"; job: CiJobName; commit: string; pr?: number; status: "ok" | "fail"; failure?: string; report?: string }
149
+ | { type: "not-run"; job: CiJobName; commit: string; pr?: number; reason: "not-fast-forward" | "skip-ci"; branch?: string; from?: string }
150
+ | { type: "ledger"; action: "deleted" | "reset"; local: string; forge?: string }
151
+ | { type: "build"; commit: string; phases: string[]; status: "started" | "ok" | "fail" | "held"; exitCode?: number }
152
+ /** `ci tick` after the pass (#4168): the tags it made from the runs. */
153
+ | { type: "tick"; made: string[] }
108
154
  | { type: "error"; message: string };
109
155
 
110
156
  /** The `ciRun` request fields `ci serve` passes through to every job: the generator's options and the runner's. */
@@ -126,6 +172,18 @@ export interface CiServeOptions {
126
172
  authors?: readonly string[];
127
173
  /** Run again the failed apply at the target's head, and the failed plans of open heads. */
128
174
  rerunFailed?: boolean;
175
+ /**
176
+ * Apply each new target-branch commit (#4168), as `ci serve` did before
177
+ * #4168. Default false: each new commit gets the record job, which applies
178
+ * nothing.
179
+ */
180
+ applyOnMerge?: boolean;
181
+ /**
182
+ * Run after each pass that settled a job (#4168): `ci tick` from the runs
183
+ * the jobs recorded. Returns the tags it made, or undefined when the
184
+ * declaration has no `ci.green`. The command passes one; a test may not.
185
+ */
186
+ tick?: () => Promise<string[] | undefined>;
129
187
  /** Passed to each job. `job.hostEnv` gets `GITHUB_REPOSITORY` set to `repo`. */
130
188
  job?: CiServeJobOptions;
131
189
  /** This process's lease identity. Default `currentHolderId()`. */
@@ -135,18 +193,26 @@ export interface CiServeOptions {
135
193
  now?: () => Date;
136
194
  /** Runs a job. Default {@link ciRun}. */
137
195
  run?: (req: CiRunRequest) => Promise<CiRunResult>;
196
+ /**
197
+ * The shell line that builds a requested commit (#4169), run in a fresh
198
+ * worktree of it. Without it `ci/build/<sha>` tags are not served.
199
+ */
200
+ buildCommand?: string;
201
+ /** Runs a build. Default: `sh -c <buildCommand>` in a detached worktree of the commit. */
202
+ build?: (input: { top: string; request: BuildRequest; command: string }) => Promise<number>;
138
203
  }
139
204
 
140
205
  /** The jobs this process runs now, by state key, shared by every pass of one loop. */
141
206
  export interface CiServeRuntime {
142
- inflight: Map<string, Promise<CiServeJobState>>;
207
+ /** Jobs and build requests (#4169) alike, by state key: they share `maxJobs`. */
208
+ inflight: Map<string, Promise<unknown>>;
143
209
  }
144
210
 
145
211
  export const newCiServeRuntime = (): CiServeRuntime => ({ inflight: new Map() });
146
212
 
147
213
  /** The state key of a job. */
148
214
  export function ciServeKey(t: { job: CiJobName; commit: string; pr?: number }): string {
149
- return t.job === "plan" ? `plan:${t.pr}:${t.commit}` : `apply:${t.commit}`;
215
+ return t.job === "plan" ? `plan:${t.pr}:${t.commit}` : `${t.job}:${t.commit}`;
150
216
  }
151
217
 
152
218
  const message = (err: unknown): string => (err instanceof Error ? err.message : String(err));
@@ -159,6 +225,78 @@ function tryGit(cwd: string, args: string[]): string | undefined {
159
225
  }
160
226
  }
161
227
 
228
+ const LEDGER = "chant/lifecycle";
229
+ /** Where the forge's ledger tip is fetched to, a ref only this module writes. */
230
+ const FORGE_LEDGER = "refs/chant/ci-serve/forge-ledger";
231
+
232
+ /** `git` with its exit status, for the calls whose answer is the status. */
233
+ function gitStatus(cwd: string, args: string[]): { code: number; out: string } {
234
+ const r = spawnSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, LC_ALL: "C" } });
235
+ return { code: r.status ?? -1, out: (r.stdout ?? "").trim() };
236
+ }
237
+
238
+ /**
239
+ * Make the clone's `chant/lifecycle` follow the forge's (#4121). The clone's
240
+ * branch outlives the forge's, so a ledger the forge deleted or rewrote would
241
+ * make the next pr-apply find the two diverged and stall.
242
+ *
243
+ * - The forge has none: the local branch and its remote-tracking ref go.
244
+ * - Neither contains the other: the local branch and its remote-tracking ref
245
+ * are reset to the forge's tip.
246
+ * - The local branch is ahead of the forge's, or behind it, or the same: it
247
+ * is left, and an apply's push sends what it holds that the forge lacks.
248
+ *
249
+ * Nothing is pushed, so a rewritten ledger never goes back to the forge. A
250
+ * forge that can't be reached leaves everything as it is; the job's own
251
+ * fetch says why.
252
+ */
253
+ export function reconcileCiServeLedger(top: string): CiServeEvent | undefined {
254
+ const local = tryGit(top, ["rev-parse", "--verify", "--quiet", `refs/heads/${LEDGER}^{commit}`]);
255
+ const listed = gitStatus(top, ["ls-remote", "--exit-code", "origin", `refs/heads/${LEDGER}`]);
256
+ if (listed.code === 2) {
257
+ // A remote-tracking ref left from a deleted branch would be the next push's lease, and refuse it.
258
+ tryGit(top, ["update-ref", "-d", `refs/remotes/origin/${LEDGER}`]);
259
+ if (!local) return undefined;
260
+ tryGit(top, ["update-ref", "-d", `refs/heads/${LEDGER}`, local]);
261
+ return { type: "ledger", action: "deleted", local };
262
+ }
263
+ if (listed.code !== 0 || !local) return undefined;
264
+ const forge = listed.out.split(/\s+/)[0];
265
+ if (!forge || forge === local) return undefined;
266
+ if (gitStatus(top, ["fetch", "--quiet", "--no-tags", "origin", `+refs/heads/${LEDGER}:${FORGE_LEDGER}`]).code !== 0) return undefined;
267
+ try {
268
+ const ahead = gitStatus(top, ["merge-base", "--is-ancestor", forge, local]).code === 0;
269
+ const behind = gitStatus(top, ["merge-base", "--is-ancestor", local, forge]).code === 0;
270
+ if (ahead || behind) return undefined;
271
+ tryGit(top, ["update-ref", `refs/heads/${LEDGER}`, forge, local]);
272
+ tryGit(top, ["update-ref", `refs/remotes/origin/${LEDGER}`, forge]);
273
+ return { type: "ledger", action: "reset", local, forge };
274
+ } finally {
275
+ tryGit(top, ["update-ref", "-d", FORGE_LEDGER]);
276
+ }
277
+ }
278
+
279
+ /** A commit message that asks for no CI run, as Actions and GitLab read it. */
280
+ const SKIP_CI = /\[(?:skip ci|ci skip)\]/i;
281
+
282
+ /** Whether `commit`'s message asks for no CI run. A commit this clone can't read is not skipped. */
283
+ function skipsCi(top: string, commit: string): boolean {
284
+ const body = tryGit(top, ["log", "-1", "--format=%B", commit]);
285
+ return body !== undefined && SKIP_CI.test(body);
286
+ }
287
+
288
+ /**
289
+ * How the target branch moved from `from` to `head` (#4122): `fast-forward`
290
+ * when `head` descends from `from`, `other` when it does not, and `unknown`
291
+ * when `head` could not be fetched. Once `head` is here its history is too,
292
+ * so a `from` this clone lacks is not in that history.
293
+ */
294
+ function targetMove(top: string, branch: string, from: string, head: string): "fast-forward" | "other" | "unknown" {
295
+ tryGit(top, ["fetch", "--quiet", "--no-tags", "origin", branch]);
296
+ if (gitStatus(top, ["cat-file", "-e", `${head}^{commit}`]).code !== 0) return "unknown";
297
+ return gitStatus(top, ["merge-base", "--is-ancestor", from, head]).code === 0 ? "fast-forward" : "other";
298
+ }
299
+
162
300
  /** Where a serve keeps its state when `--state` names nowhere: beside the run events, in the repository's common git directory. */
163
301
  export function defaultCiServeStateDir(cwd: string): string {
164
302
  const common = tryGit(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
@@ -209,34 +347,55 @@ const failedEntry = (entry: CiServeJobState | undefined, key: string, rt: CiServ
209
347
 
210
348
  /**
211
349
  * One poll of the forge, as triggers: an apply when the target's head moved
212
- * since the last apply (none on the first poll, which only notes the head),
350
+ * since the last apply by a fast-forward (none on the first poll, which only
351
+ * notes the head, and none for a head whose message says `[skip ci]`),
213
352
  * then a plan for each open pull request head not yet planned, oldest pull
214
353
  * request first. A job already in the state file is left out unless it
215
- * failed and `rerunFailed` is set. The first poll's head is written to the
216
- * state file.
354
+ * failed and `rerunFailed` is set. The first poll's head, and a head seen
355
+ * and not applied, are written to the state file.
217
356
  */
218
357
  export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime()): Promise<{ triggers: CiServeTrigger[]; events: CiServeEvent[] }> {
219
358
  const place = servePlace(opts);
220
- const state = readCiServeState(place.stateFile, { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) });
359
+ const fresh = { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) };
360
+ const state = readCiServeState(place.stateFile, fresh);
361
+ // Read again at the write: a job of this serve may have settled while the forge answered.
362
+ const update = (fn: (s: CiServeState) => void): void => {
363
+ const s = readCiServeState(place.stateFile, fresh);
364
+ fn(s);
365
+ writeState(place.stateFile, s);
366
+ };
221
367
  const triggers: CiServeTrigger[] = [];
222
368
  const events: CiServeEvent[] = [];
223
369
  const [head, prs] = await Promise.all([opts.client.branchHead(place.branch), opts.client.openPullRequests(place.branch)]);
370
+ // #4168: a new target commit is recorded, and applied only with applyOnMerge.
371
+ const onMerge: "apply" | "record" = opts.applyOnMerge ? "apply" : "record";
224
372
 
225
373
  if (head === undefined) {
226
374
  events.push({ type: "no-branch", branch: place.branch });
227
375
  } else if (state.target === undefined) {
228
376
  // What the branch held before this runner watched it was applied by whatever ran then.
229
- state.target = head;
230
- writeState(place.stateFile, state);
377
+ update((s) => {
378
+ s.target = head;
379
+ });
231
380
  events.push({ type: "baseline", commit: head });
232
381
  } else if (head !== state.target) {
233
- const entry = state.jobs[ciServeKey({ job: "apply", commit: head })];
234
- if (!entry) triggers.push({ job: "apply", commit: head, base: state.target });
235
- else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job: "apply", commit: head, base: entry.base, rerun: true });
382
+ const from = state.target;
383
+ const move = targetMove(place.top, place.branch, from, head);
384
+ const entry = state.jobs[ciServeKey({ job: onMerge, commit: head })];
385
+ if (move === "unknown") {
386
+ events.push({ type: "error", message: `${place.branch} is at ${head.slice(0, 12)} on the forge, and that commit could not be fetched; the next poll tries again` });
387
+ } else if (move === "other" || (!entry && skipsCi(place.top, head))) {
388
+ // #4122: a move that is not a fast-forward, or a head that asks for no run, is seen and not applied.
389
+ update((s) => {
390
+ s.target = head;
391
+ });
392
+ events.push({ type: "not-run", job: onMerge, commit: head, reason: move === "other" ? "not-fast-forward" : "skip-ci", ...(move === "other" ? { branch: place.branch, from } : {}) });
393
+ } else if (!entry) triggers.push({ job: onMerge, commit: head, base: from });
394
+ else if (opts.rerunFailed && failedEntry(entry, ciServeKey(entry), rt)) triggers.push({ job: onMerge, commit: head, base: entry.base, rerun: true });
236
395
  } else {
237
- const key = ciServeKey({ job: "apply", commit: head });
396
+ const key = ciServeKey({ job: onMerge, commit: head });
238
397
  const entry = state.jobs[key];
239
- if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: "apply", commit: head, base: entry.base, rerun: true });
398
+ if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: onMerge, commit: head, base: entry.base, rerun: true });
240
399
  }
241
400
 
242
401
  const authors = opts.authors?.length ? new Set(opts.authors.map((a) => a.toLowerCase())) : undefined;
@@ -252,8 +411,17 @@ export async function ciServeObserve(opts: CiServeOptions, rt: CiServeRuntime =
252
411
  const key = ciServeKey({ job: "plan", commit: pr.headSha, pr: pr.number });
253
412
  const entry = state.jobs[key];
254
413
  const base = pr.baseSha ?? head;
255
- if (!base) continue;
256
- if (!entry) triggers.push({ job: "plan", commit: pr.headSha, base, pr: pr.number });
414
+ if (!base || state.skipped?.[key]) continue;
415
+ if (!entry) {
416
+ // #4122: a head whose message asks for no run is seen and not planned.
417
+ if (gitStatus(place.top, ["cat-file", "-e", `${pr.headSha}^{commit}`]).code !== 0) tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", `refs/pull/${pr.number}/head`]);
418
+ if (skipsCi(place.top, pr.headSha)) {
419
+ update((s) => {
420
+ s.skipped = { ...s.skipped, [key]: "skip-ci" };
421
+ });
422
+ events.push({ type: "not-run", job: "plan", commit: pr.headSha, pr: pr.number, reason: "skip-ci" });
423
+ } else triggers.push({ job: "plan", commit: pr.headSha, base, pr: pr.number });
424
+ }
257
425
  else if (opts.rerunFailed && failedEntry(entry, key, rt)) triggers.push({ job: "plan", commit: pr.headSha, base: entry.base, pr: pr.number, rerun: true });
258
426
  }
259
427
  return { triggers, events };
@@ -316,6 +484,12 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
316
484
  lease = got.lease;
317
485
  }
318
486
 
487
+ // #4121: with no other job of this serve writing it, the clone's ledger follows the forge's.
488
+ if (rt.inflight.size === 0) {
489
+ const ledger = reconcileCiServeLedger(place.top);
490
+ if (ledger) emit(ledger);
491
+ }
492
+
319
493
  const previous = readCiServeState(place.stateFile, fresh).jobs[key];
320
494
  const attempt = (previous?.attempts ?? 0) + 1;
321
495
  const runId = `chant-serve-${now().getTime()}-${t.job}-${t.commit.slice(0, 8)}`;
@@ -324,7 +498,7 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
324
498
  const entry: CiServeJobState = { job: t.job, commit: t.commit, base: t.base, ...pr, status: "running", runId, attempts: attempt, startedAt: now().toISOString() };
325
499
  update((s) => {
326
500
  s.jobs[key] = entry;
327
- if (t.job === "apply" && !t.rerun) s.target = t.commit;
501
+ if (t.job !== "plan" && !t.rerun) s.target = t.commit;
328
502
  });
329
503
  emit({ type: "started", job: t.job, commit: t.commit, ...pr, base: t.base, runId, attempt, ...(resume && existsSync(resume) ? { resume } : {}) });
330
504
 
@@ -339,7 +513,7 @@ export async function ciServeSchedule(opts: CiServeOptions, triggers: readonly C
339
513
  emit({ type: "error", message: `${key} settled, and the state file could not be written: ${message(err)}` });
340
514
  }
341
515
  emit({ type: "settled", job: t.job, commit: t.commit, ...pr, status: settled.status === "ok" ? "ok" : "fail", ...(settled.failure ? { failure: settled.failure } : {}), ...(settled.report ? { report: settled.report } : {}) });
342
- return settled;
516
+ return serveTick(opts).then(() => settled);
343
517
  });
344
518
  rt.inflight.set(key, job);
345
519
  started.push(job);
@@ -367,7 +541,7 @@ async function runJob(
367
541
  tryGit(place.top, ["fetch", "--quiet", "--no-tags", "origin", t.job === "plan" ? `refs/pull/${t.pr}/head` : place.branch]);
368
542
  const hostEnv = { ...(opts.job?.hostEnv ?? process.env), GITHUB_REPOSITORY: opts.repo };
369
543
  // An apply is for a merge: pass the pull request whose merge commit it is, so the commit status link and the run index use `<pr>/<commit>`. None found, or a forge that errs, is today's behaviour.
370
- if (t.job === "apply" && opts.client.pullRequestFor) merged = (await opts.client.pullRequestFor(t.commit).catch(() => null)) ?? undefined;
544
+ if (t.job !== "plan" && opts.client.pullRequestFor) merged = (await opts.client.pullRequestFor(t.commit).catch(() => null)) ?? undefined;
371
545
  const run = opts.run ?? ciRun;
372
546
  const r = await run({
373
547
  ...opts.job,
@@ -393,13 +567,101 @@ async function runJob(
393
567
  }
394
568
  }
395
569
 
396
- /** One pass: observe, schedule, and with `wait` wait for the jobs it started. */
570
+ /** One pass: observe, schedule, serve build requests, and with `wait` wait for the jobs it started. */
397
571
  export async function ciServePass(opts: CiServeOptions, rt: CiServeRuntime = newCiServeRuntime(), wait = true): Promise<{ events: CiServeEvent[]; jobs: CiServeJobState[] }> {
398
572
  const observed = await ciServeObserve(opts, rt);
399
573
  for (const e of observed.events) opts.log?.(e);
400
574
  const scheduled = await ciServeSchedule(opts, observed.triggers, rt);
575
+ const builds = opts.buildCommand ? ciServeBuilds(opts, rt) : { started: [], events: [] };
401
576
  const jobs = wait ? await Promise.all(scheduled.started) : [];
402
- return { events: [...observed.events, ...scheduled.events], jobs };
577
+ if (wait) await Promise.all(builds.started);
578
+ return { events: [...observed.events, ...scheduled.events, ...builds.events], jobs };
579
+ }
580
+
581
+ /**
582
+ * Serve the build requests (#4169): fetch the `ci/build/*` tags, and start
583
+ * `buildCommand` for each one not in the state file yet, oldest first, while
584
+ * fewer than `maxJobs` jobs run. A build that fails is recorded and not run
585
+ * again; the tick decides what its phases say.
586
+ */
587
+ export function ciServeBuilds(opts: CiServeOptions, rt: CiServeRuntime): { started: Promise<void>[]; events: CiServeEvent[] } {
588
+ const place = servePlace(opts);
589
+ const fresh = { forge: opts.forge, repo: opts.repo, env: opts.env, branch: place.branch, ...(place.member ? { member: place.member } : {}) };
590
+ const update = (fn: (s: CiServeState) => void): void => {
591
+ const st = readCiServeState(place.stateFile, fresh);
592
+ fn(st);
593
+ writeState(place.stateFile, st);
594
+ };
595
+ const events: CiServeEvent[] = [];
596
+ const started: Promise<void>[] = [];
597
+ tryGit(place.top, ["fetch", "--quiet", "--prune", "origin", `+refs/tags/${CI_BUILD_TAG_PREFIX}*:refs/tags/${CI_BUILD_TAG_PREFIX}*`]);
598
+ const served = readCiServeState(place.stateFile, fresh).builds ?? {};
599
+ const max = opts.maxJobs ?? 1;
600
+ const now = opts.now ?? (() => new Date());
601
+ const requests = [...readBuildRequests(place.top).values()].sort((a, b) => a.at.localeCompare(b.at));
602
+ for (const request of requests) {
603
+ const key = `build:${request.commit}`;
604
+ if (served[key]?.tag === request.tag || rt.inflight.has(key)) continue;
605
+ const log = (e: CiServeEvent) => {
606
+ events.push(e);
607
+ opts.log?.(e);
608
+ };
609
+ if (rt.inflight.size >= max) {
610
+ log({ type: "build", commit: request.commit, phases: request.phases, status: "held" });
611
+ continue;
612
+ }
613
+ update((st) => {
614
+ st.builds = { ...st.builds, [key]: { tag: request.tag, status: "running", startedAt: now().toISOString() } };
615
+ });
616
+ log({ type: "build", commit: request.commit, phases: request.phases, status: "started" });
617
+ const build = opts.build ?? runBuild;
618
+ const job = build({ top: place.top, request, command: opts.buildCommand! })
619
+ .catch(() => 1)
620
+ .then((code) => {
621
+ update((st) => {
622
+ st.builds = { ...st.builds, [key]: { tag: request.tag, status: code === 0 ? "ok" : "fail", startedAt: st.builds?.[key]?.startedAt ?? now().toISOString(), settledAt: now().toISOString() } };
623
+ });
624
+ opts.log?.({ type: "build", commit: request.commit, phases: request.phases, status: code === 0 ? "ok" : "fail", exitCode: code });
625
+ });
626
+ const tracked = job.finally(() => rt.inflight.delete(key));
627
+ rt.inflight.set(key, tracked);
628
+ started.push(tracked);
629
+ }
630
+ return { started, events };
631
+ }
632
+
633
+ /** `sh -c <command>` in a fresh detached worktree of the request's commit, removed after. Resolves to the exit code. */
634
+ async function runBuild(input: { top: string; request: BuildRequest; command: string }): Promise<number> {
635
+ const dir = mkdtempSync(join(tmpdir(), "chant-ci-build-"));
636
+ try {
637
+ execFileSync("git", ["worktree", "add", "--quiet", "--detach", dir, input.request.commit], { cwd: input.top, stdio: ["ignore", "pipe", "pipe"] });
638
+ return await new Promise<number>((done) => {
639
+ const child = spawn("sh", ["-c", input.command], {
640
+ cwd: dir,
641
+ stdio: ["ignore", "inherit", "inherit"],
642
+ env: { ...process.env, CHANT_BUILD_COMMIT: input.request.commit, CHANT_BUILD_PHASES: input.request.phases.join(","), CHANT_BUILD_REASON: input.request.reason },
643
+ });
644
+ child.on("error", () => done(127));
645
+ child.on("close", (code) => done(code ?? 1));
646
+ });
647
+ } finally {
648
+ tryGit(input.top, ["worktree", "remove", "--force", dir]);
649
+ rmSync(dir, { recursive: true, force: true });
650
+ }
651
+ }
652
+
653
+ /** `ci tick` after a job settled (#4168), logged; a tick that fails is an error event and never fails the job. */
654
+ async function serveTick(opts: CiServeOptions): Promise<CiServeEvent | undefined> {
655
+ if (!opts.tick) return undefined;
656
+ let event: CiServeEvent | undefined;
657
+ try {
658
+ const made = await opts.tick();
659
+ event = made === undefined ? undefined : { type: "tick", made };
660
+ } catch (err) {
661
+ event = { type: "error", message: `ci tick after a job settled: ${message(err)}` };
662
+ }
663
+ if (event) opts.log?.(event);
664
+ return event;
403
665
  }
404
666
 
405
667
  /**
@@ -442,6 +704,21 @@ export function formatCiServeEvent(e: CiServeEvent): string {
442
704
  return `${at(e.job, e.commit, e.pr)} started, run ${e.runId}, attempt ${e.attempt}${e.resume ? ", resuming" : ""}`;
443
705
  case "settled":
444
706
  return `${at(e.job, e.commit, e.pr)} ${e.status === "ok" ? "passed" : `failed${e.failure ? `: ${e.failure}` : ""}`}${e.report ? `; report ${e.report}` : ""}`;
707
+ case "not-run":
708
+ return e.reason === "skip-ci"
709
+ ? `${at(e.job, e.commit, e.pr)} is not run: its commit message says [skip ci]`
710
+ : `${e.branch ?? "the target branch"} moved from ${e.from?.slice(0, 12) ?? "a commit"} to ${e.commit.slice(0, 12)}, which is not a fast-forward, so ${e.commit.slice(0, 12)} is recorded as seen and not applied`;
711
+ case "ledger":
712
+ return e.action === "deleted"
713
+ ? `the forge has no ${LEDGER}, so this clone's, at ${e.local.slice(0, 12)}, was deleted`
714
+ : `the forge's ${LEDGER} at ${e.forge!.slice(0, 12)} does not contain this clone's at ${e.local.slice(0, 12)}, so this clone's was reset to the forge's and not pushed`;
715
+ case "build": {
716
+ const what = `the build of ${e.phases.join(", ")} at ${e.commit.slice(0, 12)}`;
717
+ if (e.status === "held") return `${what} waits: --max-jobs jobs run`;
718
+ return e.status === "started" ? `${what} started` : e.status === "ok" ? `${what} passed` : `${what} failed${e.exitCode !== undefined ? ` with exit code ${e.exitCode}` : ""}`;
719
+ }
720
+ case "tick":
721
+ return e.made.length > 0 ? `ci tick made ${e.made.join(", ")}` : "ci tick: no tag due";
445
722
  case "error":
446
723
  return `error: ${e.message}`;
447
724
  }
@@ -478,7 +755,7 @@ export function runnerKeySetupCommand(ctx: CommandContext): number {
478
755
  const USAGE =
479
756
  "chant ci serve --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>] | " +
480
757
  "chant ci serve --forge forgejo|github --repo <owner/repo> --env <env> [--interval <s>] [--max-jobs <n>] [--state <dir>] " +
481
- "[--author <login>]... [--once] [--rerun-failed] [--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--no-status] [--status-url <url template>] " +
758
+ "[--author <login>]... [--once] [--rerun-failed] [--build-command <line>] [--gate <name>] [--wave-gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--no-status] [--status-url <url template>] " +
482
759
  "[--image <image>] [--before-script <line>]... [--pr-build] [--pr-build-command <line>] [--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [--unsigned]";
483
760
 
484
761
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -501,6 +778,8 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
501
778
  if (!args.ciRepo) throw new CiRunError("--repo names the repository whose pull requests ci serve polls: --repo <owner/repo>");
502
779
  if (!args.env) throw new CiRunError("--env names the environment the pipeline is for: --env <env>");
503
780
  for (const name of args.passEnv ?? []) if (!ENV_NAME.test(name)) throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
781
+ // #4168: a gate per wave gates the apply on merge, which a merge runs only when opted in.
782
+ if (args.waveGate && !args.applyOnMerge) throw new CiRunError("--wave-gate gates the apply on merge, so it needs --apply-on-merge (#4168); without it a merge gets the record job, which applies nothing");
504
783
  const intervalMs = ciServeInterval(args.interval);
505
784
  const cwd = process.cwd();
506
785
  const top = gitTop(cwd);
@@ -508,17 +787,33 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
508
787
  const stateDir = args.statePath ? resolve(args.statePath) : defaultCiServeStateDir(cwd);
509
788
  const branch = args.branch ?? "main";
510
789
  // #4099: the host's key, checked against .chant/trust.json at the target's head before the first poll.
511
- const keyed = ciServeRunnerKey({ top, stateDir, branch, ...(args.key !== undefined ? { keyFile: resolve(args.key) } : {}), envKey: process.env[RUNNER_KEY_ENV] || undefined, unsigned: args.unsigned });
790
+ // #4156: signing is opt-in. Without ci.green.signedSince no key is quiet, and an untrusted one warns once and serves unsigned.
791
+ const keyed = ciServeRunnerKey({
792
+ top,
793
+ stateDir,
794
+ branch,
795
+ ...(args.key !== undefined ? { keyFile: resolve(args.key) } : {}),
796
+ envKey: process.env[RUNNER_KEY_ENV] || undefined,
797
+ unsigned: args.unsigned,
798
+ required: signedRunsRequired(cwd),
799
+ });
512
800
  const fromFlag = keyed.fromFlag;
513
801
  const runnerKey = keyed.pem;
514
- console.error(`ci serve: ${runnerKey === undefined ? "runs are not signed (--unsigned)" : `signing runs with the runner key from ${keyed.source}`}`);
802
+ if (keyed.warning) console.error(formatWarning({ message: keyed.warning }));
803
+ if (runnerKey !== undefined) console.error(`ci serve: signing runs with the runner key from ${keyed.source}`);
804
+ else if (keyed.note && args.verbose) console.error(`ci serve: ${keyed.note}`);
515
805
  // #4096: read once, so a loose or broken secrets file stops the serve before its first poll.
516
806
  const secrets = ciSecretsOf(args.ciSecrets);
807
+ // #4160: without --report-url, the declaration's ci.reportUrl.
808
+ const reportUrl = await reportUrlOf(args.reportUrl, cwd);
809
+ if (reportUrl && !args.reportUrl) console.error(`ci serve: reports and the run index go to ${reportUrl}, the declaration's ci.reportUrl`);
517
810
  const hostEnv = { ...process.env, GITHUB_REPOSITORY: args.ciRepo };
518
811
  const client = forgeFromEnv(forge, hostEnv);
519
812
  const { status, warning } = ciStatusFromEnv({ forge, hostEnv, noStatus: args.noStatus, statusUrl: args.statusUrl });
520
813
  if (warning) console.error(formatWarning({ message: warning }));
521
814
  if (!client.openPullRequests || !client.branchHead) throw new CiRunError(`the ${forge} client cannot list pull requests`);
815
+ // #4168: a host that applied each merge before the upgrade hears that it no longer does.
816
+ if (!args.applyOnMerge) console.error("ci serve: each new commit on the target branch gets the record job, which applies nothing (#4168); --apply-on-merge applies it");
522
817
  const opts: CiServeOptions = {
523
818
  cwd,
524
819
  forge,
@@ -529,15 +824,19 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
529
824
  ...(args.maxJobs ? { maxJobs: args.maxJobs } : {}),
530
825
  ...(args.ciAuthors?.length ? { authors: args.ciAuthors } : {}),
531
826
  ...(args.rerunFailed ? { rerunFailed: true } : {}),
827
+ ...(args.ciBuildCommand ? { buildCommand: args.ciBuildCommand } : {}),
828
+ ...(args.applyOnMerge ? { applyOnMerge: true } : {}),
829
+ // #4168: the tick from the runs the jobs recorded, signed with the runs' key. No ci.green, no tick.
830
+ tick: async () => {
831
+ try {
832
+ return (await ciTick({ cwd, source: "runs", ...(runnerKey !== undefined ? { runnerKey } : {}) })).made;
833
+ } catch (err) {
834
+ if (err instanceof CiGreenError && err.code === "ci-green-undeclared") return undefined;
835
+ throw err;
836
+ }
837
+ },
532
838
  job: {
533
- loop: {
534
- ...(args.gate ? { gate: args.gate } : {}),
535
- ...(args.branch ? { branch: args.branch } : {}),
536
- ...(args.eventsJson ? { events: true } : {}),
537
- ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
538
- ...(args.prBuild ? { build: true } : {}),
539
- ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
540
- },
839
+ loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
541
840
  ...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
542
841
  ...(args.image ? { image: args.image } : {}),
543
842
  ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
@@ -550,7 +849,7 @@ export async function runCiServe(ctx: CommandContext): Promise<number> {
550
849
  hostEnv,
551
850
  stepOutput: (line) => void process.stderr.write(`${line}\n`),
552
851
  },
553
- log: (e) => console.error(e.type === "error" ? formatWarning({ message: formatCiServeEvent(e) }) : `ci serve: ${formatCiServeEvent(e)}`),
852
+ log: (e) => console.error(e.type === "error" || e.type === "ledger" ? formatWarning({ message: formatCiServeEvent(e) }) : `ci serve: ${formatCiServeEvent(e)}`),
554
853
  };
555
854
  if (args.once) {
556
855
  const pass = await ciServePass(opts);