@intentius/chant 0.105.1 → 0.106.3

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 (111) hide show
  1. package/dist/change-set.d.ts +30 -8
  2. package/dist/change-set.d.ts.map +1 -1
  3. package/dist/cli/commands/build.d.ts +11 -0
  4. package/dist/cli/commands/build.d.ts.map +1 -1
  5. package/dist/cli/commands/lint.d.ts.map +1 -1
  6. package/dist/cli/handlers/build.d.ts.map +1 -1
  7. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  8. package/dist/cli/handlers/operator.d.ts.map +1 -1
  9. package/dist/cli/handlers/pr.d.ts +16 -2
  10. package/dist/cli/handlers/pr.d.ts.map +1 -1
  11. package/dist/cli/handlers/promote.d.ts.map +1 -1
  12. package/dist/cli/handlers/run.d.ts.map +1 -1
  13. package/dist/cli/main.d.ts.map +1 -1
  14. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  15. package/dist/cli/registry.d.ts +7 -0
  16. package/dist/cli/registry.d.ts.map +1 -1
  17. package/dist/components/changed-files.d.ts +8 -2
  18. package/dist/components/changed-files.d.ts.map +1 -1
  19. package/dist/components/fan-out-record.d.ts +24 -0
  20. package/dist/components/fan-out-record.d.ts.map +1 -1
  21. package/dist/components/pr-pipeline.d.ts +67 -4
  22. package/dist/components/pr-pipeline.d.ts.map +1 -1
  23. package/dist/components/pr-run.d.ts +94 -3
  24. package/dist/components/pr-run.d.ts.map +1 -1
  25. package/dist/gated-waves.d.ts +4 -1
  26. package/dist/gated-waves.d.ts.map +1 -1
  27. package/dist/lexicon.d.ts +13 -2
  28. package/dist/lexicon.d.ts.map +1 -1
  29. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  30. package/dist/op/gate-name.d.ts +7 -0
  31. package/dist/op/gate-name.d.ts.map +1 -1
  32. package/dist/op/gate-summary.d.ts +2 -0
  33. package/dist/op/gate-summary.d.ts.map +1 -1
  34. package/dist/op/gate.d.ts +10 -2
  35. package/dist/op/gate.d.ts.map +1 -1
  36. package/dist/op/index.d.ts +2 -2
  37. package/dist/op/index.d.ts.map +1 -1
  38. package/dist/op/local-executor.d.ts.map +1 -1
  39. package/dist/op/local-output.d.ts +2 -2
  40. package/dist/op/local-output.d.ts.map +1 -1
  41. package/dist/op/runtime.d.ts +29 -4
  42. package/dist/op/runtime.d.ts.map +1 -1
  43. package/dist/op/runtimes/local.d.ts.map +1 -1
  44. package/dist/pr-loop.d.ts +32 -6
  45. package/dist/pr-loop.d.ts.map +1 -1
  46. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  47. package/dist/workspace/member-pipeline.d.ts +15 -1
  48. package/dist/workspace/member-pipeline.d.ts.map +1 -1
  49. package/package.json +1 -1
  50. package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +1 -1
  51. package/src/__fixtures__/change-set/warden.change-set.golden.json +2 -2
  52. package/src/__fixtures__/change-set/warden.plan.json +47 -40
  53. package/src/__fixtures__/plan-summary/normalization-vectors.json +275 -2
  54. package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +1 -1
  55. package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +2 -2
  56. package/src/__fixtures__/pr-loop/pr-plan.golden.json +2 -2
  57. package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +2 -2
  58. package/src/build.test.ts +1 -1
  59. package/src/build.ts +1 -1
  60. package/src/change-set.test.ts +42 -12
  61. package/src/change-set.ts +50 -11
  62. package/src/cli/commands/build.ts +33 -14
  63. package/src/cli/commands/lint.ts +34 -1
  64. package/src/cli/handlers/build-lexicon-output.test.ts +102 -0
  65. package/src/cli/handlers/build.ts +68 -0
  66. package/src/cli/handlers/fan-out.test.ts +12 -1
  67. package/src/cli/handlers/fan-out.ts +7 -3
  68. package/src/cli/handlers/operator.ts +1 -2
  69. package/src/cli/handlers/pr.ts +223 -35
  70. package/src/cli/handlers/promote.test.ts +15 -1
  71. package/src/cli/handlers/promote.ts +5 -3
  72. package/src/cli/handlers/run.test.ts +215 -1
  73. package/src/cli/handlers/run.ts +36 -15
  74. package/src/cli/main.ts +13 -1
  75. package/src/cli/mcp/never-over-mcp.test.ts +120 -0
  76. package/src/cli/mcp/op-approve-origin.test.ts +1 -1
  77. package/src/cli/mcp/op-tools.ts +14 -2
  78. package/src/cli/mcp/server.test.ts +2 -1
  79. package/src/cli/registry.ts +7 -0
  80. package/src/components/changed-files.test.ts +58 -0
  81. package/src/components/changed-files.ts +19 -4
  82. package/src/components/fan-out-record.test.ts +47 -2
  83. package/src/components/fan-out-record.ts +53 -0
  84. package/src/components/pr-pipeline.ts +96 -6
  85. package/src/components/pr-run.test.ts +311 -4
  86. package/src/components/pr-run.ts +198 -5
  87. package/src/gated-waves.test.ts +11 -0
  88. package/src/gated-waves.ts +7 -2
  89. package/src/lexicon.ts +13 -1
  90. package/src/lifecycle/gate-ledger.ts +4 -3
  91. package/src/op/gate-name.ts +17 -0
  92. package/src/op/gate-summary.test.ts +12 -0
  93. package/src/op/gate-summary.ts +3 -1
  94. package/src/op/gate.test.ts +15 -1
  95. package/src/op/gate.ts +21 -4
  96. package/src/op/index.ts +2 -2
  97. package/src/op/local-executor.test.ts +35 -0
  98. package/src/op/local-executor.ts +2 -2
  99. package/src/op/local-output.test.ts +20 -0
  100. package/src/op/local-output.ts +4 -4
  101. package/src/op/runtime.ts +31 -4
  102. package/src/op/runtimes/local.test.ts +15 -0
  103. package/src/op/runtimes/local.ts +3 -1
  104. package/src/pr-loop.test.ts +41 -1
  105. package/src/pr-loop.ts +41 -10
  106. package/src/workspace/change-set.schema.json +1 -1
  107. package/src/workspace/lineage-upgrade-cli.ts +4 -3
  108. package/src/workspace/lineage-upgrade.test.ts +40 -0
  109. package/src/workspace/member-pipeline.test.ts +28 -0
  110. package/src/workspace/member-pipeline.ts +21 -3
  111. package/src/workspace/pr-report.schema.json +14 -1
