@intentius/chant 0.124.40 → 0.124.41

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 (281) hide show
  1. package/dist/__fixtures__/oidc-emulator.d.ts +49 -0
  2. package/dist/__fixtures__/oidc-emulator.d.ts.map +1 -0
  3. package/dist/__fixtures__/oidc-emulator.js +89 -0
  4. package/dist/ci-handover.d.ts +117 -0
  5. package/dist/ci-handover.d.ts.map +1 -0
  6. package/dist/ci-handover.js +217 -0
  7. package/dist/ci-identity.d.ts +190 -0
  8. package/dist/ci-identity.d.ts.map +1 -0
  9. package/dist/ci-identity.js +219 -0
  10. package/dist/cli/handlers/approve.d.ts +2 -1
  11. package/dist/cli/handlers/approve.d.ts.map +1 -1
  12. package/dist/cli/handlers/approve.js +20 -6
  13. package/dist/cli/handlers/build.d.ts.map +1 -1
  14. package/dist/cli/handlers/build.js +15 -1
  15. package/dist/cli/handlers/fan-out.js +16 -3
  16. package/dist/cli/handlers/gate-resume.d.ts +3 -0
  17. package/dist/cli/handlers/gate-resume.d.ts.map +1 -1
  18. package/dist/cli/handlers/gate-resume.js +15 -2
  19. package/dist/cli/handlers/lock.d.ts +8 -0
  20. package/dist/cli/handlers/lock.d.ts.map +1 -0
  21. package/dist/cli/handlers/lock.js +197 -0
  22. package/dist/cli/handlers/pr-env.d.ts +34 -0
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -0
  24. package/dist/cli/handlers/pr-env.js +436 -0
  25. package/dist/cli/handlers/pr.d.ts +74 -0
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +503 -53
  28. package/dist/cli/handlers/promote.d.ts.map +1 -1
  29. package/dist/cli/handlers/promote.js +9 -0
  30. package/dist/cli/handlers/query.d.ts +6 -0
  31. package/dist/cli/handlers/query.d.ts.map +1 -0
  32. package/dist/cli/handlers/query.js +80 -0
  33. package/dist/cli/main.d.ts.map +1 -1
  34. package/dist/cli/main.js +41 -2
  35. package/dist/cli/parse-args.d.ts.map +1 -1
  36. package/dist/cli/parse-args.js +31 -2
  37. package/dist/cli/registry.d.ts +12 -2
  38. package/dist/cli/registry.d.ts.map +1 -1
  39. package/dist/components/fan-out-run.d.ts +28 -0
  40. package/dist/components/fan-out-run.d.ts.map +1 -1
  41. package/dist/components/fan-out-run.js +27 -3
  42. package/dist/components/pr-environment.d.ts +298 -0
  43. package/dist/components/pr-environment.d.ts.map +1 -0
  44. package/dist/components/pr-environment.js +406 -0
  45. package/dist/components/pr-hooks.d.ts +66 -0
  46. package/dist/components/pr-hooks.d.ts.map +1 -0
  47. package/dist/components/pr-hooks.js +119 -0
  48. package/dist/components/pr-pipeline.d.ts +72 -2
  49. package/dist/components/pr-pipeline.d.ts.map +1 -1
  50. package/dist/components/pr-pipeline.js +96 -5
  51. package/dist/components/pr-policy.d.ts +109 -0
  52. package/dist/components/pr-policy.d.ts.map +1 -0
  53. package/dist/components/pr-policy.js +150 -0
  54. package/dist/components/pr-run.d.ts.map +1 -1
  55. package/dist/components/pr-run.js +5 -3
  56. package/dist/gate-decision.d.ts +15 -2
  57. package/dist/gate-decision.d.ts.map +1 -1
  58. package/dist/gate-decision.js +17 -2
  59. package/dist/gate-policy.d.ts +140 -0
  60. package/dist/gate-policy.d.ts.map +1 -0
  61. package/dist/gate-policy.js +242 -0
  62. package/dist/lifecycle/gate-checkpoint.d.ts +1 -1
  63. package/dist/lifecycle/gate-checkpoint.js +1 -1
  64. package/dist/{workspace/trust → lifecycle}/gate-seal.d.ts +1 -1
  65. package/dist/lifecycle/gate-seal.d.ts.map +1 -0
  66. package/dist/lifecycle/query.d.ts +68 -0
  67. package/dist/lifecycle/query.d.ts.map +1 -0
  68. package/dist/lifecycle/query.js +246 -0
  69. package/dist/lifecycle/run-ledger.d.ts +13 -0
  70. package/dist/lifecycle/run-ledger.d.ts.map +1 -1
  71. package/dist/lifecycle/run-ledger.js +16 -0
  72. package/dist/lifecycle/target-lock.d.ts +97 -0
  73. package/dist/lifecycle/target-lock.d.ts.map +1 -0
  74. package/dist/lifecycle/target-lock.js +286 -0
  75. package/dist/op/activities/activity-contracts.d.ts +30 -0
  76. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  77. package/dist/op/activities/activity-contracts.js +26 -0
  78. package/dist/op/activities/index.d.ts +2 -0
  79. package/dist/op/activities/index.d.ts.map +1 -1
  80. package/dist/op/activities/index.js +4 -0
  81. package/dist/op/activities/review-agent.d.ts +137 -0
  82. package/dist/op/activities/review-agent.d.ts.map +1 -0
  83. package/dist/op/activities/review-agent.js +266 -0
  84. package/dist/op/builders.d.ts +9 -0
  85. package/dist/op/builders.d.ts.map +1 -1
  86. package/dist/op/builders.js +11 -0
  87. package/dist/op/cron.d.ts +8 -0
  88. package/dist/op/cron.d.ts.map +1 -1
  89. package/dist/op/cron.js +51 -0
  90. package/dist/op/gate-approval.d.ts +20 -13
  91. package/dist/op/gate-approval.d.ts.map +1 -1
  92. package/dist/op/gate-approval.js +13 -18
  93. package/dist/op/gate-quorum.d.ts +131 -0
  94. package/dist/op/gate-quorum.d.ts.map +1 -0
  95. package/dist/op/gate-quorum.js +183 -0
  96. package/dist/op/gate-resume.d.ts +4 -2
  97. package/dist/op/gate-resume.d.ts.map +1 -1
  98. package/dist/op/gate-resume.js +5 -4
  99. package/dist/op/gate-review.js +1 -1
  100. package/dist/op/gate.d.ts +32 -4
  101. package/dist/op/gate.d.ts.map +1 -1
  102. package/dist/op/gate.js +40 -24
  103. package/dist/op/index.d.ts +6 -4
  104. package/dist/op/index.d.ts.map +1 -1
  105. package/dist/op/index.js +4 -3
  106. package/dist/op/local-executor.d.ts.map +1 -1
  107. package/dist/op/local-executor.js +9 -3
  108. package/dist/op/op-waves-run.d.ts +62 -1
  109. package/dist/op/op-waves-run.d.ts.map +1 -1
  110. package/dist/op/op-waves-run.js +190 -11
  111. package/dist/op/op-waves.d.ts +55 -3
  112. package/dist/op/op-waves.d.ts.map +1 -1
  113. package/dist/op/op-waves.js +63 -8
  114. package/dist/op/schedule-overdue.d.ts +57 -0
  115. package/dist/op/schedule-overdue.d.ts.map +1 -0
  116. package/dist/op/schedule-overdue.js +138 -0
  117. package/dist/override-gate.d.ts +2 -2
  118. package/dist/override-gate.d.ts.map +1 -1
  119. package/dist/override-gate.js +1 -1
  120. package/dist/pr-forge.d.ts +36 -0
  121. package/dist/pr-forge.d.ts.map +1 -1
  122. package/dist/pr-forge.js +82 -0
  123. package/dist/pr-loop.d.ts +90 -1
  124. package/dist/pr-loop.d.ts.map +1 -1
  125. package/dist/pr-loop.js +77 -3
  126. package/dist/pr-post.d.ts +72 -0
  127. package/dist/pr-post.d.ts.map +1 -0
  128. package/dist/pr-post.js +134 -0
  129. package/dist/wave-blocked.d.ts +79 -0
  130. package/dist/wave-blocked.d.ts.map +1 -0
  131. package/dist/wave-blocked.js +245 -0
  132. package/dist/wave-engine.d.ts +6 -0
  133. package/dist/wave-engine.d.ts.map +1 -1
  134. package/dist/wave-engine.js +2 -0
  135. package/dist/wave-runtime.mjs +242 -242
  136. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +1 -0
  137. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -1
  138. package/dist/workspace/__fixtures__/two-member-workspace.js +1 -1
  139. package/dist/workspace/ci-branches.d.ts +51 -0
  140. package/dist/workspace/ci-branches.d.ts.map +1 -0
  141. package/dist/workspace/ci-branches.js +69 -0
  142. package/dist/workspace/ci-cloud-identity.d.ts +11 -0
  143. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -0
  144. package/dist/workspace/ci-cloud-identity.js +24 -0
  145. package/dist/workspace/ci-locks.d.ts +12 -0
  146. package/dist/workspace/ci-locks.d.ts.map +1 -0
  147. package/dist/workspace/ci-locks.js +6 -0
  148. package/dist/workspace/ci-run.d.ts +17 -1
  149. package/dist/workspace/ci-run.d.ts.map +1 -1
  150. package/dist/workspace/ci-run.js +64 -3
  151. package/dist/workspace/declaration.d.ts +63 -0
  152. package/dist/workspace/declaration.d.ts.map +1 -1
  153. package/dist/workspace/declaration.js +72 -1
  154. package/dist/workspace/declaration.schema.json +147 -0
  155. package/dist/workspace/identity-gate-rule.d.ts +1 -1
  156. package/dist/workspace/identity-gate-rule.d.ts.map +1 -1
  157. package/dist/workspace/identity-gate-rule.js +1 -1
  158. package/dist/workspace/identity.d.ts +1 -1
  159. package/dist/workspace/identity.d.ts.map +1 -1
  160. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  161. package/dist/workspace/lineage-upgrade.js +8 -5
  162. package/dist/workspace/member-pipeline.d.ts +6 -0
  163. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  164. package/dist/workspace/member-pipeline.js +23 -0
  165. package/dist/workspace/pr-report.schema.json +73 -2
  166. package/dist/workspace/security.d.ts.map +1 -1
  167. package/dist/workspace/security.js +4 -0
  168. package/dist/workspace/status-gates.d.ts +3 -2
  169. package/dist/workspace/status-gates.d.ts.map +1 -1
  170. package/dist/workspace/status-gates.js +8 -7
  171. package/dist/workspace/status.d.ts.map +1 -1
  172. package/dist/workspace/status.js +4 -1
  173. package/dist/workspace/trust/seal.d.ts +2 -2
  174. package/dist/workspace/trust/seal.d.ts.map +1 -1
  175. package/dist/workspace/trust/seal.js +2 -2
  176. package/package.json +1 -1
  177. package/src/__fixtures__/oidc-emulator.ts +118 -0
  178. package/src/ci-handover.test.ts +209 -0
  179. package/src/ci-handover.ts +237 -0
  180. package/src/ci-identity.test.ts +201 -0
  181. package/src/ci-identity.ts +310 -0
  182. package/src/cli/handlers/approve.ts +18 -5
  183. package/src/cli/handlers/build.ts +15 -1
  184. package/src/cli/handlers/fan-out.ts +17 -4
  185. package/src/cli/handlers/gate-resume.ts +16 -2
  186. package/src/cli/handlers/lock.test.ts +98 -0
  187. package/src/cli/handlers/lock.ts +202 -0
  188. package/src/cli/handlers/operator.test.ts +3 -1
  189. package/src/cli/handlers/pr-env.ts +462 -0
  190. package/src/cli/handlers/pr-waves-blocked.test.ts +247 -0
  191. package/src/cli/handlers/pr.test.ts +184 -0
  192. package/src/cli/handlers/pr.ts +496 -59
  193. package/src/cli/handlers/promote.ts +9 -0
  194. package/src/cli/handlers/query.ts +78 -0
  195. package/src/cli/main.ts +41 -2
  196. package/src/cli/parse-args.ts +23 -2
  197. package/src/cli/registry.ts +12 -2
  198. package/src/components/fan-out-run.ts +39 -2
  199. package/src/components/fan-out-waves.test.ts +45 -1
  200. package/src/components/pr-environment.test.ts +249 -0
  201. package/src/components/pr-environment.ts +538 -0
  202. package/src/components/pr-hooks.test.ts +118 -0
  203. package/src/components/pr-hooks.ts +157 -0
  204. package/src/components/pr-pipeline.ts +130 -5
  205. package/src/components/pr-policy.test.ts +145 -0
  206. package/src/components/pr-policy.ts +211 -0
  207. package/src/components/pr-run.ts +5 -2
  208. package/src/gate-decision-bundle.test.ts +4 -3
  209. package/src/gate-decision.ts +39 -2
  210. package/src/gate-policy-bundle.test.ts +28 -0
  211. package/src/gate-policy.test.ts +162 -0
  212. package/src/gate-policy.ts +298 -0
  213. package/src/lifecycle/gate-checkpoint.ts +1 -1
  214. package/src/{workspace/trust → lifecycle}/gate-seal.ts +1 -1
  215. package/src/lifecycle/query.test.ts +91 -0
  216. package/src/lifecycle/query.ts +315 -0
  217. package/src/lifecycle/run-ledger.ts +18 -0
  218. package/src/lifecycle/target-lock.test.ts +195 -0
  219. package/src/lifecycle/target-lock.ts +347 -0
  220. package/src/op/activities/activity-contracts.ts +31 -0
  221. package/src/op/activities/index.ts +6 -0
  222. package/src/op/activities/review-agent.test.ts +240 -0
  223. package/src/op/activities/review-agent.ts +355 -0
  224. package/src/op/builders.ts +13 -0
  225. package/src/op/cron.ts +44 -0
  226. package/src/op/gate-alias.test.ts +2 -2
  227. package/src/op/gate-approval.test.ts +30 -9
  228. package/src/op/gate-approval.ts +23 -26
  229. package/src/op/gate-quorum.test.ts +139 -0
  230. package/src/op/gate-quorum.ts +248 -0
  231. package/src/op/gate-resume.ts +6 -3
  232. package/src/op/gate-review.ts +1 -1
  233. package/src/op/gate.ts +64 -23
  234. package/src/op/index.ts +13 -1
  235. package/src/op/local-executor.ts +9 -2
  236. package/src/op/op-waves-hook-targets.test.ts +126 -0
  237. package/src/op/op-waves-policy-hold.test.ts +66 -0
  238. package/src/op/op-waves-quorum.test.ts +101 -0
  239. package/src/op/op-waves-run.ts +233 -11
  240. package/src/op/op-waves.ts +83 -10
  241. package/src/op/schedule-overdue.test.ts +220 -0
  242. package/src/op/schedule-overdue.ts +161 -0
  243. package/src/override-gate-bundle.test.ts +1 -1
  244. package/src/override-gate.ts +2 -2
  245. package/src/pr-forge-bundle.test.ts +2 -2
  246. package/src/pr-forge.test.ts +33 -0
  247. package/src/pr-forge.ts +114 -0
  248. package/src/pr-loop-bundle.test.ts +2 -2
  249. package/src/pr-loop.test.ts +58 -0
  250. package/src/pr-loop.ts +146 -4
  251. package/src/pr-post.test.ts +141 -0
  252. package/src/pr-post.ts +159 -0
  253. package/src/wave-blocked-bundle.test.ts +25 -0
  254. package/src/wave-blocked.test.ts +146 -0
  255. package/src/wave-blocked.ts +271 -0
  256. package/src/wave-engine.ts +8 -0
  257. package/src/workspace/__fixtures__/two-member-workspace.ts +2 -2
  258. package/src/workspace/ci-branches.test.ts +62 -0
  259. package/src/workspace/ci-branches.ts +79 -0
  260. package/src/workspace/ci-cloud-identity.test.ts +53 -0
  261. package/src/workspace/ci-cloud-identity.ts +23 -0
  262. package/src/workspace/ci-locks.ts +16 -0
  263. package/src/workspace/ci-run-index.test.ts +2 -1
  264. package/src/workspace/ci-run.test.ts +107 -8
  265. package/src/workspace/ci-run.ts +72 -4
  266. package/src/workspace/declaration.schema.json +147 -0
  267. package/src/workspace/declaration.test.ts +76 -0
  268. package/src/workspace/declaration.ts +134 -2
  269. package/src/workspace/identity-gate-rule.ts +1 -1
  270. package/src/workspace/identity.ts +1 -1
  271. package/src/workspace/lineage-upgrade.ts +11 -8
  272. package/src/workspace/member-pipeline.test.ts +29 -0
  273. package/src/workspace/member-pipeline.ts +30 -0
  274. package/src/workspace/pr-report.schema.json +73 -2
  275. package/src/workspace/security.ts +4 -0
  276. package/src/workspace/status-gates.ts +9 -6
  277. package/src/workspace/status.ts +4 -1
  278. package/src/workspace/trust/gate-seal.test.ts +1 -1
  279. package/src/workspace/trust/seal.ts +2 -2
  280. package/dist/workspace/trust/gate-seal.d.ts.map +0 -1
  281. /package/dist/{workspace/trust → lifecycle}/gate-seal.js +0 -0
