@intentius/chant 0.124.31 → 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 (321) 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 +44 -16
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +15 -0
  41. package/dist/cli/registry.d.ts +13 -4
  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/op-waves-run.d.ts.map +1 -1
  111. package/dist/op/op-waves-run.js +20 -5
  112. package/dist/op/operator.d.ts +2 -2
  113. package/dist/op/operator.js +2 -2
  114. package/dist/op/steward.d.ts +6 -4
  115. package/dist/op/steward.d.ts.map +1 -1
  116. package/dist/op/steward.js +5 -3
  117. package/dist/op/work-lease-run.d.ts +20 -7
  118. package/dist/op/work-lease-run.d.ts.map +1 -1
  119. package/dist/op/work-lease-run.js +56 -12
  120. package/dist/pr-forge.d.ts +10 -0
  121. package/dist/pr-forge.d.ts.map +1 -1
  122. package/dist/pr-forge.js +39 -0
  123. package/dist/pr-loop.d.ts +141 -4
  124. package/dist/pr-loop.d.ts.map +1 -1
  125. package/dist/pr-loop.js +122 -6
  126. package/dist/pr-report-url.d.ts +2 -2
  127. package/dist/pr-report-url.d.ts.map +1 -1
  128. package/dist/pr-report-url.js +5 -5
  129. package/dist/ts-loader.d.ts +38 -4
  130. package/dist/ts-loader.d.ts.map +1 -1
  131. package/dist/ts-loader.js +190 -16
  132. package/dist/wave-runtime.mjs +208 -208
  133. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  134. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  135. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  136. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  137. package/dist/workspace/chant-migrations.js +2 -0
  138. package/dist/workspace/checks/boxes.d.ts +6 -6
  139. package/dist/workspace/checks/boxes.js +9 -9
  140. package/dist/workspace/ci-green-forge.d.ts +6 -4
  141. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  142. package/dist/workspace/ci-green.d.ts +28 -12
  143. package/dist/workspace/ci-green.d.ts.map +1 -1
  144. package/dist/workspace/ci-green.js +49 -21
  145. package/dist/workspace/ci-list.schema.json +12 -6
  146. package/dist/workspace/ci-report-url.d.ts +18 -0
  147. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  148. package/dist/workspace/ci-report-url.js +33 -0
  149. package/dist/workspace/ci-run-index.d.ts +12 -0
  150. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  151. package/dist/workspace/ci-run-index.js +6 -4
  152. package/dist/workspace/ci-run-index.schema.json +5 -3
  153. package/dist/workspace/ci-run.d.ts +75 -2
  154. package/dist/workspace/ci-run.d.ts.map +1 -1
  155. package/dist/workspace/ci-run.js +195 -53
  156. package/dist/workspace/ci-runner-key.d.ts +16 -3
  157. package/dist/workspace/ci-runner-key.d.ts.map +1 -1
  158. package/dist/workspace/ci-runner-key.js +30 -10
  159. package/dist/workspace/ci-serve.d.ts +54 -4
  160. package/dist/workspace/ci-serve.d.ts.map +1 -1
  161. package/dist/workspace/ci-serve.js +175 -26
  162. package/dist/workspace/declaration.d.ts +8 -0
  163. package/dist/workspace/declaration.d.ts.map +1 -1
  164. package/dist/workspace/declaration.js +11 -2
  165. package/dist/workspace/declaration.schema.json +7 -2
  166. package/dist/workspace/export.d.ts +1 -1
  167. package/dist/workspace/export.d.ts.map +1 -1
  168. package/dist/workspace/export.js +7 -3
  169. package/dist/workspace/ls.d.ts +6 -1
  170. package/dist/workspace/ls.d.ts.map +1 -1
  171. package/dist/workspace/ls.js +1 -0
  172. package/dist/workspace/ls.schema.json +7 -1
  173. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  174. package/dist/workspace/pin-integrity.js +3 -2
  175. package/dist/workspace/pr-report.schema.json +74 -7
  176. package/dist/workspace/reason-codes.d.ts +1 -1
  177. package/dist/workspace/reason-codes.d.ts.map +1 -1
  178. package/dist/workspace/reason-codes.js +2 -2
  179. package/dist/workspace/run-event.schema.json +3 -1
  180. package/dist/workspace/status.schema.json +3 -1
  181. package/dist/workspace/write-scope.d.ts +9 -0
  182. package/dist/workspace/write-scope.d.ts.map +1 -1
  183. package/dist/workspace/write-scope.js +11 -1
  184. package/package.json +1 -1
  185. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  186. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  187. package/src/cli/commands/audit.test.ts +2 -2
  188. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  189. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  190. package/src/cli/commands/check-lexicon.test.ts +1 -1
  191. package/src/cli/commands/check-lexicon.ts +16 -11
  192. package/src/cli/commands/import.ts +40 -0
  193. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  194. package/src/cli/commands/init-lexicon.test.ts +8 -5
  195. package/src/cli/commands/onboard.test.ts +45 -0
  196. package/src/cli/commands/onboard.ts +8 -4
  197. package/src/cli/commands/update.test.ts +6 -0
  198. package/src/cli/commands/update.ts +4 -1
  199. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  200. package/src/cli/dependencies.e2e.test.ts +101 -0
  201. package/src/cli/dependencies.test.ts +93 -0
  202. package/src/cli/dependencies.ts +122 -0
  203. package/src/cli/handlers/build.test.ts +69 -0
  204. package/src/cli/handlers/build.ts +20 -4
  205. package/src/cli/handlers/fan-out.test.ts +73 -0
  206. package/src/cli/handlers/fan-out.ts +11 -4
  207. package/src/cli/handlers/graph.test.ts +76 -0
  208. package/src/cli/handlers/graph.ts +19 -12
  209. package/src/cli/handlers/lint.test.ts +42 -0
  210. package/src/cli/handlers/lint.ts +6 -0
  211. package/src/cli/handlers/operator.ts +3 -3
  212. package/src/cli/handlers/pr-waves.test.ts +449 -0
  213. package/src/cli/handlers/pr.test.ts +134 -0
  214. package/src/cli/handlers/pr.ts +523 -31
  215. package/src/cli/handlers/promote.test.ts +26 -1
  216. package/src/cli/handlers/promote.ts +3 -2
  217. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  218. package/src/cli/handlers/run-signer.test.ts +79 -0
  219. package/src/cli/handlers/run.ts +34 -16
  220. package/src/cli/main.test.ts +18 -0
  221. package/src/cli/main.ts +43 -16
  222. package/src/cli/parse-args.ts +12 -0
  223. package/src/cli/registry.ts +19 -6
  224. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  225. package/src/codegen/lexicon-regen.ts +1 -1
  226. package/src/codegen/pinned-upgrade.ts +2 -2
  227. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  228. package/src/components/capability.ts +51 -3
  229. package/src/components/changed-files.test.ts +16 -1
  230. package/src/components/changed-files.ts +9 -1
  231. package/src/components/driver.test.ts +36 -1
  232. package/src/components/driver.ts +76 -4
  233. package/src/components/fan-out-record.ts +29 -1
  234. package/src/components/fan-out-run.test.ts +35 -0
  235. package/src/components/fan-out-run.ts +40 -1
  236. package/src/components/pr-pipeline.test.ts +100 -0
  237. package/src/components/pr-pipeline.ts +132 -17
  238. package/src/components/pr-run.test.ts +323 -18
  239. package/src/components/pr-run.ts +354 -75
  240. package/src/components/wave-plan.ts +16 -4
  241. package/src/discovery/fold-host-method.test.ts +87 -0
  242. package/src/discovery/fold-import.ts +15 -5
  243. package/src/discovery/fold-package-composite.test.ts +27 -0
  244. package/src/discovery/fold-specifiers.test.ts +8 -0
  245. package/src/discovery/sandbox/bundle.ts +6 -0
  246. package/src/discovery/specifier-resolve.ts +8 -2
  247. package/src/fold/fold.test.ts +72 -0
  248. package/src/fold/fold.ts +71 -0
  249. package/src/fold/subset.test.ts +31 -0
  250. package/src/fold/subset.ts +99 -0
  251. package/src/lexicon-dist.e2e.test.ts +254 -0
  252. package/src/lexicon-module.test.ts +47 -0
  253. package/src/lexicon-module.ts +49 -7
  254. package/src/lexicon-plugin-helpers.ts +6 -4
  255. package/src/lexicon.ts +2 -0
  256. package/src/lifecycle/affected.test.ts +7 -0
  257. package/src/lifecycle/affected.ts +6 -1
  258. package/src/lifecycle/release-ledger.ts +34 -0
  259. package/src/lifecycle/work-dir.test.ts +77 -0
  260. package/src/lifecycle/work-dir.ts +82 -0
  261. package/src/lint/discover.test.ts +46 -2
  262. package/src/lint/discover.ts +55 -49
  263. package/src/module-source.test.ts +85 -0
  264. package/src/module-source.ts +112 -0
  265. package/src/op/activities/factory.ts +1 -1
  266. package/src/op/discover-root.test.ts +31 -2
  267. package/src/op/discover.ts +14 -4
  268. package/src/op/gate.test.ts +30 -0
  269. package/src/op/gate.ts +3 -0
  270. package/src/op/op-waves-run.ts +20 -5
  271. package/src/op/op-waves.test.ts +49 -0
  272. package/src/op/operator.ts +2 -2
  273. package/src/op/steward.ts +6 -4
  274. package/src/op/work-lease-run.test.ts +88 -2
  275. package/src/op/work-lease-run.ts +52 -12
  276. package/src/pr-forge-bundle.test.ts +2 -2
  277. package/src/pr-forge.test.ts +42 -0
  278. package/src/pr-forge.ts +40 -0
  279. package/src/pr-loop-bundle.test.ts +2 -1
  280. package/src/pr-loop.test.ts +117 -1
  281. package/src/pr-loop.ts +252 -10
  282. package/src/pr-report-url.ts +5 -5
  283. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  284. package/src/ts-loader.test.ts +89 -1
  285. package/src/ts-loader.ts +180 -13
  286. package/src/workspace/changes.test.ts +1 -1
  287. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  288. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  289. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  290. package/src/workspace/chant-migrations.ts +2 -0
  291. package/src/workspace/checks/boxes.ts +9 -9
  292. package/src/workspace/ci-green-forge.ts +6 -4
  293. package/src/workspace/ci-green-runs.test.ts +53 -11
  294. package/src/workspace/ci-green.test.ts +1 -1
  295. package/src/workspace/ci-green.ts +50 -22
  296. package/src/workspace/ci-list.schema.json +12 -6
  297. package/src/workspace/ci-report-url.test.ts +220 -0
  298. package/src/workspace/ci-report-url.ts +33 -0
  299. package/src/workspace/ci-run-index.schema.json +5 -3
  300. package/src/workspace/ci-run-index.test.ts +42 -3
  301. package/src/workspace/ci-run-index.ts +18 -4
  302. package/src/workspace/ci-run.test.ts +185 -7
  303. package/src/workspace/ci-run.ts +224 -54
  304. package/src/workspace/ci-runner-key.test.ts +55 -17
  305. package/src/workspace/ci-runner-key.ts +36 -11
  306. package/src/workspace/ci-serve.test.ts +150 -6
  307. package/src/workspace/ci-serve.ts +169 -25
  308. package/src/workspace/declaration.schema.json +7 -2
  309. package/src/workspace/declaration.test.ts +23 -0
  310. package/src/workspace/declaration.ts +19 -3
  311. package/src/workspace/export.ts +6 -3
  312. package/src/workspace/ls.schema.json +7 -1
  313. package/src/workspace/ls.ts +7 -2
  314. package/src/workspace/pin-integrity.test.ts +5 -0
  315. package/src/workspace/pin-integrity.ts +3 -2
  316. package/src/workspace/pr-report.schema.json +74 -7
  317. package/src/workspace/reason-codes.ts +2 -2
  318. package/src/workspace/run-event.schema.json +3 -1
  319. package/src/workspace/status.schema.json +3 -1
  320. package/src/workspace/write-scope.test.ts +29 -1
  321. package/src/workspace/write-scope.ts +12 -1
