From 70eb76eaecb73b3cd953ce9b4fcbbf78d1f6cc6d Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:27:14 +0800 Subject: [PATCH] fix(release): keep npm's own output in the publish log Retry classification needs npm's failure text, so the publish call captured its streams instead of inheriting them. That silenced npm on the success path: the log lost the tarball contents, the notices, and the '+ name@version' confirmation for every package. Pipe the streams and echo them, so the log shows what npm reported and the caller still gets the text it classifies. The registry probe behind it keeps its streams captured, since its JSON and its E404 are internal queries rather than progress. --- scripts/release/process.ts | 26 ++++++++++++++++++++++++++ scripts/release/publish.ts | 4 ++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/scripts/release/process.ts b/scripts/release/process.ts index 746f24ac36..acec98feae 100644 --- a/scripts/release/process.ts +++ b/scripts/release/process.ts @@ -38,6 +38,32 @@ export function attempt(command: string, args: readonly string[], options: RunOp return { status: result.status, stdout: result.stdout, stderr: result.stderr } } +/** + * Run a command, letting its output reach the log while also returning it. + * + * A step that both shows progress and classifies its own failure needs both: the + * output has to appear in the workflow log as the command produces it, and the + * caller has to read it to decide whether a failure is worth retrying. + * @param command - executable name. + * @param args - command arguments. + * @param options - working directory and environment. + * @returns The exit status and captured streams. + */ +export function attemptStreaming(command: string, args: readonly string[], options: RunOptions = {}): CommandResult { + const result = spawnSync(command, [...args], { + cwd: options.cwd, + env: options.env, + encoding: 'utf8', + // 'inherit' would leave nothing to capture, so the streams are piped and + // echoed instead. + stdio: ['inherit', 'pipe', 'pipe'], + }) + if (result.error !== undefined) throw result.error + if (result.stdout !== '') process.stdout.write(result.stdout) + if (result.stderr !== '') process.stderr.write(result.stderr) + return { status: result.status, stdout: result.stdout, stderr: result.stderr } +} + /** * Run a command, capture its standard output, and fail on a non-zero exit. * @param command - executable name. diff --git a/scripts/release/publish.ts b/scripts/release/publish.ts index 11ad01173c..f861da18c2 100644 --- a/scripts/release/publish.ts +++ b/scripts/release/publish.ts @@ -18,7 +18,7 @@ import { join, resolve } from 'node:path' import { setTimeout as sleep } from 'node:timers/promises' import { parseArgs } from 'node:util' import { releaseFamily } from './families.ts' -import { attempt, isEntry } from './process.ts' +import { attempt, attemptStreaming, isEntry } from './process.ts' import { packedIdentity, readPublishOrder } from './tarball.ts' /** @@ -102,7 +102,7 @@ async function publishTarball(tarball: string, name: string, version: string): P // command-line flag could not serve both and would override the manifest // that does. Each packed manifest decides, and // check-workspace-constraints holds every manifest to its sequence's level. - const result = attempt('npm', ['publish', tarball, ...tagArgs]) + const result = attemptStreaming('npm', ['publish', tarball, ...tagArgs]) const output = `${result.stdout}${result.stderr}` if (result.status === 0) return