From c56428fd9dd1a14e81ad74664ae40104fe4ac226 Mon Sep 17 00:00:00 2001 From: Christopher Monsanto Date: Mon, 17 Aug 2026 15:55:29 -0400 Subject: [PATCH] Survive an upload command that rejects the tar A dying consumer destroys the tar pack and its pending entry sinks, each of which emits the EPIPE as an unhandled streamx error and crashed the process before the child's exit code was reported. Keep the pack and its sinks quiet; the command's exit code is the report. Co-Authored-By: Claude Fable 5 --- tools/deploy/index.ts | 7 +++++-- tools/deploy/queue.ts | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/tools/deploy/index.ts b/tools/deploy/index.ts index 1ffd6c5c..ab02a72e 100644 --- a/tools/deploy/index.ts +++ b/tools/deploy/index.ts @@ -210,9 +210,12 @@ common(program.command('deploy [names...]')) console.log(`${name}: ${matched.size} files | ${entry.cmd}`); const upload = spawn(entry.cmd, {shell: true, stdio: ['pipe', 'inherit', 'inherit']}); // If the command dies early we report its exit code; don't - // also crash on the resulting EPIPE. + // also crash on the resulting EPIPE, which reaches both + // stdin and (via streamx's destroy propagation) the pack. upload.stdin!.on('error', () => {}); - aq.pack(dst => matched.has(dst)).pipe(upload.stdin!); + const pack = aq.pack(dst => matched.has(dst)); + pack.on('error', () => {}); + pack.pipe(upload.stdin!); if (await waitExit(upload) !== 0) { return 1; } diff --git a/tools/deploy/queue.ts b/tools/deploy/queue.ts index bba14691..5fb8734d 100644 --- a/tools/deploy/queue.ts +++ b/tools/deploy/queue.ts @@ -168,11 +168,11 @@ export class ActionQueue { if (entry.type !== 'Op' || (filter !== undefined && !filter(entry.dst))) continue; const op = entry.op; - if (op.type === 'Copy'){ - t.entry({name: entry.dst}, fs.readFileSync(op.src)); - } else if (op.type === 'Write') { - t.entry({name: entry.dst}, op.data); - } + const data = op.type === 'Copy' ? fs.readFileSync(op.src) : op.data; + // A dying consumer destroys the pack and every pending entry + // sink, and each sink emits the error; the consumer is the one + // reporting the failure, so keep the sinks quiet. + t.entry({name: entry.dst}, data).on('error', () => {}); } // Without this the archive has no end-of-archive marker, and strict // readers (Python tarfile in stream mode) die on the truncation.