@@ -44,6 +44,30 @@
44
44
  * With a forge client (#4097) the job also sets a commit status at its
45
45
  * commit: `pending` when it starts, `success` or `failure` when it settles.
46
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.
47
71
  */
48
72
  import { execFileSync } from "node:child_process";
49
73
  import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
@@ -53,14 +77,17 @@ import { PR_APPLY_RECORD, PR_LOOP_REPORT_DIR, prLoopJobs, prLoopSetup, PR_LOOP_I
53
77
  import { detectForge, ForgeEnvironmentError, forgeFromEnv, FORGE_KINDS } from "../pr-forge.js";
54
78
  import { PR_APPLY_GATE } from "../pr-loop.js";
55
79
  import { formatError, formatWarning } from "../cli/format.js";
56
- import { pushLifecycleReport } from "../lifecycle/git.js";
80
+ import { resolveCiJob } from "../lifecycle/release-ledger.js";
81
+ import { fetchLifecycleStatus, pushLifecycleReport } from "../lifecycle/git.js";
57
82
  import { ACTIVITY_PROFILES } from "../op/activity-profiles.js";
58
83
  import { shellCmd } from "../op/activities/shell.js";
59
84
  import { OpRunFailure, runOpLocally } from "../op/local-executor.js";
60
85
  import { redactText } from "../op/redact.js";
61
86
  import { findWorkspaceRoot } from "../project-root.js";
62
87
  import { parseReportUrl, reportKey } from "../pr-report-url.js";
63
- import { ciTagSigner, RUNNER_KEY_ENV } from "./ci-green.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";
64
91
  import { ciRedactor, ciSecretsFor, CI_SECRETS_ENV, readCiSecrets } from "./ci-secrets.js";
65
92
  import { updateCiRunIndex } from "./ci-run-index.js";
66
93
  import { resolveMemberContext } from "./member-pipeline.js";
@@ -72,6 +99,8 @@ export const CI_JOB_NAMES = ["plan", "apply"];
72
99
  export const CI_JOB_OPS = { plan: "pr-plan", apply: "pr-apply" };
73
100
  /** Where a job the chant runner ran says it ran (#4094). */
74
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" };
75
104
  /** The runner's variables every job gets, when the runner has them. */
76
105
  export const CI_RUN_BASE_ENV = ["PATH", "HOME", "USER", "LOGNAME", "SHELL", "LANG", "LC_ALL", "LC_CTYPE", "TMPDIR", "TZ"];
77
106
  /**
@@ -190,6 +219,17 @@ function exitCodeOf(error) {
190
219
  const m = /command exited (\d+)/.exec(error ?? "");
191
220
  return m ? Number(m[1]) : undefined;
192
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
+ }
193
233
  /** The first line of a step's error, for a reason. */
194
234
  const firstLine = (text) => text.split("\n").find((l) => l.trim() !== "")?.trim() ?? text;
195
235
  /**
@@ -396,7 +436,9 @@ async function recordInIndex(input) {
396
436
  ended: input.record.ended,
397
437
  runId: clean(input.record.id),
398
438
  key: clean(reportKey(pr, input.commit, input.member)),
399
- runOrigin: CHANT_RUNNER_ORIGIN,
439
+ runOrigin: input.runOrigin,
440
+ signer: input.signer,
441
+ exitCode: input.exitCode,
400
442
  };
401
443
  await updateCiRunIndex({ target, entry, ...(input.limit ? { limit: input.limit } : {}), warn: input.warn });
402
444
  }
@@ -413,12 +455,21 @@ export async function ciRun(req) {
413
455
  if (!top)
414
456
  throw new CiRunError("a job runs in a fresh checkout of a commit, and this directory is not in a git repository");
415
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");
416
462
  const place = ciJobPlace(cwd, top);
417
- const jobs = prLoopJobs(req.forge, req.env, req.loop ?? {}, place.member);
418
- const job = jobs.find((j) => j.jobName === req.job);
419
- 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)
420
466
  throw new CiRunError(`the pull-request pipeline has no ${req.job} job; it has ${jobs.map((j) => j.jobName).join(" and ")}`);
421
- const jobEnv = ciJobEnv(job, {
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, {
422
473
  forge: req.forge,
423
474
  env: req.env,
424
475
  values: { base: req.base, pr: req.pr, commit },
@@ -426,11 +477,15 @@ export async function ciRun(req) {
426
477
  passEnv: req.passEnv,
427
478
  secrets: ciSecretsFor(req.secrets, req.job),
428
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];
429
483
  // #4096: the secrets and the job's secret-looking variables, redacted from everything the run writes and returns.
430
- const redact = ciRedactor(jobEnv, req.secrets);
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);
431
486
  try {
432
487
  const result = await runCiJob(req, { cwd, top, commit, place, job, jobEnv, ...(redact ? { redact } : {}) });
433
- return redact ? { ...result, warnings: result.warnings.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
488
+ return redact ? { ...result, warnings: result.warnings.map(redact), notes: result.notes.map(redact), ...(result.pushWarning ? { pushWarning: redact(result.pushWarning) } : {}) } : result;
434
489
  }
435
490
  catch (err) {
436
491
  if (redact && err instanceof Error)
@@ -438,23 +493,72 @@ export async function ciRun(req) {
438
493
  throw err;
439
494
  }
440
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)}`);
532
+ }
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
+ }
441
546
  /** {@link ciRun} once the job and its environment are known. */
442
547
  async function runCiJob(req, at) {
443
548
  const { cwd, top, commit, place, job, jobEnv, redact } = at;
444
- // 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.
445
- let signer;
446
- const early = [];
447
- if (req.runnerKey !== undefined) {
448
- try {
449
- signer = ciTagSigner(top, commit, req.runnerKey, "the runs it signed");
450
- }
451
- catch (err) {
452
- if (!req.runnerKeyFromEnv)
453
- throw new CiRunError(`the run would not be signed: ${message(err)}`);
454
- early.push(`${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message(err)}`);
455
- }
456
- }
457
- const checkout = freshCheckout(top, commit);
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);
458
562
  try {
459
563
  const jobDir = place.dir === "." ? checkout.dir : join(checkout.dir, place.dir);
460
564
  const op = ciJobOp({
@@ -462,7 +566,7 @@ async function runCiJob(req, at) {
462
566
  env: req.env,
463
567
  cwd: jobDir,
464
568
  jobEnv,
465
- setup: prLoopSetup(req.image ?? PR_LOOP_IMAGE),
569
+ setup: req.inPlace ? [] : prLoopSetup(req.image ?? PR_LOOP_IMAGE),
466
570
  build: [...(req.beforeScript ?? []), ...(job.build ?? [])],
467
571
  ...(req.chant ? { chant: req.chant } : {}),
468
572
  });
@@ -474,14 +578,14 @@ async function runCiJob(req, at) {
474
578
  copyFileSync(resume.host, resume.job);
475
579
  }
476
580
  const statusContext = ciStatusContext(req.job, req.loop?.gate);
477
- const statusUrl = req.status
478
- ? ciStatusUrl({ ...(req.status.urlTemplate ? { urlTemplate: req.status.urlTemplate } : {}), ...(req.loop?.reportUrl ? { reportUrl: req.loop.reportUrl } : {}), ...(req.pr !== undefined ? { pr: req.pr } : {}), commit, ...(place.member ? { member: place.member } : {}) })
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 } : {}) })
479
583
  : undefined;
