@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
@@ -2,9 +2,10 @@
2
2
  * #4099: the runner key on a `chant ci serve` or `chant ci run` host. The
3
3
  * key is made in the state directory with mode 0600, `--setup-key --trust`
4
4
  * proposes its runner entry on a branch through `trust runner add` (#3510)
5
- * and never writes the target branch, and `ci serve` refuses to start when
6
- * `.chant/trust.json` at the target's head does not list its key, unless
7
- * `--unsigned`.
5
+ * and never writes the target branch. Signing is opt-in (#4156): `ci serve`
6
+ * starts with no key and says nothing about it, and serves unsigned with one
7
+ * warning when `.chant/trust.json` at the target's head does not list its
8
+ * key. With `ci.green.signedSince` it refuses to start in either case.
8
9
  *
9
10
  * Every test works in its own temp repository with a bare origin and its own
10
11
  * state directory. The jobs' `chant` is a stub script, and no job runs: the
@@ -126,9 +127,12 @@ describe("the host's runner key (#4099)", () => {
126
127
  expect(trust.roles.runner).toEqual(["chant-runner:demo"]);
127
128
  expect(git(origin, "rev-parse", `${branch}^`)).toBe(main);
128
129
 
129
- // Until it merges, ci serve refuses the key; once it has, the key signs as the principal.
130
+ // Until it merges, ci serve serves unsigned with a warning (refuses with the opt-in); once it has, the key signs as the principal.
130
131
  const key = hostRunnerKey(state)!;
131
- expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main" })).toThrow(/not trusted at main/);
132
+ const pending = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
133
+ expect(pending.pem).toBeUndefined();
134
+ expect(pending.warning).toMatch(/not trusted at main/);
135
+ expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", required: main })).toThrow(/not trusted at main/);
132
136
  git(root, "push", "-q", "origin", `refs/remotes/origin/${branch}:refs/heads/main`);
133
137
  expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main" })).toMatchObject({ pem: key.pem, source: join(state, RUNNER_KEY_FILE) });
134
138
  expect(ciTagSigner(root, git(root, "rev-parse", "refs/remotes/origin/main"), key.pem).principal).toBe("chant-runner:demo");
@@ -138,29 +142,63 @@ describe("the host's runner key (#4099)", () => {
138
142
  expect(listed.err).toContain("lists it for chant-runner:demo, with the runner role");
139
143
  });
140
144
 
141
- test("ci serve refuses to start with a key trust.json does not list at the target's head, and --unsigned starts without one", async () => {
145
+ test("ci serve starts with no key and says nothing, and serves unsigned with one warning for a key trust.json does not list (#4156)", async () => {
142
146
  const { root } = workspace();
143
147
  const state = scratchDir("chant-runner-key-state-");
144
148
 
149
+ // No key: nothing about keys is printed. The serve goes on to the forge, which this test has no token for.
145
150
  const none = await cli(root, SERVE(state));
146
- expect(none.code).toBe(1);
147
- expect(none.err).toContain("there is no runner key");
148
-
151
+ expect(none.err.split("\n")).toHaveLength(2);
152
+ expect(none.err.split("\n")[0]).toContain("chant ci serve: --forge forgejo needs GITHUB_TOKEN");
153
+ const keyless = ciServeRunnerKey({ top: root, stateDir: state, branch: "main" });
154
+ expect(keyless.pem).toBeUndefined();
155
+ expect(keyless.warning).toBeUndefined();
156
+ expect(keyless.note).toContain("not signed");
157
+ // --verbose prints that one line.
158
+ const verbose = await cli(root, [...SERVE(state), "--verbose"]);
159
+ expect(verbose.err).toContain("ci serve: runs are not signed: there is no runner key");
160
+
161
+ // A key the head does not list: one warning at startup, and no key goes to the jobs.
149
162
  ensureHostRunnerKey(state);
150
163
  const unlisted = await cli(root, SERVE(state));
151
- expect(unlisted.code).toBe(1);
152
- expect(unlisted.err).toMatch(/not trusted at main [0-9a-f]{12}/);
164
+ expect(unlisted.err.match(/not trusted at main [0-9a-f]{12}/g)).toHaveLength(1);
165
+ expect(unlisted.err).toContain("Runs are not signed until it is");
153
166
  expect(unlisted.err).toContain("chant ci serve --setup-key --trust");
154
- expect(unlisted.err).toContain("chant workspace trust runner add");
167
+ expect(unlisted.err).not.toContain("signing runs with");
155
168
 
156
- // A key listed only in the working tree, not at the head, is still refused.
169
+ // A key listed only in the working tree, not at the head, is not trusted either.
157
170
  const other = generateKeyPairSync("ed25519").privateKey.export({ type: "pkcs8", format: "pem" }).toString();
158
171
  writeFileSync(join(state, "other.pem"), other, { mode: 0o600 });
159
172
  expect(trustWrite({ cwd: root, action: "runner add", name: "ci-bot", key: join(state, "other.pem") })).toMatchObject({ changed: true });
160
- const flagged = await cli(root, [...SERVE(state), "--key", join(state, "other.pem")]);
161
- expect(flagged.code).toBe(1);
162
- expect(flagged.err).toContain("lists no runner with this key");
173
+ const flagged = ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "other.pem") });
174
+ expect(flagged.pem).toBeUndefined();
175
+ expect(flagged.warning).toContain("lists no runner with this key");
176
+
177
+ // A key file that is not a key is an error either way.
178
+ writeFileSync(join(state, "bad.pem"), "not a key", { mode: 0o600 });
179
+ expect(() => ciServeRunnerKey({ top: root, stateDir: state, branch: "main", keyFile: join(state, "bad.pem") })).toThrow(/not an Ed25519 private key in PEM/);
180
+
181
+ // --unsigned is accepted and serves unsigned.
182
+ expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main", unsigned: true })).toMatchObject({ fromFlag: false, source: "none (--unsigned)" });
183
+ });
184
+
185
+ test("with ci.green.signedSince, ci serve refuses to start without a trusted key, --unsigned or not (#4156)", async () => {
186
+ const { root, main } = workspace();
187
+ const state = scratchDir("chant-runner-key-state-");
188
+ const green = { branch: "main", phases: { plan: ["pr-plan"] }, require: ["plan"], source: "runs", signedSince: main };
189
+ writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green } }) });
163
190
 