@@ -21,6 +21,14 @@ import { LOCK_FILE, fileHash, readLock, writeLock } from "./lineage-lock";
21
21
  import { stageUpgrade, isGovernancePath, UPGRADE_DIR, type ChantRunner } from "./lineage-upgrade";
22
22
  import { upgradeCommand } from "./lineage-upgrade-cli";
23
23
 
24
+ // #3521: whether the workspace seals a gate is read from identity.gates in the
25
+ // working directory; a test says which gates are sealed instead.
26
+ const sealedGates = new Set<string>();
27
+ vi.mock("../op/gate", async () => {
28
+ const actual = await vi.importActual<typeof import("../op/gate")>("../op/gate");
29
+ return { ...actual, gateIsSealed: async (gate: string) => sealedGates.has(gate) };
30
+ });
31
+
24
32
  const ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@t" };
25
33
 
26
34
  let root: string;
@@ -290,6 +298,38 @@ describe("chant workspace upgrade", () => {
290
298
  expect(again.outcome).toBe("up-to-date");
291
299
  });
292
300
 
301
+ describe("the gated hint's approve command (#3521)", () => {
302
+ /** Runs the upgrade into its gate, and again against an approval of another patch; returns what both printed. */
303
+ async function gatedOutput(): Promise<string> {
304
+ release("v2.0.0", { "README.md": "starter v2\n" });
305
+ const port = ledger();
306
+ const lines: string[] = [];
307
+ vi.spyOn(console, "error").mockImplementation((s: unknown) => { lines.push(String(s)); });
308
+ await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:00:00.000Z" });
309
+ port.approve(`sha256:${"0".repeat(64)}`, "2026-09-23T10:01:00.000Z");
310
+ await upgradeCommand({ root: proj, to: "v2.0.0", runChant: passing, ledger: port, now: "2026-09-23T10:02:00.000Z" });
311
+ return lines.join("\n");
312
+ }
313
+
314
+ test("unsealed gate: the gated line and the mismatch line carry no --sign", async () => {
315
+ const out = await gatedOutput();
316
+ expect(out).toContain("then run `chant approve workspace-upgrade .`");
317
+ expect(out).toContain("needs a fresh one: chant approve workspace-upgrade .");
318
+ expect(out).not.toContain("--sign");
319
+ });
320
+
321
+ test("sealed gate: the gated line and the mismatch line end in --sign", async () => {
322
+ sealedGates.add(".");
323
+ try {
324
+ const out = await gatedOutput();
325
+ expect(out).toContain("then run `chant approve workspace-upgrade . --sign`");
326
+ expect(out).toContain("needs a fresh one: chant approve workspace-upgrade . --sign");
327
+ } finally {
328
+ sealedGates.clear();
329
+ }
330
+ });
331
+ });
332
+
293
333
  test("a lock that a .gitignore covers is applied beside the patch, and the digest covers it", async () => {
294
334
  put(proj, ".gitignore", ".chant/\n");
295
335
  git(proj, ["rm", "-q", "--cached", LOCK_FILE]);
@@ -79,6 +79,34 @@ describe("the component pipeline's file and command", () => {
79
79
  expect(plan.command).toBe("chant build --components --generate github --env staging --promote-to prod");
80
80
  });
81
81
 
82
+ test("a pull-request pipeline gets its own file and a command that regenerates it (#3465)", () => {
83
+ expect(memberPipelineFile("github", "api", "staging", "pr")).toBe(".github/workflows/chant-pr.api.staging.yml");
84
+ expect(memberPipelineFile("gitlab", "api", "staging", "pr")).toBe(".gitlab/ci/chant-pr.api.staging.gitlab-ci.yml");
85
+ const dir = join(repo, "services/api");
86
+ const plan = planMemberComponentPipeline({
87
+ projectDir: dir,
88
+ found: findWorkspaceRoot(dir)!,
89
+ lexicon: "github",
90
+ env: "staging",
91
+ prLoop: { gate: "infra-apply", branch: "trunk" },
92
+ })!;
93
+ expect(plan.file).toBe(".github/workflows/chant-pr.api.staging.yml");
94
+ expect(plan.member).toEqual({ name: "api", dir: "services/api", file: ".github/workflows/chant-pr.api.staging.yml" });
95
+ expect(plan.command).toBe("chant build --components --generate github --env staging --pr-loop --gate infra-apply --branch trunk");
96
+ });
97
+
98
+ test("the recorded command keeps --wave-gate, --canary and --base (#3049)", () => {
99
+ const dir = join(repo, "services/api");
100
+ const plan = planMemberComponentPipeline({
101
+ projectDir: dir,
102
+ found: findWorkspaceRoot(dir)!,
103
+ lexicon: "github",
104
+ env: "staging",
105
+ waveGate: { gate: "release", canary: ["a", "b"], base: "origin/main" },
106
+ })!;
107
+ expect(plan.command).toBe("chant build --components --generate github --env staging --wave-gate release --canary a,b --base origin/main");
108
+ });
109
+
82
110
  test("--output keeps the path, relative to the member in the command", () => {
83
111
  const dir = join(repo, "services/api");
84
112
  const plan = planMemberComponentPipeline({
@@ -134,11 +134,15 @@ export function memberPipelineDir(provider: string): string | undefined {
134
134
  * The default path of a member's component pipeline for one environment,
135
135
  * relative to the repository root: `chant-<member>-<env>.yml` in the forge's
136
136
  * workflow directory, or `.gitlab/ci/chant-<member>-<env>.gitlab-ci.yml`.
137
+ * A pull-request pipeline (`kind` `"pr"`, #3465) is `chant-pr.<member>.<env>`, with the dots the apply group uses so no
138
+ * two member and environment pairs share one name, and it sits beside the
139
+ * member's deploy pipeline for the same environment.
137
140
  */
138
- export function memberPipelineFile(provider: string, member: string, env: string): string | undefined {
141
+ export function memberPipelineFile(provider: string, member: string, env: string, kind: "components" | "pr" = "components"): string | undefined {
139
142
  const dir = PIPELINE_DIRS[provider];
140
143
  if (dir === undefined) return undefined;
141
- return `${dir}/chant-${member}-${env}${provider === "gitlab" ? ".gitlab-ci.yml" : ".yml"}`;
144
+ const stem = kind === "pr" ? `chant-pr.${member}.${env}` : `chant-${member}-${env}`;
145
+ return `${dir}/${stem}${provider === "gitlab" ? ".gitlab-ci.yml" : ".yml"}`;
142
146
  }
143
147
 
144
148
  /** A repository-relative path for an absolute one. */
@@ -205,6 +209,10 @@ export interface ComponentPlanInput {
205
209
  output?: string;
206
210
  params?: string[];
207
211
  paramsFile?: string;
212
+ /** `--pr-loop`, with its `--gate` and `--branch` (#3465). */
213
+ prLoop?: { gate?: string; branch?: string };
214
+ /** `--wave-gate` with its `--canary` and `--base` (#3049). */
215
+ waveGate?: { gate: string; canary?: string[]; base?: string };
208
216
  }
209
217
 
210
218
  /** The generators' own default environment when `--env` is not given. */
@@ -224,7 +232,7 @@ export function planMemberComponentPipeline(input: ComponentPlanInput): MemberCo
224
232
  if (input.output) {
225
233
  target = resolve(input.output);
226
234
  } else {
227
- const file = memberPipelineFile(input.lexicon, ctx.member.name, env);
235
+ const file = memberPipelineFile(input.lexicon, ctx.member.name, env, input.prLoop ? "pr" : "components");
228
236
  if (file === undefined) {
229
237
  throw new Error(`there is no default pipeline path for ${input.lexicon} in a workspace member; pass --output <file>`);
230
238
  }
@@ -234,6 +242,16 @@ export function planMemberComponentPipeline(input: ComponentPlanInput): MemberCo
234
242
 
235
243
  const parts = ["chant", "build", "--components", "--generate", input.lexicon, "--env", env];
236
244
  if (input.promoteTo) parts.push("--promote-to", input.promoteTo);
245
+ if (input.waveGate) {
246
+ parts.push("--wave-gate", input.waveGate.gate);
247
+ if (input.waveGate.canary?.length) parts.push("--canary", input.waveGate.canary.join(","));
248
+ if (input.waveGate.base) parts.push("--base", input.waveGate.base);
249
+ }
250
+ if (input.prLoop) {
251
+ parts.push("--pr-loop");
252
+ if (input.prLoop.gate) parts.push("--gate", input.prLoop.gate);
253
+ if (input.prLoop.branch) parts.push("--branch", input.prLoop.branch);
254
+ }
237
255
  for (const p of input.params ?? []) parts.push("--param", p);
238
256
  if (input.paramsFile) parts.push("--params-file", relPath(ctx.memberRoot, resolve(input.paramsFile)));
239
257
  if (input.output) parts.push("--output", relPath(ctx.memberRoot, target));
@@ -15,11 +15,16 @@
15
15
  "description": "The pull or merge request number. null when an apply found no pull request that merged its commit."
16
16
  },
17
17
  "env": { "type": "string" },
18
+ "member": {
19
+ "type": "string",
20
+ "pattern": "^[a-z0-9][a-z0-9-]{0,39}$",
21
+ "description": "The workspace member whose pipeline ran the stage. Absent outside a workspace member."
22
+ },
18
23
  "base": { "type": "string", "pattern": "^[0-9a-f]{40,64}$", "description": "The commit the change is measured from: the merge base of the given base and head." },
19
24
  "head": { "type": "string", "pattern": "^[0-9a-f]{40,64}$" },
20
25
  "op": {
21
26
  "type": "string",
22
- "description": "The gate's op, pr-<number>. Its ledger is _gates/<op>.jsonl on chant/lifecycle. pr-unknown when no pull request was found."
27
+ "description": "The gate's op, pr-<number>, or pr-<number>-<member> for a workspace member's pipeline. Its ledger is _gates/<op>.jsonl on chant/lifecycle. pr-unknown when no pull request was found."
23
28
  },
24
29
  "gate": { "type": "string", "description": "The gate the apply waits on, pr-apply unless the pipeline names another." },
25
30
  "digest": {
@@ -36,6 +41,14 @@
36
41
  "description": "Why an apply refused. plan-changed: an approval stands for another digest, so the plan changed after review."
37
42
  },
38
43
  "message": { "type": "string", "description": "The refusal or failure as printed." },
44
+ "resumed": {
45
+ "type": "object",
46
+ "required": ["applied"],
47
+ "description": "Apply stage with --resume (#3464): an earlier attempt at the approved set failed partway. This run applied the rest under the same approval.",
48
+ "properties": {
49
+ "applied": { "type": "array", "items": { "type": "string" }, "description": "Components the earlier attempt applied, which this run did not run again." }
50
+ }
51
+ },
39
52
  "approval": {
40
53
  "type": "object",
41
54
  "required": ["status"],