480
584
  const postStatus = async (state, description) => {
481
- if (!req.status)
585
+ if (!status)
482
586
  return;
483
587
  try {
484
- await req.status.client.setCommitStatus(commit, { context: statusContext, state, description: cut(redact ? redact(description) : description), ...(statusUrl ? { targetUrl: statusUrl } : {}) });
588
+ await status.client.setCommitStatus(commit, { context: statusContext, state, description: cut(redact ? redact(description) : description), ...(statusUrl ? { targetUrl: statusUrl } : {}) });
485
589
  }
486
590
  catch (err) {
487
591
  warnings.push(`could not set the ${statusContext} status: ${message(err)}`);
@@ -498,7 +602,7 @@ async function runCiJob(req, at) {
498
602
  cwd: jobDir,
499
603
  commit,
500
604
  ledger: { cwd: jobDir },
501
- runner: { kind: "pipeline", runtime: "ci", runOrigin: CHANT_RUNNER_ORIGIN },
605
+ runner: { kind: "pipeline", runtime: local ? "local" : "ci", runOrigin: origin },
502
606
  describeFailure: (records) => ciJobFailure({ job: req.job, op, records, reportDir: join(jobDir, PR_LOOP_REPORT_DIR) }),
503
607
  ...(req.runEvents ? { runEvents: { write: req.runEvents } } : {}),
504
608
  ...(req.stepOutput ? { stepOutput: req.stepOutput } : {}),
@@ -533,28 +637,40 @@ async function runCiJob(req, at) {
533
637
  let report = null;
534
638
  if (existsSync(written)) {
535
639
  report = resolve(cwd, req.output ?? PR_LOOP_REPORT_DIR);
536
- cpSync(written, report, { recursive: true });
537
- if (redact)
538
- redactTree(report, redact);
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
+ }
539
646
  }
540
- if (req.loop?.reportUrl) {
541
- await recordInIndex({ url: req.loop.reportUrl, job: req.job, commit, member: place.member, pr: req.pr, record: result.record, reportDir: written, limit: req.runIndexLimit, ...(redact ? { redact } : {}), warn: (w) => warnings.push(w) });
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) });
542
651
  }
