Skip to content

Commit d547ef8

Browse files
fix: zip based deploys ignore pattern and subdir case (#7636)
* fix: zip based deploys ignore pattern and subdir case * fix: typescript issue * fix: move mkdir to async * fix: windows tests --------- Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 28de6c7 commit d547ef8

File tree

2 files changed

+87
-19
lines changed

2 files changed

+87
-19
lines changed

src/utils/deploy/upload-source-zip.ts

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { execFile } from 'child_process'
2-
import { readFile } from 'fs/promises'
3-
import { join } from 'path'
2+
import { readFile, mkdir } from 'fs/promises'
3+
import { join, dirname } from 'path'
44
import { promisify } from 'util'
55
import type { PathLike } from 'fs'
66
import { platform } from 'os'
@@ -21,26 +21,26 @@ interface UploadSourceZipOptions {
2121
}
2222

2323
const DEFAULT_IGNORE_PATTERNS = [
24-
'node_modules',
25-
'.git',
26-
'.netlify',
27-
'.next',
28-
'dist',
29-
'build',
30-
'.nuxt',
31-
'.output',
32-
'.vercel',
33-
'__pycache__',
34-
'.venv',
24+
'node_modules*',
25+
'.git*',
26+
'.netlify*',
27+
'.next*',
28+
'dist*',
29+
'build*',
30+
'.nuxt*',
31+
'.output*',
32+
'.vercel*',
33+
'__pycache__*',
34+
'.venv*',
3535
'.env',
3636
'.DS_Store',
3737
'Thumbs.db',
3838
'*.log',
39-
'.nyc_output',
40-
'coverage',
41-
'.cache',
42-
'.tmp',
43-
'.temp',
39+
'.nyc_output*',
40+
'coverage*',
41+
'.cache*',
42+
'.tmp*',
43+
'.temp*',
4444
]
4545

4646
const createSourceZip = async ({
@@ -60,6 +60,12 @@ const createSourceZip = async ({
6060
const tmpDir = temporaryDirectory()
6161
const zipPath = join(tmpDir, filename)
6262

63+
// Ensure the directory for the zip file exists
64+
// The filename from the API includes a subdirectory path (e.g., 'workspace-snapshots/source-xxx.zip')
65+
// While temporaryDirectory() creates a new empty directory, the subdirectory within it doesn't exist
66+
// so we need to create it before the zip command can write the file
67+
await mkdir(dirname(zipPath), { recursive: true })
68+
6369
statusCb({
6470
type: 'source-zip-upload',
6571
msg: `Creating source zip...`,

tests/unit/utils/deploy/upload-source-zip.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { describe, expect, test, vi, beforeEach } from 'vitest'
2+
import { join } from 'path'
23
import type { Response } from 'node-fetch'
34
import type { ChildProcess } from 'child_process'
45

@@ -14,6 +15,7 @@ vi.mock('child_process', () => ({
1415
vi.mock('fs/promises', () => ({
1516
readFile: vi.fn(),
1617
unlink: vi.fn(),
18+
mkdir: vi.fn(),
1719
}))
1820

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

200202
expect(mockChildProcess.execFile).toHaveBeenCalledWith(
201203
'zip',
202-
expect.arrayContaining(['-x', 'node_modules', '-x', '.git', '-x', '.netlify', '-x', '.env']),
204+
expect.arrayContaining(['-x', 'node_modules*', '-x', '.git*', '-x', '.netlify*', '-x', '.env']),
203205
expect.objectContaining({
204206
cwd: '/test/source',
205207
maxBuffer: 104857600,
@@ -368,4 +370,64 @@ describe('uploadSourceZip', () => {
368370

369371
expect(result).toHaveProperty('sourceZipFileName')
370372
})
373+
374+
test('creates subdirectories when filename includes path', async () => {
375+
// Ensure OS platform mock returns non-Windows
376+
const mockOs = await import('os')
377+
vi.mocked(mockOs.platform).mockReturnValue('darwin')
378+
379+
const { uploadSourceZip } = await import('../../../../src/utils/deploy/upload-source-zip.js')
380+
381+
const mockFetch = await import('node-fetch')
382+
const mockChildProcess = await import('child_process')
383+
const mockFs = await import('fs/promises')
384+
const mockCommandHelpers = await import('../../../../src/utils/command-helpers.js')
385+
const mockTempFile = await import('../../../../src/utils/temporary-file.js')
386+
387+
vi.mocked(mockFetch.default).mockResolvedValue({
388+
ok: true,
389+
status: 200,
390+
statusText: 'OK',
391+
} as unknown as Response)
392+
393+
vi.mocked(mockChildProcess.execFile).mockImplementation((_command, _args, _options, callback) => {
394+
if (callback) {
395+
callback(null, '', '')
396+
}
397+
return {} as ChildProcess
398+
})
399+
400+
vi.mocked(mockFs.readFile).mockResolvedValue(Buffer.from('mock zip content'))
401+
vi.mocked(mockFs.mkdir).mockResolvedValue(undefined)
402+
vi.mocked(mockCommandHelpers.log).mockImplementation(() => {})
403+
vi.mocked(mockTempFile.temporaryDirectory).mockReturnValue('/tmp/test-temp-dir')
404+
405+
const mockStatusCb = vi.fn()
406+
407+
// Test with a filename that includes a subdirectory path (like the API provides)
408+
await uploadSourceZip({
409+
sourceDir: '/test/source',
410+
uploadUrl: 'https://s3.example.com/upload-url',
411+
filename: 'workspace-snapshots/source-abc123-def456.zip',
412+
statusCb: mockStatusCb,
413+
})
414+
415+
// Should create the subdirectory before attempting zip creation
416+
expect(mockFs.mkdir).toHaveBeenCalledWith(join('/tmp/test-temp-dir', 'workspace-snapshots'), { recursive: true })
417+
418+
// Should still call zip command with the full path
419+
expect(mockChildProcess.execFile).toHaveBeenCalledWith(
420+
'zip',
421+
expect.arrayContaining([
422+
'-r',
423+
join('/tmp/test-temp-dir', 'workspace-snapshots', 'source-abc123-def456.zip'),
424+
'.',
425+
]),
426+
expect.objectContaining({
427+
cwd: '/test/source',
428+
maxBuffer: 104857600,
429+
}),
430+
expect.any(Function),
431+
)
432+
})
371433
})

0 commit comments

Comments
 (0)