Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions src/utils/deploy/upload-source-zip.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execFile } from 'child_process'
import { readFile } from 'fs/promises'
import { join } from 'path'
import { readFile, mkdir } from 'fs/promises'
import { join, dirname } from 'path'
import { promisify } from 'util'
import type { PathLike } from 'fs'
import { platform } from 'os'
Expand All @@ -21,26 +21,26 @@ interface UploadSourceZipOptions {
}

const DEFAULT_IGNORE_PATTERNS = [
'node_modules',
'.git',
'.netlify',
'.next',
'dist',
'build',
'.nuxt',
'.output',
'.vercel',
'__pycache__',
'.venv',
'node_modules*',
'.git*',
'.netlify*',
'.next*',
'dist*',
'build*',
'.nuxt*',
'.output*',
'.vercel*',
'__pycache__*',
'.venv*',
'.env',
'.DS_Store',
'Thumbs.db',
'*.log',
'.nyc_output',
'coverage',
'.cache',
'.tmp',
'.temp',
'.nyc_output*',
'coverage*',
'.cache*',
'.tmp*',
'.temp*',
]

const createSourceZip = async ({
Expand All @@ -60,6 +60,12 @@ const createSourceZip = async ({
const tmpDir = temporaryDirectory()
const zipPath = join(tmpDir, filename)

// Ensure the directory for the zip file exists
// The filename from the API includes a subdirectory path (e.g., 'workspace-snapshots/source-xxx.zip')
// While temporaryDirectory() creates a new empty directory, the subdirectory within it doesn't exist
// so we need to create it before the zip command can write the file
await mkdir(dirname(zipPath), { recursive: true })

statusCb({
type: 'source-zip-upload',
msg: `Creating source zip...`,
Expand Down
64 changes: 63 additions & 1 deletion tests/unit/utils/deploy/upload-source-zip.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect, test, vi, beforeEach } from 'vitest'
import { join } from 'path'
import type { Response } from 'node-fetch'
import type { ChildProcess } from 'child_process'

Expand All @@ -14,6 +15,7 @@ vi.mock('child_process', () => ({
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
unlink: vi.fn(),
mkdir: vi.fn(),
}))

vi.mock('../../../../src/utils/command-helpers.js', () => ({
Expand Down Expand Up @@ -199,7 +201,7 @@ describe('uploadSourceZip', () => {

expect(mockChildProcess.execFile).toHaveBeenCalledWith(
'zip',
expect.arrayContaining(['-x', 'node_modules', '-x', '.git', '-x', '.netlify', '-x', '.env']),
expect.arrayContaining(['-x', 'node_modules*', '-x', '.git*', '-x', '.netlify*', '-x', '.env']),
expect.objectContaining({
cwd: '/test/source',
maxBuffer: 104857600,
Expand Down Expand Up @@ -368,4 +370,64 @@ describe('uploadSourceZip', () => {

expect(result).toHaveProperty('sourceZipFileName')
})

test('creates subdirectories when filename includes path', async () => {
// Ensure OS platform mock returns non-Windows
const mockOs = await import('os')
vi.mocked(mockOs.platform).mockReturnValue('darwin')

const { uploadSourceZip } = await import('../../../../src/utils/deploy/upload-source-zip.js')

const mockFetch = await import('node-fetch')
const mockChildProcess = await import('child_process')
const mockFs = await import('fs/promises')
const mockCommandHelpers = await import('../../../../src/utils/command-helpers.js')
const mockTempFile = await import('../../../../src/utils/temporary-file.js')

vi.mocked(mockFetch.default).mockResolvedValue({
ok: true,
status: 200,
statusText: 'OK',
} as unknown as Response)

vi.mocked(mockChildProcess.execFile).mockImplementation((_command, _args, _options, callback) => {
if (callback) {
callback(null, '', '')
}
return {} as ChildProcess
})

vi.mocked(mockFs.readFile).mockResolvedValue(Buffer.from('mock zip content'))
vi.mocked(mockFs.mkdir).mockResolvedValue(undefined)
vi.mocked(mockCommandHelpers.log).mockImplementation(() => {})
vi.mocked(mockTempFile.temporaryDirectory).mockReturnValue('/tmp/test-temp-dir')

const mockStatusCb = vi.fn()

// Test with a filename that includes a subdirectory path (like the API provides)
await uploadSourceZip({
sourceDir: '/test/source',
uploadUrl: 'https://s3.example.com/upload-url',
filename: 'workspace-snapshots/source-abc123-def456.zip',
statusCb: mockStatusCb,
})

// Should create the subdirectory before attempting zip creation
expect(mockFs.mkdir).toHaveBeenCalledWith(join('/tmp/test-temp-dir', 'workspace-snapshots'), { recursive: true })

// Should still call zip command with the full path
expect(mockChildProcess.execFile).toHaveBeenCalledWith(
'zip',
expect.arrayContaining([
'-r',
join('/tmp/test-temp-dir', 'workspace-snapshots', 'source-abc123-def456.zip'),
'.',
]),
expect.objectContaining({
cwd: '/test/source',
maxBuffer: 104857600,
}),
expect.any(Function),
)
})
})
Loading