@intentius/chant 0.124.32 → 0.124.34

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 (320) hide show
  1. package/dist/ci-jobs.d.ts +4 -0
  2. package/dist/ci-jobs.d.ts.map +1 -1
  3. package/dist/ci-jobs.js +9 -2
  4. package/dist/cli/handlers/build.d.ts.map +1 -1
  5. package/dist/cli/handlers/build.js +18 -3
  6. package/dist/cli/handlers/lifecycle-health.d.ts +16 -0
  7. package/dist/cli/handlers/lifecycle-health.d.ts.map +1 -0
  8. package/dist/cli/handlers/lifecycle-health.js +150 -0
  9. package/dist/cli/handlers/lifecycle.d.ts +26 -0
  10. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  11. package/dist/cli/handlers/lifecycle.js +1 -1
  12. package/dist/cli/handlers/pr.d.ts +2 -0
  13. package/dist/cli/handlers/pr.d.ts.map +1 -1
  14. package/dist/cli/handlers/pr.js +124 -2
  15. package/dist/cli/handlers/promote.d.ts.map +1 -1
  16. package/dist/cli/handlers/promote.js +277 -74
  17. package/dist/cli/handlers/release.d.ts +16 -1
  18. package/dist/cli/handlers/release.d.ts.map +1 -1
  19. package/dist/cli/handlers/release.js +83 -11
  20. package/dist/cli/main.d.ts.map +1 -1
  21. package/dist/cli/main.js +99 -22
  22. package/dist/cli/parse-args.d.ts.map +1 -1
  23. package/dist/cli/parse-args.js +30 -1
  24. package/dist/cli/registry.d.ts +21 -5
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/components/deploy-inputs.d.ts +91 -0
  27. package/dist/components/deploy-inputs.d.ts.map +1 -0
  28. package/dist/components/deploy-inputs.js +184 -0
  29. package/dist/components/driver.d.ts.map +1 -1
  30. package/dist/components/driver.js +9 -0
  31. package/dist/components/gate-plan.d.ts +8 -0
  32. package/dist/components/gate-plan.d.ts.map +1 -1
  33. package/dist/components/gate-plan.js +6 -0
  34. package/dist/components/pr-pipeline.d.ts +33 -10
  35. package/dist/components/pr-pipeline.d.ts.map +1 -1
  36. package/dist/components/pr-pipeline.js +42 -11
  37. package/dist/components/pr-run.d.ts +1 -1
  38. package/dist/components/pr-run.d.ts.map +1 -1
  39. package/dist/components/promote-rounds.d.ts +102 -0
  40. package/dist/components/promote-rounds.d.ts.map +1 -0
  41. package/dist/components/promote-rounds.js +174 -0
  42. package/dist/components/release-compile.d.ts +37 -2
  43. package/dist/components/release-compile.d.ts.map +1 -1
  44. package/dist/components/release-compile.js +64 -8
  45. package/dist/components/release-gate.d.ts +30 -0
  46. package/dist/components/release-gate.d.ts.map +1 -1
  47. package/dist/components/release-gate.js +92 -42
  48. package/dist/components/release-promote.d.ts +17 -3
  49. package/dist/components/release-promote.d.ts.map +1 -1
  50. package/dist/components/release-promote.js +79 -18
  51. package/dist/components/verbs/bom-writer.d.ts +1 -1
  52. package/dist/components/verbs/bom-writer.d.ts.map +1 -1
  53. package/dist/components/verbs/lockfile-sbom-generator.d.ts +20 -0
  54. package/dist/components/verbs/lockfile-sbom-generator.d.ts.map +1 -1
  55. package/dist/components/verbs/lockfile-sbom-generator.js +41 -4
  56. package/dist/components/wave-plan.d.ts +10 -0
  57. package/dist/components/wave-plan.d.ts.map +1 -1
  58. package/dist/components/wave-plan.js +32 -4
  59. package/dist/graph-declared.d.ts +4 -0
  60. package/dist/graph-declared.d.ts.map +1 -1
  61. package/dist/graph-declared.js +11 -1
  62. package/dist/graph-detail.d.ts.map +1 -1
  63. package/dist/graph-detail.js +32 -7
  64. package/dist/health.d.ts +110 -0
  65. package/dist/health.d.ts.map +1 -0
  66. package/dist/health.js +105 -0
  67. package/dist/health.schema.json +191 -0
  68. package/dist/lexicon.d.ts +27 -0
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/merge-record.d.ts +104 -0
  71. package/dist/lifecycle/merge-record.d.ts.map +1 -0
  72. package/dist/lifecycle/merge-record.js +102 -0
  73. package/dist/lifecycle/publish-attachments.d.ts +8 -5
  74. package/dist/lifecycle/publish-attachments.d.ts.map +1 -1
  75. package/dist/lifecycle/publish-attachments.js +27 -13
  76. package/dist/lifecycle/publish-npm.d.ts +3 -2
  77. package/dist/lifecycle/publish-npm.d.ts.map +1 -1
  78. package/dist/lifecycle/publish-npm.js +4 -3
  79. package/dist/lifecycle/release-at.d.ts +40 -0
  80. package/dist/lifecycle/release-at.d.ts.map +1 -0
  81. package/dist/lifecycle/release-at.js +83 -0
  82. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  83. package/dist/lifecycle/release-cut.js +23 -12
  84. package/dist/lifecycle/release-publish.d.ts +28 -12
  85. package/dist/lifecycle/release-publish.d.ts.map +1 -1
  86. package/dist/lifecycle/release-publish.js +29 -17
  87. package/dist/lifecycle/release-store.d.ts +25 -2
  88. package/dist/lifecycle/release-store.d.ts.map +1 -1
  89. package/dist/lifecycle/release-store.js +40 -13
  90. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  91. package/dist/lifecycle/run-ledger.js +1 -0
  92. package/dist/lint/rules/op/index.d.ts +2 -1
  93. package/dist/lint/rules/op/index.d.ts.map +1 -1
  94. package/dist/lint/rules/op/index.js +4 -2
  95. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts +10 -0
  96. package/dist/lint/rules/op/ops018-shell-image-digest.d.ts.map +1 -0
  97. package/dist/lint/rules/op/ops018-shell-image-digest.js +38 -0
  98. package/dist/op/activities/activity-contracts.d.ts +5 -0
  99. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  100. package/dist/op/activities/activity-contracts.js +3 -0
  101. package/dist/op/activities/shell.d.ts +38 -0
  102. package/dist/op/activities/shell.d.ts.map +1 -1
  103. package/dist/op/activities/shell.js +89 -3
  104. package/dist/op/builders.d.ts +7 -0
  105. package/dist/op/builders.d.ts.map +1 -1
  106. package/dist/op/builders.js +7 -0
  107. package/dist/op/runtime.d.ts +2 -0
  108. package/dist/op/runtime.d.ts.map +1 -1
  109. package/dist/pr-loop.d.ts +14 -3
  110. package/dist/pr-loop.d.ts.map +1 -1
  111. package/dist/pr-loop.js +15 -5
  112. package/dist/pr-report-url.d.ts +2 -2
  113. package/dist/pr-report-url.d.ts.map +1 -1
  114. package/dist/runner-start.d.ts +179 -0
  115. package/dist/runner-start.d.ts.map +1 -0
  116. package/dist/runner-start.js +277 -0
  117. package/dist/wave-runtime.mjs +236 -229
  118. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  119. package/dist/workspace/__fixtures__/contract-repo.js +13 -2
  120. package/dist/workspace/check.schema.json +88 -2
  121. package/dist/workspace/checks/ci-workflow.d.ts +17 -0
  122. package/dist/workspace/checks/ci-workflow.d.ts.map +1 -0
  123. package/dist/workspace/checks/ci-workflow.js +123 -0
  124. package/dist/workspace/checks/security.d.ts +50 -0
  125. package/dist/workspace/checks/security.d.ts.map +1 -0
  126. package/dist/workspace/checks/security.js +305 -0
  127. package/dist/workspace/checks.d.ts +2 -0
  128. package/dist/workspace/checks.d.ts.map +1 -1
  129. package/dist/workspace/checks.js +8 -0
  130. package/dist/workspace/ci-builds.d.ts +163 -0
  131. package/dist/workspace/ci-builds.d.ts.map +1 -0
  132. package/dist/workspace/ci-builds.js +315 -0
  133. package/dist/workspace/ci-green-cli.d.ts +2 -1
  134. package/dist/workspace/ci-green-cli.d.ts.map +1 -1
  135. package/dist/workspace/ci-green-cli.js +25 -5
  136. package/dist/workspace/ci-green-forge.d.ts +12 -0
  137. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  138. package/dist/workspace/ci-green-forge.js +12 -0
  139. package/dist/workspace/ci-green-workflow.d.ts +55 -1
  140. package/dist/workspace/ci-green-workflow.d.ts.map +1 -1
  141. package/dist/workspace/ci-green-workflow.js +128 -1
  142. package/dist/workspace/ci-green.d.ts +89 -4
  143. package/dist/workspace/ci-green.d.ts.map +1 -1
  144. package/dist/workspace/ci-green.js +310 -23
  145. package/dist/workspace/ci-init.d.ts +100 -0
  146. package/dist/workspace/ci-init.d.ts.map +1 -0
  147. package/dist/workspace/ci-init.js +434 -0
  148. package/dist/workspace/ci-last-green.schema.json +49 -0
  149. package/dist/workspace/ci-list.schema.json +35 -1
  150. package/dist/workspace/ci-run-index.d.ts +1 -1
  151. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  152. package/dist/workspace/ci-run-index.schema.json +1 -1
  153. package/dist/workspace/ci-run.d.ts +12 -6
  154. package/dist/workspace/ci-run.d.ts.map +1 -1
  155. package/dist/workspace/ci-run.js +72 -16
  156. package/dist/workspace/ci-runner-start.d.ts +77 -0
  157. package/dist/workspace/ci-runner-start.d.ts.map +1 -0
  158. package/dist/workspace/ci-runner-start.js +185 -0
  159. package/dist/workspace/ci-serve.d.ts +76 -7
  160. package/dist/workspace/ci-serve.d.ts.map +1 -1
  161. package/dist/workspace/ci-serve.js +153 -19
  162. package/dist/workspace/ci-workflow.d.ts +91 -0
  163. package/dist/workspace/ci-workflow.d.ts.map +1 -0
  164. package/dist/workspace/ci-workflow.js +33 -0
  165. package/dist/workspace/declaration.d.ts +35 -0
  166. package/dist/workspace/declaration.d.ts.map +1 -1
  167. package/dist/workspace/declaration.js +12 -2
  168. package/dist/workspace/declaration.schema.json +51 -0
  169. package/dist/workspace/init.js +2 -2
  170. package/dist/workspace/ls.d.ts +2 -0
  171. package/dist/workspace/ls.d.ts.map +1 -1
  172. package/dist/workspace/ls.js +1 -1
  173. package/dist/workspace/ls.schema.json +2 -1
  174. package/dist/workspace/member-pipeline.d.ts +1 -0
  175. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  176. package/dist/workspace/member-pipeline.js +2 -0
  177. package/dist/workspace/merge-record.schema.json +49 -0
  178. package/dist/workspace/pr-report.schema.json +3 -3
  179. package/dist/workspace/reason-codes.d.ts +26 -0
  180. package/dist/workspace/reason-codes.d.ts.map +1 -1
  181. package/dist/workspace/reason-codes.js +28 -0
  182. package/dist/workspace/release-candidates.d.ts +129 -0
  183. package/dist/workspace/release-candidates.d.ts.map +1 -0
  184. package/dist/workspace/release-candidates.js +233 -0
  185. package/dist/workspace/release-candidates.schema.json +353 -0
  186. package/dist/workspace/release-workflow.d.ts +31 -0
  187. package/dist/workspace/release-workflow.d.ts.map +1 -0
  188. package/dist/workspace/release-workflow.js +150 -0
  189. package/dist/workspace/security.d.ts +40 -0
  190. package/dist/workspace/security.d.ts.map +1 -0
  191. package/dist/workspace/security.js +99 -0
  192. package/package.json +8 -1
  193. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +1 -1
  194. package/src/ci-jobs.ts +11 -2
  195. package/src/cli/handlers/build.test.ts +36 -0
  196. package/src/cli/handlers/build.ts +18 -3
  197. package/src/cli/handlers/lifecycle-health.test.ts +123 -0
  198. package/src/cli/handlers/lifecycle-health.ts +157 -0
  199. package/src/cli/handlers/lifecycle.ts +1 -1
  200. package/src/cli/handlers/pr-deploy-inputs.test.ts +209 -0
  201. package/src/cli/handlers/pr-record.test.ts +201 -0
  202. package/src/cli/handlers/pr.test.ts +22 -0
  203. package/src/cli/handlers/pr.ts +127 -4
  204. package/src/cli/handlers/promote.test.ts +17 -0
  205. package/src/cli/handlers/promote.ts +296 -84
  206. package/src/cli/handlers/release-cut-at.e2e.test.ts +115 -0
  207. package/src/cli/handlers/release-publish-targets.e2e.test.ts +2 -2
  208. package/src/cli/handlers/release-verify.e2e.test.ts +12 -10
  209. package/src/cli/handlers/release.e2e.test.ts +19 -28
  210. package/src/cli/handlers/release.ts +81 -11
  211. package/src/cli/main.ts +99 -22
  212. package/src/cli/parse-args.ts +22 -1
  213. package/src/cli/registry.ts +21 -5
  214. package/src/components/deploy-inputs.test.ts +108 -0
  215. package/src/components/deploy-inputs.ts +225 -0
  216. package/src/components/driver.ts +11 -0
  217. package/src/components/gate-plan.ts +9 -0
  218. package/src/components/pr-pipeline.ts +56 -15
  219. package/src/components/pr-run.ts +1 -1
  220. package/src/components/promote-rounds.test.ts +220 -0
  221. package/src/components/promote-rounds.ts +232 -0
  222. package/src/components/release-compile.test.ts +34 -0
  223. package/src/components/release-compile.ts +89 -8
  224. package/src/components/release-gate.test.ts +47 -0
  225. package/src/components/release-gate.ts +111 -46
  226. package/src/components/release-promote.test.ts +55 -2
  227. package/src/components/release-promote.ts +93 -23
  228. package/src/components/verbs/bom-writer.ts +1 -1
  229. package/src/components/verbs/lockfile-sbom-generator.test.ts +36 -0
  230. package/src/components/verbs/lockfile-sbom-generator.ts +43 -5
  231. package/src/components/wave-plan.ts +32 -4
  232. package/src/graph-declared-exports.test.ts +40 -0
  233. package/src/graph-declared.ts +9 -1
  234. package/src/graph-detail.test.ts +57 -0
  235. package/src/graph-detail.ts +33 -7
  236. package/src/health.schema.json +191 -0
  237. package/src/health.test.ts +62 -0
  238. package/src/health.ts +178 -0
  239. package/src/lexicon.ts +41 -1
  240. package/src/lifecycle/merge-record.test.ts +111 -0
  241. package/src/lifecycle/merge-record.ts +154 -0
  242. package/src/lifecycle/publish-attachments.ts +25 -13
  243. package/src/lifecycle/publish-npm.ts +4 -3
  244. package/src/lifecycle/publish-targets.test.ts +34 -4
  245. package/src/lifecycle/release-at.test.ts +48 -0
  246. package/src/lifecycle/release-at.ts +100 -0
  247. package/src/lifecycle/release-cut.test.ts +41 -0
  248. package/src/lifecycle/release-cut.ts +22 -11
  249. package/src/lifecycle/release-publish.test.ts +43 -13
  250. package/src/lifecycle/release-publish.ts +38 -18
  251. package/src/lifecycle/release-store.ts +57 -13
  252. package/src/lifecycle/run-ledger.test.ts +13 -0
  253. package/src/lifecycle/run-ledger.ts +1 -0
  254. package/src/lint/rules/op/index.ts +4 -2
  255. package/src/lint/rules/op/ops018-shell-image-digest.test.ts +60 -0
  256. package/src/lint/rules/op/ops018-shell-image-digest.ts +48 -0
  257. package/src/op/activities/activity-contracts.ts +3 -0
  258. package/src/op/activities/shell-container.e2e.test.ts +67 -0
  259. package/src/op/activities/shell.test.ts +44 -1
  260. package/src/op/activities/shell.ts +110 -3
  261. package/src/op/builders.ts +7 -0
  262. package/src/op/runtime.ts +2 -0
  263. package/src/op/step-console.test.ts +4 -2
  264. package/src/pr-loop.test.ts +29 -0
  265. package/src/pr-loop.ts +26 -7
  266. package/src/pr-report-url.ts +2 -2
  267. package/src/runner-start-bundle.test.ts +29 -0
  268. package/src/runner-start.test.ts +179 -0
  269. package/src/runner-start.ts +329 -0
  270. package/src/workspace/__fixtures__/contract-repo.ts +11 -2
  271. package/src/workspace/check-contract.test.ts +4 -2
  272. package/src/workspace/check.schema.json +88 -2
  273. package/src/workspace/checks/ci-workflow.test.ts +150 -0
  274. package/src/workspace/checks/ci-workflow.ts +129 -0
  275. package/src/workspace/checks/security.test.ts +138 -0
  276. package/src/workspace/checks/security.ts +305 -0
  277. package/src/workspace/checks.ts +8 -0
  278. package/src/workspace/ci-builds.e2e.test.ts +208 -0
  279. package/src/workspace/ci-builds.test.ts +174 -0
  280. package/src/workspace/ci-builds.ts +385 -0
  281. package/src/workspace/ci-green-builds.test.ts +180 -0
  282. package/src/workspace/ci-green-cli.test.ts +54 -0
  283. package/src/workspace/ci-green-cli.ts +26 -5
  284. package/src/workspace/ci-green-forge.ts +24 -0
  285. package/src/workspace/ci-green-workflow.test.ts +84 -1
  286. package/src/workspace/ci-green-workflow.ts +139 -2
  287. package/src/workspace/ci-green.test.ts +5 -2
  288. package/src/workspace/ci-green.ts +359 -27
  289. package/src/workspace/ci-init.test.ts +159 -0
  290. package/src/workspace/ci-init.ts +445 -0
  291. package/src/workspace/ci-last-green.schema.json +49 -0
  292. package/src/workspace/ci-list.schema.json +35 -1
  293. package/src/workspace/ci-report-url.test.ts +6 -1
  294. package/src/workspace/ci-run-index.schema.json +1 -1
  295. package/src/workspace/ci-run-index.ts +1 -1
  296. package/src/workspace/ci-run.test.ts +24 -7
  297. package/src/workspace/ci-run.ts +78 -16
  298. package/src/workspace/ci-runner-start.test.ts +151 -0
  299. package/src/workspace/ci-runner-start.ts +193 -0
  300. package/src/workspace/ci-serve-builds.test.ts +86 -0
  301. package/src/workspace/ci-serve.test.ts +59 -0
  302. package/src/workspace/ci-serve.ts +177 -22
  303. package/src/workspace/ci-workflow.ts +71 -0
  304. package/src/workspace/declaration.schema.json +51 -0
  305. package/src/workspace/declaration.test.ts +17 -0
  306. package/src/workspace/declaration.ts +53 -3
  307. package/src/workspace/init.ts +2 -2
  308. package/src/workspace/ls.schema.json +2 -1
  309. package/src/workspace/ls.ts +3 -2
  310. package/src/workspace/member-pipeline.ts +2 -1
  311. package/src/workspace/merge-record.schema.json +49 -0
  312. package/src/workspace/pr-report.schema.json +3 -3
  313. package/src/workspace/reason-codes.test.ts +4 -0
  314. package/src/workspace/reason-codes.ts +28 -0
  315. package/src/workspace/release-candidates.schema.json +353 -0
  316. package/src/workspace/release-candidates.test.ts +106 -0
  317. package/src/workspace/release-candidates.ts +315 -0
  318. package/src/workspace/release-workflow.ts +167 -0
  319. package/src/workspace/security.test.ts +70 -0
  320. package/src/workspace/security.ts +118 -0
