diff --git a/scripts/release/families.ts b/scripts/release/families.ts index 09acbe2da9..f43939f17d 100644 --- a/scripts/release/families.ts +++ b/scripts/release/families.ts @@ -180,8 +180,11 @@ export abstract class ReleaseFamily { } for (const member of byNameSorted) checkInstall(member, []) - // Emit the order over both kinds of edge. A node already on the stack is a - // cycle only peer edges can form, and skipping it drops just that edge. + // Emit the order over both kinds of edge. A node already on the stack closes + // a cycle, and that cycle carries at least one peer edge because the install + // edges were just proved acyclic — but the back edge that reaches the stacked + // node is not necessarily the peer one, so the post-condition below decides + // whether the emitted order survived. const ordered: ReleaseMember[] = [] const droppedPeerEdges: DroppedPeerEdge[] = [] const placed = new Set() @@ -216,7 +219,6 @@ export abstract class ReleaseFamily { visit(peer) } onStack.delete(member.name) - if (placed.has(member.name)) return placed.add(member.name) ordered.push(member) } diff --git a/scripts/release/process.ts b/scripts/release/process.ts index acec98feae..392a6cab66 100644 --- a/scripts/release/process.ts +++ b/scripts/release/process.ts @@ -39,17 +39,25 @@ export function attempt(command: string, args: readonly string[], options: RunOp } /** - * Run a command, letting its output reach the log while also returning it. + * Run a command, capture its output, and echo it once the command exits. * - * 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. + * A step that both shows what a command said and classifies its own failure + * needs both halves: the output has to reach the workflow log, and the caller has + * to read it to decide whether a failure is worth retrying. + * + * This is not live progress. `spawnSync` returns only after the child exits, so + * nothing appears while the command runs, and the two streams are echoed one + * after the other — all of stdout, then all of stderr — which loses their + * interleaving. For an npm publish that matters in one visible way: `npm notice` + * lines go to stderr while the `+ name@version` confirmation goes to stdout, so + * the log shows the confirmation first. Live progress would need an + * asynchronous spawn with data listeners. * @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 { +export function attemptEchoed(command: string, args: readonly string[], options: RunOptions = {}): CommandResult { const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, diff --git a/scripts/release/publish.ts b/scripts/release/publish.ts index f861da18c2..6301241ed7 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, attemptStreaming, isEntry } from './process.ts' +import { attempt, attemptEchoed, 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 = attemptStreaming('npm', ['publish', tarball, ...tagArgs]) + const result = attemptEchoed('npm', ['publish', tarball, ...tagArgs]) const output = `${result.stdout}${result.stderr}` if (result.status === 0) return