@@ -26,6 +26,7 @@ import { dirname, join, resolve } from "node:path";
26
26
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
27
27
  import { loadChantConfig, type ChantConfig } from "../../config";
28
28
  import { promotionOrder } from "../../workspace/promotion-order";
29
+ import { releaseTargetRefusal } from "../../components/pr-environment";
29
30
  import { resolvePromotionSource } from "../../components/promote-order";
30
31
  import { fetchLifecycle, getHeadCommit, pushLifecycle } from "../../lifecycle/git";
31
32
  import {
@@ -897,6 +898,14 @@ export async function runComponentsPromote(ctx: CommandContext): Promise<number>
897
898
  console.error(formatError({ message: "--to <env> is required", hint: PROMOTE_USAGE }));
898
899
  return 1;
899
900
  }
901
+ // #4298: a pull request's environment is neither a release's target nor its source.
902
+ for (const env of [to, args.migrateFrom]) {
903
+ const refusal = env ? releaseTargetRefusal(env) : undefined;
904
+ if (refusal) {
905
+ console.error(formatError({ message: refusal }));
906
+ return 1;
907
+ }
908
+ }
900
909
  const who = actorOrRefuse(ctx);
901
910
  if (typeof who === "number") return who;
902
911
 
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `chant query "<sql>" [dir] [--member <name>] [--json]` (#4297): one
3
+ * read-only SQL statement over the snapshots, Op runs, gates, deploys and
4
+ * releases on the local `chant/lifecycle` branch. The tables and the engine
5
+ * live in `../../lifecycle/query.ts`; this reads the ledgers and prints.
6
+ *
7
+ * Inside a workspace every member's ledger is read, each row naming its
8
+ * member; `--member` keeps the members it names. Outside one the project's
9
+ * own ledger is read and `member` is NULL. It reads the local branch and
10
+ * never fetches.
11
+ */
12
+ import { existsSync } from "node:fs";
13
+ import { resolve } from "node:path";
14
+ import { formatError, formatWarning } from "../format";
15
+ import type { CommandContext } from "../registry";
16
+ import type { QueryLedger } from "../../lifecycle/query";
17
+ import { findWorkspaceRoot } from "../../project-root";
18
+
19
+ const USAGE = 'chant query "<sql>" [dir] [--member <name>] [--json]';
20
+
21
+ /** The ledgers `chant query` reads from `cwd`: every member's in a workspace, else the project's own. */
22
+ export async function queryLedgers(cwd: string, members?: readonly string[]): Promise<QueryLedger[]> {
23
+ const { gitLedgerReadPort } = await import("../../lifecycle/git");
24
+ const found = findWorkspaceRoot(cwd);
25
+ if (!found) return [{ member: null, port: gitLedgerReadPort({ cwd }) }];
26
+ // Workspace code loads only when there is a workspace, so a level-0 project never pays for it.
27
+ const { readDeclaration, WorkspaceReadError } = await import("../../workspace/declaration");
28
+ const { workingTree } = await import("../../workspace/tree");
29
+ const { QueryError } = await import("../../lifecycle/query");
30
+ let declaration;
31
+ try {
32
+ declaration = readDeclaration(workingTree(found.dir), "");
33
+ } catch (err) {
34
+ if (err instanceof WorkspaceReadError) throw new QueryError(`${err.code}: ${err.message}`);
35
+ throw err;
36
+ }
37
+ const wanted = members && members.length > 0 ? new Set(members) : null;
38
+ if (wanted) {
39
+ const unknown = [...wanted].filter((n) => !declaration.members.some((m) => m.name === n));
40
+ if (unknown.length > 0) throw new QueryError(`--member ${unknown.join(", ")} names no member of the workspace`);
41
+ }
42
+ return declaration.members
43
+ .filter((m) => !wanted || wanted.has(m.name))
44
+ .map((m) => ({ member: m.name, port: gitLedgerReadPort({ cwd: found.dir, member: m.dir === "." ? "." : m.name }) }));
45
+ }
46
+
47
+ export async function runQueryCommand(ctx: CommandContext): Promise<number> {
48
+ const { args } = ctx;
49
+ const sql = args.positionals?.[0];
50
+ if (sql === undefined || sql.trim() === "") {
51
+ console.error(formatError({ message: "chant query needs a statement", hint: `${USAGE}, such as chant query "SELECT env, count(*) FROM resources GROUP BY env"` }));
52
+ return 1;
53
+ }
54
+ const cwd = resolve(args.positionals?.[1] ?? ".");
55
+ if (!existsSync(cwd)) {
56
+ console.error(formatError({ message: `${cwd} does not exist`, hint: USAGE }));
57
+ return 1;
58
+ }
59
+ const { loadQueryData, runQuery, formatQueryResult, QueryError } = await import("../../lifecycle/query");
60
+ try {
61
+ const load = await loadQueryData(await queryLedgers(cwd, args.members));
62
+ const result = await runQuery(sql, load);
63
+ if (args.json) {
64
+ console.log(JSON.stringify(result, null, 2));
65
+ } else {
66
+ console.log(formatQueryResult(result));
67
+ const bad = Object.entries(result.malformed).filter(([, n]) => n > 0);
68
+ if (bad.length > 0) console.error(formatWarning({ message: `left out what could not be read: ${bad.map(([t, n]) => `${n} in ${t}`).join(", ")}` }));
69
+ }
70
+ return 0;
71
+ } catch (err) {
72
+ if (err instanceof QueryError) {
73
+ console.error(formatError({ message: err.message, hint: USAGE }));
74
+ return 1;
75
+ }
76
+ throw err;
77
+ }
78
+ }
package/src/cli/main.ts CHANGED
@@ -173,6 +173,11 @@ Commands:
173
173
  terms; --live or --at <ref> with --env <name> reads the
174
174
  estate or a recorded snapshot; --show <attr>,
175
175
  --check-live, --check-snapshot, --fail-on-drift)
