@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
@@ -24,6 +24,8 @@ const writtenTemplates: unknown[] = [];
24
24
  let configEnvironments: unknown[] | undefined;
25
25
  let publishedDigest = "sha256:api";
26
26
  let failApplyFor: string | undefined;
27
+ /** What `failApplyFor`'s apply throws; a plain error when unset. */
28
+ let applyError: Error | undefined;
27
29
 
28
30
  vi.mock("../../lifecycle/git", () => ({
29
31
  getHeadCommit: async () => "abc123",
@@ -58,7 +60,7 @@ vi.mock("../../components/fan-out-support", () => ({
58
60
  step("docker-build", () => ({ digest: "sha256:rebuilt" }));
59
61
  step("publish-image", () => ({ digest: publishedDigest, uri: `reg/api@${publishedDigest}` }));
60
62
  step("apply", (ctx) => {
61
- if (ctx.component === failApplyFor) throw new Error("apply failed");
63
+ if (ctx.component === failApplyFor) throw applyError ?? new Error("apply failed");
62
64
  return {};
63
65
  });
64
66
  // #4045: a planner whose member is provisional.
@@ -221,6 +223,7 @@ beforeEach(() => {
221
223
  configEnvironments = undefined;
222
224
  publishedDigest = "sha256:api";
223
225
  failApplyFor = undefined;
226
+ applyError = undefined;
224
227
  errors = [];
225
228
  logs = [];
226
229
  vi.spyOn(console, "error").mockImplementation((...m: unknown[]) => { errors.push(m.join(" ")); });
@@ -358,6 +361,21 @@ describe("chant components promote", () => {
358
361
  expect(appendReleaseRecordMock.mock.calls[0][0]).toMatchObject({ component: "api", env: "prod" });
359
362
  });
360
363
 
364
+ test.each([
365
+ ["marked planMoved", 3, { planMoved: true }],
366
+ ["with exitCode 3", 3, { exitCode: 3 }],
367
+ ["with exitCode 7", 1, { exitCode: 7 }],
368
+ ])("an apply that refuses its plan %s exits %i, and what deployed is still recorded (#3997)", async (_label, code, mark) => {
369
+ const web: DriverComponent = { ...api, name: "web" };
370
+ resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
371
+ readReleaseLedgerMock.mockResolvedValue({ records: [staging, { ...staging, component: "web" }], malformed: 0 });
372
+ failApplyFor = "web";
373
+ applyError = Object.assign(new Error("web: refusing to apply, the plan moved"), mark);
374
+ expect(await runComponentsPromote(ctx({}))).toBe(code);
375
+ expect(appendReleaseRecordMock).toHaveBeenCalledTimes(1);
376
+ expect(errors.join("\n")).toMatch(/promote failed at "web"/);
377
+ });
378
+
361
379
  test("--digest <component>=<digest> promotes the pinned release, not the latest one (#2602)", async () => {
362
380
  const web: DriverComponent = { ...api, name: "web" };
363
381
  resolveTargetsMock.mockResolvedValue({ success: true, targets: [api, web] });
@@ -795,6 +813,13 @@ describe("chant components redeploy", () => {
795
813
  ]);
796
814
  expect(errors.join("\n")).toMatch(/redeploy failed at "api"/);
797
815
  });
816
+
817
+ test("a redeploy whose apply refuses a moved plan exits 3 (#3997)", async () => {
818
+ failApplyFor = "api";
819
+ applyError = Object.assign(new Error("api: refusing to apply, the plan moved"), { planMoved: true });
820
+ expect(await runComponentsRedeploy(rd({}))).toBe(3);
821
+ expect(appendReleaseRecordMock).not.toHaveBeenCalled();
822
+ });
798
823
  });
799
824
 
800
825
  describe("--events-json on promote, rollback and redeploy (#4041)", () => {
@@ -58,7 +58,7 @@ import {
58
58
  type PromotionItem,
59
59
  type PromotionPlan,
60
60
  } from "../../components/promote";
61
- import type { DriverComponent } from "../../components/driver";
61
+ import { failedRunExitCode, type DriverComponent } from "../../components/driver";
62
62
  import { summaryLedgerPrefix, writeGatedRunSummary } from "../../op/gate-summary";
63
63
  import { approveCommand, describeGateMismatch, gateIsSealed, gitGateLedgerPort, type GateLedgerPort } from "../../op/gate";
64
64
  import type { ReleaseGateBinding } from "../../components/driver";
@@ -522,7 +522,8 @@ async function deployPlan(
522
522
 
523
523
  if (run.status !== "ok") {
524
524
  console.error(formatError({ message: `${verb} failed at "${run.failedComponent ?? "unknown"}"` }));
525
- return 1;
525
+ // A step that refused a plan that moved since it was approved exits 3 (#3997).
526
+ return failedRunExitCode(run.results);
526
527
  }
527
528
  return 0;
528
529
  }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * `chant run --components` exits 3 when a step refuses a plan that moved
3
+ * since it was approved (#3997), on the single-component path and on the
4
+ * driver's `all` and list paths. Discovery and the capability registry are
5
+ * fakes; the run is the real `runComponents` on the local runtime.
6
+ */
7
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
8
+ import type { Component } from "../../components/component";
9
+ import type { ParsedArgs } from "../registry";
10
+
11
+ const discoverComponentsMock = vi.fn();
12
+ vi.mock("../../components/discover", () => ({
13
+ discoverComponents: (...args: unknown[]) => discoverComponentsMock(...args),
14
+ }));
15
+
16
+ vi.mock("../../config", async () => {
17
+ const actual = await vi.importActual<typeof import("../../config")>("../../config");
18
+ return { ...actual, loadChantConfig: async () => ({ config: {} }) };
19
+ });
20
+
21
+ /** The error a component's step throws, in place of applying. */
22
+ const refusal = new Map<string, Error>();
23
+
24
+ vi.mock("../../op/runtimes/local", async () => {
25
+ const { CapabilityRegistry } = await import("../../components/capability");
26
+ const { runComponents } = await import("../../components/cli-support");
27
+ const { memoryGateLedgerPort } = await import("../../op/gate");
28
+ const registry = new CapabilityRegistry();
29
+ registry.register({
30
+ kind: "fake-apply",
31
+ async run(ctx: { component: string }) {
32
+ const err = refusal.get(ctx.component);
33
+ if (err) throw err;
34
+ return {};
35
+ },
36
+ } as never);
37
+ return {
38
+ createLocalOpRuntime: () => ({
39
+ name: "local",
40
+ runComponents: (path: string, selector: string, options: object) =>
41
+ runComponents(path, selector, { ...options, registry, config: {}, revision: null, gates: memoryGateLedgerPort() }),
42
+ }),
43
+ };
44
+ });
45
+
46
+ const { runOpComponents } = await import("./run");
47
+
48
+ const component = (name: string, dependsOn: string[] = []): Component => ({
49
+ name,
50
+ dependsOn,
51
+ deploy: [{ phase: "Apply", steps: [{ kind: "fake-apply" }] }],
52
+ });
53
+
54
+ const ctx = (selector: string) =>
55
+ ({
56
+ args: { command: "run", path: selector, components: true, on: "local", format: "", fix: false, watch: false, verbose: false, help: false, live: false } as ParsedArgs,
57
+ plugins: [],
58
+ }) as never;
59
+
60
+ beforeEach(() => {
61
+ refusal.clear();
62
+ const estate = [component("net"), component("app", ["net"])];
63
+ discoverComponentsMock.mockResolvedValue({
64
+ components: new Map(estate.map((c) => [c.name, { component: c, exportName: c.name, filePath: `${c.name}.component.ts` }])),
65
+ sourceFiles: [],
66
+ errors: [],
67
+ });
68
+ vi.spyOn(console, "error").mockImplementation(() => {});
69
+ vi.spyOn(console, "log").mockImplementation(() => {});
70
+ vi.spyOn(process.stderr, "write").mockImplementation(() => true);
71
+ });
72
+
73
+ afterEach(() => {
74
+ vi.restoreAllMocks();
75
+ });
76
+
77
+ const marks: Array<[string, number, object]> = [
78
+ ["marked planMoved", 3, { planMoved: true }],
79
+ ["with exitCode 3", 3, { exitCode: 3 }],
80
+ ["with exitCode 7", 1, { exitCode: 7 }],
81
+ ["unmarked", 1, {}],
82
+ ];
83
+
84
+ describe("chant run --components: a step that refuses a moved plan exits 3 (#3997)", () => {
85
+ test.each(marks)("one component, its step refusing %s: exit %i", async (_label, code, mark) => {
86
+ refusal.set("app", Object.assign(new Error("app: refusing to apply, the plan moved"), mark));
87
+ expect(await runOpComponents(ctx("app"))).toBe(code);
88
+ });
89
+
90
+ test.each(marks)("all, a step refusing %s: exit %i", async (_label, code, mark) => {
91
+ refusal.set("net", Object.assign(new Error("net: refusing to apply, the plan moved"), mark));
92
+ expect(await runOpComponents(ctx("all"))).toBe(code);
93
+ });
94
+
95
+ test("a list that pulls in a dependency whose step refuses a moved plan exits 3", async () => {
96
+ refusal.set("net", Object.assign(new Error("net: refusing to apply, the plan moved"), { planMoved: true }));
97
+ expect(await runOpComponents(ctx("app,net"))).toBe(3);
98
+ });
99
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * #4156: `chant run`'s runner key is opt-in hardening. A `CHANT_RUNNER_KEY`
3
+ * that `.chant/trust.json` does not list leaves the run unsigned with no
4
+ * warning, unless the declaration sets `ci.green.signedSince`. A key that is
5
+ * not an Ed25519 PEM is an error either way.
6
+ */
7
+
8
+ import { createPublicKey, generateKeyPairSync } from "node:crypto";
9
+ import { afterEach, describe, expect, test, vi } from "vitest";
10
+ import { cleanScratch, declaration, git, scratchDir, writeFiles } from "../../workspace/__fixtures__/contract-repo";
11
+ import { sshFromEd25519 } from "../../workspace/trust/dsse";
12
+ import type { CommandContext } from "../registry";
13
+ import { runSignerFor } from "./run";
14
+
15
+ const home = process.cwd();
16
+ const saved = process.env.CHANT_RUNNER_KEY;
17
+ afterEach(() => {
18
+ process.chdir(home);
19
+ if (saved === undefined) delete process.env.CHANT_RUNNER_KEY;
20
+ else process.env.CHANT_RUNNER_KEY = saved;
21
+ vi.restoreAllMocks();
22
+ cleanScratch();
23
+ });
24
+
25
+ function key(): { pem: string; ssh: string } {
26
+ const { privateKey } = generateKeyPairSync("ed25519");
27
+ const jwk = createPublicKey(privateKey).export({ format: "jwk" }) as { x: string };
28
+ return { pem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(), ssh: sshFromEd25519(Buffer.from(jwk.x, "base64url")) };
29
+ }
30
+
31
+ function repo(green?: Record<string, unknown>): string {
32
+ const root = scratchDir("chant-run-signer-");
33
+ git(root, "init", "-q");
34
+ for (const [k, v] of [["user.name", "t"], ["user.email", "t@example.com"], ["commit.gpgsign", "false"]]) git(root, "config", k!, v!);
35
+ writeFiles(root, {
36
+ "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], green ? { ci: { green } } : {}),
37
+ ".chant/trust.json": JSON.stringify({ schema: 1, runners: [{ principal: "ci-bot", class: "runner", key: key().ssh }], roles: { runner: ["ci-bot"] } }),
38
+ "app/a.txt": "a\n",
39
+ });
40
+ git(root, "add", "-A");
41
+ git(root, "commit", "-q", "-m", "init");
42
+ return root;
43
+ }
44
+
45
+ async function signer(root: string, pem: string, verbose = false) {
46
+ process.chdir(root);
47
+ process.env.CHANT_RUNNER_KEY = pem;
48
+ const err: string[] = [];
49
+ vi.spyOn(console, "error").mockImplementation((s: string) => void err.push(String(s)));
50
+ const result = await runSignerFor({ args: { verbose } } as unknown as CommandContext);
51
+ return { result, err: err.join("\n") };
52
+ }
53
+
54
+ describe("chant run's runner key from the environment (#4156)", () => {
55
+ test("a key trust.json does not list leaves the run unsigned and quiet, with one line under --verbose", async () => {
56
+ const root = repo();
57
+ const quiet = await signer(root, key().pem);
58
+ expect(quiet.result).toBeUndefined();
59
+ expect(quiet.err).toBe("");
60
+ const verbose = await signer(root, key().pem, true);
61
+ expect(verbose.result).toBeUndefined();
62
+ expect(verbose.err).toContain("CHANT_RUNNER_KEY is set, and the run is not signed");
63
+ });
64
+
65
+ test("with ci.green.signedSince the same key warns", async () => {
66
+ const root = repo();
67
+ const head = git(root, "rev-parse", "HEAD");
68
+ writeFiles(root, { "chant.workspace.json": declaration([{ name: "app", dir: "app", kind: "other", because: "the app" }], { ci: { green: { branch: "main", phases: { test: ["test"] }, require: ["test"], signedSince: head } } }) });
69
+ const r = await signer(root, key().pem);
70
+ expect(r.result).toBeUndefined();
71
+ expect(r.err).toContain(`ci.green.signedSince (${head}) will not count it`);
72
+ });
73
+
74
+ test("a key that is not an Ed25519 PEM is an error", async () => {
75
+ const r = await signer(repo(), "not a key");
76
+ expect(r.result).toBeNull();
77
+ expect(r.err).toContain("CHANT_RUNNER_KEY is not an Ed25519 private key in PEM");
78
+ });
79
+ });
@@ -31,7 +31,7 @@ import type { RunEvent } from "../../run-event";
31
31
  import { openCliRunEvents, refusesBothStreams, stdoutLine } from "../run-events";
32
32
  import { maybeRecordAutoRelease } from "../../components/auto-release";
33
33
  import { maybePersistBuildManifest } from "../../components/manifest-persistence";
34
- import type { DriverComponentResult } from "../../components/driver";
34
+ import { failedRunExitCode, type DriverComponentResult } from "../../components/driver";
35
35
  import { isComponentList, type ComponentRunPlan, type RunComponentsResult } from "../../components/cli-support";
36
36
  import { resolveCiRun, type ReleaseRecord, type RunOrigin } from "../../lifecycle/release-ledger";
37
37
  import { gateAdmission, gateApprover } from "../../components/promote";
@@ -730,9 +730,9 @@ type StewardTurnGate =
730
730
  * `chant run <op>` for an Op a declared steward lists is one of that
731
731
  * steward's turns, not a run beside it (#2750, follow-up to #2731).
732
732
  *
733
- * On fountain, the run belongs on the steward's thread: without `--on
733
+ * On fountain, the run belongs in the steward's conversation: without `--on
734
734
  * fountain` this refuses outright, naming the command that posts it there
735
- * instead (the fountain runtime's own turn already refuses a busy teammate,
735
+ * instead (the fountain runtime's own turn already refuses a busy steward,
736
736
  * unretried — see chant-fountain-ops).
737
737
  *
738
738
  * In the local form, the run takes the steward's turn lease
@@ -771,15 +771,15 @@ async function stewardTurnGate(opName: string, ctx: CommandContext): Promise<Ste
771
771
  const form = stewardFormFor(owner, env);
772
772
 
773
773
  if (form === "fountain") {
774
- // `run.on: "fountain"` asks for the steward's thread as much as the flag
774
+ // `run.on: "fountain"` asks for the steward's conversation as much as the flag
775
775
  // does (#2523).
776
776
  if ((await chooseOpRuntime(ctx, true))?.on === "fountain") return { ok: true };
777
777
  return {
778
778
  ok: false,
779
779
  message: `Op "${opName}" is one of steward "${owner.name}"'s Ops, which runs on fountain in environment "${env}"`,
780
780
  hint:
781
- `Run \`chant run ${opName} --on fountain\` so it posts to "${owner.name}"'s thread. ` +
782
- `A plain \`chant run ${opName}\` would run it outside that thread, beside the steward instead of one of its turns.`,
781
+ `Run \`chant run ${opName} --on fountain\` so it posts to "${owner.name}"'s conversation. ` +
782
+ `A plain \`chant run ${opName}\` would run it outside that conversation, beside the steward instead of one of its turns.`,
783
783
  };
784
784
  }
785
785
 
@@ -1283,7 +1283,10 @@ async function runComponentSelection(ctx: CommandContext): Promise<number> {
1283
1283
  }
1284
1284
  }
1285
1285
 
1286
- return result.success ? 0 : 1;
1286
+ // A step that refused a plan that moved since it was approved exits 3
1287
+ // (#3997). `--gated-exit` doesn't remap it: the run failed.
1288
+ if (result.success) return 0;
1289
+ return result.run ? failedRunExitCode(result.run.results) : 1;
1287
1290
  }
1288
1291
 
1289
1292
  /**
@@ -1359,9 +1362,10 @@ async function runOpOnResolvedRuntime(ctx: CommandContext): Promise<number> {
1359
1362
  return 1;
1360
1363
  }
1361
1364
 
1362
- // #4044: a runner key signs the run's record as it settles, so `ci tick`
1363
- // can count the run. `--key` must be a runner key; the PEM in
1364
- // CHANT_RUNNER_KEY that is not one leaves the run unsigned, with a warning.
1365
+ // #4044: a runner key signs the run's record as it settles, so a reader
1366
+ // can see who ran it. `--key` must be a runner key; the PEM in
1367
+ // CHANT_RUNNER_KEY that trust.json does not list leaves the run unsigned,
1368
+ // with a warning only when ci.green.signedSince asks for signed runs (#4156).
1365
1369
  const signRecord = await runSignerFor(ctx);
1366
1370
  if (signRecord === null) {
1367
1371
  await stewardGate?.release?.();
@@ -1564,10 +1568,13 @@ const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
1564
1568
  * What signs the run's record (#4044): the runner key `--key` names, or the
1565
1569
  * PEM in `CHANT_RUNNER_KEY`, checked against `.chant/trust.json` at `HEAD`.
1566
1570
  * Undefined when neither is given. Null, after printing why, when `--key`
1567
- * is not a runner key; an unusable `CHANT_RUNNER_KEY` only warns, since the
1568
- * variable may be set for `ci tick` in a job that also runs Ops.
1571
+ * is not a runner key, or either is not an Ed25519 PEM. A `CHANT_RUNNER_KEY`
1572
+ * that trust.json does not list leaves the run unsigned (#4156): quietly,
1573
+ * with a line under `--verbose`, unless `ci.green.signedSince` asks for
1574
+ * signed runs, which warns. The variable may be set for `ci tick` in a job
1575
+ * that also runs Ops.
1569
1576
  */
1570
- async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord) => NonNullable<OpRunRecord["statement"]>) | undefined | null> {
1577
+ export async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord) => NonNullable<OpRunRecord["statement"]>) | undefined | null> {
1571
1578
  const fromFlag = ctx.args.key !== undefined;
1572
1579
  let pem: string | undefined;
1573
1580
  try {
@@ -1577,9 +1584,17 @@ async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord)
1577
1584
  return null;
1578
1585
  }
1579
1586
  if (pem === undefined) return undefined;
1587
+ // Loaded only when a key is given, so a run without one loads no workspace code (level 0, #2525).
1588
+ const { opRunSigner, signedRunsRequired } = await import("../../workspace/ci-green");
1589
+ const { loadRunnerKey } = await import("../../workspace/trust/dsse");
1590
+ try {
1591
+ loadRunnerKey(pem);
1592
+ } catch (err) {
1593
+ // A key that is there and is not one is an error either way: the caller asked to sign (#4156).
1594
+ console.error(formatError({ message: `${fromFlag ? `--key ${ctx.args.key}` : RUNNER_KEY_ENV} is not an Ed25519 private key in PEM: ${err instanceof Error ? err.message : String(err)}` }));
1595
+ return null;
1596
+ }
1580
1597
  try {
1581
- // Loaded only when a key is given, so a run without one loads no workspace code (level 0, #2525).
1582
- const { opRunSigner } = await import("../../workspace/ci-green");
1583
1598
  return opRunSigner(process.cwd(), pem);
1584
1599
  } catch (err) {
1585
1600
  const message = err instanceof Error ? err.message : String(err);
@@ -1587,7 +1602,10 @@ async function runSignerFor(ctx: CommandContext): Promise<((record: OpRunRecord)
1587
1602
  console.error(formatError({ message: `the run would not be signed: ${message}` }));
1588
1603
  return null;
1589
1604
  }
1590
- console.error(formatWarning({ message: `${RUNNER_KEY_ENV} is set, and the run will not be signed: ${message}` }));
1605
+ // #4156: signing is opt-in, so a key trust.json does not list warns only when ci.green.signedSince asks for signed runs.
1606
+ const required = signedRunsRequired(process.cwd());
1607
+ if (required) console.error(formatWarning({ message: `${RUNNER_KEY_ENV} is set, and the run will not be signed, so ci.green.signedSince (${required}) will not count it: ${message}` }));
1608
+ else if (ctx.args.verbose) console.error(`${RUNNER_KEY_ENV} is set, and the run is not signed: ${message}`);
1591
1609
  return undefined;
1592
1610
  }
1593
1611
  }
@@ -666,6 +666,18 @@ describe("resolveCommand", () => {
666
666
  expect(result!.compound).toBe(false);
667
667
  });
668
668
 
669
+ test("`sync` is an alias for `update` (#3988)", () => {
670
+ const result = resolveCommand(makeArgs({ command: "sync" }), [...testRegistry, { name: "update", handler: noop }]);
671
+ expect(result!.def.name).toBe("update");
672
+ expect(result!.compound).toBe(false);
673
+ });
674
+
675
+ test("`sync <path>` resolves to `update` with the path kept (#3988)", () => {
676
+ const result = resolveCommand(makeArgs({ command: "sync", path: "./proj" }), [...testRegistry, { name: "update", handler: noop }]);
677
+ expect(result!.def.name).toBe("update");
678
+ expect(result!.compound).toBe(false);
679
+ });
680
+
669
681
  test("resolves components status as compound command", () => {
670
682
  const result = resolveCommand(makeArgs({ command: "components", path: "status" }), testRegistry);
671
683
  expect(result!.def.name).toBe("components status");
@@ -1021,4 +1033,10 @@ describe("chant --help", () => {
1021
1033
  const commands = new Set(commandRegistry.map((def) => def.name.split(" ")[0]));
1022
1034
  expect([...commands].filter((name) => !listed.has(name))).toEqual([]);
1023
1035
  });
1036
+
1037
+ test("names `sync` as the alias of `update` (#3988)", () => {
1038
+ const source = readFileSync(join(import.meta.dirname, "main.ts"), "utf-8");
1039
+ const help = source.slice(source.indexOf("function printHelp("), source.indexOf("Options:", source.indexOf("function printHelp(")));
1040
+ expect(help).toMatch(/^ {2}update \(alias: sync\)/m);
1041
+ });
1024
1042
  });
package/src/cli/main.ts CHANGED
@@ -238,33 +238,39 @@ CI:
238
238
  its window, newest first, each green, revoked or
239
239
  undecided with its required phases' results. Reads
240
240
  local tags; --forge reads check runs for undecided
241
- commits, and source runs reads the signed Op runs on
242
- the local chant/lifecycle. --since replaces the window.
241
+ commits, and source runs reads the Op runs on the
242
+ local chant/lifecycle, signed or not. --since replaces the window.
243
243
  ci tick [--dry-run] [--forge github] [--source checks|runs] [--key <runner key.pem>]
244
244
  Tag each commit within ci.green's window whose
245
245
  required phases all passed ci/green/<sha>, and revoke
246
246
  a green one that now fails ci/revoked/<sha>. Reads
247
247
  check runs from the forge, or with source runs
248
248
  (ci.green.source or --source) the Op runs on
249
- chant/lifecycle that a runner key signed. Pushes the
249
+ chant/lifecycle; with ci.green.signedSince, the ones a
250
+ runner key signed. Pushes the
250
251
  tags. --key, or the PEM in CHANT_RUNNER_KEY, signs
251
252
  each tag with a runner key.
252
253
  ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>]
253
- [--forge github|gitlab|forgejo] [--gate <name>] [--branch <name>]
254
- [--events-json] [--report-url <url>] [--image <image>]
254
+ [--forge github|gitlab|forgejo] [--gate <name>] [--wave-gate <name>]
255
+ [--branch <name>] [--events-json] [--report-url <url>] [--image <image>]
255
256
  [--before-script <line>]... [--pr-build]
256
257
  [--pr-build-command <line>] [--chant <command>]
257
258
  [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
258
- [-o <dir>]
259
+ [-o <dir>] [--wave <k>] [--no-require-review]
259
260
  [--no-status] [--status-url <url template>]
261
+ [--in-place | --local] [--no-push] [--verbose]
260
262
  Run the pull-request pipeline's plan or apply job as
261
263
  a pipeline Op in a fresh worktree of --commit (default
262
264
  HEAD) with a clean environment. --base and --pr stand
263
265
  in for the forge's event values. --pr-build runs
264
266
  the job's build and lint lines first. Records the run as
265
- pr-plan or pr-apply on chant/lifecycle, signed with
266
- --key or CHANT_RUNNER_KEY, so ci tick from runs counts
267
- it, and copies the report to -o (default .chant/pr). With a forge
267
+ pr-plan or pr-apply on chant/lifecycle, which ci tick
268
+ from runs counts, signed when --key or CHANT_RUNNER_KEY
269
+ gives a runner key, and copies the report to -o
270
+ (default .chant/pr). --in-place runs in this checkout
271
+ at HEAD, as a generated forge job does, and names the
272
+ forge's run in the record; --no-push keeps the record
273
+ local. Exits with the command's code. With a forge
268
274
  token it sets a chant/pr-plan or chant/pr-apply commit
269
275
  status (pending, then success or failure); --no-status
270
276
  turns that off, and --status-url links it.
@@ -275,9 +281,14 @@ CI:
275
281
  With --report-url it also adds the run to
276
282
  <url>/runs/index.json, newest first, cut to the
277
283
  newest --run-index-limit entries (default 200).
278
- ci runs --report-url <url> [--json]
279
- List the runs in the index at the report URL, newest
280
- first.
284
+ Without --report-url, the workspace's ci.reportUrl.
285
+ --local runs the job on this machine: the record is
286
+ kept on the local chant/lifecycle with runOrigin
287
+ local and is never pushed, no status is set, nothing
288
+ goes under a report URL, and ci tick does not count it.
289
+ ci runs [--report-url <url>] [--json]
290
+ List the runs in the index at the report URL (default
291
+ the workspace's ci.reportUrl), newest first.
281
292
  ci serve --forge forgejo|github --repo <owner/repo> --env <env>
282
293
  [--interval <s>] [--max-jobs <n>] [--state <dir>]
283
294
  [--author <login>]... [--once] [--rerun-failed] [--unsigned]
@@ -290,9 +301,10 @@ CI:
290
301
  each apply's resume record there, and runs at most
291
302
  --max-jobs (default 1) jobs at once. --once runs one
292
303
  poll and waits for its jobs. --rerun-failed runs a
293
- failed apply or plan again. Refuses to start when its
294
- runner key is not listed in .chant/trust.json at the
295
- target's head, unless --unsigned
304
+ failed apply or plan again. Signs runs with a runner
305
+ key when .chant/trust.json at the target's head lists
306
+ it, and serves unsigned otherwise; with
307
+ ci.green.signedSince it refuses to start without one
296
308
  ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
297
309
  Make the host's runner key (Ed25519, mode 0600) in the
298
310
  state directory when there is none and print its public
@@ -929,6 +941,10 @@ Component release ledger + status:
929
941
  member's loop, gate op pr-<n>-<member>;
930
942
  --resume <file> finishes an apply that failed
931
943
  partway under the same approval;
944
+ --wave-gate <name>: apply a wave at a time, each
945
+ planned after the one before applied, a moved
946
+ wave waiting at <name>-wave-<k>, with --wave <k>
947
+ for one wave per CI job;
932
948
  --events-json, both commands: the run-event
933
949
  stream on stdout; --report-url <s3 or file URL>,
934
950
  both commands: copy the report, the change set
@@ -998,7 +1014,8 @@ Servers:
998
1014
  serve mcp Start the MCP server (stdio)
999
1015
 
1000
1016
  Project:
1001
- update Sync lexicon types into .chant/types/
1017
+ update (alias: sync) Sync lexicon types into .chant/types/ (does not install
1018
+ packages: run npm install first)
1002
1019
  doctor Check project health and configuration
1003
1020
 
1004
1021
  Options:
@@ -1217,6 +1234,9 @@ async function loadPluginsOrExit(path: string, lexicon?: string): Promise<import
1217
1234
  plugins = await loadPlugins(lexiconNames);
1218
1235
  } catch (error) {
1219
1236
  const errorMessage = error instanceof Error ? error.message : String(error);
1237
+ // #4116: a lexicon package the project has not installed is named as such first.
1238
+ const { reportUninstalledDependencies } = await import("./dependencies");
1239
+ reportUninstalledDependencies([errorMessage], resolve(path));
1220
1240
  console.error(formatError({ message: errorMessage }));
1221
1241
  process.exit(1);
1222
1242
  }
@@ -1725,6 +1745,13 @@ const isMain = isEntryPoint(process.argv[1], import.meta.url);
1725
1745
  if (isMain) {
1726
1746
  main().catch(async (err) => {
1727
1747
  const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
1748
+ // #4116: a failure to import a package the project has not installed is named as such first.
1749
+ try {
1750
+ const { reportUninstalledDependencies } = await import("./dependencies");
1751
+ reportUninstalledDependencies([err instanceof Error ? err.message : String(err)], process.cwd());
1752
+ } catch {
1753
+ // The original error below is what matters.
1754
+ }
1728
1755
  if (verbose && err instanceof Error && err.stack) {
1729
1756
  console.error(err.stack);
1730
1757
  } else {
@@ -69,6 +69,9 @@ const BOOLEAN_FLAGS = new Set([
69
69
  "--setup-key",
70
70
  "--trust",
71
71
  "--unsigned",
72
+ "--in-place",
73
+ "--no-push",
74
+ "--no-require-review",
72
75
  "--expire",
73
76
  "--check-live",
74
77
  "--check-snapshot",
@@ -777,6 +780,15 @@ export function parseArgs(args: string[]): ParsedArgs {
777
780
  } else if (arg === "--trust") {
778
781
  // `chant ci serve|run --setup-key --trust` (#4099): list the key in .chant/trust.json on a branch.
779
782
  result.trustKey = true;
783
+ } else if (arg === "--in-place") {
784
+ // `chant ci run --in-place` (#4156): run the job in this checkout, as a generated forge job does.
785
+ result.ciInPlace = true;
786
+ } else if (arg === "--no-push") {
787
+ // `chant ci run --no-push` (#4156): keep the run record on the local chant/lifecycle.
788
+ result.noPush = true;
789
+ } else if (arg === "--no-require-review") {
790
+ // `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review.
791
+ result.requireReview = false;
780
792
  } else if (arg === "--unsigned") {
781
793
  // `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.
782
794
  result.unsigned = true;
@@ -23,7 +23,7 @@ export interface ParsedArgs {
23
23
  /** `--version` / `-V` (#2701): print the installed chant's version. */
24
24
  version?: boolean;
25
25
  report?: boolean;
26
- /** `chant run` — force the local in-process executor (the default). */
26
+ /** `chant run` — force the local in-process executor (the default). `chant ci run --local` (#4161): run the job on this machine with no push, status or report upload. */
27
27
  local?: boolean;
28
28
  /**
29
29
  * `chant run <op> --on <lexicon>` (#2121) — which runtime hosts the run.
@@ -311,7 +311,8 @@ export interface ParsedArgs {
311
311
  /**
312
312
  * `chant components pr-apply --require-review` (#3183) — count an approval
313
313
  * only when its approver has a standing approving review on the pull
314
- * request. Needs `--forge`.
314
+ * request. Needs `--forge`. `chant ci run apply --no-require-review`
315
+ * (#4156) sets it false, for a pipeline generated with `requireReview: false`.
315
316
  */
316
317
  requireReview?: boolean;
317
318
  /**
@@ -466,7 +467,7 @@ export interface ParsedArgs {
466
467
  ciSecrets?: string;
467
468
  /** `chant ci workflow --chant <command>` (#3573): the command the tick step runs chant with, in place of the npx pin. Also `chant ci run --chant` (#4094): what runs chant in the job. */
468
469
  ciChant?: string;
469
- /** `chant ci run|serve --no-status` (#4097): post no commit status for the job. */
470
+ /** `chant ci run|serve --no-status` (#4097): post no commit status for the job. `chant components pr-plan|pr-apply --no-status` (#4161): post neither the status nor the note. */
470
471
  noStatus?: boolean;
471
472
  /** `chant ci run|serve --status-url <template>` (#4097): the link a job's status carries. `{key}` is `<pr>/<commit>[/<member>]`, as `--report-url` keys it. */
472
473
  statusUrl?: string;
@@ -502,8 +503,16 @@ export interface ParsedArgs {
502
503
  setupKey?: boolean;
503
504
  /** `chant ci serve|run --setup-key --trust` (#4099): list the runner key in .chant/trust.json on a branch pushed for review. */
504
505
  trustKey?: boolean;
505
- /** `chant ci serve --unsigned` (#4099): start without a runner key listed in .chant/trust.json at the target's head. */
506
+ /**
507
+ * `chant ci serve --unsigned` (#4099): serve without signing. Since #4156
508
+ * that is the default without a trusted key, so it only skips the key, and
509
+ * is ignored when ci.green.signedSince requires signed runs.
510
+ */
506
511
  unsigned?: boolean;
512
+ /** `chant ci run --in-place` (#4156): run the job in this checkout at HEAD, as a generated forge job does. */
513
+ ciInPlace?: boolean;
514
+ /** `chant ci run --no-push` (#4156): write the run record on the local chant/lifecycle and push nothing. */
515
+ noPush?: boolean;
507
516
  /** `chant ci serve|run --setup-key --trust --principal <name>` (#4099): the principal the runner key is listed for. */
508
517
  ciPrincipal?: string;
509
518
  /** `chant workspace trust runner add <principal> --class runner|service` (#3510): the runner entry's class. */
@@ -757,8 +766,12 @@ export interface ResolvedCommand {
757
766
  * Supports compound commands like "dev generate" where args.command="dev"
758
767
  * and args.path="generate". Falls back to simple command matching.
759
768
  */
760
- /** Short command aliases. `chant lc …` is sugar for `chant lifecycle …`. */
761
- const COMMAND_ALIASES: Record<string, string> = { lc: "lifecycle" };
769
+ /**
770
+ * Command aliases. `chant lc …` is sugar for `chant lifecycle …`, and
771
+ * `chant sync` for `chant update`, the command that syncs lexicon types
772
+ * (chant #3988).
773
+ */
774
+ const COMMAND_ALIASES: Record<string, string> = { lc: "lifecycle", sync: "update" };
762
775
 
763
776
  export function resolveCommand(args: ParsedArgs, registry: CommandDef[]): ResolvedCommand | null {
764
777
  const command = COMMAND_ALIASES[args.command] ?? args.command;
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  import { createRequire } from "module";
17
- import { listRuleFiles } from "../lint/discover";
17
+ import { listRuleFiles, ruleModuleBase } from "../lint/discover";
18
18
  import { isPostSynthCheck } from "../lint/post-synth";
19
19
 
20
20
  export interface BarrelEntry {
@@ -34,7 +34,7 @@ export function scanPostSynthChecks(dir: string, importMetaUrl: string): BarrelE
34
34
  const require = createRequire(importMetaUrl);
35
35
  const entries: BarrelEntry[] = [];
36
36
  for (const file of listRuleFiles(dir)) {
37
- const base = file.replace(/\.ts$/, "");
37
+ const base = ruleModuleBase(file);
38
38
  let mod: Record<string, unknown>;
39
39
  try {
40
40
  mod = require(`${dir}/${base}`) as Record<string, unknown>;
@@ -402,7 +402,7 @@ function runExampleHarness(lexiconDir: string, verbose: boolean): RegenFailure |
402
402
  /**
403
403
  * Extract the fresh surface from the generated artifacts in the lexicon directory.
404
404
  */
405
- function extractFreshSurface(lexiconDir: string): SurfaceSnapshot | null {
405
+ export function extractFreshSurface(lexiconDir: string): SurfaceSnapshot | null {
406
406
  const generatedDir = join(lexiconDir, "src", "generated");
407
407
  if (!existsSync(generatedDir)) return null;
408
408