@intentius/chant 0.124.30 → 0.124.32

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 (346) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +74 -12
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +65 -0
  41. package/dist/cli/registry.d.ts +29 -2
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/local-executor.d.ts +12 -0
  111. package/dist/op/local-executor.d.ts.map +1 -1
  112. package/dist/op/local-executor.js +22 -3
  113. package/dist/op/op-waves-run.d.ts.map +1 -1
  114. package/dist/op/op-waves-run.js +20 -5
  115. package/dist/op/operator.d.ts +2 -2
  116. package/dist/op/operator.js +2 -2
  117. package/dist/op/redact.d.ts +34 -0
  118. package/dist/op/redact.d.ts.map +1 -0
  119. package/dist/op/redact.js +94 -0
  120. package/dist/op/step-console.d.ts +2 -0
  121. package/dist/op/step-console.d.ts.map +1 -1
  122. package/dist/op/step-console.js +5 -2
  123. package/dist/op/steward.d.ts +6 -4
  124. package/dist/op/steward.d.ts.map +1 -1
  125. package/dist/op/steward.js +5 -3
  126. package/dist/op/work-lease-run.d.ts +20 -7
  127. package/dist/op/work-lease-run.d.ts.map +1 -1
  128. package/dist/op/work-lease-run.js +56 -12
  129. package/dist/pr-forge.d.ts +27 -0
  130. package/dist/pr-forge.d.ts.map +1 -1
  131. package/dist/pr-forge.js +51 -6
  132. package/dist/pr-loop.d.ts +141 -4
  133. package/dist/pr-loop.d.ts.map +1 -1
  134. package/dist/pr-loop.js +122 -6
  135. package/dist/pr-report-url.d.ts +10 -2
  136. package/dist/pr-report-url.d.ts.map +1 -1
  137. package/dist/pr-report-url.js +52 -6
  138. package/dist/ts-loader.d.ts +38 -4
  139. package/dist/ts-loader.d.ts.map +1 -1
  140. package/dist/ts-loader.js +190 -16
  141. package/dist/wave-runtime.mjs +208 -208
  142. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  143. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  144. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  145. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  146. package/dist/workspace/chant-migrations.js +2 -0
  147. package/dist/workspace/checks/boxes.d.ts +6 -6
  148. package/dist/workspace/checks/boxes.js +9 -9
  149. package/dist/workspace/ci-green-forge.d.ts +6 -4
  150. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  151. package/dist/workspace/ci-green.d.ts +28 -12
  152. package/dist/workspace/ci-green.d.ts.map +1 -1
  153. package/dist/workspace/ci-green.js +49 -21
  154. package/dist/workspace/ci-list.schema.json +12 -6
  155. package/dist/workspace/ci-report-url.d.ts +18 -0
  156. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  157. package/dist/workspace/ci-report-url.js +33 -0
  158. package/dist/workspace/ci-run-index.d.ts +108 -0
  159. package/dist/workspace/ci-run-index.d.ts.map +1 -0
  160. package/dist/workspace/ci-run-index.js +164 -0
  161. package/dist/workspace/ci-run-index.schema.json +52 -0
  162. package/dist/workspace/ci-run.d.ts +159 -9
  163. package/dist/workspace/ci-run.d.ts.map +1 -1
  164. package/dist/workspace/ci-run.js +419 -50
  165. package/dist/workspace/ci-runner-key.d.ts +133 -0
  166. package/dist/workspace/ci-runner-key.d.ts.map +1 -0
  167. package/dist/workspace/ci-runner-key.js +237 -0
  168. package/dist/workspace/ci-secrets.d.ts +65 -0
  169. package/dist/workspace/ci-secrets.d.ts.map +1 -0
  170. package/dist/workspace/ci-secrets.js +211 -0
  171. package/dist/workspace/ci-serve.d.ts +67 -4
  172. package/dist/workspace/ci-serve.d.ts.map +1 -1
  173. package/dist/workspace/ci-serve.js +240 -35
  174. package/dist/workspace/declaration.d.ts +8 -0
  175. package/dist/workspace/declaration.d.ts.map +1 -1
  176. package/dist/workspace/declaration.js +11 -2
  177. package/dist/workspace/declaration.schema.json +7 -2
  178. package/dist/workspace/export.d.ts +1 -1
  179. package/dist/workspace/export.d.ts.map +1 -1
  180. package/dist/workspace/export.js +7 -3
  181. package/dist/workspace/ls.d.ts +6 -1
  182. package/dist/workspace/ls.d.ts.map +1 -1
  183. package/dist/workspace/ls.js +1 -0
  184. package/dist/workspace/ls.schema.json +7 -1
  185. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  186. package/dist/workspace/pin-integrity.js +3 -2
  187. package/dist/workspace/pr-report.schema.json +74 -7
  188. package/dist/workspace/reason-codes.d.ts +7 -3
  189. package/dist/workspace/reason-codes.d.ts.map +1 -1
  190. package/dist/workspace/reason-codes.js +9 -4
  191. package/dist/workspace/run-event.schema.json +3 -1
  192. package/dist/workspace/status.schema.json +3 -1
  193. package/dist/workspace/trust/trust-write.d.ts +109 -0
  194. package/dist/workspace/trust/trust-write.d.ts.map +1 -0
  195. package/dist/workspace/trust/trust-write.js +320 -0
  196. package/dist/workspace/trust-write.schema.json +265 -0
  197. package/dist/workspace/write-scope.d.ts +9 -0
  198. package/dist/workspace/write-scope.d.ts.map +1 -1
  199. package/dist/workspace/write-scope.js +11 -1
  200. package/package.json +1 -1
  201. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  202. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  203. package/src/cli/commands/audit.test.ts +2 -2
  204. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  205. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  206. package/src/cli/commands/check-lexicon.test.ts +1 -1
  207. package/src/cli/commands/check-lexicon.ts +16 -11
  208. package/src/cli/commands/import.ts +40 -0
  209. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  210. package/src/cli/commands/init-lexicon.test.ts +8 -5
  211. package/src/cli/commands/onboard.test.ts +45 -0
  212. package/src/cli/commands/onboard.ts +8 -4
  213. package/src/cli/commands/update.test.ts +6 -0
  214. package/src/cli/commands/update.ts +4 -1
  215. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  216. package/src/cli/dependencies.e2e.test.ts +101 -0
  217. package/src/cli/dependencies.test.ts +93 -0
  218. package/src/cli/dependencies.ts +122 -0
  219. package/src/cli/handlers/build.test.ts +69 -0
  220. package/src/cli/handlers/build.ts +20 -4
  221. package/src/cli/handlers/fan-out.test.ts +73 -0
  222. package/src/cli/handlers/fan-out.ts +11 -4
  223. package/src/cli/handlers/graph.test.ts +76 -0
  224. package/src/cli/handlers/graph.ts +19 -12
  225. package/src/cli/handlers/lint.test.ts +42 -0
  226. package/src/cli/handlers/lint.ts +6 -0
  227. package/src/cli/handlers/operator.ts +3 -3
  228. package/src/cli/handlers/pr-waves.test.ts +449 -0
  229. package/src/cli/handlers/pr.test.ts +134 -0
  230. package/src/cli/handlers/pr.ts +523 -31
  231. package/src/cli/handlers/promote.test.ts +26 -1
  232. package/src/cli/handlers/promote.ts +3 -2
  233. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  234. package/src/cli/handlers/run-signer.test.ts +79 -0
  235. package/src/cli/handlers/run.ts +34 -16
  236. package/src/cli/main.test.ts +19 -0
  237. package/src/cli/main.ts +73 -12
  238. package/src/cli/parse-args.ts +48 -0
  239. package/src/cli/registry.ts +35 -4
  240. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  241. package/src/codegen/lexicon-regen.ts +1 -1
  242. package/src/codegen/pinned-upgrade.ts +2 -2
  243. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  244. package/src/components/capability.ts +51 -3
  245. package/src/components/changed-files.test.ts +16 -1
  246. package/src/components/changed-files.ts +9 -1
  247. package/src/components/driver.test.ts +36 -1
  248. package/src/components/driver.ts +76 -4
  249. package/src/components/fan-out-record.ts +29 -1
  250. package/src/components/fan-out-run.test.ts +35 -0
  251. package/src/components/fan-out-run.ts +40 -1
  252. package/src/components/pr-pipeline.test.ts +100 -0
  253. package/src/components/pr-pipeline.ts +132 -17
  254. package/src/components/pr-run.test.ts +323 -18
  255. package/src/components/pr-run.ts +354 -75
  256. package/src/components/wave-plan.ts +16 -4
  257. package/src/discovery/fold-host-method.test.ts +87 -0
  258. package/src/discovery/fold-import.ts +15 -5
  259. package/src/discovery/fold-package-composite.test.ts +27 -0
  260. package/src/discovery/fold-specifiers.test.ts +8 -0
  261. package/src/discovery/sandbox/bundle.ts +6 -0
  262. package/src/discovery/specifier-resolve.ts +8 -2
  263. package/src/fold/fold.test.ts +72 -0
  264. package/src/fold/fold.ts +71 -0
  265. package/src/fold/subset.test.ts +31 -0
  266. package/src/fold/subset.ts +99 -0
  267. package/src/lexicon-dist.e2e.test.ts +254 -0
  268. package/src/lexicon-module.test.ts +47 -0
  269. package/src/lexicon-module.ts +49 -7
  270. package/src/lexicon-plugin-helpers.ts +6 -4
  271. package/src/lexicon.ts +2 -0
  272. package/src/lifecycle/affected.test.ts +7 -0
  273. package/src/lifecycle/affected.ts +6 -1
  274. package/src/lifecycle/release-ledger.ts +34 -0
  275. package/src/lifecycle/work-dir.test.ts +77 -0
  276. package/src/lifecycle/work-dir.ts +82 -0
  277. package/src/lint/discover.test.ts +46 -2
  278. package/src/lint/discover.ts +55 -49
  279. package/src/module-source.test.ts +85 -0
  280. package/src/module-source.ts +112 -0
  281. package/src/op/activities/factory.ts +1 -1
  282. package/src/op/discover-root.test.ts +31 -2
  283. package/src/op/discover.ts +14 -4
  284. package/src/op/gate.test.ts +30 -0
  285. package/src/op/gate.ts +3 -0
  286. package/src/op/local-executor.ts +31 -3
  287. package/src/op/op-waves-run.ts +20 -5
  288. package/src/op/op-waves.test.ts +49 -0
  289. package/src/op/operator.ts +2 -2
  290. package/src/op/redact.ts +93 -0
  291. package/src/op/step-console.ts +7 -2
  292. package/src/op/steward.ts +6 -4
  293. package/src/op/work-lease-run.test.ts +88 -2
  294. package/src/op/work-lease-run.ts +52 -12
  295. package/src/pr-forge-bundle.test.ts +2 -2
  296. package/src/pr-forge.test.ts +71 -0
  297. package/src/pr-forge.ts +70 -6
  298. package/src/pr-loop-bundle.test.ts +2 -1
  299. package/src/pr-loop.test.ts +117 -1
  300. package/src/pr-loop.ts +252 -10
  301. package/src/pr-report-url.ts +58 -6
  302. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  303. package/src/ts-loader.test.ts +89 -1
  304. package/src/ts-loader.ts +180 -13
  305. package/src/workspace/changes.test.ts +1 -1
  306. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  307. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  308. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  309. package/src/workspace/chant-migrations.ts +2 -0
  310. package/src/workspace/checks/boxes.ts +9 -9
  311. package/src/workspace/ci-green-forge.ts +6 -4
  312. package/src/workspace/ci-green-runs.test.ts +53 -11
  313. package/src/workspace/ci-green.test.ts +1 -1
  314. package/src/workspace/ci-green.ts +50 -22
  315. package/src/workspace/ci-list.schema.json +12 -6
  316. package/src/workspace/ci-report-url.test.ts +220 -0
  317. package/src/workspace/ci-report-url.ts +33 -0
  318. package/src/workspace/ci-run-index.schema.json +52 -0
  319. package/src/workspace/ci-run-index.test.ts +392 -0
  320. package/src/workspace/ci-run-index.ts +206 -0
  321. package/src/workspace/ci-run.test.ts +520 -42
  322. package/src/workspace/ci-run.ts +483 -51
  323. package/src/workspace/ci-runner-key.test.ts +204 -0
  324. package/src/workspace/ci-runner-key.ts +275 -0
  325. package/src/workspace/ci-secrets.test.ts +106 -0
  326. package/src/workspace/ci-secrets.ts +213 -0
  327. package/src/workspace/ci-serve.test.ts +282 -39
  328. package/src/workspace/ci-serve.ts +231 -34
  329. package/src/workspace/declaration.schema.json +7 -2
  330. package/src/workspace/declaration.test.ts +23 -0
  331. package/src/workspace/declaration.ts +19 -3
  332. package/src/workspace/export.ts +6 -3
  333. package/src/workspace/ls.schema.json +7 -1
  334. package/src/workspace/ls.ts +7 -2
  335. package/src/workspace/pin-integrity.test.ts +5 -0
  336. package/src/workspace/pin-integrity.ts +3 -2
  337. package/src/workspace/pr-report.schema.json +74 -7
  338. package/src/workspace/reason-codes.test.ts +2 -0
  339. package/src/workspace/reason-codes.ts +9 -4
  340. package/src/workspace/run-event.schema.json +3 -1
  341. package/src/workspace/status.schema.json +3 -1
  342. package/src/workspace/trust/trust-write.test.ts +179 -0
  343. package/src/workspace/trust/trust-write.ts +377 -0
  344. package/src/workspace/trust-write.schema.json +265 -0
  345. package/src/workspace/write-scope.test.ts +29 -1
  346. package/src/workspace/write-scope.ts +12 -1
