From 485f6ca913be4b0a3cf3fafc827aa827c6ae2916 Mon Sep 17 00:00:00 2001 From: David Berlin Date: Wed, 1 Jul 2026 12:14:11 +0300 Subject: [PATCH] fix: surface zip stderr and exit code in source-zip upload errors When `--upload-source-zip` fails, the CLI logged only execFile's generic "Command failed: zip -r ..." message and discarded the underlying `zip` stderr and exit code, making the failure undiagnosable. Include them in the reported error. Co-Authored-By: Claude Opus 4.8 --- src/utils/deploy/upload-source-zip.ts | 8 +++- .../utils/deploy/upload-source-zip.test.ts | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/utils/deploy/upload-source-zip.ts b/src/utils/deploy/upload-source-zip.ts index ed0ca64f86e..e3241da598b 100644 --- a/src/utils/deploy/upload-source-zip.ts +++ b/src/utils/deploy/upload-source-zip.ts @@ -121,7 +121,13 @@ export const uploadSourceZip = async ({ try { zipPath = await createSourceZip({ sourceDir, filename, statusCb }) } catch (error) { - const errorMsg = error instanceof Error ? error.message : String(error) + const execError = error as { message?: string; stderr?: string | Buffer; code?: number | string } + const stderr = execError.stderr ? String(execError.stderr).trim() : '' + const detail = [stderr, execError.code != null ? `exit code ${String(execError.code)}` : ''] + .filter(Boolean) + .join('; ') + const baseMsg = error instanceof Error ? error.message : String(error) + const errorMsg = detail ? `${baseMsg} (${detail})` : baseMsg statusCb({ type: 'source-zip-upload', msg: `Failed to create source zip: ${errorMsg}`, diff --git a/tests/unit/utils/deploy/upload-source-zip.test.ts b/tests/unit/utils/deploy/upload-source-zip.test.ts index 201becc5afb..dcf40717cae 100644 --- a/tests/unit/utils/deploy/upload-source-zip.test.ts +++ b/tests/unit/utils/deploy/upload-source-zip.test.ts @@ -282,6 +282,54 @@ describe('uploadSourceZip', () => { expect(mockCommandHelpers.warn).toHaveBeenCalledWith('Failed to create source zip: zip command failed') }) + test('surfaces zip stderr and exit code in the error message', async () => { + const mockOs = await import('os') + vi.mocked(mockOs.platform).mockReturnValue('darwin') + + const { uploadSourceZip } = await import('../../../../src/utils/deploy/upload-source-zip.js') + + const mockChildProcess = await import('child_process') + const mockCommandHelpers = await import('../../../../src/utils/command-helpers.js') + const mockTempFile = await import('../../../../src/utils/temporary-file.js') + + // util.promisify(execFile) rejects with an error carrying `stderr` and `code`. + const zipError = Object.assign(new Error('Command failed: zip -r /tmp/x.zip .'), { + stderr: 'zip error: Nothing to do! (/tmp/x.zip)', + code: 12, + }) + vi.mocked(mockChildProcess.execFile).mockImplementation((_command, _args, _options, callback) => { + if (callback) { + callback(zipError, '', '') + } + return {} as ChildProcess + }) + + vi.mocked(mockCommandHelpers.warn).mockImplementation(() => {}) + vi.mocked(mockTempFile.temporaryDirectory).mockReturnValue('/tmp/test-temp-dir') + + const mockStatusCb = vi.fn() + + await expect( + uploadSourceZip({ + sourceDir: '/test/source', + uploadUrl: 'https://s3.example.com/upload-url', + filename: 'test-source.zip', + statusCb: mockStatusCb, + }), + ).rejects.toThrow('Command failed: zip -r /tmp/x.zip .') + + const expectedMsg = + 'Failed to create source zip: Command failed: zip -r /tmp/x.zip . (zip error: Nothing to do! (/tmp/x.zip); exit code 12)' + expect(mockCommandHelpers.warn).toHaveBeenCalledWith(expectedMsg) + expect(mockStatusCb).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'source-zip-upload', + phase: 'error', + msg: expectedMsg, + }), + ) + }) + test('cleans up zip file even when upload fails', async () => { // Ensure OS platform mock returns non-Windows const mockOs = await import('os')