176
+ query "<sql>" [dir] Run one read-only SQL statement over chant/lifecycle:
177
+ the resources and snapshots each lexicon observed, Op
178
+ runs, gates, deploys, releases and cuts, loaded into
179
+ node:sqlite (schema v1, listed in chant_schema).
180
+ --member <name> keeps members; --json prints rows
176
181
  generate Generate typed code from sources chant.config declares
177
182
  (k8s.crds, helm.charts) into src/generated/<lexicon>/
178
183
  (codegen.outDir); --check exits 1 when the committed
@@ -558,6 +563,15 @@ Ops:
558
563
  applies it once, if .chant/overriders at base lists
559
564
  --actor
560
565
 
566
+ lock <target>... Lock targets for a pull request on chant/lifecycle
567
+ (--pr <n>, --stage plan|apply|lock, --head <sha>,
568
+ --by <who>, --gate <name>, --json). --from-report
569
+ <pr-plan.json> locks the components a plan selected.
570
+ --forge <kind> reads which holders are still open and
571
+ posts chant/lock on --head. Exits 3 when another open
572
+ pull request holds a target. --list prints the locks
573
+ unlock [<target>...] Release the locks a pull request holds (--pr <n>,
574
+ --gate <name>, --json)
561
575
  graph Show Op dependency graph (--stacks for cross-stack order,
562
576
  --format ir|mermaid|dot|layout for the lint-gated graph IR,
563
577
  a Mermaid flowchart, Graphviz DOT, or node positions;
@@ -1003,11 +1017,26 @@ Component release ledger + status:
1003
1017
  report and note (--base <ref> --pr <n>
1004
1018
  [--forge github|gitlab|forgejo] posts them);
1005
1019
  applies nothing
1020
+ components pr-post Post the plan note and the chant/plan status
1021
+ from the report pr-plan --no-status wrote
1022
+ (--pr <n> --head <sha> --env <env> --forge
1023
+ <kind>), in a job that runs none of the
1024
+ change's code (#4295)
1006
1025
  components pr-record On merge (#4168): plan the same members again,
1007
1026
  store the change set and write the merge record
1008
1027
  _merges/<commit>.json; applies and refuses
1009
1028
  nothing, and a plan that differs from the
1010
1029
  approved one sets a failing chant/record status
1030
+ components pr-env An environment per open pull request (#4298): up
1031
+ --base <ref> --pr <n> applies the change to pr-<n>,
1032
+ derived from ci.prEnvironment.template, once an
1033
+ approval of its plan stands (chant approve pr-<n>
1034
+ pr-env --plan <digest>); post --pr <n> --head
1035
+ <sha> keeps the record up --no-push handed on and
1036
+ posts the note; down --pr <n> tears down
1037
+ only what carries pr-<n>'s ownership marker; sweep
1038
+ tears down each one whose TTL passed or whose pull
1039
+ request closed. Never a release target
1011
1040
  components pr-apply With --apply-on-merge: plan the same members again and apply
1012
1041
  them only if an approval stands for that digest
1013
1042
  (chant approve pr-<n> pr-apply --plan <digest>);
@@ -1201,7 +1230,8 @@ Options:
1201
1230
  chant.config.ts's build.sandbox: true; #1045)
1202
1231
  --param <name=value> (build, graph, run --components, components fan-out,
1203
1232
  components promote, components rollback,
1204
- components redeploy, release cut) Bind a
1233
+ components redeploy, components pr-env up,
1234
+ release cut) Bind a
1205
1235
  declared build-time parameter (chant.config.ts's buildParams)
1206
1236
  to a value, for source to read as params.<name>
1207
1237
  (#1064) instead of process.env — repeatable.
@@ -1213,7 +1243,8 @@ Options:
1213
1243
  chant.template.json declares instead (#2627).
1214
1244
  --params-file <path> (build, graph, run --components, components fan-out,
1215
1245
  components promote, components rollback,
1216
- components redeploy, release cut) JSON file
1246
+ components redeploy, components pr-env up,
1247
+ release cut) JSON file
1217
1248
  of { "name": value } build-time parameter values
1218
1249
  (#1064). Second precedence, after --param.
1219
1250
  init --from refuses it: pass --param instead.
@@ -1441,6 +1472,8 @@ export const commandRegistry: CommandDef[] = [
1441
1472
  { name: "describe", handler: runDescribe },
1442
1473
  { name: "explain", handler: runExplain },
1443
1474
  { name: "search", handler: runSearch },
1475
+ // #4297 — SQL over the snapshots, runs, gates, deploys and releases on chant/lifecycle. Imported on first use.
1476
+ { name: "query", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/query")).runQueryCommand(ctx) },
1444
1477
  // chant#2591 — `import --agents` and `audit` read chant.config statically
1445
1478
  // and never evaluate it (chant#2589 for audit, which audits code it must not run).
1446
1479
  { name: "import", handler: runImport, runsNoConfig: (args) => args.agents === true },
@@ -1524,6 +1557,9 @@ export const commandRegistry: CommandDef[] = [
1524
1557
  { name: "operator log", handler: runOperatorLog },
1525
1558
  { name: "operator", handler: runOperator },
1526
1559
  { name: "approve", handler: runApprove },
1560
+ // #4293 — target locks a pull request holds on chant/lifecycle. Imported on first use.
1561
+ { name: "lock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runLock(ctx) },
1562
+ { name: "unlock", runsNoConfig: true, handler: async (ctx) => (await import("./handlers/lock")).runUnlock(ctx) },
1527
1563
 
1528
1564
  { name: "graph", handler: runGraph },
1529
1565
  { name: "vendor", handler: runVendor },
@@ -1613,7 +1649,10 @@ export const commandRegistry: CommandDef[] = [
1613
1649
  // #3183 — the pull-request loop the generated CI runs. Imported on first use.
1614
1650
  { name: "components pr-plan", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPlan(ctx) },
1615
1651
  { name: "components pr-apply", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrApply(ctx) },
1652
+ // #4295: posts from the plan report in a job that runs none of the change's code, so it loads no plugins.
1653
+ { name: "components pr-post", handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrPost(ctx) },
1616
1654
  { name: "components pr-record", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr")).runComponentsPrRecord(ctx) },
1655
+ { name: "components pr-env", requiresPlugins: true, handler: async (ctx) => (await import("./handlers/pr-env")).runComponentsPrEnv(ctx) },
1617
1656
  { name: "components status", requiresPlugins: true, handler: runComponentsStatus },
1618
1657
  { name: "components release", handler: runComponentsReleaseRecord },
1619
1658
  { name: "components export", handler: runComponentsExport },
@@ -43,6 +43,7 @@ const BOOLEAN_FLAGS = new Set([
43
43
  "--stacks",
44
44
  "--components",
45
45
  "--pr-build",
46
+ "--pr-env",
46
47
  "--up",
47
48
  "--down",
48
49
  "--include-dependents",
@@ -72,6 +73,7 @@ const BOOLEAN_FLAGS = new Set([
72
73
  "--in-place",
73
74
  "--require-runner-start",
74
75
  "--no-push",
76
+ "--list",
75
77
  "--no-require-review",
76
78
  "--expire",
77
79
  "--check-live",
@@ -396,11 +398,26 @@ export function parseArgs(args: string[]): ParsedArgs {
396
398
  if (!result.forge || result.forge.startsWith("-")) throw new Error("--forge needs a forge: --forge github|gitlab|forgejo");
397
399
  } else if (arg === "--require-review") {
398
400
  result.requireReview = true;
401
+ } else if (arg === "--list") {
402
+ // `chant lock --list` (#4293): print the target locks instead of taking any.
403
+ result.list = true;
404
+ } else if (arg === "--stage") {
405
+ // `chant lock --stage plan|apply|lock` (#4293): how the lock was taken.
406
+ result.lockStage = args[++i];
407
+ if (!result.lockStage || result.lockStage.startsWith("-")) throw new Error("--stage needs plan, apply or lock: --stage <stage>");
408
+ } else if (arg === "--from-report") {
409
+ // `chant lock --from-report <pr-plan.json>` (#4293): lock the components a pull-request report lists.
410
+ result.fromReport = args[++i];
411
+ if (!result.fromReport || result.fromReport.startsWith("-")) throw new Error("--from-report needs a report file: --from-report <pr-plan.json>");
399
412
  } else if (arg === "--report-url") {
400
413
  // `chant components pr-plan|pr-apply --report-url <url>` (#4007).
401
414
  const url = args[++i];
402
415
  if (!url || url.startsWith("-")) throw new Error("--report-url needs a URL: --report-url s3://<bucket>/<prefix> or file://<directory>");
403
416
  result.reportUrl = url;
417
+ } else if (arg === "--result") {
418
+ // `chant components pr-post --result <result>` (#4295): the plan job's own result, which fails the status when it failed.
419
+ result.planResult = args[++i];
420
+ if (!result.planResult || result.planResult.startsWith("-")) throw new Error("--result needs the plan job's result: --result success|failure|cancelled");
404
421
  } else if (arg === "--no-status") {
405
422
  // `chant ci run|serve --no-status` (#4097): post no commit status for the job.
406
423
  result.noStatus = true;
@@ -422,6 +439,9 @@ export function parseArgs(args: string[]): ParsedArgs {
422
439
  } else if (arg === "--apply-on-merge") {
423
440
  // `chant build --pr-loop --apply-on-merge` and `chant ci serve --apply-on-merge` (#4168): opt into applying on merge.
424
441
  result.applyOnMerge = true;
442
+ } else if (arg === "--pr-env") {
443
+ // `chant build --pr-loop --pr-env` (#4298): add the pull request environment's jobs.
444
+ result.prEnv = true;
425
445
  } else if (arg === "--pr-build") {
426
446
  // `chant build --pr-loop --pr-build` (#4088), and `chant ci run --pr-build` (#4094).
427
447
  result.prBuild = true;
@@ -864,8 +884,9 @@ export function parseArgs(args: string[]): ParsedArgs {
864
884
  } else if (arg === "--expire") {
865
885
  result.expire = true;
866
886
  } else if (arg === "--role") {
867
- // #2508 — a role the approver holds, for a gate whose quorum names
868
- // roles. Repeatable, and a comma list works too.
887
+ // #2508 — a role the approver claims, which a gate's policy sees. A
888
+ // quorum reads roles from .chant/trust.json at base instead (#4290).
889
+ // Repeatable, and a comma list works too.
869
890
  const value = args[++i] ?? "";
870
891
  result.roles = [...(result.roles ?? []), ...value.split(",").map((r) => r.trim()).filter(Boolean)];
871
892
  } else if (arg === "--agent") {
@@ -302,6 +302,12 @@ export interface ParsedArgs {
302
302
  * pr-apply finds it from the commit through `--forge` when omitted.
303
303
  */
304
304
  pr?: number;
305
+ /** `chant lock --list` (#4293): print the target locks instead of taking any. */
306
+ list?: boolean;
307
+ /** `chant lock --stage plan|apply|lock` (#4293): how the lock is taken. Default `lock`. */
308
+ lockStage?: string;
309
+ /** `chant lock --from-report <file>` (#4293): lock the components a `pr-plan.json` lists. */
310
+ fromReport?: string;
305
311
  /** Every `--pr <n>`, in order: `chant release compose` (#4168 4.3) takes several. */
306
312
  prs?: number[];
307
313
  /** `chant release cut --channel <tag>` (#4168 4.3): the tag a publish of the release moves in place of `latest`. */
@@ -345,6 +351,8 @@ export interface ParsedArgs {
345
351
  * approved plans of the merges between or a gate of its own.
346
352
  */
347
353
  green?: boolean;
354
+ /** `chant build --pr-loop --pr-env` (#4298) — add the env-up, env-down and env-sweep jobs of a pull request's environment. */
355
+ prEnv?: boolean;
348
356
  /** `chant build --pr-loop --pr-build` (#4088) — build and lint before pr-plan and pr-apply. */
349
357
  prBuild?: boolean;
350
358
  /** `chant ci serve --build-command <line>` (#4169) — the line that builds a commit a `ci/build/<sha>` tag asks for, in a worktree of it. */
@@ -477,7 +485,7 @@ export interface ParsedArgs {
477
485
  records?: boolean;
478
486
  /** `chant workspace wip save --label <text>` (#3172): what the caller calls the snapshot, such as a turn id. */
479
487
  label?: string;
480
- /** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. */
488
+ /** `chant workspace wip --branch <branch>` (#3172): list one branch's snapshots. `chant components pr-apply|pr-record --branch <branch>` (#4292): the branch pushed, which `ci.branches` routes members by; default the forge's GITHUB_REF_NAME or CI_COMMIT_BRANCH. */
481
489
  branch?: string;
482
490
  /** `chant ci workflow --workflow <name>`, repeatable (#3573): a workflow whose completion triggers a tick, beside those the scan finds. Also `chant ci init --workflow <name>` (#4168 step 6), the prescribed workflow to set up. */
483
491
  ciWorkflows?: string[];
@@ -491,6 +499,8 @@ export interface ParsedArgs {
491
499
  ciSecrets?: string;
492
500
  /** `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. */
493
501
  ciChant?: string;
502
+ /** `chant components pr-post --result <result>` (#4295): the plan job's own result. `failure` or `cancelled` fails the `chant/plan` status. */
503
+ planResult?: string;
494
504
  /** `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. */
495
505
  noStatus?: boolean;
496
506
  /** `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. */
@@ -737,7 +747,7 @@ export interface ParsedArgs {
737
747
  * on a model-authored channel.
738
748
  */
739
749
  allowSameOrigin?: boolean;
740
- /** `chant approve --role <name>` (#2508) — roles the approver claims, for a gate whose quorum counts only some roles. Repeatable; a comma list also works. */
750
+ /** `chant approve --role <name>` (#2508) — roles the approver claims, which a gate's policy sees. A quorum never counts them (#4290): its roles come from `.chant/trust.json` at base. Repeatable; a comma list also works. */
741
751
  roles?: string[];
742
752
  /** `chant approve --agent` (#2508) — record the approval as an agent's. An agent never counts toward a quorum; it passes a gate only through a policy permit in `enforce` mode. */
743
753
  agent?: boolean;
@@ -129,6 +129,19 @@ export interface FanOutWaveGate {
129
129
  * do not exist yet. A later wave is left for its own job.
130
130
  */
131
131
  only?: number;
132
+ /**
133
+ * With `only` (#4291): members of earlier waves the record says failed to
134
+ * plan or to apply. Each blocks its dependents in this wave and names
135
+ * itself as what blocked them; the rest of the wave runs. An earlier
136
+ * member that neither applied nor failed still refuses the run.
137
+ */
138
+ failedEarlier?: readonly string[];
139
+ /**
140
+ * With `only` (#4291): earlier waves the record says stopped at their gate.
141
+ * A member left in one of them stops this wave too ({@link EarlierWaveWaitingError}),
142
+ * since a gate nobody approved stops every wave after it.
143
+ */
144
+ waitingEarlier?: ReadonlyArray<{ wave: number; gate: string; components: readonly string[] }>;
132
145
  }
133
146
 
134
147
  /** Thrown when `FanOutWaveGate.only` names a wave whose predecessors have not all applied. */
@@ -146,6 +159,22 @@ export class EarlierWaveNotAppliedError extends Error {
146
159
  }
147
160
  }
148
161
 
162
+ /**
163
+ * Thrown when `FanOutWaveGate.only` names a wave after one that waits at its
164
+ * gate (#4291): nothing in this wave runs until that gate is approved and
165
+ * its job runs again. The CLI exits 3, as the waiting wave's job does.
166
+ */
167
+ export class EarlierWaveWaitingError extends Error {
168
+ constructor(
169
+ readonly wave: number,
170
+ readonly earlier: number,
171
+ readonly gate: string,
172
+ ) {
173
+ super(`wave ${wave} does not run yet: wave ${earlier} waits at its gate ${gate}. Approve it and run wave ${earlier} again, then this wave.`);
174
+ this.name = "EarlierWaveWaitingError";
175
+ }
176
+ }
177
+
149
178
  /** The single gate over a whole fan-out. Omit it to run without one. */
150
179
  export interface FanOutGate {
151
180
  /** The name `chant approve <op> <gate>` takes — the fan-out's name, not a component's. */
@@ -518,7 +547,13 @@ async function runGatedWaves(
518
547
  if (!wave) return { end: "stop" };
519
548
  const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
520
549
  if (waveGate.only !== undefined && waveNum < waveGate.only) {
521
- if (runnable.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, runnable);
550
+ // #4291: an earlier member that failed blocks only its dependents; one that waits at its gate stops this wave.
551
+ const failedBefore = new Set(waveGate.failedEarlier ?? []);
552
+ const open = runnable.filter((name) => !failedBefore.has(name));
553
+ const waiting = waveGate.waitingEarlier?.find((w) => w.wave === waveNum && open.some((name) => w.components.includes(name)));
554
+ if (waiting) throw new EarlierWaveWaitingError(waveGate.only, waveNum, waiting.gate);
555
+ if (open.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, open);
556
+ for (const name of runnable) markBlocked(name);
522
557
  return { end: "next" };
523
558
  }
524
559
  if (waveGate.only !== undefined && waveNum > waveGate.only) return { end: "stop" };
@@ -664,7 +699,9 @@ async function runGatedWaves(
664
699
  .sort(([a], [b]) => a.localeCompare(b))
665
700
  .map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
666
701
 
667
- const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
702
+ // #4291: a member of this job's wave that an earlier job's failure blocked fails the job, though the rest of the wave applied.
703
+ const blockedHere = waveGate.only !== undefined && (plan.waves[waveGate.only - 1] ?? []).some((name) => blockedBy.has(name));
704
+ const status: FanOutRunResult["status"] = failed.length > 0 || blockedHere ? "fail" : gatedFact ? "gated" : "ok";
668
705
  emit?.({ type: "run-settle", status: status === "fail" ? "failed" : status, durationMs: null });
669
706
 
670
707
  return {
@@ -11,7 +11,7 @@ import { describe, expect, test } from "vitest";
11
11
  import { CapabilityRegistry, type DeployContext } from "./capability";
12
12
  import { memoryGateLedgerPort } from "../op/gate";
13
13
  import { planFanOut } from "./fan-out";
14
- import { EarlierWaveNotAppliedError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
14
+ import { EarlierWaveNotAppliedError, EarlierWaveWaitingError, runFanOut, type FanOutRunOptions } from "./fan-out-run";
15
15
  import { computePlanDigest } from "../lifecycle/plan-digest";
16
16
  import type { DriverComponent } from "./driver";
17
17
  import type { GateResolutionRecord, PendingGateRecord } from "../lifecycle/gate-ledger";
@@ -354,6 +354,50 @@ describe("one wave per CI job", () => {
354
354
  });
355
355
  });
356
356
 
357
+ describe("a failed member blocks only its dependents across wave jobs (#4291)", () => {
358
+ /** net, then a and b, then app (reads a) and web (reads b). */
359
+ const WIDE: DriverComponent[] = [...ESTATE, root("web", ["b"])];
360
+
361
+ test("wave 3's job blocks what an earlier job's failure holds, and plans the rest", async () => {
362
+ const w = world();
363
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
364
+ const result = await runFanOut(plan, WIDE, w.registry, {
365
+ ...waveOptions(ledger()),
366
+ waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
367
+ progress: { completed: ["net", "b"] },
368
+ });
369
+ expect(result.blocked).toEqual([{ component: "app", reason: "blocked", blockedBy: "a" }]);
370
+ expect(w.planned.map((p) => p.root)).toEqual(["web"]);
371
+ // A member of the job's own wave was blocked, so the job fails though web went on to its gate.
372
+ expect(result.status).toBe("fail");
373
+ });
374
+
375
+ test("an earlier member that neither applied nor failed still refuses the job", async () => {
376
+ const w = world();
377
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
378
+ await expect(
379
+ runFanOut(plan, WIDE, w.registry, {
380
+ ...waveOptions(ledger()),
381
+ waveGate: { op: "fan-out", gate: "release", only: 3, failedEarlier: ["a"] },
382
+ progress: { completed: ["net"] },
383
+ }),
384
+ ).rejects.toThrow(EarlierWaveNotAppliedError);
385
+ });
386
+
387
+ test("an earlier wave that waits at its gate stops the job", async () => {
388
+ const w = world();
389
+ const plan = planFanOut({ components: WIDE, changed: ["net"] });
390
+ await expect(
391
+ runFanOut(plan, WIDE, w.registry, {
392
+ ...waveOptions(ledger()),
393
+ waveGate: { op: "fan-out", gate: "release", only: 3, waitingEarlier: [{ wave: 2, gate: "release-wave-2", components: ["a", "b"] }] },
394
+ progress: { completed: ["net"] },
395
+ }),
396
+ ).rejects.toThrow(EarlierWaveWaitingError);
397
+ expect(w.planned).toEqual([]);
398
+ });
399
+ });
400
+
357
401
  describe("both modes", () => {
358
402
  test("a run takes one gate over the set or one per wave, not both", async () => {
359
403
  const w = world();