@@ -22,7 +22,10 @@
22
22
  * own variables. The forge values a generated job reads from `${{ ... }}`
23
23
  * expressions or GitLab's predefined variables come from flags here:
24
24
  * `BASE_SHA` from `--base`, `PR_NUMBER` from `--pr`, and the commit from
25
- * `--commit`.
25
+ * `--commit`. The values a forge job gets from repository secrets come from
26
+ * the host's secrets file (`--secrets`, ./ci-secrets.ts, #4096), and every
27
+ * secret value is redacted from the run's events, record, copied report and
28
+ * output by the executor's `redact` option.
26
29
  *
27
30
  * The steps run in a fresh checkout of the commit: a detached `git worktree`
28
31
  * in a temporary directory outside the repository, removed when the run
@@ -37,28 +40,67 @@
37
40
  * key is given, so `chant ci tick --source runs` (#4044) judges the commit
38
41
  * from it. Its events are `kind: "pipeline"` and `runtime: "ci"`, and the
39
42
  * record and the events carry `runOrigin: { forge: "chant" }`.
43
+ *
44
+ * With a forge client (#4097) the job also sets a commit status at its
45
+ * commit: `pending` when it starts, `success` or `failure` when it settles.
46
+ * A status the forge refuses is a warning and never fails the job.
47
+ *
48
+ * Signing is opt-in (#4156). With no runner key the record is unsigned, and
49
+ * a reader counts it with no signer. A key `.chant/trust.json` does not list
50
+ * leaves it unsigned with a note `--verbose` prints, unless the declaration
51
+ * sets `ci.green.signedSince`; then a run left unsigned is a warning.
52
+ *
53
+ * In place (`--in-place`, #4156) the job runs in the checkout it is started
54
+ * in, as a generated forge job does: no worktree, no setup phase and no
55
+ * commit status of its own (the forge shows the job). The steps get the
56
+ * job's whole environment less `CHANT_RUNNER_KEY`, since the forge job is
57
+ * already the boundary, and the record's `runOrigin` names the forge's run,
58
+ * job and attempt (`resolveCiJob`).
59
+ *
60
+ * Local (`--local`, #4161) runs the job the same way on this machine and
61
+ * leaves nothing outside it. It implies `--no-push` and `--no-status`, and
62
+ * writes nothing under a report URL, given or declared: the command gets no
63
+ * `--report-url`, and gets `--no-status`, so pr-plan and pr-apply post no
64
+ * status or note either. The record is still written to the local
65
+ * `chant/lifecycle`, where a reader on this machine finds it beside CI's for
66
+ * the same commit. It says `runOrigin: { forge: "local" }` and its events say
67
+ * `runtime: "local"`. `ci tick --source runs` and `ci list` never count it.
68
+ * A pr-apply still pushes its own release records and gate facts, as the
69
+ * command always does, since a local apply changes the same infrastructure
70
+ * a CI one does.
40
71
  */
41
72
  import { execFileSync } from "node:child_process";
42
- import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
73
+ import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
43
74
  import { tmpdir } from "node:os";
44
75
  import { dirname, join, relative, resolve, sep } from "node:path";
45
76
  import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prLoopJobs, prLoopSetup, PR_LOOP_IMAGE } from "../components/pr-pipeline.js";
46
- import { detectForge, FORGE_KINDS } from "../pr-forge.js";
77
+ import { detectForge, ForgeEnvironmentError, forgeFromEnv, FORGE_KINDS } from "../pr-forge.js";
78
+ import { PR_APPLY_GATE } from "../pr-loop.js";
47
79
  import { formatError, formatWarning } from "../cli/format.js";
48
- import { pushLifecycleReport } from "../lifecycle/git.js";
80
+ import { resolveCiJob } from "../lifecycle/release-ledger.js";
81
+ import { fetchLifecycleStatus, pushLifecycleReport } from "../lifecycle/git.js";
49
82
  import { ACTIVITY_PROFILES } from "../op/activity-profiles.js";
50
83
  import { shellCmd } from "../op/activities/shell.js";
51
84
  import { OpRunFailure, runOpLocally } from "../op/local-executor.js";
85
+ import { redactText } from "../op/redact.js";
52
86
  import { findWorkspaceRoot } from "../project-root.js";
53
- import { ciTagSigner, RUNNER_KEY_ENV } from "./ci-green.js";
87
+ import { parseReportUrl, reportKey } from "../pr-report-url.js";
88
+ import { reportUrlOf } from "./ci-report-url.js";
89
+ import { ciTagSigner, RUNNER_KEY_ENV, signedRunsRequired } from "./ci-green.js";
90
+ import { loadRunnerKey } from "./trust/dsse.js";
91
+ import { ciRedactor, ciSecretsFor, CI_SECRETS_ENV, readCiSecrets } from "./ci-secrets.js";
92
+ import { updateCiRunIndex } from "./ci-run-index.js";
54
93
  import { resolveMemberContext } from "./member-pipeline.js";
55
94
  import { signOpRunRecord } from "./trust/op-run-statement.js";
56
95
  import { gitTop } from "./tree.js";
96
+ import { hostRunnerKey } from "./ci-runner-key.js";
57
97
  export const CI_JOB_NAMES = ["plan", "apply"];
58
98
  /** The Op each job is recorded as, which `ci.green` phases match. */
59
99
  export const CI_JOB_OPS = { plan: "pr-plan", apply: "pr-apply" };
60
100
  /** Where a job the chant runner ran says it ran (#4094). */
61
101
  export const CHANT_RUNNER_ORIGIN = { forge: "chant" };
102
+ /** Where a job `chant ci run --local` ran says it ran (#4161): this machine, outside CI. */
103
+ export const CHANT_LOCAL_ORIGIN = { forge: "local" };
62
104
  /** The runner's variables every job gets, when the runner has them. */
63
105
  export const CI_RUN_BASE_ENV = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TZ"];
64
106
  /**
@@ -89,19 +131,27 @@ export class CiRunError extends Error {
89
131
  }
90
132
  /**
91
133
  * The job's environment: the runner's base variables, the forge variables
92
- * and `passEnv` names it has, `CHANT_ENV` as a generated workflow sets it,
93
- * the commit as the forge spells it, and the job's own variables. A job
94
- * variable the forge fills from an expression comes from a flag, or from
95
- * the runner's environment for a token such as `GITHUB_TOKEN`; a literal
96
- * one, such as `CHANT_RUN_EVENTS_DIR`, is kept as it is.
134
+ * and `passEnv` names it has, the job's secrets from the host's secrets file
135
+ * (#4096), `CHANT_ENV` as a generated workflow sets it, the commit as the
136
+ * forge spells it, and the job's own variables. A job variable the forge
137
+ * fills from an expression comes from a flag, or from a secret or the
138
+ * runner's environment for a token such as `GITHUB_TOKEN`; a literal one,
139
+ * such as `CHANT_RUN_EVENTS_DIR`, is kept as it is. A secret replaces the
140
+ * runner's variable of the same name, and none replaces a variable the
141
+ * runner sets for the job. `CHANT_RUNNER_KEY` is never in it.
97
142
  */
98
143
  export function ciJobEnv(job, input) {
144
+ if (input.passEnv?.includes(RUNNER_KEY_ENV))
145
+ throw new CiRunError(`--pass-env names ${RUNNER_KEY_ENV}, and the runner key is never passed to a job`);
99
146
  const out = {};
100
147
  for (const name of [...CI_RUN_BASE_ENV, ...CI_RUN_FORGE_ENV, ...(input.passEnv ?? [])]) {
101
148
  const value = input.hostEnv[name];
102
149
  if (value !== undefined)
103
150
  out[name] = value;
104
151
  }
152
+ for (const [name, value] of Object.entries(input.secrets ?? {}))
153
+ if (name !== RUNNER_KEY_ENV)
154
+ out[name] = value;
105
155
  out.CHANT_ENV = input.env;
106
156
  out[input.forge === "gitlab" ? "CI_COMMIT_SHA" : "GITHUB_SHA"] = input.values.commit;
107
157
  const flags = {
@@ -118,10 +168,14 @@ export function ciJobEnv(job, input) {
118
168
  else if (!value.includes("$")) {
119
169
  out[name] = value;
120
170
  }
171
+ else if (input.secrets?.[name] !== undefined) {
172
+ out[name] = input.secrets[name];
173
+ }
121
174
  else if (input.hostEnv[name] !== undefined) {
122
175
  out[name] = input.hostEnv[name];
123
176
  }
124
177
  }
178
+ delete out[RUNNER_KEY_ENV];
125
179
  return out;
126
180
  }
127
181
  /** `line` with a `chant` at its start replaced by the command the caller names, when it names one. */
@@ -165,6 +219,17 @@ function exitCodeOf(error) {
165
219
  const m = /command exited (\d+)/.exec(error ?? "");
166
220
  return m ? Number(m[1]) : undefined;
167
221
  }
222
+ /**
223
+ * The code `chant ci run` exits with for a settled job: 0 when it passed,
224
+ * else its command's own code when that is 1 to 255, such as 3 for a moved
225
+ * plan or a waiting wave (#4156), else 1. The run index records the same
226
+ * code (#4165).
227
+ */
228
+ export function ciJobExitCode(result) {
229
+ if (result.status === "ok")
230
+ return 0;
231
+ return result.exitCode && result.exitCode > 0 && result.exitCode < 256 ? result.exitCode : 1;
232
+ }
168
233
  /** The first line of a step's error, for a reason. */
169
234
  const firstLine = (text) => text.split("\n").find((l) => l.trim() !== "")?.trim() ?? text;
170
235
  /**
@@ -220,6 +285,67 @@ export function ciJobFailure(input) {
220
285
  }
221
286
  return { code: "command-failed", reason: firstLine(failed.error ?? "the command failed"), phase: failed.phase, ...exit };
222
287
  }
288
+ /** A status description is cut to what GitHub and Forgejo keep. */
289
+ const STATUS_DESCRIPTION = 140;
290
+ const cut = (text, max = STATUS_DESCRIPTION) => (text.length > max ? `${text.slice(0, max - 3)}...` : text);
291
+ /**
292
+ * A job's status context: `chant/pr-plan` for the plan, and for the apply
293
+ * `chant/<gate>`, which is `chant/pr-apply` unless `--gate` renamed the gate.
294
+ * pr-plan's own status is `chant/plan` (../pr-loop.ts), a different context.
295
+ */
296
+ export function ciStatusContext(job, gate) {
297
+ return job === "apply" ? `chant/${gate ?? PR_APPLY_GATE}` : `chant/${CI_JOB_OPS.plan}`;
298
+ }
299
+ /**
300
+ * The link a job's status carries: the `--status-url` template with `{key}`
301
+ * filled, else the https `--report-url` followed by the key and a slash, else
302
+ * none (an `s3://` or `file://` URL is not one a browser opens).
303
+ */
304
+ export function ciStatusUrl(input) {
305
+ const key = reportKey(input.pr ?? null, input.commit, input.member);
306
+ if (input.urlTemplate)
307
+ return input.urlTemplate.split("{key}").join(key);
308
+ const url = input.reportUrl?.trim();
309
+ if (url && /^https:\/\//.test(url))
310
+ return `${url.replace(/\/+$/, "")}/${key}/`;
311
+ return undefined;
312
+ }
313
+ /** What the settled status says: a short summary, or on failure the record's code and reason. */
314
+ export function ciStatusSettled(op, result, seconds) {
315
+ if (result.status === "ok")
316
+ return { state: "success", description: cut(`${op} passed in ${seconds}s`) };
317
+ const why = result.record.failure;
318
+ const text = why ? `${why.code}: ${why.reason}` : result.failure ? `${result.failure.phase}: ${firstLine(result.failure.error)}` : `${op} failed`;
319
+ return { state: "failure", description: cut(text) };
320
+ }
321
+ /**
322
+ * The status client for `chant ci run` and `chant ci serve` (#4097), or none:
323
+ * off with `--no-status`, on GitLab (its client has no commit status call
324
+ * here), and without a forge token. A token with the rest of the forge's
325
+ * variables missing gives a warning instead.
326
+ */
327
+ export function ciStatusFromEnv(input) {
328
+ if (input.noStatus || input.forge === "gitlab")
329
+ return {};
330
+ if (input.statusUrl !== undefined && !/^https?:\/\/\S+$/.test(input.statusUrl)) {
331
+ throw new CiRunError(`--status-url takes an http or https URL template such as https://host/reports/{key}, not ${JSON.stringify(input.statusUrl)}`);
332
+ }
333
+ const env = input.hostEnv;
334
+ const hasToken = Boolean(env.CHANT_FORGE_TOKEN ?? env.GITHUB_TOKEN ?? env.GH_TOKEN);
335
+ if (!hasToken)
336
+ return {};
337
+ try {
338
+ const client = forgeFromEnv(input.forge, env);
339
+ if (!client.setCommitStatus)
340
+ return {};
341
+ return { status: { client: { setCommitStatus: client.setCommitStatus.bind(client) }, ...(input.statusUrl ? { urlTemplate: input.statusUrl } : {}) } };
342
+ }
343
+ catch (err) {
344
+ if (err instanceof ForgeEnvironmentError)
345
+ return { warning: `no commit status will be set: ${err.message}` };
346
+ throw err;
347
+ }
348
+ }
223
349
  const message = (err) => (err instanceof Error ? err.message : String(err));
224
350
  function git(cwd, args) {
225
351
  return execFileSync("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
@@ -275,6 +401,47 @@ function freshCheckout(top, commit) {
275
401
  },
276
402
  };
277
403
  }
404
+ /**
405
+ * Add the settled job to the run index at its report URL (#4098). The pull
406
+ * request is the one the job's own report names, since a pr-apply finds it
407
+ * itself, else `--pr`. A warning, never a failure, when the index can't be
408
+ * updated.
409
+ */
410
+ async function recordInIndex(input) {
411
+ const target = parseReportUrl(input.url);
412
+ if ("error" in target)
413
+ return input.warn(`the run index is not updated: ${target.error}`);
414
+ let pr = input.pr ?? null;
415
+ try {
416
+ const file = join(input.reportDir, `${CI_JOB_OPS[input.job]}.json`);
417
+ if (existsSync(file)) {
418
+ const said = JSON.parse(readFileSync(file, "utf-8")).pr;
419
+ if (Number.isInteger(said) && said > 0)
420
+ pr = said;
421
+ }
422
+ }
423
+ catch {
424
+ // The report is not readable; the flag stands.
425
+ }
426
+ const failure = input.record.failure;
427
+ const clean = (text) => (input.redact ? input.redact(text) : text);
428
+ const entry = {
429
+ job: input.job,
430
+ pr,
431
+ commit: input.commit,
432
+ status: input.record.status === "ok" ? "ok" : "fail",
433
+ ...(input.record.status !== "ok" ? { failure: { code: clean(failure?.code ?? "command-failed") } } : {}),
434
+ ...(input.member ? { member: clean(input.member) } : {}),
435
+ started: input.record.started,
436
+ ended: input.record.ended,
437
+ runId: clean(input.record.id),
438
+ key: clean(reportKey(pr, input.commit, input.member)),
439
+ runOrigin: input.runOrigin,
440
+ signer: input.signer,
441
+ exitCode: input.exitCode,
442
+ };
443
+ await updateCiRunIndex({ target, entry, ...(input.limit ? { limit: input.limit } : {}), warn: input.warn });
444
+ }
278
445
  const ACTIVITIES = new Map([["shellCmd", shellCmd]]);
279
446
  /**
280
447
  * Run one pull-request job as a pipeline Op in a fresh checkout of the
@@ -288,26 +455,110 @@ export async function ciRun(req) {
288
455
  if (!top)
289
456
  throw new CiRunError("a job runs in a fresh checkout of a commit, and this directory is not in a git repository");
290
457
  const commit = resolveJobCommit(top, req.commit ?? "HEAD");
458
+ if (req.inPlace && commit !== resolveJobCommit(top, "HEAD"))
459
+ throw new CiRunError(`--in-place runs the checkout's HEAD, and --commit names ${commit.slice(0, 12)}`);
460
+ if (req.local && req.inPlace)
461
+ throw new CiRunError("--in-place runs a forge job in the forge's checkout, and --local runs one on this machine; pass one of them");
291
462
  const place = ciJobPlace(cwd, top);
292
- const jobs = prLoopJobs(req.forge, req.env, req.loop ?? {}, place.member);
293
- const job = jobs.find((j) => j.jobName === req.job);
294
- if (!job)
463
+ const jobs = prLoopJobs(req.forge, req.env, req.local ? localLoop(req.loop) : (req.loop ?? {}), place.member);
464
+ const found = jobs.find((j) => j.jobName === req.job);
465
+ if (!found)
295
466
  throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
296
- const jobEnv = ciJobEnv(job, { forge: req.forge, env: req.env, values: { base: req.base, pr: req.pr, commit }, hostEnv, passEnv: req.passEnv });
297
- // The record is signed against .chant/trust.json at the commit the job runs, so a key that commit does not list is refused before anything runs.
298
- let signer;
299
- const early = [];
300
- if (req.runnerKey !== undefined) {
301
- try {
302
- signer = ciTagSigner(top, commit, req.runnerKey, "the runs it signed");
303
- }
304
- catch (err) {
305
- if (!req.runnerKeyFromEnv)
306
- throw new CiRunError(`the run would not be signed: ${message(err)}`);
307
- early.push(`${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message(err)}`);
308
- }
467
+ if (req.wave !== undefined && (req.job !== "apply" || !req.loop?.waveGate))
468
+ throw new CiRunError("--wave names the one wave an apply runs, and takes --wave-gate: ci run apply --wave-gate <gate> --wave <k>");
469
+ // A generated wave job's command, as prLoopWaveJobs writes it. #4161: a local job's command posts no status or note.
470
+ const command = `${found.command}${req.wave !== undefined ? ` --wave ${req.wave}` : ""}${req.local ? " --no-status" : ""}`;
471
+ const job = { ...found, command, ...(req.wave !== undefined ? { wave: req.wave } : {}) };
472
+ const own = ciJobEnv(job, {
473
+ forge: req.forge,
474
+ env: req.env,
475
+ values: { base: req.base, pr: req.pr, commit },
476
+ hostEnv,
477
+ passEnv: req.passEnv,
478
+ secrets: ciSecretsFor(req.secrets, req.job),
479
+ });
480
+ // #4156: in place, the forge job's environment is the job's, less the runner key.
481
+ const jobEnv = req.inPlace ? { ...definedEnv(hostEnv), ...own } : own;
482
+ delete jobEnv[RUNNER_KEY_ENV];
483
+ // #4096: the secrets and the job's secret-looking variables, redacted from everything the run writes and returns.
484
+ // In place the forge's own variables come too; one whose value is short, such as a flag, is left out, so it can't blank common words.
485
+ const redact = ciRedactor(req.inPlace ? { ...longValues(jobEnv), ...own } : jobEnv, req.secrets);
486
+ try {
487
+ const result = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
488
+ return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
489
+ }
490
+ catch (err) {
491
+ if (redact && err instanceof Error)
492
+ err.message = redact(err.message);
493
+ throw err;
494
+ }
495
+ }
496
+ /** The generator's options for a `--local` job (#4161): the same, with no report URL. */
497
+ function localLoop(loop) {
498
+ const { reportUrl: _dropped, ...rest } = loop ?? {};
499
+ return rest;
500
+ }
501
+ /** The variables of `env` whose value is long enough to be a credential, 8 characters or more. */
502
+ function longValues(env) {
503
+ return Object.fromEntries(Object.entries(env).filter(([, value]) => value.length >= 8));
504
+ }
505
+ /** The variables of `env` that have a value. */
506
+ function definedEnv(env) {
507
+ const out = {};
508
+ for (const [name, value] of Object.entries(env))
509
+ if (value !== undefined)
510
+ out[name] = value;
511
+ return out;
512
+ }
513
+ /**
514
+ * Who signs the run (#4156): the runner key, checked against
515
+ * `.chant/trust.json` at the commit. A key that isn't an Ed25519 PEM is an
516
+ * error, and so is a `--key` the file does not list. A key from the
517
+ * environment that it does not list, and no key at all, leave the run
518
+ * unsigned: a warning when the declaration's `ci.green.signedSince` asks
519
+ * for signed runs, a note for `--verbose` otherwise.
520
+ */
521
+ function ciRunSigner(req, at) {
522
+ // #4161: a local run never counts, signed or not, so nothing about its signature warns.
523
+ const required = req.local ? null : signedRunsRequired(at.cwd);
524
+ const say = (text) => (required ? { warnings: [`${text}, and ci.green.signedSince is ${required}, so it will not count`], notes: [] } : { warnings: [], notes: [text] });
525
+ if (req.runnerKey === undefined)
526
+ return say("there is no runner key, so the run's record is unsigned");
527
+ try {
528
+ loadRunnerKey(req.runnerKey);
529
+ }
530
+ catch (err) {
531
+ throw new CiRunError(`the runner key${req.runnerKeyFromEnv ? ` in ${RUNNER_KEY_ENV}` : ""} is not an Ed25519 private key in PEM: ${message(err)}`);
309
532
  }
310
- const checkout = freshCheckout(top, commit);
533
+ try {
534
+ return { signer: ciTagSigner(at.top, at.commit, req.runnerKey, "the runs it signed"), warnings: [], notes: [] };
535
+ }
536
+ catch (err) {
537
+ if (!req.runnerKeyFromEnv)
538
+ throw new CiRunError(`the run would not be signed: ${message(err)}`);
539
+ return say(`${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message(err)}`);
540
+ }
541
+ }
542
+ /** Where the job runs: a fresh worktree of the commit, or with `inPlace` the checkout itself, which is left as it is. */
543
+ function jobCheckout(top, commit, inPlace) {
544
+ return inPlace ? { dir: top, remove() { } } : freshCheckout(top, commit);
545
+ }
546
+ /** {@link ciRun} once the job and its environment are known. */
547
+ async function runCiJob(req, at) {
548
+ const { cwd, top, commit, place, job, jobEnv, redact } = at;
549
+ const local = req.local === true;
550
+ const status = local ? undefined : req.status;
551
+ const reportUrl = local ? undefined : req.loop?.reportUrl;
552
+ // The record is signed against .chant/trust.json at the commit the job runs, so a --key that commit does not list is refused before anything runs.
553
+ const signing = ciRunSigner(req, { cwd, top, commit });
554
+ const signer = signing.signer;
555
+ const early = signing.warnings;
556
+ const notes = signing.notes;
557
+ const origin = local ? CHANT_LOCAL_ORIGIN : req.inPlace ? (resolveCiJob(req.hostEnv ?? process.env)?.runOrigin ?? CHANT_RUNNER_ORIGIN) : CHANT_RUNNER_ORIGIN;
558
+ // In place, the job writes its record on the checkout's chant/lifecycle, which a forge checkout may not have fetched.
559
+ if (req.inPlace && req.push !== false)
560
+ await fetchLifecycleStatus({ cwd: top }).catch(() => undefined);
561
+ const checkout = jobCheckout(top, commit, req.inPlace);
311
562
  try {
312
563
  const jobDir = place.dir === "." ? checkout.dir : join(checkout.dir, place.dir);
313
564
  const op = ciJobOp({
@@ -315,7 +566,7 @@ export async function ciRun(req) {
315
566
  env: req.env,
316
567
  cwd: jobDir,
317
568
  jobEnv,
318
- setup: prLoopSetup(req.image ?? PR_LOOP_IMAGE),
569
+ setup: req.inPlace ? [] : prLoopSetup(req.image ?? PR_LOOP_IMAGE),
319
570
  build: [...(req.beforeScript ?? []), ...(job.build ?? [])],
320
571
  ...(req.chant ? { chant: req.chant } : {}),
321
572
  });
@@ -326,6 +577,22 @@ export async function ciRun(req) {
326
577
  mkdirSync(dirname(resume.job), { recursive: true });
327
578
  copyFileSync(resume.host, resume.job);
328
579
  }
580
+ const statusContext = ciStatusContext(req.job, req.loop?.gate);
581
+ const statusUrl = status
582
+ ? ciStatusUrl({ ...(status.urlTemplate ? { urlTemplate: status.urlTemplate } : {}), ...(reportUrl ? { reportUrl } : {}), ...(req.pr !== undefined ? { pr: req.pr } : {}), commit, ...(place.member ? { member: place.member } : {}) })
583
+ : undefined;
584
+ const postStatus = async (state, description) => {
585
+ if (!status)
586
+ return;
587
+ try {
588
+ await status.client.setCommitStatus(commit, { context: statusContext, state, description: cut(redact ? redact(description) : description), ...(statusUrl ? { targetUrl: statusUrl } : {}) });
589
+ }
590
+ catch (err) {
591
+ warnings.push(`could not set the ${statusContext} status: ${message(err)}`);
592
+ }
593
+ };
594
+ const startedAt = Date.now();
595
+ await postStatus("pending", `${op.name} running`);
329
596
  let ledgerError;
330
597
  let result;
331
598
  try {
@@ -335,22 +602,28 @@ export async function ciRun(req) {
335
602
  cwd: jobDir,
336
603
  commit,
337
604
  ledger: { cwd: jobDir },
338
- runner: { kind: "pipeline", runtime: "ci", runOrigin: CHANT_RUNNER_ORIGIN },
605
+ runner: { kind: "pipeline", runtime: local ? "local" : "ci", runOrigin: origin },
339
606
  describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir, PR_LOOP_REPORT_DIR) }),
340
607
  ...(req.runEvents ? { runEvents: { write: req.runEvents } } : {}),
341
608
  ...(req.stepOutput ? { stepOutput: req.stepOutput } : {}),
342
609
  ...(signer ? { signRecord: (record) => signOpRunRecord(record, signer) } : {}),
343
610
  onSignError: (err) => warnings.push(`the run was not signed: ${message(err)}`),
611
+ ...(redact ? { redact } : {}),
344
612
  onLedgerError: (err) => {
345
613
  ledgerError = `the job ran, and its record could not be written to chant/lifecycle: ${message(err)}`;
346
614
  },
347
615
  });
348
616
  }
349
617
  catch (err) {
350
- if (!(err instanceof OpRunFailure))
618
+ if (!(err instanceof OpRunFailure)) {
619
+ await postStatus("error", `${op.name} stopped: ${firstLine(message(err))}`);
351
620
  throw err;
621
+ }
352
622
  result = err.result;
353
623
  }
624
+ const failedStep = result.records.find((r) => r.status === "fail");
625
+ const settled = ciStatusSettled(op.name, { status: result.status, record: result.record, ...(failedStep ? { failure: { phase: failedStep.phase, error: failedStep.error ?? "the step failed" } } : {}) }, Math.round((Date.now() - startedAt) / 1000));
626
+ await postStatus(settled.state, settled.description);
354
627
  if (resume && existsSync(resume.job)) {
355
628
  try {
356
629
  mkdirSync(dirname(resume.host), { recursive: true });
@@ -364,29 +637,69 @@ export async function ciRun(req) {
364
637
  let report = null;
365
638
  if (existsSync(written)) {
366
639
  report = resolve(cwd, req.output ?? PR_LOOP_REPORT_DIR);
367
- cpSync(written, report, { recursive: true });
640
+ // In place the report is already where the forge keeps it, and stays as the command wrote it.
641
+ if (!(existsSync(report) && realpathSync(report) === realpathSync(written))) {
642
+ cpSync(written, report, { recursive: true });
643
+ if (redact)
644
+ redactTree(report, redact);
645
+ }
368
646
  }
369
- if (ledgerError)
370
- throw new CiRunError(ledgerError);
371
- const pushed = req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
372
647
  const failed = result.records.find((r) => r.status === "fail");
648
+ const exitCode = failed?.phase === req.job ? exitCodeOf(failed.error) : undefined;
649
+ if (reportUrl) {
650
+ await recordInIndex({ url: reportUrl, job: req.job, commit, member: place.member, pr: req.pr, record: result.record, runOrigin: origin, signer: result.record.statement && signer ? signer.principal : null, exitCode: ciJobExitCode({ status: result.status, ...(exitCode !== undefined ? { exitCode } : {}) }), reportDir: written, limit: req.runIndexLimit, ...(redact ? { redact } : {}), warn: (w) => warnings.push(w) });
651
+ }
652
+ // In place the forge job's outcome is the command's, so a record that could not be written is a warning there.
653
+ if (ledgerError && req.inPlace)
654
+ warnings.push(ledgerError);
655
+ else if (ledgerError)
656
+ throw new CiRunError(ledgerError);
657
+ const pushed = local
658
+ ? { pushed: false }
659
+ : req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
373
660
  return {
374
661
  op: op.name,
375
662
  status: result.status,
376
663
  record: result.record,
377
664
  ...(failed ? { failure: { phase: failed.phase, error: failed.error ?? "the step failed" } } : {}),
665
+ ...(exitCode !== undefined ? { exitCode } : {}),
378
666
  report,
379
- signer: signer?.principal ?? null,
667
+ signer: result.record.statement && signer ? signer.principal : null,
380
668
  pushed: pushed.pushed,
381
669
  ...(pushed.pushWarning ? { pushWarning: pushed.pushWarning } : {}),
382
670
  warnings,
671
+ notes,
383
672
  checkout: checkout.dir,
673
+ ...(local ? { local: true } : {}),
384
674
  };
385
675
  }
386
676
  finally {
387
677
  checkout.remove();
388
678
  }
389
679
  }
680
+ /**
681
+ * Redact every text file under `dir` in place (#4096): the job's report, its
682
+ * change set and its component events, as `ci run` copied them out. A
683
+ * JSON file stays JSON. A file with a NUL byte is taken as binary and left.
684
+ */
685
+ export function redactTree(dir, redact) {
686
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
687
+ const path = join(dir, entry.name);
688
+ if (entry.isDirectory()) {
689
+ redactTree(path, redact);
690
+ continue;
691
+ }
692
+ if (!entry.isFile())
693
+ continue;
694
+ const bytes = readFileSync(path);
695
+ if (bytes.includes(0))
696
+ continue;
697
+ const text = bytes.toString("utf-8");
698
+ const clean = redactText(text, redact);
699
+ if (clean !== text)
700
+ writeFileSync(path, clean);
701
+ }
702
+ }
390
703
  /** The text `chant ci run` prints when the run settles. */
391
704
  export function formatCiRun(r) {
392
705
  const lines = [`${r.op} ${r.status === "ok" ? "passed" : r.status === "fail" ? "failed" : r.status} at ${r.record.gitSha?.slice(0, 12) ?? "an unknown commit"}, run ${r.record.id} in ${r.record.env}`];
@@ -395,23 +708,64 @@ export function formatCiRun(r) {
395
708
  lines.push(` failure: ${why.code}${why.phase ? ` in ${why.phase}` : ""}${why.exitCode !== undefined ? `, exit ${why.exitCode}` : ""}: ${why.reason}`);
396
709
  else if (r.failure)
397
710
  lines.push(` ${r.failure.phase}: ${r.failure.error}`);
398
- lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned, so ci tick from runs does not count it"}`);
711
+ lines.push(` record: ${r.record.env}/runs__${r.op}.jsonl on chant/lifecycle, ${r.signer ? `signed by ${r.signer}` : "unsigned"}`);
399
712
  if (r.report)
400
713
  lines.push(` report: ${r.report}`);
401
- if (!r.pushed)
714
+ if (r.local)
715
+ lines.push(" local run: the record stays on this machine, with no push, commit status or report upload");
716
+ else if (!r.pushed)
402
717
  lines.push(` ${r.pushWarning ?? "chant/lifecycle was not pushed"}`);
403
718
  return lines.join("\n");
404
719
  }
405
720
  // ── The command ──────────────────────────────────────────────────────────────
721
+ /**
722
+ * The generator's options from `ci run`'s and `ci serve`'s flags, as `chant
723
+ * build --components --generate <lexicon> --pr-loop` takes them: `--gate`,
724
+ * `--branch`, `--events-json`, `--report-url`, `--pr-build`,
725
+ * `--pr-build-command`, `--wave-gate` (#4003) and `--no-require-review`
726
+ * (#4156).
727
+ */
728
+ export function ciLoopOptions(args) {
729
+ return {
730
+ ...(args.gate ? { gate: args.gate } : {}),
731
+ ...(args.branch ? { branch: args.branch } : {}),
732
+ ...(args.eventsJson ? { events: true } : {}),
733
+ ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
734
+ ...(args.prBuild ? { build: true } : {}),
735
+ ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
736
+ ...(args.waveGate ? { waveGate: args.waveGate } : {}),
737
+ ...(args.requireReview === false ? { requireReview: false } : {}),
738
+ };
739
+ }
406
740
  const USAGE = "chant ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
407
- "[--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--image <image>] [--before-script <line>]... " +
408
- "[--pr-build] [--pr-build-command <line>] " +
409
- "[--chant <command>] [--pass-env <NAME>]... [--key <runner key.pem>] [-o <dir>]";
741
+ "[--gate <name>] [--wave-gate <name>] [--wave <k>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
742
+ "[--pr-build] [--pr-build-command <line>] [--no-require-review] [--no-status] [--status-url <url template>] " +
743
+ "[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] [--in-place | --local] [--no-push] [--verbose] | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
410
744
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
745
+ /** The host's runner key in `--state`, or in ci serve's default state directory, when `--setup-key` made one there (#4099). */
746
+ function hostRunnerKeyAt(cwd, statePath) {
747
+ const common = statePath ? null : (() => {
748
+ try {
749
+ return execFileSync("git", ["rev-parse", "--path-format=absolute", "--git-common-dir"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
750
+ }
751
+ catch {
752
+ return null;
753
+ }
754
+ })();
755
+ const dir = statePath ? resolve(statePath) : common ? join(common, "chant", "ci-serve") : null;
756
+ return dir ? hostRunnerKey(dir) : undefined;
757
+ }
758
+ /** The secrets file `--secrets` names, else the one `CHANT_CI_SECRETS` names, read; undefined when neither names one (#4096). */
759
+ export function ciSecretsOf(flag, env = process.env) {
760
+ const file = flag ?? (env[CI_SECRETS_ENV] || undefined);
761
+ return file === undefined ? undefined : readCiSecrets(resolve(file));
762
+ }
411
763
  /** `chant ci run plan|apply` (#4094). */
412
764
  export async function runCiRun(ctx) {
413
765
  const { args } = ctx;
414
766
  try {
767
+ if (args.setupKey)
768
+ return (await import("./ci-serve.js")).runnerKeySetupCommand(ctx);
415
769
  const job = args.extraPositional;
416
770
  if (job !== "plan" && job !== "apply")
417
771
  throw new CiRunError(`ci run takes the job to run, plan or apply${job ? `, not ${JSON.stringify(job)}` : ""}`);
@@ -426,27 +780,38 @@ export async function runCiRun(ctx) {
426
780
  if (!ENV_NAME.test(name))
427
781
  throw new CiRunError(`--pass-env takes a variable's name, not ${JSON.stringify(name)}`);
428
782
  const fromFlag = args.key !== undefined;
429
- const runnerKey = fromFlag ? readFileSync(resolve(args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || undefined;
783
+ // #4099: without --key or CHANT_RUNNER_KEY, the host's key that --setup-key made, if there is one. Like the variable, an unlisted one only warns.
784
+ const hostKey = fromFlag || process.env[RUNNER_KEY_ENV] ? undefined : hostRunnerKeyAt(process.cwd(), args.statePath);
785
+ const runnerKey = fromFlag ? readFileSync(resolve(args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || hostKey?.pem;
786
+ const hostEnv = process.env;
787
+ // #4156: in place the forge shows the job, so ci run sets no status of its own.
788
+ // #4161: a local run sets no status, so it builds no client and warns about none.
789
+ const { status, warning } = ciStatusFromEnv({ forge: forge, hostEnv, noStatus: args.noStatus || args.ciInPlace || args.local, statusUrl: args.statusUrl });
790
+ if (warning)
791
+ console.error(formatWarning({ message: warning }));
792
+ const secrets = ciSecretsOf(args.ciSecrets);
793
+ // #4160: without --report-url, the declaration's ci.reportUrl.
794
+ const reportUrl = await reportUrlOf(args.reportUrl, process.cwd());
430
795
  const result = await ciRun({
431
796
  cwd: process.cwd(),
797
+ ...(status ? { status } : {}),
432
798
  job,
433
799
  env: args.env,
434
800
  forge: forge,
435
801
  ...(args.base ? { base: args.base } : {}),
436
802
  ...(args.pr !== undefined ? { pr: args.pr } : {}),
437
803
  ...(args.commit ? { commit: args.commit } : {}),
438
- loop: {
439
- ...(args.gate ? { gate: args.gate } : {}),
440
- ...(args.branch ? { branch: args.branch } : {}),
441
- ...(args.eventsJson ? { events: true } : {}),
442
- ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
443
- ...(args.prBuild ? { build: true } : {}),
444
- ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
445
- },
804
+ loop: ciLoopOptions(reportUrl ? { ...args, reportUrl } : args),
805
+ ...(args.ciInPlace ? { inPlace: true } : {}),
806
+ ...(args.local ? { local: true } : {}),
807
+ ...(args.noPush ? { push: false } : {}),
808
+ ...(args.wave !== undefined ? { wave: args.wave } : {}),
809
+ ...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
446
810
  ...(args.image ? { image: args.image } : {}),
447
811
  ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
448
812
  ...(args.ciChant ? { chant: args.ciChant } : {}),
449
813
  ...(args.passEnv?.length ? { passEnv: args.passEnv } : {}),
814
+ ...(secrets ? { secrets } : {}),
450
815
  ...(args.output ? { output: args.output } : {}),
451
816
  ...(runnerKey !== undefined ? { runnerKey, runnerKeyFromEnv: !fromFlag } : {}),
452
817
  ...(args.eventsJson ? { runEvents: (line) => void process.stdout.write(line) } : {}),
@@ -454,8 +819,12 @@ export async function runCiRun(ctx) {
454
819
  });
455
820
  for (const w of result.warnings)
456
821
  console.error(formatWarning({ message: w }));
822
+ if (args.verbose)
823
+ for (const n of result.notes)
824
+ console.error(`ci run: ${n}`);
457
825
  console.error(formatCiRun(result));
458
- return result.status === "ok" ? 0 : 1;
826
+ // #4156: a failed command's own exit code, such as 3 for a moved plan, is the job's, as it was before the job ran through ci run.
827
+ return ciJobExitCode(result);
459
828
  }
460
829
  catch (err) {
461
830
  console.error(formatError({ message: `chant ci run: ${message(err)}`, hint: USAGE }));