@@ -3,8 +3,10 @@
3
3
  * (#4049), in a temp repo with a temp bare remote and throwaway ssh keys. A
4
4
  * release cut signed with a key `.chant/allowed_signers` lists verifies, and a
5
5
  * publish takes it; an unsigned one and one signed by a key the file does not
6
- * list fail `release verify`, and a publish refuses them unless
7
- * `--allow-unsigned`. Nothing is published: the member declares no target.
6
+ * list fail `release verify`. With no release gate or verified-releases
7
+ * group a publish takes them and records their status (#4168);
8
+ * release-publish.test.ts covers the refusals under the group. Nothing is
9
+ * published: the member declares no target.
8
10
  */
9
11
 
10
12
  import { describe, expect, test } from "vitest";
@@ -42,7 +44,7 @@ const BUILD = `mkdir -p dist && printf '{"env":"%s"}' "$CHANT_ENV" > dist/api.te
42
44
 
43
45
  describe("chant release verify", () => {
44
46
  test.skipIf(!hasSshKeygen)(
45
- "passes for a signed release, fails for an unsigned one and for one signed by an unlisted key; a publish refuses both unless --allow-unsigned",
47
+ "passes for a signed release, fails for an unsigned one and for one signed by an unlisted key; a publish with no security group takes both and records their status",
46
48
  () => {
47
49
  const root = mkdtempSync(join(tmpdir(), "chant-release-verify-"));
48
50
  try {
@@ -89,10 +91,10 @@ describe("chant release verify", () => {
89
91
  expect(unsigned.code).toBe(1);
90
92
  expect(unsigned.stderr).toMatch(/v2.* does not verify: no cut of the release carries a signature \[release-unsigned\]/);
91
93
  expect(JSON.parse(chant(dir, ["release", "verify", "v2", "--json"]).stdout)).toMatchObject({ status: "unsigned", error: { code: "release-unsigned" } });
92
- const pubUnsigned = chant(dir, ["release", "publish", "v2"]);
93
- expect(pubUnsigned.code).toBe(1);
94
- expect(pubUnsigned.stderr).toMatch(/\[release-unsigned\]/);
95
- expect(chant(dir, ["release", "publish", "v2", "--allow-unsigned"]).code).toBe(0);
94
+ // #4168: with no release gate or verified-releases group, a publish takes it and records it unsigned.
95
+ const pubUnsigned = chant(dir, ["release", "publish", "v2", "--json"]);
96
+ expect(pubUnsigned.code, pubUnsigned.stderr).toBe(0);
97
+ expect(JSON.parse(pubUnsigned.stdout)).toMatchObject({ signature: { status: "unsigned" }, warnings: [] });
96
98
 
97
99
  // v3: signed by mallory, whom the signers file does not list.
98
100
  next("v3");
@@ -101,9 +103,9 @@ describe("chant release verify", () => {
101
103
  const unlisted = chant(dir, ["release", "verify", "v3"]);
102
104
  expect(unlisted.code).toBe(1);
103
105
  expect(unlisted.stderr).toMatch(/mallory has no key in \.chant\/allowed_signers at [0-9a-f]{12}.*\[release-signature-unverified\]/);
104
- const pubUnlisted = chant(dir, ["release", "publish", "v3"]);
105
- expect(pubUnlisted.code).toBe(1);
106
- expect(pubUnlisted.stderr).toMatch(/\[release-signature-unverified\]/);
106
+ const pubUnlisted = chant(dir, ["release", "publish", "v3", "--json"]);
107
+ expect(pubUnlisted.code, pubUnlisted.stderr).toBe(0);
108
+ expect(JSON.parse(pubUnlisted.stdout)).toMatchObject({ signature: { status: "unverified" }, warnings: [] });
107
109
 
108
110
  const missing = chant(dir, ["release", "verify", "v9"]);
109
111
  expect(missing.code).toBe(1);
@@ -128,8 +128,8 @@ describe("chant release cut, publish and list", () => {
128
128
  expect(result.release.releaseDigest).toBe(dryJson.releaseDigest);
129
129
  expect(result.release.commit).toBe(sha);
130
130
  expect(result.release.targets).toEqual([
131
- { env: "dev", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"dev"}`) }] },
132
- { env: "prod", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"prod"}`) }] },
131
+ { env: "dev", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"dev"}`) }], trees: [] },
132
+ { env: "prod", params: [], templates: [{ component: "api", path: "dist/api.template.json", digest: contentDigest(`{"env":"prod"}`) }], trees: [] },
133
133
  ]);
