@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
@@ -0,0 +1,202 @@
1
+ /**
2
+ * `chant lock` and `chant unlock` (#4293): take, list and release the target
3
+ * locks a pull request holds (../../lifecycle/target-lock.ts).
4
+ *
5
+ * ```
6
+ * chant lock <target>... --pr <n> [--stage plan|apply|lock] [--head <sha>] [--by <who>] [--gate <name>] [--forge <kind>] [--json]
7
+ * chant lock --from-report <pr-plan.json> --pr <n> [--stage plan] [--forge <kind>] [--json]
8
+ * chant lock --list [--gate <name>] [--json]
9
+ * chant unlock --pr <n> [<target>...] [--gate <name>] [--json]
10
+ * ```
11
+ *
12
+ * The targets are names: the components `chant components pr-plan` selects,
13
+ * which are what `pr-apply` checks under `ci.locks`, or any other name a
14
+ * pipeline locks by. `--from-report` reads them from a pull-request report
15
+ * (`selection.waves`), so a lock job that runs the target branch's code can
16
+ * lock what the plan job, which ran the pull request's code, selected.
17
+ *
18
+ * `--forge` reads which holders are still open, so a lock whose pull
19
+ * request merged or closed is taken over, and, with `--head`, posts the
20
+ * `chant/lock` status on that commit. Without it every lock counts as held.
21
+ *
22
+ * Exit 0 when the locks are taken or released, 3 when another open pull
23
+ * request holds a target (nothing is locked), 1 on an error.
24
+ */
25
+ import { readFileSync } from "node:fs";
26
+ import { resolve } from "node:path";
27
+ import type { CommandContext } from "../registry";
28
+ import { formatError, formatInfo, formatSuccess, formatWarning } from "../format";
29
+ import { FORGE_KINDS, forgeFromEnv, type ForgeKind, type PrForge } from "../../pr-forge";
30
+ import { PR_APPLY_GATE } from "../../pr-loop";
31
+ import {
32
+ describeHeldTargetLocks,
33
+ readTargetLocks,
34
+ releaseTargetLocks,
35
+ takeTargetLocks,
36
+ targetLockGateProblem,
37
+ targetLockPathAt,
38
+ TARGET_LOCK_STAGES,
39
+ type TargetLockStage,
40
+ } from "../../lifecycle/target-lock";
41
+
42
+ /** The exit for a target another open pull request holds: 3, as a run stopped at a gate exits (`GATED_EXIT_CODE` in ./run.ts). */
43
+ const HELD_EXIT_CODE = 3;
44
+
45
+ /** The status `chant lock --head <sha> --forge <kind>` posts. */
46
+ export const LOCK_STATUS_CONTEXT = "chant/lock";
47
+
48
+ const fail = (message: string, hint?: string): number => {
49
+ console.error(formatError({ message, ...(hint ? { hint } : {}) }));
50
+ return 1;
51
+ };
52
+
53
+ /** The targets named after the command word. */
54
+ function targetsOf(ctx: CommandContext): string[] {
55
+ return (ctx.args.positionals ?? []).filter((t) => t !== "");
56
+ }
57
+
58
+ function gateOf(ctx: CommandContext): string | { error: string } {
59
+ const gate = ctx.args.gate ?? PR_APPLY_GATE;
60
+ const problem = targetLockGateProblem(gate);
61
+ return problem ? { error: problem } : gate;
62
+ }
63
+
64
+ function forgeOf(ctx: CommandContext): PrForge | undefined | { error: string } {
65
+ const kind = ctx.args.forge;
66
+ if (kind === undefined) return undefined;
67
+ if (!(FORGE_KINDS as readonly string[]).includes(kind)) return { error: `--forge ${kind} is not one of ${FORGE_KINDS.join(", ")}` };
68
+ try {
69
+ return forgeFromEnv(kind as ForgeKind);
70
+ } catch (err) {
71
+ return { error: err instanceof Error ? err.message : String(err) };
72
+ }
73
+ }
74
+
75
+ const prName = (forge: PrForge | undefined) => (n: number) => (forge?.kind === "gitlab" ? `merge request !${n}` : `pull request ${n}`);
76
+
77
+ /** The components a `pr-plan.json` selected, and the head it planned, checked against `--pr`. */
78
+ function fromReport(file: string, pr: number): { targets: string[]; head?: string } | { error: string } {
79
+ let doc: { pr?: unknown; head?: unknown; selection?: { waves?: unknown } };
80
+ try {
81
+ doc = JSON.parse(readFileSync(resolve(file), "utf8"));
82
+ } catch (err) {
83
+ return { error: `--from-report: could not read "${file}": ${err instanceof Error ? err.message : String(err)}` };
84
+ }
85
+ if (doc.pr !== pr) return { error: `--from-report: "${file}" is the report of ${doc.pr === null || doc.pr === undefined ? "no pull request" : `pull request ${String(doc.pr)}`}, not ${pr}` };
86
+ const waves = doc.selection?.waves;
87
+ if (!Array.isArray(waves) || !waves.every((w) => Array.isArray(w) && w.every((n) => typeof n === "string"))) {
88
+ return { error: `--from-report: "${file}" has no selection.waves; pass a pr-plan.json that chant components pr-plan wrote` };
89
+ }
90
+ return { targets: (waves as string[][]).flat(), ...(typeof doc.head === "string" ? { head: doc.head } : {}) };
91
+ }
92
+
93
+ async function postStatus(forge: PrForge | undefined, head: string | undefined, state: "success" | "failure", description: string): Promise<void> {
94
+ if (!forge || !head) return;
95
+ try {
96
+ await forge.setStatus(head, { context: LOCK_STATUS_CONTEXT, state, description: description.length > 140 ? `${description.slice(0, 137)}...` : description, ...(forge.runUrl ? { url: forge.runUrl } : {}) });
97
+ } catch (err) {
98
+ console.error(formatWarning({ message: `could not post ${LOCK_STATUS_CONTEXT} on ${head}: ${err instanceof Error ? err.message : String(err)}` }));
99
+ }
100
+ }
101
+
102
+ /** chant lock */
103
+ export async function runLock(ctx: CommandContext): Promise<number> {
104
+ const { args } = ctx;
105
+ const gate = gateOf(ctx);
106
+ if (typeof gate !== "string") return fail(gate.error);
107
+
108
+ if (args.list) {
109
+ if (targetsOf(ctx).length > 0 || args.pr !== undefined) return fail("--list prints every lock; drop the targets and --pr");
110
+ let file;
111
+ try {
112
+ file = await readTargetLocks(gate);
113
+ } catch (err) {
114
+ return fail(err instanceof Error ? err.message : String(err));
115
+ }
116
+ const rows = Object.entries(file.locks).sort(([a], [b]) => a.localeCompare(b));
117
+ if (args.json) {
118
+ console.log(JSON.stringify({ gate, path: await targetLockPathAt(gate), locks: Object.fromEntries(rows) }, null, 2));
119
+ return 0;
120
+ }
121
+ if (rows.length === 0) console.error(formatInfo(`no target locks on ${gate}`));
122
+ for (const [target, l] of rows) console.log(`${target}\tpull request ${l.pr}\t${l.stage}\t${l.by}\t${l.at}`);
123
+ return 0;
124
+ }
125
+
126
+ if (args.pr === undefined) return fail("chant lock needs the pull or merge request that holds the locks: --pr <n>", "chant lock --list prints the locks held now.");
127
+ const stage = (args.lockStage ?? "lock") as TargetLockStage;
128
+ if (!(TARGET_LOCK_STAGES as readonly string[]).includes(stage)) return fail(`--stage ${args.lockStage} is not one of ${TARGET_LOCK_STAGES.join(", ")}`);
129
+
130
+ let targets = targetsOf(ctx);
131
+ let head = args.head;
132
+ if (args.fromReport) {
133
+ if (targets.length > 0) return fail("--from-report reads the targets from the report; drop the names");
134
+ const read = fromReport(args.fromReport, args.pr);
135
+ if ("error" in read) return fail(read.error);
136
+ targets = read.targets;
137
+ head ??= read.head;
138
+ } else if (targets.length === 0 && stage !== "plan") {
139
+ return fail("chant lock needs the targets to lock: chant lock <target>... --pr <n>", "Or --from-report <pr-plan.json> to lock what a plan selected.");
140
+ }
141
+
142
+ const forge = forgeOf(ctx);
143
+ if (forge && "error" in forge) return fail(forge.error);
144
+ const isOpen = forge?.pullRequestOpen?.bind(forge);
145
+ const by = args.by ?? process.env.GITHUB_ACTOR ?? process.env.GITLAB_USER_LOGIN ?? process.env.USER ?? "unknown";
146
+ const name = prName(forge);
147
+
148
+ let result;
149
+ try {
150
+ result = await takeTargetLocks(
151
+ gate,
152
+ targets,
153
+ { pr: args.pr, by, at: new Date().toISOString(), stage, ...(head ? { head } : {}) },
154
+ { ...(isOpen ? { isOpen } : {}), releaseStalePlan: stage === "plan" },
155
+ );
156
+ } catch (err) {
157
+ return fail(`could not take the locks of ${gate}: ${err instanceof Error ? err.message : String(err)}`);
158
+ }
159
+
160
+ if (!result.ok) {
161
+ const holders = [...new Set(result.held.map((h) => h.pr))].sort((a, b) => a - b);
162
+ const text = `${describeHeldTargetLocks(result.held, name)}, so ${name(args.pr)} is not locked`;
163
+ await postStatus(forge, head, "failure", `locked by ${holders.map(name).join(", ")}: ${result.held.map((h) => h.target).join(", ")}`);
164
+ if (args.json) console.log(JSON.stringify({ gate, pr: args.pr, ok: false, held: result.held }, null, 2));
165
+ console.error(formatError({ message: text, hint: `Lock again once ${holders.length === 1 ? "it merges or closes" : "they merge or close"}, or after chant unlock --pr ${holders[0]}.${isOpen ? "" : " Without --forge every lock counts as held."}` }));
166
+ return HELD_EXIT_CODE;
167
+ }
168
+
169
+ const holds = result.taken.length > 0 ? `holds ${result.taken.join(", ")}` : "reaches no target";
170
+ await postStatus(forge, head, "success", holds);
171
+ if (args.json) {
172
+ console.log(JSON.stringify({ gate, pr: args.pr, ok: true, stage, taken: result.taken, released: result.released, expired: result.expired, pushed: result.pushed }, null, 2));
173
+ return 0;
174
+ }
175
+ console.error(formatSuccess(`${name(args.pr)} ${holds} on ${gate}`));
176
+ if (result.released.length > 0) console.error(formatInfo(`released ${result.released.join(", ")}, which it no longer reaches`));
177
+ if (result.expired.length > 0) console.error(formatInfo(`took over ${result.expired.join(", ")} from pull requests that merged or closed`));
178
+ if (!result.pushed && (result.taken.length > 0 || result.released.length > 0)) console.error(formatInfo("chant/lifecycle was not pushed: this repository has no remote, or recording is off"));
179
+ return 0;
180
+ }
181
+
182
+ /** chant unlock */
183
+ export async function runUnlock(ctx: CommandContext): Promise<number> {
184
+ const { args } = ctx;
185
+ const gate = gateOf(ctx);
186
+ if (typeof gate !== "string") return fail(gate.error);
187
+ if (args.pr === undefined) return fail("chant unlock needs the pull or merge request whose locks it releases: --pr <n>");
188
+ const targets = targetsOf(ctx);
189
+ let result;
190
+ try {
191
+ result = await releaseTargetLocks(gate, args.pr, targets.length > 0 ? { targets } : {});
192
+ } catch (err) {
193
+ return fail(`could not release the locks of ${gate}: ${err instanceof Error ? err.message : String(err)}`);
194
+ }
195
+ if (args.json) {
196
+ console.log(JSON.stringify({ gate, pr: args.pr, released: result.released, pushed: result.pushed }, null, 2));
197
+ return 0;
198
+ }
199
+ if (result.released.length === 0) console.error(formatInfo(`pull request ${args.pr} holds no lock${targets.length > 0 ? ` on ${targets.join(", ")}` : ""} on ${gate}`));
200
+ else console.error(formatSuccess(`released the locks pull request ${args.pr} held on ${result.released.join(", ")}`));
201
+ return 0;
202
+ }
@@ -1278,7 +1278,9 @@ describe("runApprove — a gate with an approval policy (#2508)", () => {
1278
1278
  }));
1279
1279
  const out = errSpy.mock.calls.map((c) => String(c[0])).join("\n");
1280
1280
  expect(out).toContain("log-only mode, so the decision is recorded and does not change the outcome");
1281
- expect(out).toContain("Quorum: 1 of 2 human approval(s) with role maintainer for this plan (alex)");
1281
+ // #4290: a claimed role is recorded for the policy, and the quorum never counts it.
1282
+ expect(out).toContain("Quorum: 0 of 2 human approval(s) with role maintainer for this plan (none yet)");
1283
+ expect(out).toContain("its roles are read from .chant/trust.json at base");
1282
1284
  errSpy.mockRestore();
1283
1285
  });
1284
1286