@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.
- package/dist/change-set.d.ts +30 -8
- package/dist/change-set.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +11 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +16 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +8 -2
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +24 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +67 -4
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-run.d.ts +94 -3
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/gated-waves.d.ts +4 -1
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/lexicon.d.ts +13 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +2 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +10 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -2
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +29 -4
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +32 -6
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.d.ts +15 -1
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +1 -1
- package/src/__fixtures__/change-set/warden.change-set.golden.json +2 -2
- package/src/__fixtures__/change-set/warden.plan.json +47 -40
- package/src/__fixtures__/plan-summary/normalization-vectors.json +275 -2
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +1 -1
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +2 -2
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +2 -2
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +2 -2
- package/src/build.test.ts +1 -1
- package/src/build.ts +1 -1
- package/src/change-set.test.ts +42 -12
- package/src/change-set.ts +50 -11
- package/src/cli/commands/build.ts +33 -14
- package/src/cli/commands/lint.ts +34 -1
- package/src/cli/handlers/build-lexicon-output.test.ts +102 -0
- package/src/cli/handlers/build.ts +68 -0
- package/src/cli/handlers/fan-out.test.ts +12 -1
- package/src/cli/handlers/fan-out.ts +7 -3
- package/src/cli/handlers/operator.ts +1 -2
- package/src/cli/handlers/pr.ts +223 -35
- package/src/cli/handlers/promote.test.ts +15 -1
- package/src/cli/handlers/promote.ts +5 -3
- package/src/cli/handlers/run.test.ts +215 -1
- package/src/cli/handlers/run.ts +36 -15
- package/src/cli/main.ts +13 -1
- package/src/cli/mcp/never-over-mcp.test.ts +120 -0
- package/src/cli/mcp/op-approve-origin.test.ts +1 -1
- package/src/cli/mcp/op-tools.ts +14 -2
- package/src/cli/mcp/server.test.ts +2 -1
- package/src/cli/registry.ts +7 -0
- package/src/components/changed-files.test.ts +58 -0
- package/src/components/changed-files.ts +19 -4
- package/src/components/fan-out-record.test.ts +47 -2
- package/src/components/fan-out-record.ts +53 -0
- package/src/components/pr-pipeline.ts +96 -6
- package/src/components/pr-run.test.ts +311 -4
- package/src/components/pr-run.ts +198 -5
- package/src/gated-waves.test.ts +11 -0
- package/src/gated-waves.ts +7 -2
- package/src/lexicon.ts +13 -1
- package/src/lifecycle/gate-ledger.ts +4 -3
- package/src/op/gate-name.ts +17 -0
- package/src/op/gate-summary.test.ts +12 -0
- package/src/op/gate-summary.ts +3 -1
- package/src/op/gate.test.ts +15 -1
- package/src/op/gate.ts +21 -4
- package/src/op/index.ts +2 -2
- package/src/op/local-executor.test.ts +35 -0
- package/src/op/local-executor.ts +2 -2
- package/src/op/local-output.test.ts +20 -0
- package/src/op/local-output.ts +4 -4
- package/src/op/runtime.ts +31 -4
- package/src/op/runtimes/local.test.ts +15 -0
- package/src/op/runtimes/local.ts +3 -1
- package/src/pr-loop.test.ts +41 -1
- package/src/pr-loop.ts +41 -10
- package/src/workspace/change-set.schema.json +1 -1
- package/src/workspace/lineage-upgrade-cli.ts +4 -3
- package/src/workspace/lineage-upgrade.test.ts +40 -0
- package/src/workspace/member-pipeline.test.ts +28 -0
- package/src/workspace/member-pipeline.ts +21 -3
- 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
|
-
|
|
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
|
|
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"],
|