diff --git a/scripts/release-prepare.ts b/scripts/release-prepare.ts index fae05224266e..b0637858a91f 100644 --- a/scripts/release-prepare.ts +++ b/scripts/release-prepare.ts @@ -166,6 +166,7 @@ export function createReleasePrepareSteps( export function runReleasePrepareSteps(params: { cwd: string; + json?: boolean; mode: ReleasePrepareMode; runStep?: (step: ReleasePrepareStep, cwd: string) => number; steps: ReleasePrepareStep[]; @@ -174,7 +175,10 @@ export function runReleasePrepareSteps(params: { return params.steps.map((step) => ({ ...step, durationMs: 0, status: "planned" })); } - const runStep = params.runStep ?? runReleasePrepareStep; + const runStep = + params.runStep ?? + ((step: ReleasePrepareStep, cwd: string) => + runReleasePrepareStep(step, cwd, { json: params.json ?? false })); const results: ReleasePrepareStepResult[] = []; let blocked = false; for (const step of params.steps) { @@ -236,6 +240,7 @@ export function main(argv = process.argv.slice(2)): number { const before = readWorktreeState(args.rootDir); const results = runReleasePrepareSteps({ cwd: args.rootDir, + json: args.json, mode: args.mode, steps, }); @@ -265,16 +270,31 @@ export function main(argv = process.argv.slice(2)): number { return manifest.status === "failed" ? 1 : 0; } -function runReleasePrepareStep(step: ReleasePrepareStep, cwd: string): number { - process.stdout.write(`\n[release-prepare] ${step.name}\n`); +export function runReleasePrepareStep( + step: ReleasePrepareStep, + cwd: string, + options: { json?: boolean } = {}, +): number { + const json = options.json ?? false; + const progressStream = json ? process.stderr : process.stdout; + progressStream.write(`\n[release-prepare] ${step.name}\n`); const result = spawnSync(step.command, step.args, { cwd, + encoding: json ? "utf8" : undefined, env: process.env, - stdio: "inherit", + stdio: json ? ["ignore", "pipe", "pipe"] : "inherit", }); if (result.error) { throw result.error; } + if (json) { + if (typeof result.stdout === "string" && result.stdout) { + process.stderr.write(result.stdout); + } + if (typeof result.stderr === "string" && result.stderr) { + process.stderr.write(result.stderr); + } + } return result.status ?? 1; } diff --git a/test/scripts/release-prepare.test.ts b/test/scripts/release-prepare.test.ts index 27f9365e25b3..d5424256783f 100644 --- a/test/scripts/release-prepare.test.ts +++ b/test/scripts/release-prepare.test.ts @@ -1,9 +1,10 @@ // Release prepare tests cover shadow planning, cutover commands, and candidate manifests. -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { buildReleasePreparationManifest, createReleasePrepareSteps, parseReleasePrepareArgs, + runReleasePrepareStep, runReleasePrepareSteps, } from "../../scripts/release-prepare.ts"; @@ -117,6 +118,43 @@ describe("release preparation plan", () => { expect(results.map((result) => result.status)).toEqual(["failed", "skipped"]); }); + + it("keeps JSON mode child output off stdout", () => { + const stdout: string[] = []; + const stderr: string[] = []; + const stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation((chunk) => { + stdout.push(String(chunk)); + return true; + }); + const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation((chunk) => { + stderr.push(String(chunk)); + return true; + }); + try { + const status = runReleasePrepareStep( + { + args: [ + "-e", + 'process.stdout.write("child stdout\\n"); process.stderr.write("child stderr\\n");', + ], + command: process.execPath, + id: "release-version", + name: "JSON child", + }, + process.cwd(), + { json: true }, + ); + + expect(status).toBe(0); + expect(stdout.join("")).toBe(""); + expect(stderr.join("")).toContain("[release-prepare] JSON child"); + expect(stderr.join("")).toContain("child stdout"); + expect(stderr.join("")).toContain("child stderr"); + } finally { + stdoutSpy.mockRestore(); + stderrSpy.mockRestore(); + } + }); }); describe("release preparation manifest", () => {