543
- if (ledgerError)
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)
544
656
  throw new CiRunError(ledgerError);
545
- const pushed = req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
546
- const failed = result.records.find((r) => r.status === "fail");
657
+ const pushed = local
658
+ ? { pushed: false }
659
+ : req.push === false ? { pushed: false, pushWarning: "chant/lifecycle was not pushed" } : await pushLifecycleReport({ cwd: top });
547
660
  return {
548
661
  op: op.name,
549
662
  status: result.status,
550
663
  record: result.record,
551
664
  ...(failed ? { failure: { phase: failed.phase, error: failed.error ?? "the step failed" } } : {}),
665
+ ...(exitCode !== undefined ? { exitCode } : {}),
552
666
  report,
553
- signer: signer?.principal ?? null,
667
+ signer: result.record.statement && signer ? signer.principal : null,
554
668
  pushed: pushed.pushed,
555
669
  ...(pushed.pushWarning ? { pushWarning: pushed.pushWarning } : {}),
556
670
  warnings,
671
+ notes,
557
672
  checkout: checkout.dir,
673
+ ...(local ? { local: true } : {}),
558
674
  };
559
675
  }
560
676
  finally {
@@ -592,18 +708,39 @@ export function formatCiRun(r) {
592
708
  lines.push(` failure: ${why.code}${why.phase ? ` in ${why.phase}` : ""}${why.exitCode !== undefined ? `, exit ${why.exitCode}` : ""}: ${why.reason}`);
593
709
  else if (r.failure)
594
710
  lines.push(` ${r.failure.phase}: ${r.failure.error}`);
595
- 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"}`);
596
712
  if (r.report)
597
713
  lines.push(` report: ${r.report}`);
598
- 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)
599
717
  lines.push(` ${r.pushWarning ?? "chant/lifecycle was not pushed"}`);
600
718
  return lines.join("\n");
601
719
  }