164
- expect(ciServeRunnerKey({ top: root, stateDir: state, branch: "main", unsigned: true })).toEqual({ fromFlag: false, source: "none (--unsigned)" });
191
+ const none = await cli(root, SERVE(state));
192
+ expect(none.code).toBe(1);
193
+ expect(none.err).toContain(`ci.green.signedSince is ${main}, so every run must be signed, and there is no runner key`);
194
+ const unsigned = await cli(root, [...SERVE(state), "--unsigned"]);
195
+ expect(unsigned.code).toBe(1);
196
+ expect(unsigned.err).toContain("there is no runner key");
197
+
198
+ ensureHostRunnerKey(state);
199
+ const unlisted = await cli(root, SERVE(state));
200
+ expect(unlisted.code).toBe(1);
201
+ expect(unlisted.err).toMatch(/not trusted at main [0-9a-f]{12}/);
202
+ expect(unlisted.err).toContain("chant workspace trust runner add");
165
203
  });
166
204
  });
@@ -217,17 +217,38 @@ export function formatRunnerKeySetup(s: RunnerKeySetup, branch: string): string
217
217
  /**
218
218
  * The key `ci serve` signs with, checked before its first poll (#4099): the
219
219
  * `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
220
- * directory. Throws, with the command that lists it, when there is none or
221
- * `.chant/trust.json` at the target's head does not list it with the runner
222
- * role. With `unsigned` there is no key and nothing is checked.
220
+ * directory. Signing is opt-in (#4156):
221
+ *
222
+ * - With no key, `ci serve` serves unsigned and says so only in `note`.
223
+ * - A key `.chant/trust.json` at the target's head does not list with the
224
+ * runner role, or a target branch it can't find to check against, leaves
225
+ * it serving unsigned, with one `warning`.
226
+ * - With `required` (the declaration's `ci.green.signedSince`) each of
227
+ * those refuses to start, as before #4156.
228
+ *
229
+ * A key that is not an Ed25519 private key in PEM, or a `--key` file that
230
+ * can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
231
+ * key without `required` and is ignored with it.
223
232
  */