134
134
  expect(result.cut).toMatchObject({ actor: "t@chant.dev", runId: expect.stringMatching(/^local-\d+$/), runOrigin: { forge: "local" }, signature: null });
135
135
  expect(result.next).toEqual(["git push origin release/v1", "chant release publish v1", "chant components promote --release v1 --to dev"]);
@@ -151,41 +151,32 @@ describe("chant release cut, publish and list", () => {
151
151
  expect(doc.releases[0]).toMatchObject({ releaseDigest: result.release.releaseDigest, label: "v1", environments: [] });
152
152
  expect(doc.releases[0].cuts[0]).toMatchObject({ runId: result.cut.runId, runOrigin: { forge: "local" } });
153
153
 
154
- // #4049: the cut is not signed, so a publish refuses it unless --allow-unsigned.
155
- const unsignedPub = chant(dir, ["release", "publish", "v1"]);
156
- expect(unsignedPub.code).toBe(1);
157
- expect(unsignedPub.stderr).toMatch(/v1 has no signed cut.*\[release-unsigned\]/);
158
- expect(unsignedPub.stderr).toContain("pass --allow-unsigned");
159
-
154
+ // #4168: the cut is not signed, and with no release gate or verified-releases group a publish takes it quietly.
160
155
  // A publish is a dry run without --yes, as a cut is.
161
- const dryPub = chant(dir, ["release", "publish", "v1", "--allow-unsigned"]);
156
+ const dryPub = chant(dir, ["release", "publish", "v1"]);
162
157
  expect(dryPub.code, dryPub.stderr).toBe(0);
163
- expect(dryPub.stderr).toContain("v1 has no signed cut; publishing it anyway under --allow-unsigned");
158
+ expect(dryPub.stderr).not.toContain("release-unsigned");
159
+ expect(dryPub.stderr).not.toContain("no signed cut");
164
160
  expect(dryPub.stdout).toContain("What would be published where:\n nothing: this member declares no publish target (release.publish in chant.config.ts)");
165
- expect(dryPub.stderr).toContain("Dry run: nothing was published. Run again with --yes to publish: chant release publish v1 --allow-unsigned --yes");
161
+ expect(dryPub.stderr).toContain("Dry run: nothing was published. Run again with --yes to publish: chant release publish v1 --yes");
166
162
 
167
- // Publish from the workstation: the warning comes before anything else is said about publishing.
168
- const pub = chant(dir, ["release", "publish", "v1", "--allow-unsigned", "--yes"]);
163
+ // Publish from the workstation: with no supply-chain group there is no OIDC warning either.
164
+ const pub = chant(dir, ["release", "publish", "v1", "--yes"]);
169
165
  expect(pub.code, pub.stderr).toBe(0);
170
- const warning = pub.stderr.indexOf("this publish runs without OIDC, so what it publishes carries no provenance");
171
- const nothing = pub.stderr.indexOf("Nothing was published: this member declares no publish target (release.publish in chant.config.ts).");
172
- expect(warning).toBeGreaterThan(-1);
173
- expect(nothing).toBeGreaterThan(warning);
166
+ expect(pub.stderr).not.toContain("without OIDC");
167
+ expect(pub.stderr).toContain("Nothing was published: this member declares no publish target (release.publish in chant.config.ts).");
174
168
  expect(pub.stderr).toContain("chant components promote --release v1 --to dev");
175
169
 
176
- // In GitHub Actions with id-token: write there is no warning.
170
+ // The --json document records what is missing: no signature and no OIDC provenance.
171
+ const asJson = chant(dir, ["release", "publish", "v1", "--json"]);
172
+ expect(asJson.code, asJson.stderr).toBe(0);
173
+ expect(JSON.parse(asJson.stdout)).toMatchObject({ dryRun: true, label: "v1", signature: { status: "unsigned" }, provenance: { oidc: false }, warnings: [], targets: [], published: [] });
174
+
175
+ // In GitHub Actions with id-token: write the provenance is OIDC.
177
176
  const oidc = { GITHUB_ACTIONS: "true", ACTIONS_ID_TOKEN_REQUEST_URL: "https://token.example", ACTIONS_ID_TOKEN_REQUEST_TOKEN: "t" };
178
- const fromCi = chant(dir, ["release", "publish", "v1", "--allow-unsigned", "--json"], oidc);
177
+ const fromCi = chant(dir, ["release", "publish", "v1", "--json"], oidc);
179
178
  expect(fromCi.code, fromCi.stderr).toBe(0);
180
- expect(fromCi.stderr).not.toContain("without OIDC");
181
- expect(JSON.parse(fromCi.stdout)).toMatchObject({
182
- dryRun: true,
183
- label: "v1",
184
- provenance: { oidc: true, ci: "github" },
185
- warnings: ["v1 has no signed cut; publishing it anyway under --allow-unsigned, so chant release verify v1 will fail for it"],
186
- targets: [],
187
- published: [],
188
- });
179
+ expect(JSON.parse(fromCi.stdout)).toMatchObject({ dryRun: true, label: "v1", provenance: { oidc: true, ci: "github" }, warnings: [], targets: [], published: [] });
189
180
 
190
181
  const unknown = chant(dir, ["release", "publish", "v9"]);
191
182
  expect(unknown.code).toBe(1);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `chant release` (#4026, ws-104): the release is what graduates.
3
3
  *
4
- * release cut [<label>] [--yes] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
4
+ * release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param k=v]... [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json]
5
5
  * a dry run of a cut of this member from the checkout's HEAD, which must be
6
6
  * clean, on trunk and pushed, and green (#4027); `--yes` cuts, records,
7
7
  * and tags the commit `release/<label>` locally;
@@ -17,6 +17,9 @@
17
17
  * release verify <label|digest> [--json]
18
18
  * check the release's signature offline against .chant/allowed_signers
19
19
  * at the release's commit (#4049)
20
+ * release candidates [--env <env>] [--json]
21
+ * the green commits of trunk a release can be cut from, each with what it
22
+ * would ship, and what each environment runs (#4168 4.2)
20
23
  * release list [--json]
21
24
  * every release of this member with its targets, its cuts and where it runs;
22
25
  * `--json` prints the read contract's `releases` document
@@ -39,13 +42,19 @@ import { parseParamFlags } from "../build-params-cli";
39
42
  import type { CommandContext } from "../registry";
40
43
 
41
44
  const USAGE =
42
- "chant release cut [<label>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
45
+ "chant release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>] [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>] [--actor <name>] [--sign [<key file>]] [--json] | release publish <label|digest> [--yes] [--allow-unsigned] [--require-provenance] [--json] | release verify <label|digest> [--json] | release candidates [--env <env>] [--json] | release list [--json] | release export <label|digest> --env <env> [-o <dir>] [--json]";
43
46
 
44
47
  /** The build inputs `release cut` and `release compile` share. */
45
- function compileInputs(ctx: CommandContext, params: Record<string, string> | undefined): import("../../components/release-compile").CompileInputs {
48
+ function compileInputs(
49
+ ctx: CommandContext,
50
+ params: Record<string, string> | undefined,
51
+ projectDir: string = resolve("."),
52
+ releaseLabel?: string | null,
53
+ ): import("../../components/release-compile").CompileInputs {
46
54
  const { args } = ctx;
47
55
  return {
48
- projectDir: resolve("."),
56
+ projectDir,
57
+ ...(releaseLabel ? { releaseLabel } : {}),
49
58
  ...(args.buildCommand ? { buildCommand: args.buildCommand } : {}),
50
59
  ...(params ? { params } : {}),
51
60
  ...(args.paramsFile ? { paramsFile: args.paramsFile } : {}),
@@ -102,6 +111,11 @@ function describeActorSource(source: import("../../lifecycle/release-workstation
102
111
  * would record, build and tag, and what publishes where. With `--yes` it
103
112
  * cuts, signs with `--sign`, records the cut with its actor and run id,
104
113
  * tags the commit locally and prints the next command.
114
+ *
115
+ * `--at <sha>` (#4168 4.2) cuts that commit instead of `HEAD`, from a
116
+ * temporary worktree (../../lifecycle/release-at.ts), so the checkout need
117
+ * not be clean or at the commit. The label reaches every compile and
118
+ * artifact build as `CHANT_RELEASE_LABEL` (decision 5).
105
119
  */
106
120
  export async function runReleaseCut(ctx: CommandContext): Promise<number> {
107
121
  const { args } = ctx;
@@ -124,6 +138,8 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
124
138
  console.error(formatError({ message: `${message} [${code}]`, ...(fix ? { hint: fix } : {}) }));
125
139
  return 1;
126
140
  };
141
+ const at = await import("../../lifecycle/release-at");
142
+ let checkout: import("../../lifecycle/release-at").CommitCheckout | null = null;
127
143
 
128
144
  try {
129
145
  // The workstation checks come first, in the order #4027 lists them; the
@@ -139,9 +155,16 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
139
155
  let trunk: import("../../lifecycle/release-workstation").Trunk | null = null;
140
156
  let signers: readonly import("../../workspace/trust/trust-files").Signer[] = [];
141
157
  let member: string | null = null;
158
+ if (args.at !== undefined && !top) {
159
+ return fail("not-a-git-repository", "--at names a commit, and this directory is not in a git repository");
160
+ }
142
161
  if (top) {
143
- commit = await getHeadCommit({ cwd });
144
- ws.checkCleanTree(top, commit);
162
+ if (args.at !== undefined) {
163
+ commit = at.resolveCutCommit(top, args.at);
164
+ } else {
165
+ commit = await getHeadCommit({ cwd });
166
+ ws.checkCleanTree(top, commit);
167
+ }
145
168
  const { scopeSource } = await import("../../workspace/write-scope");
146
169
  const { releaseRuleFrom } = await import("../../workspace/identity");
147
170
  const source = scopeSource(cwd);
@@ -179,8 +202,11 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
179
202
  const tag = label !== null && commit !== null && top ? { name: ws.releaseTagName(member, label), state: ws.checkReleaseTag(top, ws.releaseTagName(member, label), commit) } : null;
180
203
  const ci = resolveCiRun(process.env);
181
204
  const params = parseParamFlags(args.param);
182
- const compiler = spawnTargetCompiler(compileInputs(ctx, params));
183
- const request = { cwd, compiler, label, actor, ...(args.sign !== undefined ? { sign: args.sign } : {}) };
205
+ // --at: the cut reads, compiles and builds the commit's tree, not the checkout's.
206
+ if (args.at !== undefined && top && commit) checkout = at.checkoutCommit(top, cwd, commit);
207
+ const cutCwd = checkout?.memberDir ?? cwd;
208
+ const compiler = spawnTargetCompiler(compileInputs(ctx, params, cutCwd, label));
209
+ const request = { cwd: cutCwd, compiler, label, actor, ...(args.sign !== undefined ? { sign: args.sign } : {}) };
184
210
 
185
211
  if (dryRun) {
186
212
  const plan = await planRelease(request);
@@ -195,7 +221,7 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
195
221
  const ref = plan.label ?? plan.alreadyCut ?? plan.releaseDigest;
196
222
  const provenance = detectProvenance();
197
223
  // The targets release publish would run, as the config here declares them; their files are read at publish.
198
- const publishTargets = await releasePublishTargets(cwd).then(
224
+ const publishTargets = await releasePublishTargets(cutCwd).then(
199
225
  (ts) => ts.map((t) => `${t.name} to ${t.destination}`),
200
226
  (err: unknown) => (err instanceof Error ? err : new Error(String(err))),
201
227
  );
@@ -207,6 +233,7 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
207
233
  actorSource: who.source,
208
234
  run: ci ? { runId: ci.runId, runOrigin: ci.runOrigin } : { runId: null, runOrigin: { forge: "local" as const } },
209
235
  trunk,
236
+ at: args.at !== undefined ? { ref: args.at, commit } : null,
210
237
  sign: args.sign !== undefined ? { key: publicKey !== null ? publicKey.split(" ")[0] : null, readable: publicKey !== null } : null,
211
238
  tag: tag ? { name: tag.name, exists: tag.state === "present" } : null,
212
239
  publish: { targets: Array.isArray(publishTargets) ? publishTargets : [], provenance },
@@ -294,7 +321,7 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
294
321
  if (tagWarning) console.error(formatWarning({ message: tagWarning }));
295
322
  return 0;
296
323
  } catch (err) {
297
- if (err instanceof ws.WorkstationCheckError || err instanceof ReleaseCutError) {
324
+ if (err instanceof ws.WorkstationCheckError || err instanceof ReleaseCutError || err instanceof at.CutAtError) {
298
325
  return fail(err.code, err.message, err.fix);
299
326
  }
300
327
  if (err instanceof ReleaseLabelError) return fail(err.code, err.message);
@@ -303,6 +330,8 @@ export async function runReleaseCut(ctx: CommandContext): Promise<number> {
303
330
  return 1;
304
331
  }
305
332
  throw err;
333
+ } finally {
334
+ checkout?.remove();
306
335
  }
307
336
  }
308
337
 
@@ -480,6 +509,47 @@ export async function runReleaseVerify(ctx: CommandContext): Promise<number> {
480
509
  return 0;
481
510
  }
482
511
 
512
+ /**
513
+ * `release candidates [--env <env>] [--json]` (#4168 4.2): the green commits
514
+ * of trunk a release can be cut from, newest first, each with what it would
515
+ * ship since the previous one, and what each environment runs. `--json`
516
+ * prints the read contract's `release-candidates` document.
517
+ */
518
+ export async function runReleaseCandidates(ctx: CommandContext): Promise<number> {
519
+ const { releaseCandidatesDocument } = await import("../../workspace/release-candidates");
520
+ const doc = await releaseCandidatesDocument({ cwd: resolve("."), ...(ctx.args.env ? { env: ctx.args.env } : {}) });
521
+ if (ctx.args.json) {
522
+ console.log(JSON.stringify(doc, null, 2));
523
+ return "error" in doc ? 1 : 0;
524
+ }
525
+ if ("error" in doc) {
526
+ console.error(formatError({ message: `${doc.error.message} [${doc.error.code}]`, hint: USAGE }));
527
+ return 1;
528
+ }
529
+ for (const e of doc.environments) {
530
+ const runs = e.running.map((r) => `${r.label ?? r.releaseDigest.slice(0, 19)}${r.commit ? ` at ${r.commit.slice(0, 12)}` : ""}`).join(", ");
531
+ console.log(`${e.env}: ${runs || "runs no release yet"}`);
532
+ }
533
+ if (!doc.tip) {
534
+ console.error(`No candidates: this clone knows no ${doc.branch}. Fetch it: git fetch origin ${doc.branch} --tags`);
535
+ return 0;
536
+ }
537
+ if (doc.pending.length > 0) console.log(`${doc.pending.length} commit(s) on ${doc.branch} after the newest candidate are not green yet`);
538
+ if (doc.candidates.length === 0) console.log(`No green commit on ${doc.branch}${doc.since ? ` since ${doc.since.slice(0, 12)}` : ""} to cut from.`);
539
+ for (const c of doc.candidates) {
540
+ const tags = [...(c.newFor.length > 0 ? [`new for ${c.newFor.join(", ")}`] : []), ...(c.cut.length > 0 ? [`cut as ${c.cut.join(", ")}`] : [])];
541
+ console.log(`${c.commit.slice(0, 12)} ${c.subject}${tags.length > 0 ? ` (${tags.join("; ")})` : ""}`);
542
+ for (const ch of c.changes) {
543
+ const links = [...(ch.pr !== null ? [`#${ch.pr}`] : []), ...ch.work.map((w) => `work:${w}`)];
544
+ console.log(` ${ch.commit.slice(0, 12)} ${ch.subject}${links.length > 0 ? ` [${links.join(", ")}]` : ""}`);
545
+ }
546
+ for (const f of c.flags) console.log(` flag ${f.id} (${f.member} ${f.param}) changed`);
547
+ }
548
+ for (const sk of doc.skipped) console.log(`skipped ${sk.commit.slice(0, 12)} ${sk.subject} (revoked)`);
549
+ if (doc.candidates.length > 0) console.error(`Cut one: chant release cut <label> --at <sha>`);
550
+ return 0;
551
+ }
552
+
483
553
  export async function runReleaseList(ctx: CommandContext): Promise<number> {
484
554
  const { releasesDocument } = await import("../../workspace/releases");
485
555
  const doc = await releasesDocument({ cwd: resolve(".") });
@@ -540,7 +610,7 @@ export async function runReleaseExport(ctx: CommandContext): Promise<number> {
540
610
  console.error(formatError({ message: target.error }));
541
611
  return 1;
542
612
  }
543
- const read = await targetFiles(found.release, target.target, (digest) => readReleaseTemplate(digest));
613
+ const read = await targetFiles(found.release, target.target, (digest, encoding) => readReleaseTemplate(digest, undefined, encoding));
544
614
  if ("errors" in read) {
545
615
  for (const e of read.errors) console.error(formatError({ message: e }));
546
616
  return 1;
package/src/cli/main.ts CHANGED
@@ -249,22 +249,28 @@ CI:
249
249
  chant/lifecycle; with ci.green.signedSince, the ones a
250
250
  runner key signed. Pushes the
251
251
  tags. --key, or the PEM in CHANT_RUNNER_KEY, signs
252
- each tag with a runner key.
252
+ each tag with a runner key. A cancelled run leaves a
253
+ commit unbuilt, not red, and a failed run is run
254
+ again once. A phase with build: newest leaves the
255
+ commits a newer run superseded unbuilt, and a red
256
+ one starts a bisect: a ci/build/<sha> tag the tick
257
+ starts the phase's workflow at. Each commit judged
258
+ gets a _ci/<sha>.json record on chant/lifecycle.
253
259
  ci run plan|apply --env <env> --base <sha> [--pr <n>] [--commit <sha>]
254
260
  [--forge github|gitlab|forgejo] [--gate <name>] [--wave-gate <name>]
255
261
  [--branch <name>] [--events-json] [--report-url <url>] [--image <image>]
256
262
  [--before-script <line>]... [--pr-build]
257
263
  [--pr-build-command <line>] [--chant <command>]
258
264
  [--pass-env <NAME>]... [--secrets <file>] [--key <runner key.pem>]
259
- [-o <dir>] [--wave <k>] [--no-require-review]
265
+ [-o <dir>] [--wave <k>] [--require-review]
260
266
  [--no-status] [--status-url <url template>]
261
- [--in-place | --local] [--no-push] [--verbose]
262
- Run the pull-request pipeline's plan or apply job as
267
+ [--in-place | --local] [--no-push] [--require-runner-start] [--verbose]
268
+ Run the pull-request pipeline's plan, record or apply job as
263
269
  a pipeline Op in a fresh worktree of --commit (default
264
270
  HEAD) with a clean environment. --base and --pr stand
265
271
  in for the forge's event values. --pr-build runs
266
272
  the job's build and lint lines first. Records the run as
267
- pr-plan or pr-apply on chant/lifecycle, which ci tick
273
+ pr-plan, pr-record or pr-apply on chant/lifecycle, which ci tick
268
274
  from runs counts, signed when --key or CHANT_RUNNER_KEY
269
275
  gives a runner key, and copies the report to -o
270
276
  (default .chant/pr). --in-place runs in this checkout
@@ -286,17 +292,39 @@ CI:
286
292
  kept on the local chant/lifecycle with runOrigin
287
293
  local and is never pushed, no status is set, nothing
288
294
  goes under a report URL, and ci tick does not count it.
295
+ --require-runner-start refuses the job before its
296
+ first step unless a runner-start statement, signed by
297
+ a runner key .chant/trust.json at --base lists, names
298
+ this forge job.
299
+ ci runner-start sign --key <runner key.pem> --principal <name> --job <json|@file> [-o <file>]
300
+ For the provisioner that starts a runner for one job:
301
+ sign the job's system, repo, run, attempt, job,
302
+ commit and runner name, and print the envelope.
303
+ ci runner-start verify [--base <rev>] [--commit <sha>] [--json]
304
+ In the job: check the statement in CHANT_RUNNER_START,
305
+ or the file CHANT_RUNNER_START_FILE names (default
306
+ /run/chant/runner-start.json), against the forge's
307
+ variables and .chant/trust.json at --base (default
308
+ origin/HEAD). Exits 1 unless it verifies.
309
+ ci run --at <sha> Ask for every required phase at a commit of the
310
+ green branch with a ci/build/<sha> tag, and start the
311
+ workflows holding them there on GitHub when a token
312
+ is set; otherwise the next tick, or ci serve
313
+ --build-command, does.
289
314
  ci runs [--report-url <url>] [--json]
290
315
  List the runs in the index at the report URL (default
291
316
  the workspace's ci.reportUrl), newest first.
292
317
  ci serve --forge forgejo|github --repo <owner/repo> --env <env>
293
318
  [--interval <s>] [--max-jobs <n>] [--state <dir>]
294
319
  [--author <login>]... [--once] [--rerun-failed] [--unsigned]
295
- [ci run's job flags]
320
+ [--apply-on-merge] [--build-command <line>] [ci run's job flags]
296
321
  Poll the forge for open pull requests and the target
297
322
  branch's head, and run ci run plan for each new pull
298
- request head and ci run apply for each new commit on
299
- the branch. Dedupes on job and commit in a state file
323
+ request head and ci run record for each new commit on
324
+ the branch, then ci tick from the runs when the
325
+ workspace declares ci.green. A merge deploys nothing;
326
+ --apply-on-merge runs ci run apply in place of the
327
+ record, and --wave-gate needs it. Dedupes on job and commit in a state file
300
328
  under --state, takes a lease per apply group, keeps
301
329
  each apply's resume record there, and runs at most
302
330
  --max-jobs (default 1) jobs at once. --once runs one
@@ -304,13 +332,22 @@ CI:
304
332
  failed apply or plan again. Signs runs with a runner
305
333
  key when .chant/trust.json at the target's head lists
306
334
  it, and serves unsigned otherwise; with
307
- ci.green.signedSince it refuses to start without one
335
+ ci.green.signedSince it refuses to start without one.
336
+ --build-command runs <line> once per ci/build/<sha>
337
+ tag, in a fresh worktree of the commit, with
338
+ CHANT_BUILD_COMMIT and CHANT_BUILD_PHASES set
308
339
  ci serve|run --setup-key [--trust [--principal <name>]] [--state <dir>] [--branch <name>]
309
340
  Make the host's runner key (Ed25519, mode 0600) in the
310
341
  state directory when there is none and print its public
311
342
  key. --trust commits its runner entry on a branch of its
312
343
  own and pushes it for review; the target branch is never
313
344
  written
345
+ ci init --workflow trunk-curated|trunk-curated+apply-on-merge
346
+ [--forge github|gitlab|forgejo] [--security <group>]... [--yes]
347
+ Set the workspace up for a prescribed delivery workflow:
348
+ print what it would write as a diff, then write it on
349
+ --yes or a yes at the prompt and run the generators. A
350
+ rerun proposes only what workspace check still reports
314
351
  ci workflow [--workflow <name>]... [--output <file>] Write
315
352
  [--chant <command>] [--install <command>]
316
353
  [--token-secret <NAME> | --app-id-var <VAR> --app-key-secret <NAME>]
@@ -322,10 +359,14 @@ CI:
322
359
  or a GitHub App's id and key, pushes the tags with a
323
360
  token that may tag commits changing workflow files.
324
361
  --runner-key-secret hands the tick the runner key it
325
- signs its tags with.
362
+ signs its tags with. For each phase that sets build,
363
+ it also writes the triggers and concurrency of the
364
+ workflows holding it: newest builds on pushes and by
365
+ hand, one run at a time, and every gives each commit
366
+ a run of its own.
326
367
 
327
368
  Releases:
328
- release cut [<label>] [--yes] [--build <command>] [--param <k=v>]
369
+ release cut [<label>] [--at <sha>] [--yes] [--build <command>] [--param <k=v>]
329
370
  [--params-file <file>] [--src <dir>] [-o <file>] [--lexicon <name>]
330
371
  [--actor <name>] [--sign [<key file>]] [--json]
331
372
  A dry run of a cut of this member from HEAD, which
@@ -344,22 +385,37 @@ Releases:
344
385
  requires it. A project with no components releases
345
386
  what chant build --env <env> writes: --src <dir> is
346
387
  the build root, -o <file> the output (dist/build.yaml),
347
- --lexicon narrows it.
388
+ --lexicon narrows it. --at <sha> cuts that commit of
389
+ trunk from a temporary worktree instead of HEAD, so
390
+ the checkout need not be clean. The label reaches
391
+ every compile and build as CHANT_RELEASE_LABEL.
392
+ release candidates [--env <env>] [--json]
393
+ The green commits of trunk a release can be cut from,
394
+ newest first, each with the commits, work items, pull
395
+ requests and flags it would ship, and what each
396
+ environment runs.
348
397
  release publish <label|digest> [--yes] [--allow-unsigned]
349
398
  [--require-provenance] [--json]
350
399
  Publish a cut release to the targets release.publish
351
400
  in chant.config.ts declares (npm, OCI registries,
352
- forge release assets), each with an SBOM and the
353
- release's provenance. A dry run without --yes.
354
- Refuses a revoked release, one whose signature does
355
- not verify against .chant/allowed_signers unless
356
- --allow-unsigned, and with a target a checkout that
357
- is not the release's commit. Warns first when the run
358
- has no OIDC token, so what it publishes carries no
359
- OIDC provenance; --require-provenance refuses instead.
401
+ forge release assets), each with the release's
402
+ provenance and an SBOM when it has a source. A dry
403
+ run without --yes. Refuses a revoked release, and
404
+ with a target a checkout that is not the release's
405
+ commit. Under security verified-releases it refuses
406
+ one whose signature does not verify against
407
+ .chant/allowed_signers unless --allow-unsigned; under
408
+ supply-chain it warns when the run has no OIDC token
409
+ and refuses a target with no SBOM source.
410
+ --require-provenance refuses a run with no OIDC.
360
411
  release verify <label|digest> [--json]
361
412
  Check, offline, that the release was signed by a key
362
413
  .chant/allowed_signers lists at the release's commit.
414
+ release workflow [--forge github|gitlab|forgejo] [--install <command>]
415
+ [--build <command>] [--chant <command>] [--output <file>]
416
+ Write the publish workflow, run on a release/** tag.
417
+ Under supply-chain it passes --require-provenance, and
418
+ under verified-releases it runs release verify first
363
419
  release list [--json] Every release of this member with its targets, its
364
420
  cuts and the environments that run it.
365
421
  release export <label|digest> --env <env> [-o <dir>] [--json]
@@ -887,6 +943,10 @@ Lifecycle (alias: lc):
887
943
  estate size rather than stack count (#2405)
888
944
  lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
889
945
  --json: emit the ChangeSet as JSON
946
+ lifecycle health <env> Whether each declared service is up, separate from
947
+ drift: ECS running vs desired, Route 53 health
948
+ checks, Container Apps revisions, Cloud Run Ready.
949
+ A failed read is "not read", never unhealthy (--json)
890
950
  lifecycle whoami <env> Who chant would act as in each configured lexicon,
891
951
  and what that principal is scoped to — read-only,
892
952
  before anything acts (--json, --strict)
@@ -931,7 +991,12 @@ Component release ledger + status:
931
991
  report and note (--base <ref> --pr <n>
932
992
  [--forge github|gitlab|forgejo] posts them);
933
993
  applies nothing
934
- components pr-apply On merge: plan the same members again and apply
994
+ components pr-record On merge (#4168): plan the same members again,
995
+ store the change set and write the merge record
996
+ _merges/<commit>.json; applies and refuses
997
+ nothing, and a plan that differs from the
998
+ approved one sets a failing chant/record status
999
+ components pr-apply With --apply-on-merge: plan the same members again and apply
935
1000
  them only if an approval stands for that digest
936
1001
  (chant approve pr-<n> pr-apply --plan <digest>);
937
1002
  a moved plan refuses with exit 3
@@ -968,7 +1033,11 @@ Component release ledger + status:
968
1033
  [--component <name> [--digest <sha256:...>]];
969
1034
  --from defaults to the environment before --to
970
1035
  in the config's environments; --dry-run prints
971
- the plan; the target's gates still apply)
1036
+ the plan; the target's gates still apply. Under
1037
+ --release a dependent plans against its
1038
+ upstream's planned outputs and plans again once
1039
+ it applies; --wave-gate <gate> checks each wave
1040
+ at <gate>-wave-<k>; a stop writes --resume <file>)
972
1041
  components rollback <env> Redeploy an earlier release of an environment
973
1042
  from its recorded digest (--component <name>
974
1043
  [--digest <sha256:...>]; defaults to the release
@@ -1383,9 +1452,12 @@ export const commandRegistry: CommandDef[] = [
1383
1452
  { name: "ci list", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiList(ctx) },
1384
1453
  { name: "ci tick", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiTick(ctx) },
1385
1454
  { name: "ci workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-green-cli")).runCiWorkflow(ctx) },
1455
+ { name: "ci init", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-init")).runCiInit(ctx) },
1386
1456
  // #4094: one pull-request job as a pipeline Op in a fresh checkout, the chant runner's first slice.
1387
1457
  { name: "ci run", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run")).runCiRun(ctx) },
1388
1458
  // #4098: the run index at the report URL.
1459
+ // #4147: a provisioner's signed word that it started a runner for one job, signed and checked.
1460
+ { name: "ci runner-start", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-runner-start")).runCiRunnerStart(ctx) },
1389
1461
  { name: "ci runs", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-run-index")).runCiRuns(ctx) },
1390
1462
  { name: "ci serve", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/ci-serve")).runCiServe(ctx) },
1391
1463
  // #4026: the release cut from a green commit (ws-104). Imported on first use.
@@ -1394,6 +1466,8 @@ export const commandRegistry: CommandDef[] = [
1394
1466
  { name: "release publish", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleasePublish(ctx) },
1395
1467
  // #4049: check a release's signature offline.
1396
1468
  { name: "release verify", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseVerify(ctx) },
1469
+ { name: "release workflow", runsNoConfig: true, handler: async (ctx) => (await import("../workspace/release-workflow")).runReleaseWorkflow(ctx) },
1470
+ { name: "release candidates", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCandidates(ctx) },
1397
1471
  { name: "release list", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseList(ctx) },
1398
1472
  { name: "release export", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseExport(ctx) },
1399
1473
  { name: "release compile", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/release")).runReleaseCompile(ctx) },
@@ -1504,6 +1578,8 @@ export const commandRegistry: CommandDef[] = [
1504
1578
  { name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
1505
1579
  { name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
1506
1580
  { name: "lifecycle whoami", requiresPlugins: true, handler: runLifecycleWhoami },
1581
+ // #4177 — runtime health, separate from drift. Imported on first use.
1582
+ { name: "lifecycle health", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/lifecycle-health")).runLifecycleHealth(ctx) },
1507
1583
  { name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
1508
1584
  { name: "lifecycle log", handler: runLifecycleLog },
1509
1585
  // #4036 — retention's garbage collection. Imported on first use.
@@ -1519,6 +1595,7 @@ export const commandRegistry: CommandDef[] = [
1519
1595
  // #3183 — the pull-request loop the generated CI runs. Imported on first use.
1520
1596
  { name: "components pr-plan", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPlan(ctx) },
1521
1597
  { name: "components pr-apply", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrApply(ctx) },
1598
+ { name: "components pr-record", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrRecord(ctx) },
1522
1599
  { name: "components status", requiresPlugins: true, handler: runComponentsStatus },
1523
1600
  { name: "components release", handler: runComponentsReleaseRecord },
1524
1601
  { name: "components export", handler: runComponentsExport },
@@ -70,6 +70,7 @@ const BOOLEAN_FLAGS = new Set([
70
70
  "--trust",
71
71
  "--unsigned",
72
72
  "--in-place",
73
+ "--require-runner-start",
73
74
  "--no-push",
74
75
  "--no-require-review",
75
76
  "--expire",
@@ -409,9 +410,17 @@ export function parseArgs(args: string[]): ParsedArgs {
409
410
  result.requireProvenance = true;
410
411
  } else if (arg === "--pr-loop") {
411
412
  result.prLoop = true;
413
+ } else if (arg === "--apply-on-merge") {
414
+ // `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
415
+ result.applyOnMerge = true;
412
416
  } else if (arg === "--pr-build") {
413
417
  // `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
414
418
  result.prBuild = true;
419
+ } else if (arg === "--build-command") {
420
+ // `chant ci serve --build-command <line>` (#4169): the line that builds a requested commit.
421
+ const line = args[++i];
422
+ if (line === undefined || line === "") throw new Error("--build-command needs a shell line: --build-command '<line>'");
423
+ result.ciBuildCommand = line;
415
424
  } else if (arg === "--pr-build-command") {
416
425
  const line = args[++i];
417
426
  if (line === undefined || line === "") throw new Error("--pr-build-command needs a shell line: --pr-build-command '<line>'");
@@ -531,6 +540,11 @@ export function parseArgs(args: string[]): ParsedArgs {
531
540
  // `chant workspace wip --branch <branch>` (#3172)
532
541
  result.branch = args[++i];
533
542
  if (!result.branch || result.branch.startsWith("-")) throw new Error("--branch needs a branch name: --branch <branch>");
543
+ } else if (arg === "--security") {
544
+ // `chant ci init --security <group>`, repeatable (#4168 step 6): a security group to turn on.
545
+ const group = args[++i];
546
+ if (!group || group.startsWith("-")) throw new Error("--security needs a group: --security verified-releases|supply-chain|who-ran-it|review-rules|isolated-runners");
547
+ result.securityGroups = [...(result.securityGroups ?? []), group];
534
548
  } else if (arg === "--workflow") {
535
549
  // `chant ci workflow --workflow <name>`, repeatable (#3573)
536
550
  const name = args[++i];
@@ -783,11 +797,18 @@ export function parseArgs(args: string[]): ParsedArgs {
783
797
  } else if (arg === "--in-place") {
784
798
  // `chant ci run --in-place` (#4156): run the job in this checkout, as a generated forge job does.
785
799
  result.ciInPlace = true;
800
+ } else if (arg === "--require-runner-start") {
801
+ // `chant ci run --require-runner-start` (#4147): refuse the job unless a provisioner's runner-start statement names it.
802
+ result.requireRunnerStart = true;
803
+ } else if (arg === "--job") {
804
+ // `chant ci runner-start sign --job <json|@file>` (#4147): the CI job a runner was started for.
805
+ result.ciJob = args[++i];
806
+ if (!result.ciJob || result.ciJob.startsWith("-")) throw new Error("--job needs the job as JSON or @<file>: --job @job.json");
786
807
  } else if (arg === "--no-push") {
787
808
  // `chant ci run --no-push` (#4156): keep the run record on the local chant/lifecycle.
788
809
  result.noPush = true;
789
810
  } else if (arg === "--no-require-review") {
790
- // `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review.
811
+ // `chant ci run apply --no-require-review` (#4156): the apply without pr-apply's --require-review, the default since #4168.
791
812
  result.requireReview = false;
792
813
  } else if (arg === "--unsigned") {
793
814
  // `chant ci serve --unsigned` (#4099): serve without a runner key listed at the target's head.