602
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
+ }
603
740
  const USAGE = "chant ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>] [--forge github|gitlab|forgejo] " +
604
- "[--gate <name>] [--branch <name>] [--events-json] [--report-url <url>] [--run-index-limit <n>] [--image <image>] [--before-script <line>]... " +
605
- "[--pr-build] [--pr-build-command <line>] [--no-status] [--status-url <url template>] " +
606
- "[--chant <command>] [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>] [-o <dir>] | chant ci run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]";
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>]";
607
744
  const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
608
745
  /** The host's runner key in `--state`, or in ci serve's default state directory, when `--setup-key` made one there (#4099). */
609
746
  function hostRunnerKeyAt(cwd, statePath) {
@@ -647,10 +784,14 @@ export async function runCiRun(ctx) {
647
784
  const hostKey = fromFlag || process.env[RUNNER_KEY_ENV] ? undefined : hostRunnerKeyAt(process.cwd(), args.statePath);
648
785
  const runnerKey = fromFlag ? readFileSync(resolve(args.key), "utf-8") : process.env[RUNNER_KEY_ENV] || hostKey?.pem;
649
786
  const hostEnv = process.env;
650
- const { status, warning } = ciStatusFromEnv({ forge: forge, hostEnv, noStatus: args.noStatus, statusUrl: args.statusUrl });
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 });
651
790
  if (warning)