224
- export function ciServeRunnerKey(req: { top: string; stateDir: string; branch: string; keyFile?: string; envKey?: string; unsigned?: boolean }): { pem?: string; fromFlag: boolean; source: string } {
225
- if (req.unsigned) return { fromFlag: false, source: "none (--unsigned)" };
233
+ export function ciServeRunnerKey(req: {
234
+ top: string;
235
+ stateDir: string;
236
+ branch: string;
237
+ keyFile?: string;
238
+ envKey?: string;
239
+ unsigned?: boolean;
240
+ /** The declaration's `ci.green.signedSince`, when it sets one: every run must then be signed. */
241
+ required?: string | null;
242
+ }): { pem?: string; fromFlag: boolean; source: string; warning?: string; note?: string } {
243
+ const required = req.required ?? null;
244
+ const optIn = `ci.green.signedSince is ${required}, so every run must be signed`;
245
+ if (req.unsigned && !required) return { fromFlag: false, source: "none (--unsigned)", note: "runs are not signed (--unsigned)" };
226
246
  const host = req.keyFile === undefined && !req.envKey ? hostRunnerKey(req.stateDir) : undefined;
227
247
  const pem = req.keyFile !== undefined ? readFileSync(req.keyFile, "utf-8") : req.envKey || host?.pem;
228
248
  const source = req.keyFile !== undefined ? req.keyFile : req.envKey ? "CHANT_RUNNER_KEY" : (host?.path ?? join(req.stateDir, RUNNER_KEY_FILE));
229
249
  if (pem === undefined) {
230
- throw new CiRunError(`there is no runner key: no --key, no CHANT_RUNNER_KEY and no ${source}. Make one with chant ci serve --setup-key, list it with --trust, or start with --unsigned`);
250
+ if (!required) return { fromFlag: false, source: "none", note: "runs are not signed: there is no runner key. To verify who ran a job, add one with chant ci serve --setup-key --trust" };
251
+ throw new CiRunError(`${optIn}, and there is no runner key: no --key, no CHANT_RUNNER_KEY and no ${source}. Make one with chant ci serve --setup-key and list it with --trust`);
231
252
  }
232
253
  let publicKey: string;
233
254
  try {
@@ -236,13 +257,17 @@ export function ciServeRunnerKey(req: { top: string; stateDir: string; branch: s
236
257
  throw new CiRunError(`the runner key from ${source} is not an Ed25519 private key in PEM: ${message(err)}`);
237
258
  }
238
259
  const head = targetHead(req.top, req.branch);
239
- if (!head) throw new CiRunError(`there is no branch ${req.branch} here or on origin, so the runner key can't be checked against its .chant/trust.json; fetch it, or start with --unsigned`);
260
+ if (!head) {
261
+ const why = `there is no branch ${req.branch} here or on origin, so the runner key can't be checked against its .chant/trust.json`;
262
+ if (required) throw new CiRunError(`${optIn}, and ${why}; fetch it`);
263
+ return { fromFlag: false, source, warning: `${why}, so runs are not signed; fetch it to sign them` };
264
+ }
240
265
  const listed = runnerKeyListed(req.top, head.commit, pem);
241
266
  if (!listed.ok) {
242
- throw new CiRunError(
243
- `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}\n` +
244
- ` List it on a branch for review with chant ci serve --setup-key --trust, or run ${trustCommand(defaultRunnerPrincipal(), publicKey)} in a checkout of ${req.branch} and merge the change. Start with --unsigned to serve without signing.`,
245
- );
267
+ const why = `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}`;
268
+ const fix = `List it on a branch for review with chant ci serve --setup-key --trust, or run ${trustCommand(defaultRunnerPrincipal(), publicKey)} in a checkout of ${req.branch} and merge the change.`;
269
+ if (required) throw new CiRunError(`${optIn}, and ${why}\n ${fix}`);
270
+ return { fromFlag: false, source, warning: `${why}. Runs are not signed until it is. ${fix}` };
246
271
  }
247
272
  return { pem, fromFlag: req.keyFile !== undefined, source };
248
273
  }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * `chant ci runner-start` and `chant ci run --require-runner-start` (#4147):
3
+ * the statement checked against `.chant/trust.json` at base, and the
4
+ * readiness the isolated-runners security group reads (#4168).
5
+ */
6
+
7
+ import { createPublicKey, generateKeyPairSync } from "node:crypto";
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { afterAll, describe, expect, test, vi } from "vitest";
11
+ import { parseArgs } from "../cli/parse-args";
12
+ import type { CommandContext } from "../cli/registry";
13
+ import { signRunnerStart, type CiJobIdentity } from "../runner-start";
14
+ import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
15
+ import { checkRunnerStart, parseJobSpec, requireRunnerStart, runCiRunnerStart, runnerStartReadiness, runnerStartReadinessAt } from "./ci-runner-start";
16
+ import { ciRun, CiRunError } from "./ci-run";
17
+ import { sshFromEd25519 } from "./trust/dsse";
18
+ import { emptyPolicy } from "./trust/policy";
19
+
20
+ afterAll(cleanScratch);
21
+
22
+ function key(principal: string): { principal: string; pem: string; ssh: string; signer: Parameters<typeof signRunnerStart>[1] } {
23
+ const { privateKey } = generateKeyPairSync("ed25519");
24
+ const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
25
+ const ssh = sshFromEd25519(Buffer.from(jwk.x, "base64url"));
26
+ return { principal, pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh, signer: { principal, key: privateKey, publicKey: ssh } };
27
+ }
28
+
29
+ const PROVISIONER = key("provisioner");
30
+ const LATER = key("late-provisioner");
31
+
32
+ /** A repository whose first commit lists PROVISIONER as a runner, and whose second adds LATER. */
33
+ function repo(): { root: string; base: string; head: string } {
34
+ const root = scratchDir("chant-runner-start-");
35
+ git(root, "init", "-q", "-b", "main");
36
+ git(root, "config", "user.name", "t");
37
+ git(root, "config", "user.email", "t@example.com");
38
+ git(root, "config", "commit.gpgsign", "false");
39
+ const trust = (runners: Array<{ principal: string; ssh: string }>) =>
40
+ JSON.stringify({ schema: 1, runners: runners.map((r) => ({ principal: r.principal, class: "runner", key: r.ssh })), roles: { runner: runners.map((r) => r.principal) } });
41
+ writeFiles(root, { ".chant/trust.json": trust([PROVISIONER]) });
42
+ git(root, "add", "-A");
43
+ git(root, "commit", "-q", "-m", "trust");
44
+ const base = git(root, "rev-parse", "HEAD");
45
+ writeFiles(root, { ".chant/trust.json": trust([PROVISIONER, LATER]) });
46
+ git(root, "add", "-A");
47
+ git(root, "commit", "-q", "-m", "a pull request adds its own provisioner");
48
+ return { root, base, head: git(root, "rev-parse", "HEAD") };
49
+ }
50
+
51
+ const R = repo();
52
+
53
+ const job = (commit: string): CiJobIdentity => ({ system: "github", repo: "acme/infra", run: "4242", attempt: "1", job: "apply", commit, runner: "jit-1" });
54
+ const env = (commit: string, statement?: unknown): Record<string, string> => ({
55
+ GITHUB_ACTIONS: "true",
56
+ GITHUB_REPOSITORY: "acme/infra",
57
+ GITHUB_RUN_ID: "4242",
58
+ GITHUB_RUN_ATTEMPT: "1",
59
+ GITHUB_JOB: "apply",
60
+ GITHUB_SHA: commit,
61
+ RUNNER_NAME: "jit-1",
62
+ CHANT_RUNNER_START_FILE: "/nonexistent/runner-start.json",
63
+ ...(statement ? { CHANT_RUNNER_START: JSON.stringify(statement) } : {}),
64
+ });
65
+
66
+ describe("checkRunnerStart reads the runner keys at base", () => {
67
+ test("a statement by a provisioner listed at base verifies", () => {
68
+ const v = checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) });
69
+ expect(v).toMatchObject({ status: "verified", signer: "provisioner" });
70
+ });
71
+
72
+ test("a provisioner the change itself lists does not count at the base before it", () => {
73
+ const statement = signRunnerStart(job(R.head), LATER.signer);
74
+ expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement) }).status).toBe("untrusted");
75
+ expect(checkRunnerStart({ top: R.root, base: R.head, env: env(R.head, statement) }).status).toBe("verified");
76
+ });
77
+
78
+ test("the job's commit stands in for the forge's variable, so a statement for another commit is refused", () => {
79
+ const statement = signRunnerStart(job(R.head), PROVISIONER.signer);
80
+ expect(checkRunnerStart({ top: R.root, base: R.base, env: env(R.head, statement), commit: R.base }).status).toBe("mismatch");
81
+ });
82
+
83
+ test("requireRunnerStart throws with the reason, and a missing statement is named", () => {
84
+ expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head) })).toThrow(/--require-runner-start: missing: .*has no runner-start statement/);
85
+ expect(() => requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: {} })).toThrow(/needs a forge job to check/);
86
+ expect(requireRunnerStart({ top: R.root, base: R.base, commit: R.head, env: env(R.head, signRunnerStart(job(R.head), PROVISIONER.signer)) }).signer).toBe("provisioner");
87
+ });
88
+ });
89
+
90
+ describe("runnerStartReadiness, for the isolated-runners group", () => {
91
+ test("a policy with a runner key whose principal holds the runner role is configured", () => {
92
+ expect(runnerStartReadinessAt(R.root, R.base)).toEqual({ configured: true, gaps: [] });
93
+ });
94
+
95
+ test("no runner key, a key with no runner role, and an unreadable policy each name their gap", () => {
96
+ expect(runnerStartReadiness(emptyPolicy("x")).gaps.map((g) => g.gap)).toEqual(["no-runner-key"]);
97
+ const noRole = { ...emptyPolicy("x"), runners: [{ principal: "p", class: "runner" as const, key: PROVISIONER.ssh }] };
98
+ expect(runnerStartReadiness(noRole)).toMatchObject({ configured: false, gaps: [{ gap: "no-runner-role" }] });
99
+ expect(runnerStartReadiness(emptyPolicy("x", ["bad json"])).gaps.map((g) => g.gap)).toEqual(["trust-unreadable"]);
100
+ });
101
+ });
102
+
103
+ describe("chant ci runner-start sign and verify", () => {
104
+ test("--job takes JSON or @file, and refuses fields chant does not know", () => {
105
+ const dir = scratchDir("chant-runner-start-job-");
106
+ writeFiles(dir, { "job.json": JSON.stringify(job(R.head)) });
107
+ expect(parseJobSpec(`@${join(dir, "job.json")}`)).toEqual(job(R.head));
108
+ expect(parseJobSpec('{"system":"gitlab","repo":"g/s/p","run":9,"job":77,"commit":"x"}')).toEqual({ system: "gitlab", repo: "g/s/p", run: "9", attempt: "1", job: "77", commit: "x" });
109
+ expect(() => parseJobSpec('{"system":"github","pr":7}')).toThrow(/fields chant does not know: pr/);
110
+ expect(parseArgs(["ci", "runner-start", "sign", "--key", "k.pem", "--principal", "provisioner", "--job", "@job.json"])).toMatchObject({ path: "runner-start", extraPositional: "sign", key: "k.pem", ciPrincipal: "provisioner", ciJob: "@job.json" });
111
+ expect(parseArgs(["ci", "run", "apply", "--env", "prod", "--require-runner-start"])).toMatchObject({ requireRunnerStart: true });
112
+ });
113
+
114
+ test("sign writes an envelope the job then verifies, and verify exits 1 for another attempt", async () => {
115
+ const dir = scratchDir("chant-runner-start-cli-");
116
+ writeFiles(dir, { "key.pem": PROVISIONER.pem, "job.json": JSON.stringify(job(R.head)) });
117
+ const out = join(dir, "runner-start.json");
118
+ const ctx = (argv: string[]) => ({ args: parseArgs(argv) }) as unknown as CommandContext;
119
+ const quiet = vi.spyOn(console, "error").mockImplementation(() => {});
120
+ try {
121
+ expect(await runCiRunnerStart(ctx(["ci", "runner-start", "sign", "--key", join(dir, "key.pem"), "--principal", "provisioner", "--job", `@${join(dir, "job.json")}`, "-o", out]))).toBe(0);
122
+ const statement = JSON.parse(readFileSync(out, "utf8"));
123
+ const cwd = process.cwd();
124
+ process.chdir(R.root);
125
+ try {
126
+ vi.stubEnv("GITHUB_ACTIONS", "true");
127
+ for (const [k, v] of Object.entries(env(R.head))) vi.stubEnv(k, v);
128
+ vi.stubEnv("CHANT_RUNNER_START_FILE", out);
129
+ expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(0);
130
+ vi.stubEnv("GITHUB_RUN_ATTEMPT", "2");
131
+ expect(await runCiRunnerStart(ctx(["ci", "runner-start", "verify", "--base", R.base]))).toBe(1);
132
+ } finally {
133
+ vi.unstubAllEnvs();
134
+ process.chdir(cwd);
135
+ }
136
+ expect(statement.payloadType).toBe("application/vnd.in-toto+json");
137
+ expect(await runCiRunnerStart(ctx(["ci", "runner-start", "nope"]))).toBe(1);
138
+ } finally {
139
+ quiet.mockRestore();
140
+ }
141
+ });
142
+ });
143
+
144
+ describe("ci run --require-runner-start", () => {
145
+ test("a job with no statement is refused before anything runs, and --local is refused with it", async () => {
146
+ const run = (extra: Partial<Parameters<typeof ciRun>[0]>) => ciRun({ cwd: R.root, job: "apply", env: "prod", forge: "github", base: R.base, hostEnv: env(R.head), push: false, requireRunnerStart: true, ...extra });
147
+ await expect(run({})).rejects.toThrow(CiRunError);
148
+ await expect(run({})).rejects.toThrow(/has no runner-start statement/);
149
+ await expect(run({ local: true, hostEnv: {} })).rejects.toThrow(/--local runs the job on this machine/);
150
+ });
151
+ });
@@ -0,0 +1,193 @@
1
+ /**
2
+ * `chant ci runner-start sign|verify` and `chant ci run --require-runner-start`
3
+ * (#4147): the runner-start statement (../runner-start.ts) checked against
4
+ * `.chant/trust.json` at base.
5
+ *
6
+ * - `sign` is for the provisioner that starts a runner for one job: it signs
7
+ * the job's ids with a runner key the job never holds and prints the
8
+ * envelope, which the provisioner puts where the job looks for it.
9
+ * - `verify` is for the job: it names itself from the forge's variables,
10
+ * reads the statement, and fails unless a runner key `.chant/trust.json`
11
+ * at base lists, whose principal holds the `runner` role, signed it for
12
+ * this job.
13
+ * - `ci run --require-runner-start` makes the same check before the job's
14
+ * first step, so an apply refuses before it trades for any credentials.
15
+ *
16
+ * Requiring the statement is opt-in (#4156). With nothing asking for it,
17
+ * nothing reads it, and a runner with none never refuses, warns or hides
18
+ * anything.
19
+ *
20
+ * {@link runnerStartReadiness} says whether a repository could require it
21
+ * at base, and what is missing, for the isolated-runners security group
22
+ * (#4168, section 8), whose WSP165 check reports these gaps.
23
+ */
24
+
25
+ import { execFileSync } from "node:child_process";
26
+ import { readFileSync, writeFileSync } from "node:fs";
27
+ import { resolve } from "node:path";
28
+ import type { CommandContext } from "../cli/registry";
29
+ import { formatError } from "../cli/format";
30
+ import {
31
+ ciJobFromEnv,
32
+ RUNNER_START_SIGNER_ROLE,
33
+ signRunnerStart,
34
+ verifyRunnerStartForJob,
35
+ type CiJobIdentity,
36
+ type CiSystem,
37
+ type RunnerStartVerdict,
38
+ } from "../runner-start";
39
+ import { gitRevisionSource } from "./record-source";
40
+ import { gitTop } from "./tree";
41
+ import { loadRunnerKey } from "./trust/dsse";
42
+ import { emptyPolicy, readTrustPolicy, type TrustPolicy } from "./trust/policy";
43
+
44
+ /** The policy at `base` in the repository around `cwd`. */
45
+ function policyAt(top: string, base: string): TrustPolicy {
46
+ return readTrustPolicy(gitRevisionSource(top, base), base);
47
+ }
48
+
49
+ /**
50
+ * Check this job's runner-start statement against `.chant/trust.json` at
51
+ * `base`. `commit` is the commit the job runs, in place of the forge's
52
+ * variable. Never throws.
53
+ */
54
+ export function checkRunnerStart(input: { top: string; base: string; env?: Record<string, string | undefined>; commit?: string; readFile?: (path: string) => string }): RunnerStartVerdict {
55
+ const policy = policyAt(input.top, input.base);
56
+ if (policy.problems.length > 0) return { status: "untrusted", reason: `.chant/trust.json at ${input.base.slice(0, 12)} cannot be read: ${policy.problems.join("; ")}` };
57
+ return verifyRunnerStartForJob({
58
+ runners: policy.runners,
59
+ roles: policy.roles,
60
+ ...(input.env ? { env: input.env } : {}),
61
+ ...(input.commit ? { commit: input.commit } : {}),
62
+ ...(input.readFile ? { readFile: input.readFile } : {}),
63
+ });
64
+ }
65
+
66
+ /** One thing a repository lacks before it can require runner-start statements. */
67
+ export interface RunnerStartGap {
68
+ /** Not a read-contract reason code: the isolated-runners group (#4168) maps each to its own security code. */
69
+ gap: "no-runner-key" | "no-runner-role" | "trust-unreadable";
70
+ reason: string;
71
+ }
72
+
73
+ /**
74
+ * Whether `.chant/trust.json` in `policy` lets a job require a runner-start
75
+ * statement: a key under `runners` whose principal holds the `runner` role.
76
+ * `configured` is true when there are no gaps. For the isolated-runners
77
+ * security group (#4168); nothing here refuses or warns.
78
+ */
79
+ export function runnerStartReadiness(policy: TrustPolicy): { configured: boolean; gaps: RunnerStartGap[] } {
80
+ const gaps: RunnerStartGap[] = [];
81
+ if (policy.problems.length > 0) {
82
+ gaps.push({ gap: "trust-unreadable", reason: `.chant/trust.json cannot be read: ${policy.problems.join("; ")}` });
83
+ return { configured: false, gaps };
84
+ }
85
+ if (policy.runners.length === 0) {
86
+ gaps.push({ gap: "no-runner-key", reason: ".chant/trust.json lists no key under runners for a provisioner to sign with: chant workspace trust runner add <principal> --key <ssh-ed25519 line>" });
87
+ } else {
88
+ const holders = new Set((policy.roles[RUNNER_START_SIGNER_ROLE] ?? []).map((p) => p.normalize("NFKC").trim().toLowerCase()));
89
+ if (!policy.runners.some((r) => holders.has(r.principal.normalize("NFKC").trim().toLowerCase()))) {
90
+ gaps.push({ gap: "no-runner-role", reason: `no principal under runners holds the ${RUNNER_START_SIGNER_ROLE} role in .chant/trust.json: "roles": { "${RUNNER_START_SIGNER_ROLE}": ["<principal>"] }` });
91
+ }
92
+ }
93
+ return { configured: gaps.length === 0, gaps };
94
+ }
95
+
96
+ /** {@link runnerStartReadiness} for the repository around `cwd` at `base` (default `HEAD`). */
97
+ export function runnerStartReadinessAt(cwd: string, base = "HEAD"): { configured: boolean; gaps: RunnerStartGap[] } {
98
+ const top = gitTop(cwd);
99
+ if (!top) return runnerStartReadiness(emptyPolicy(null, [`${cwd} is not in a git repository`]));
100
+ const commit = revParse(top, base);
101
+ return runnerStartReadiness(commit ? policyAt(top, commit) : emptyPolicy(null, [`${base} is not a commit`]));
102
+ }
103
+
104
+ function revParse(top: string, rev: string): string | undefined {
105
+ try {
106
+ return execFileSync("git", ["rev-parse", "--verify", "-q", `${rev}^{commit}`], { cwd: top, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim() || undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ /** The base `verify` reads trust at: `--base`, else the remote's default branch, else `HEAD`. */
113
+ function verifyBase(top: string, base?: string): string {
114
+ const rev = base ?? (revParse(top, "refs/remotes/origin/HEAD") ? "refs/remotes/origin/HEAD" : "HEAD");
115
+ const commit = revParse(top, rev);
116
+ if (!commit) throw new Error(`--base ${rev} is not a commit in this repository`);
117
+ return commit;
118
+ }
119
+
120
+ /** The job named by `--job`: JSON, or `@<file>` holding it. */
121
+ export function parseJobSpec(spec: string): CiJobIdentity {
122
+ const text = spec.startsWith("@") ? readFileSync(resolve(spec.slice(1)), "utf8") : spec;
123
+ let raw: unknown;
124
+ try {
125
+ raw = JSON.parse(text);
126
+ } catch {
127
+ throw new Error("--job takes the job as JSON, or @<file> holding it: {\"system\":\"github\",\"repo\":\"owner/name\",\"run\":\"<id>\",\"attempt\":\"1\",\"job\":\"apply\",\"commit\":\"<sha>\",\"runner\":\"<name>\"}");
128
+ }
129
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("--job takes a JSON object");
130
+ const o = raw as Record<string, unknown>;
131
+ const str = (k: string): string | undefined => (o[k] === undefined ? undefined : String(o[k]));
132
+ const known = ["system", "repo", "run", "attempt", "job", "commit", "runner"];
133
+ const extra = Object.keys(o).filter((k) => !known.includes(k));
134
+ if (extra.length > 0) throw new Error(`--job has fields chant does not know: ${extra.join(", ")}`);
135
+ return {
136
+ system: (str("system") ?? "") as CiSystem,
137
+ repo: str("repo") ?? "",
138
+ run: str("run") ?? "",
139
+ attempt: str("attempt") ?? "1",
140
+ job: str("job") ?? "",
141
+ commit: str("commit") ?? "",
142
+ ...(str("runner") ? { runner: str("runner")! } : {}),
143
+ };
144
+ }
145
+
146
+ const USAGE = "chant ci runner-start sign --key <runner key.pem> --principal <name> --job <json|@file> [-o <file>]\n chant ci runner-start verify [--base <rev>] [--commit <sha>] [--json]";
147
+
148
+ /** `chant ci runner-start sign|verify`. */
149
+ export async function runCiRunnerStart(ctx: CommandContext): Promise<number> {
150
+ const { args } = ctx;
151
+ const action = args.extraPositional;
152
+ try {
153
+ if (action === "sign") {
154
+ if (!args.key) throw new Error("--key names the provisioner's runner key, an Ed25519 private key in PEM, which the job never holds");
155
+ if (!args.ciPrincipal) throw new Error("--principal names the principal .chant/trust.json lists the key for under runners");
156
+ if (!args.ciJob) throw new Error("--job names the job the runner was started for, as JSON or @<file>");
157
+ const job = parseJobSpec(args.ciJob);
158
+ const pem = readFileSync(resolve(args.key), "utf8");
159
+ const loaded = loadRunnerKey(pem);
160
+ const envelope = signRunnerStart(job, { principal: args.ciPrincipal, key: loaded.key, publicKey: loaded.publicKey });
161
+ const text = `${JSON.stringify(envelope)}\n`;
162
+ if (args.output) writeFileSync(resolve(args.output), text, { mode: 0o644 });
163
+ else process.stdout.write(text);
164
+ return 0;
165
+ }
166
+ if (action === "verify") {
167
+ const top = gitTop(process.cwd());
168
+ if (!top) throw new Error("verify reads .chant/trust.json at a commit, and this directory is not in a git repository");
169
+ const base = verifyBase(top, args.base);
170
+ const verdict = checkRunnerStart({ top, base, ...(args.commit ? { commit: args.commit } : {}) });
171
+ if (args.json) process.stdout.write(`${JSON.stringify({ ...verdict, base })}\n`);
172
+ else console.error(`ci runner-start: ${verdict.status}: ${verdict.reason}`);
173
+ return verdict.status === "verified" ? 0 : 1;
174
+ }
175
+ throw new Error(`ci runner-start takes sign or verify${action ? `, not ${JSON.stringify(action)}` : ""}`);
176
+ } catch (err) {
177
+ console.error(formatError({ message: `chant ci runner-start: ${err instanceof Error ? err.message : String(err)}`, hint: USAGE }));
178
+ return 1;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * The check `ci run --require-runner-start` makes before the job's first
184
+ * step: the statement for this job, verified against `.chant/trust.json` at
185
+ * `base`. Throws with the verdict's reason unless it verified.
186
+ */
187
+ export function requireRunnerStart(input: { top: string; base: string; commit: string; env: Record<string, string | undefined> }): Extract<RunnerStartVerdict, { status: "verified" }> {
188
+ const named = ciJobFromEnv(input.env);
189
+ if (!named.ok) throw new Error(`--require-runner-start needs a forge job to check: ${named.reason}`);
190
+ const verdict = checkRunnerStart({ top: input.top, base: input.base, env: input.env, commit: input.commit });
191
+ if (verdict.status !== "verified") throw new Error(`--require-runner-start: ${verdict.status}: ${verdict.reason}`);
192
+ return verdict;
193
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * #4169 — `ci serve --build-command` serves each `ci/build/<sha>` tag once,
3
+ * within `maxJobs`, and records how each build ended.
4
+ */
5
+
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { afterAll, describe, expect, test } from "vitest";
9
+ import { cleanScratch, git, scratchDir, writeFiles } from "./__fixtures__/contract-repo";
10
+ import { buildRequestAnnotation, requestedBuild } from "./ci-builds";
11
+ import { ciServeBuilds, ciServePass, formatCiServeEvent, newCiServeRuntime, type CiServeEvent, type CiServeOptions } from "./ci-serve";
12
+ import type { CiGreen } from "./declaration";
13
+
14
+ afterAll(cleanScratch);
15
+
16
+ const GREEN = { branch: "main", phases: [], require: ["suites"] } as unknown as CiGreen;
17
+
18
+ function clone(): { root: string; shas: string[] } {
19
+ const origin = scratchDir("chant-serve-builds-origin-");
20
+ git(origin, "init", "-q", "--bare");
21
+ const root = scratchDir("chant-serve-builds-");
22
+ git(root, "init", "-q");
23
+ git(root, "symbolic-ref", "HEAD", "refs/heads/main");
24
+ git(root, "config", "user.name", "t");
25
+ git(root, "config", "user.email", "t@example.com");
26
+ git(root, "remote", "add", "origin", origin);
27
+ const shas: string[] = [];
28
+ for (const n of ["a", "b"]) {
29
+ writeFiles(root, { [`${n}.txt`]: `${n}\n` });
30
+ git(root, "add", "-A");
31
+ git(root, "commit", "-q", "-m", n);
32
+ shas.push(git(root, "rev-parse", "HEAD"));
33
+ const r = requestedBuild(GREEN, shas.at(-1)!, ["suites"], new Date(`2026-10-20T1${shas.length}:00:00Z`));
34
+ git(root, "tag", "-a", "-m", buildRequestAnnotation(r), r.tag, r.commit);
35
+ }
36
+ git(root, "push", "-q", "origin", "main", "--tags");
37
+ // The serve's clone learns the tags from the forge.
38
+ for (const s of shas) git(root, "tag", "-d", `ci/build/${s}`);
39
+ return { root, shas };
40
+ }
41
+
42
+ const options = (root: string, built: string[], log: CiServeEvent[], extra: Partial<CiServeOptions> = {}): CiServeOptions => ({
43
+ cwd: root,
44
+ forge: "github",
45
+ repo: "acme/app",
46
+ env: "prod",
47
+ client: { openPullRequests: async () => [], branchHead: async () => undefined },
48
+ stateDir: join(root, ".serve"),
49
+ buildCommand: "true",
50
+ build: async ({ request }) => (built.push(request.commit), request.commit === built[0] ? 0 : 2),
51
+ log: (e) => log.push(e),
52
+ ...extra,
53
+ });
54
+
55
+ describe("ci serve --build-command (#4169)", () => {
56
+ test("serves each request once, oldest first, within maxJobs, and records how it ended", async () => {
57
+ const { root, shas } = clone();
58
+ const built: string[] = [];
59
+ const log: CiServeEvent[] = [];
60
+ const rt = newCiServeRuntime();
61
+ const first = ciServeBuilds(options(root, built, log), rt);
62
+ expect(first.events.map((e) => e.type === "build" && e.status)).toEqual(["started", "held"]);
63
+ await Promise.all(first.started);
64
+ const second = ciServeBuilds(options(root, built, log), rt);
65
+ await Promise.all(second.started);
66
+ expect(built).toEqual(shas);
67
+ const state = JSON.parse(readFileSync(join(root, ".serve", "chant-apply-prod.json"), "utf-8"));
68
+ expect(Object.fromEntries(Object.entries(state.builds).map(([k, v]) => [k, (v as { status: string }).status]))).toEqual({ [`build:${shas[0]}`]: "ok", [`build:${shas[1]}`]: "fail" });
69
+ // Served once: another pass starts nothing.
70
+ expect(ciServeBuilds(options(root, built, log), rt).started).toEqual([]);
71
+ expect(log.map((e) => formatCiServeEvent(e))).toContain(`the build of suites at ${shas[1].slice(0, 12)} failed with exit code 2`);
72
+ });
73
+
74
+ test("without --build-command a pass leaves the tags alone; the default build runs the command in a worktree of the commit", async () => {
75
+ const { root, shas } = clone();
76
+ const log: CiServeEvent[] = [];
77
+ await ciServePass({ ...options(root, [], log), buildCommand: undefined });
78
+ expect(log.filter((e) => e.type === "build")).toEqual([]);
79
+ const out = join(root, ".serve-out");
80
+ await ciServePass({ ...options(root, [], log), build: undefined, buildCommand: `git rev-parse HEAD >> ${out}; echo "$CHANT_BUILD_PHASES $CHANT_BUILD_REASON" >> ${out}`, maxJobs: 2 });
81
+ expect(existsSync(out)).toBe(true);
82
+ const lines = readFileSync(out, "utf-8").trim().split("\n");
83
+ expect(lines.filter((l) => /^[0-9a-f]{40}$/.test(l)).sort()).toEqual([...shas].sort());
84
+ expect(lines.filter((l) => l === "suites requested")).toHaveLength(2);
85
+ });
86
+ });