From d30fd09bbbe9e2a8d13a4bf26b6484a3c361cc62 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:42:36 -0600 Subject: [PATCH] fix(core): chant build --components --generate --format json forwards env The JSON printer at build.ts:66 only emitted stages, jobs and yaml, dropping the environment the generator resolved and returned on GenerateComponentsResult.env (#2046, PR #2050). A consumer reading the structured output, like behold, had no environment identity except by parsing the YAML back, which #2046 was meant to retire. Forward result.env into the JSON payload the same way handlers/graph.ts already does for the IR pipeline projection: {stages, jobs, yaml, ...(result.env ? { env: result.env } : {})}. The text/yaml output path is unchanged. Closes #2060 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01FoXyD9UvKQ5ZdhiR9JT1yB --- docs/src/content/docs/cli/build.mdx | 2 +- packages/core/src/cli/handlers/build.test.ts | 80 ++++++++++++++++++++ packages/core/src/cli/handlers/build.ts | 16 +++- 3 files changed, 96 insertions(+), 2 deletions(-) diff --git a/docs/src/content/docs/cli/build.mdx b/docs/src/content/docs/cli/build.mdx index ea359366f..9787c66b4 100644 --- a/docs/src/content/docs/cli/build.mdx +++ b/docs/src/content/docs/cli/build.mdx @@ -123,7 +123,7 @@ The `--sandbox` flag always wins over `build.sandbox` when both are present. A components-only project need not declare a lexicon plugin at all. Lexicons are loaded best-effort here, so a project whose `chant.config.ts` lists none still generates. -`--env ` is threaded into the generated pipeline as the environment it deploys, and `--param` / `--params-file` bind build-time parameters the same way they do for a resource build. With `--format json` the result is `{ stages, jobs, yaml }` on stdout. The same stages and jobs are available as graph IR through [`chant graph --components --format ir --projection `](/chant/cli/graph/#--projection-lexicon--the-cipipeline-projection-989). +`--env ` is threaded into the generated pipeline as the environment it deploys, and `--param` / `--params-file` bind build-time parameters the same way they do for a resource build. With `--format json` the result is `{ stages, jobs, yaml, env }` on stdout, where `env` is the generator's own resolved environment (present whenever the generator returns one; omitted otherwise) rather than something a consumer has to re-derive by parsing the YAML back. The same stages, jobs and env are available as graph IR through [`chant graph --components --format ir --projection `](/chant/cli/graph/#--projection-lexicon--the-cipipeline-projection-989). ## Build-time parameters diff --git a/packages/core/src/cli/handlers/build.test.ts b/packages/core/src/cli/handlers/build.test.ts index ddfd8fdd3..2ebd04c75 100644 --- a/packages/core/src/cli/handlers/build.test.ts +++ b/packages/core/src/cli/handlers/build.test.ts @@ -149,3 +149,83 @@ describe("runBuild --components --generate (chant #1108 build-time parameters)", expect(generateComponentsPipelineMock).not.toHaveBeenCalled(); }); }); + +/** + * chant #2060. `chant build --components --generate --format json` forwarded + * only `{ stages, jobs, yaml }`, dropping `result.env` even though the + * generator (#2046, PR #2050) resolves and returns it. A consumer reading the + * structured output (behold does) had no environment identity except by + * parsing the YAML back, which #2046 was meant to retire. + */ +describe("runBuild --components --generate --format json (chant #2060 env passthrough)", () => { + beforeEach(() => { + generateComponentsPipelineMock.mockReset(); + loadChantConfigUpwardMock.mockReset().mockResolvedValue({ config: {} }); + }); + + test("--format json includes result.env alongside stages, jobs and yaml", async () => { + generateComponentsPipelineMock.mockResolvedValue({ + success: true, + yaml: "name: chant-components-prod\nenv:\n CHANT_ENV: prod\n", + stages: ["deploy"], + jobs: [{ jobName: "deploy-web", component: "web", stage: "deploy", needs: [] }], + env: "prod", + }); + const stdout: string[] = []; + vi.spyOn(console, "log").mockImplementation((s: string) => { stdout.push(s); }); + + const exit = await runBuild({ + args: makeArgs({ format: "json", env: "prod" }), + plugins: [], + serializers: [], + }); + + expect(exit).toBe(0); + expect(stdout).toHaveLength(1); + const printed = JSON.parse(stdout[0]); + expect(printed).toEqual({ + stages: ["deploy"], + jobs: [{ jobName: "deploy-web", component: "web", stage: "deploy", needs: [] }], + yaml: "name: chant-components-prod\nenv:\n CHANT_ENV: prod\n", + env: "prod", + }); + vi.restoreAllMocks(); + }); + + test("--format json omits env when the generator's result carries none", async () => { + generateComponentsPipelineMock.mockResolvedValue({ + success: true, + yaml: "stages: []", + stages: [], + jobs: [], + }); + const stdout: string[] = []; + vi.spyOn(console, "log").mockImplementation((s: string) => { stdout.push(s); }); + + const exit = await runBuild({ args: makeArgs({ format: "json" }), plugins: [], serializers: [] }); + + expect(exit).toBe(0); + const printed = JSON.parse(stdout[0]); + expect(printed).toEqual({ stages: [], jobs: [], yaml: "stages: []" }); + expect(printed).not.toHaveProperty("env"); + vi.restoreAllMocks(); + }); + + test("the default text format is unchanged: env is not forwarded and only the raw yaml is printed", async () => { + generateComponentsPipelineMock.mockResolvedValue({ + success: true, + yaml: "name: chant-components-prod\nenv:\n CHANT_ENV: prod\n", + stages: ["deploy"], + jobs: [{ jobName: "deploy-web", component: "web", stage: "deploy", needs: [] }], + env: "prod", + }); + const stdout: string[] = []; + vi.spyOn(console, "log").mockImplementation((s: string) => { stdout.push(s); }); + + const exit = await runBuild({ args: makeArgs({ env: "prod" }), plugins: [], serializers: [] }); + + expect(exit).toBe(0); + expect(stdout).toEqual(["name: chant-components-prod\nenv:\n CHANT_ENV: prod\n"]); + vi.restoreAllMocks(); + }); +}); diff --git a/packages/core/src/cli/handlers/build.ts b/packages/core/src/cli/handlers/build.ts index 0c0c0b087..5b990dba1 100644 --- a/packages/core/src/cli/handlers/build.ts +++ b/packages/core/src/cli/handlers/build.ts @@ -63,7 +63,21 @@ async function runGenerateComponents(ctx: CommandContext): Promise { const yaml = result.yaml ?? ""; if (args.format === "json") { - console.log(JSON.stringify({ stages: result.stages, jobs: result.jobs, yaml }, null, 2)); + console.log( + JSON.stringify( + { + stages: result.stages, + jobs: result.jobs, + yaml, + // The environment the generated pipeline deploys (#2046), the + // generator's own resolution, forwarded rather than left for a + // consumer to re-derive by parsing the YAML back (#2060). + ...(result.env ? { env: result.env } : {}), + }, + null, + 2, + ), + ); } else if (args.output) { const outputPath = resolve(args.output); mkdirSync(dirname(outputPath), { recursive: true });