652
791
  console.error(formatWarning({ message: warning }));
653
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());
654
795
  const result = await ciRun({
655
796
  cwd: process.cwd(),
656
797
  ...(status ? { status } : {}),
@@ -660,14 +801,11 @@ export async function runCiRun(ctx) {
660
801
  ...(args.base ? { base: args.base } : {}),
661
802
  ...(args.pr !== undefined ? { pr: args.pr } : {}),
662
803
  ...(args.commit ? { commit: args.commit } : {}),
663
- loop: {
664
- ...(args.gate ? { gate: args.gate } : {}),
665
- ...(args.branch ? { branch: args.branch } : {}),
666
- ...(args.eventsJson ? { events: true } : {}),
667
- ...(args.reportUrl ? { reportUrl: args.reportUrl } : {}),
668
- ...(args.prBuild ? { build: true } : {}),
669
- ...(args.prBuildCommand ? { buildCommand: args.prBuildCommand } : {}),
670
- },
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 } : {}),
671
809
  ...(args.runIndexLimit ? { runIndexLimit: args.runIndexLimit } : {}),
672
810
  ...(args.image ? { image: args.image } : {}),
673
811
  ...(args.beforeScript?.length ? { beforeScript: args.beforeScript } : {}),
@@ -681,8 +819,12 @@ export async function runCiRun(ctx) {
681
819
  });
682
820
  for (const w of result.warnings)
683
821
  console.error(formatWarning({ message: w }));
822
+ if (args.verbose)
823
+ for (const n of result.notes)
824
+ console.error(`ci run: ${n}`);
684
825
  console.error(formatCiRun(result));
685
- 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);
686
828
  }
687
829
  catch (err) {
688
830
  console.error(formatError({ message: `chant ci run: ${message(err)}`, hint: USAGE }));
@@ -101,9 +101,18 @@ export declare function formatRunnerKeySetup(s: RunnerKeySetup, branch: string):
101
101
  /**
102
102
  * The key `ci serve` signs with, checked before its first poll (#4099): the
103
103
  * `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
104
- * directory. Throws, with the command that lists it, when there is none or
105
- * `.chant/trust.json` at the target's head does not list it with the runner
106
- * role. With `unsigned` there is no key and nothing is checked.
104
+ * directory. Signing is opt-in (#4156):
105
+ *
106
+ * - With no key, `ci serve` serves unsigned and says so only in `note`.
107
+ * - A key `.chant/trust.json` at the target's head does not list with the
108
+ * runner role, or a target branch it can't find to check against, leaves
109
+ * it serving unsigned, with one `warning`.
110
+ * - With `required` (the declaration's `ci.green.signedSince`) each of
111
+ * those refuses to start, as before #4156.
112
+ *
113
+ * A key that is not an Ed25519 private key in PEM, or a `--key` file that
114
+ * can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
115
+ * key without `required` and is ignored with it.
107
116
  */
108
117
  export declare function ciServeRunnerKey(req: {
109
118
  top: string;
@@ -112,9 +121,13 @@ export declare function ciServeRunnerKey(req: {
112
121
  keyFile?: string;
113
122
  envKey?: string;
114
123
  unsigned?: boolean;
124
+ /** The declaration's `ci.green.signedSince`, when it sets one: every run must then be signed. */
125
+ required?: string | null;
115
126
  }): {
116
127
  pem?: string;
117
128
  fromFlag: boolean;
118
129
  source: string;
130
+ warning?: string;
131
+ note?: string;
119
132
  };
120
133
  //# sourceMappingURL=ci-runner-key.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ci-runner-key.d.ts","sourceRoot":"","sources":["../../src/workspace/ci-runner-key.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ci-runner-key.d.ts","sourceRoot":"","sources":["../../src/workspace/ci-runner-key.ts"],"names":[],"mappings":"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"}
@@ -184,18 +184,31 @@ export function formatRunnerKeySetup(s, branch) {
184
184
  /**
185
185
  * The key `ci serve` signs with, checked before its first poll (#4099): the
186
186
  * `--key` file, else `CHANT_RUNNER_KEY`, else the host's key in the state
187
- * directory. Throws, with the command that lists it, when there is none or
188
- * `.chant/trust.json` at the target's head does not list it with the runner
189
- * role. With `unsigned` there is no key and nothing is checked.
187
+ * directory. Signing is opt-in (#4156):
188
+ *
189
+ * - With no key, `ci serve` serves unsigned and says so only in `note`.
190
+ * - A key `.chant/trust.json` at the target's head does not list with the
191
+ * runner role, or a target branch it can't find to check against, leaves
192
+ * it serving unsigned, with one `warning`.
193
+ * - With `required` (the declaration's `ci.green.signedSince`) each of
194
+ * those refuses to start, as before #4156.
195
+ *
196
+ * A key that is not an Ed25519 private key in PEM, or a `--key` file that
197
+ * can't be read, refuses either way. `unsigned` (`--unsigned`) skips the
198
+ * key without `required` and is ignored with it.
190
199
  */
191
200
  export function ciServeRunnerKey(req) {
192
- if (req.unsigned)
193
- return { fromFlag: false, source: "none (--unsigned)" };
201
+ const required = req.required ?? null;
202
+ const optIn = `ci.green.signedSince is ${required}, so every run must be signed`;
203
+ if (req.unsigned && !required)
204
+ return { fromFlag: false, source: "none (--unsigned)", note: "runs are not signed (--unsigned)" };
194
205
  const host = req.keyFile === undefined && !req.envKey ? hostRunnerKey(req.stateDir) : undefined;
195
206
  const pem = req.keyFile !== undefined ? readFileSync(req.keyFile, "utf-8") : req.envKey || host?.pem;
196
207
  const source = req.keyFile !== undefined ? req.keyFile : req.envKey ? "CHANT_RUNNER_KEY" : (host?.path ?? join(req.stateDir, RUNNER_KEY_FILE));
197
208
  if (pem === undefined) {
198
- 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`);
209
+ if (!required)
210
+ 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" };
211
+ 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`);
199
212
  }
200
213
  let publicKey;
201
214
  try {
@@ -205,12 +218,19 @@ export function ciServeRunnerKey(req) {
205
218
  throw new CiRunError(`the runner key from ${source} is not an Ed25519 private key in PEM: ${message(err)}`);
206
219
  }
207
220
  const head = targetHead(req.top, req.branch);
208
- if (!head)
209
- 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`);
221
+ if (!head) {
222
+ 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`;
223
+ if (required)
224
+ throw new CiRunError(`${optIn}, and ${why}; fetch it`);
225
+ return { fromFlag: false, source, warning: `${why}, so runs are not signed; fetch it to sign them` };
226
+ }
210
227
  const listed = runnerKeyListed(req.top, head.commit, pem);
211
228
  if (!listed.ok) {
212
- throw new CiRunError(`the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}\n` +
213
- ` 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.`);
229
+ const why = `the runner key from ${source} is not trusted at ${branchAt(req.branch, head.commit)}: ${listed.reason}`;
230
+ 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.`;
231
+ if (required)
232
+ throw new CiRunError(`${optIn}, and ${why}\n ${fix}`);
233
+ return { fromFlag: false, source, warning: `${why}. Runs are not signed until it is. ${fix}` };
214
234
  }
215
235
  return { pem, fromFlag: req.keyFile !== undefined, source };
216
236
  }
@@ -23,8 +23,26 @@
23
23
  * as its `--resume` record, so a re-run of a failed apply at the same
24
24
  * commit finishes under the approval it has. This replaces the forge's CI
25
25
  * cache.
26
+ * - A gate per wave (#4003). With `--wave-gate` the apply job runs
27
+ * `pr-apply --wave-gate`, which applies the waves in order and stops at
28
+ * the first whose moved plan waits on its own gate. The job fails, and the
29
+ * resume record keeps the waves that applied; once the wave's plan is
30
+ * approved, `rerunFailed` runs the apply again and it carries on from
31
+ * that wave.
26
32
  * - Capacity. At most `maxJobs` jobs run at once. A job over the limit is not
27
33
  * recorded, so a later poll starts it.
34
+ * - Fast-forwards only (#4122). An apply runs only when the target's new head
35
+ * descends from the head seen before it. Any other move, such as a force
36
+ * push back to an older commit, is recorded as seen and not applied.
37
+ * - `[skip ci]` (#4122). A target head or pull request head whose commit
38
+ * message says `[skip ci]` or `[ci skip]` is recorded as seen and not run,
39
+ * as Actions does.
40
+ * - The forge's ledger (#4121). Before each job, when no other job of this
41
+ * serve runs, the clone's `chant/lifecycle` follows the forge's: it is
42
+ * deleted when the forge has none, and reset to the forge's when neither
43
+ * contains the other. Each is a warning. A local branch ahead of the
44
+ * forge's keeps its facts, and the job's push sends them. Nothing here
45
+ * pushes the ledger.
28
46
  *
29
47
  * Host mode only: each job runs in a fresh worktree on this host as this
30
48
  * user, with the clean environment `ciRun` gives it. A pull request from a
@@ -85,9 +103,11 @@ export interface CiServeState {
85
103
  env: string;
86
104
  branch: string;
87
105
  member?: string;
88
- /** The target branch's head the last apply was measured to, or the head the first poll found. */
106
+ /** The target branch's head the last apply was measured to, or the last head seen and not applied. */
89
107
  target?: string;
90
108
  jobs: Record<string, CiServeJobState>;
109
+ /** Plans seen and not run, by state key, with why (#4122). */
110
+ skipped?: Record<string, "skip-ci">;
91
111
  }
92
112
  /** What a pass did, in order, for the log. */
93
113
  export type CiServeEvent = {
@@ -125,6 +145,19 @@ export type CiServeEvent = {
125
145
  status: "ok" | "fail";
126
146
  failure?: string;
127
147
  report?: string;
148
+ } | {
149
+ type: "not-run";
150
+ job: CiJobName;
151
+ commit: string;
152
+ pr?: number;
153
+ reason: "not-fast-forward" | "skip-ci";
154
+ branch?: string;
155
+ from?: string;
156
+ } | {
157
+ type: "ledger";
158
+ action: "deleted" | "reset";
159
+ local: string;
160
+ forge?: string;
128
161
  } | {
129
162
  type: "error";
130
163
  message: string;
@@ -168,17 +201,34 @@ export declare function ciServeKey(t: {
168
201
  commit: string;
169
202
  pr?: number;
170
203
  }): string;
204
+ /**
205
+ * Make the clone's `chant/lifecycle` follow the forge's (#4121). The clone's
206
+ * branch outlives the forge's, so a ledger the forge deleted or rewrote would
207
+ * make the next pr-apply find the two diverged and stall.
208
+ *
209
+ * - The forge has none: the local branch and its remote-tracking ref go.
210
+ * - Neither contains the other: the local branch and its remote-tracking ref
211
+ * are reset to the forge's tip.
212
+ * - The local branch is ahead of the forge's, or behind it, or the same: it
213
+ * is left, and an apply's push sends what it holds that the forge lacks.
214
+ *
215
+ * Nothing is pushed, so a rewritten ledger never goes back to the forge. A
216
+ * forge that can't be reached leaves everything as it is; the job's own
217
+ * fetch says why.
218
+ */
219
+ export declare function reconcileCiServeLedger(top: string): CiServeEvent | undefined;
171
220
  /** Where a serve keeps its state when `--state` names nowhere: beside the run events, in the repository's common git directory. */
172
221
  export declare function defaultCiServeStateDir(cwd: string): string;
173
222
  /** The state file, or a new one. */
174
223
  export declare function readCiServeState(file: string, fresh: Omit<CiServeState, "schema" | "jobs">): CiServeState;
175
224
  /**
176
225
  * One poll of the forge, as triggers: an apply when the target's head moved
177
- * since the last apply (none on the first poll, which only notes the head),
226
+ * since the last apply by a fast-forward (none on the first poll, which only
227
+ * notes the head, and none for a head whose message says `[skip ci]`),
178
228
  * then a plan for each open pull request head not yet planned, oldest pull
179
229
  * request first. A job already in the state file is left out unless it
180
- * failed and `rerunFailed` is set. The first poll's head is written to the
181
- * state file.
230
+ * failed and `rerunFailed` is set. The first poll's head, and a head seen
231
+ * and not applied, are written to the state file.
182
232
  */
183
233
  export declare function ciServeObserve(opts: CiServeOptions, rt?: CiServeRuntime): Promise<{
184
234
  triggers: CiServeTrigger[];