Skip to content

fix(deploy): skip deploy creation when the source zip is empty#8339

Open
davbree wants to merge 1 commit into
mainfrom
empty-source-zip
Open

fix(deploy): skip deploy creation when the source zip is empty#8339
davbree wants to merge 1 commit into
mainfrom
empty-source-zip

Conversation

@davbree

@davbree davbree commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Problem

When deploying with --upload-source-zip, the CLI creates the deploy record (createSiteDeploy) before building the source zip. If the zip is empty — every file matched an ignore pattern, or the working tree has no source — zip exits 12 (Nothing to do!). Today that means:

  • the deploy is already created, so a dangling/failed deploy shows up in the UI (the --build path doesn't even cancelDeploy), and
  • the command fails with a generic exit code, indistinguishable from a real error.

Change

  • Build the source zip first; only call createSiteDeploy when it's non-empty. An empty source therefore never creates a deploy.
  • On empty, exit with code 12 (mirroring zip's own "nothing to do" code) so upstream tooling can treat it as a non-failure rather than a deploy error.
  • Refactor uploadSourceZip into createSourceZip (throws a typed EmptySourceZipError) + uploadSourceZipFile, plus cleanupSourceZip.

Both deploy paths (--build and --no-build) are updated.

Testing

  • Unit tests rewritten for the new API (9 passing); tsc + eslint clean.
  • Manually verified against the built CLI: an all-excluded/empty dir throws EmptySourceZipError; a normal source produces a zip (excluding build output); the temp zip is cleaned up.

🤖 Generated with Claude Code

@davbree davbree requested a review from a team as a code owner July 7, 2026 11:23
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Deploys now build the source archive before creating a deploy, improving zip-based uploads.
    • Empty source archives are detected earlier and stop the deploy with a dedicated exit code.
  • Bug Fixes

    • Uploads now use the prebuilt archive and clean up temporary files afterward.
    • Improved handling of archive creation and upload failures, including clearer status updates and messages.
  • Tests

    • Refreshed unit tests to better cover archive creation, empty-archive behavior, upload flow, and cleanup.

Walkthrough

This PR refactors source-zip handling for deploys. upload-source-zip.ts adds EmptySourceZipError, detects empty zip results, changes createSourceZip to return a zip path, and changes uploadSourceZip to upload an existing zip and clean it up afterward. deploy.ts now creates the zip before createSiteDeploy, exits with code 12 for empty zips, and uploads only when the deploy response includes upload metadata. Tests were updated for the new utility boundaries.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • netlify/cli#8323: Both PRs modify src/utils/deploy/upload-source-zip.ts around source-zip creation and zip error handling.

Suggested reviewers: pieh, aitchiss

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: skipping deploy creation when the source zip is empty.
Description check ✅ Passed The description is directly related to the changeset and accurately describes the empty-zip deploy fix.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch empty-source-zip

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

📊 Benchmark results

Comparing with 7c10162

  • Dependency count: 1,127 (no change)
  • Package size: 379 MB ⬇️ 0.00% decrease vs. 7c10162
  • Number of ts-expect-error directives: 354 ⬇️ 1.41% decrease vs. 7c10162

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/deploy/deploy.ts (1)

1425-1447: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Move the source-zip upload under the cancel-on-error try at src/commands/deploy/deploy.ts:1425-1447

api.createSiteDeploy() creates the deploy before uploadSourceZipFile() runs, but this upload is still outside the try that calls cancelDeploy(). If the upload throws, the new deploy is left open. Move the upload/cleanup block into that same canceling try, like the --no-build path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/deploy/deploy.ts` around lines 1425 - 1447, The source-zip
upload and cleanup logic in the deploy flow is still outside the cancel-on-error
try, so a failure in uploadSourceZipFile can leave the deploy created by
api.createSiteDeploy open. Move the sourceZipPath handling block into the same
try/catch that calls cancelDeploy, mirroring the --no-build path, and keep using
deployMetadata, deployId, and sourceZipFileName so any upload or cleanup failure
triggers deploy cancellation.
🧹 Nitpick comments (3)
tests/unit/utils/deploy/upload-source-zip.test.ts (1)

171-187: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider asserting statusCb/warn on upload failure too.

The createSourceZip failure test (Lines 106-122) asserts both statusCb and warn are invoked on error, but this analogous uploadSourceZipFile failure test only checks the thrown error and unlink. Per uploadSourceZipFile's contract, it also emits a statusCb error event and calls warn before rethrowing — worth covering for parity and to guard against future regressions in that error-reporting path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/utils/deploy/upload-source-zip.test.ts` around lines 171 - 187,
The upload failure test for uploadSourceZipFile only verifies the thrown error
and cleanup, but it should also cover the error-reporting contract. Update the
test to assert that the statusCb passed into uploadSourceZipFile receives the
expected error status and that warn is called when the fetch upload fails,
similar to the createSourceZip failure test. Use the uploadSourceZipFile
function and its existing error path to locate where to exercise these callbacks
while keeping the unlink assertion.
src/utils/deploy/upload-source-zip.ts (1)

75-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drop comments that restate what the code does.

// Check for Windows, // Create exclusion list for zip command, and // Use system zip command to create the archive describe the mechanics rather than intent; the code is already self-explanatory here.

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead."

Also applies to: 88-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/deploy/upload-source-zip.ts` at line 75, Remove the comments in
uploadSourceZip that merely restate the code’s mechanics, including the Windows
check and the zip/exclusion list setup, and rely on the existing code structure
in upload-source-zip.ts to remain self-explanatory. Keep only comments that add
intent or non-obvious context; if needed, clarify the relevant logic around
uploadSourceZip and any helper blocks rather than describing what each statement
does.

Source: Coding guidelines

src/commands/deploy/deploy.ts (1)

627-627: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Comment restates the code.

// We created a zip but the deploy didn't provide an upload URL — clean it up. describes what the following line does. Prefer self-explanatory code without the narration.

As per coding guidelines: "Never write comments on what the code does; make the code clean and self-explanatory instead."

Also applies to: 1445-1446

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/deploy/deploy.ts` at line 627, Remove the redundant narration
comment in the deploy flow and any similar comments around the upload-URL
cleanup path; the code in deploy.ts should be self-explanatory without restating
what the cleanup logic does. Locate the cleanup branch in the deploy command
(near the zip/upload URL handling) and delete the comment(s) so the intent is
conveyed by the surrounding code and variable names instead.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/deploy/upload-source-zip.ts`:
- Around line 154-164: cleanupSourceZip currently only deletes the source.zip
file, but createSourceZip allocates a temporary mkdtemp directory that also
needs removal. Update cleanupSourceZip to remove the parent temp directory (and
its contents if needed) instead of just unlinking zipPath, and make sure every
early-exit/error path that follows createSourceZip also invokes this cleanup so
no empty temp dirs are left behind.

---

Outside diff comments:
In `@src/commands/deploy/deploy.ts`:
- Around line 1425-1447: The source-zip upload and cleanup logic in the deploy
flow is still outside the cancel-on-error try, so a failure in
uploadSourceZipFile can leave the deploy created by api.createSiteDeploy open.
Move the sourceZipPath handling block into the same try/catch that calls
cancelDeploy, mirroring the --no-build path, and keep using deployMetadata,
deployId, and sourceZipFileName so any upload or cleanup failure triggers deploy
cancellation.

---

Nitpick comments:
In `@src/commands/deploy/deploy.ts`:
- Line 627: Remove the redundant narration comment in the deploy flow and any
similar comments around the upload-URL cleanup path; the code in deploy.ts
should be self-explanatory without restating what the cleanup logic does. Locate
the cleanup branch in the deploy command (near the zip/upload URL handling) and
delete the comment(s) so the intent is conveyed by the surrounding code and
variable names instead.

In `@src/utils/deploy/upload-source-zip.ts`:
- Line 75: Remove the comments in uploadSourceZip that merely restate the code’s
mechanics, including the Windows check and the zip/exclusion list setup, and
rely on the existing code structure in upload-source-zip.ts to remain
self-explanatory. Keep only comments that add intent or non-obvious context; if
needed, clarify the relevant logic around uploadSourceZip and any helper blocks
rather than describing what each statement does.

In `@tests/unit/utils/deploy/upload-source-zip.test.ts`:
- Around line 171-187: The upload failure test for uploadSourceZipFile only
verifies the thrown error and cleanup, but it should also cover the
error-reporting contract. Update the test to assert that the statusCb passed
into uploadSourceZipFile receives the expected error status and that warn is
called when the fetch upload fails, similar to the createSourceZip failure test.
Use the uploadSourceZipFile function and its existing error path to locate where
to exercise these callbacks while keeping the unlink assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a65f7f1-5c6f-4f4e-b7b2-0d3119be881b

📥 Commits

Reviewing files that changed from the base of the PR and between 7c10162 and 10aa6b8.

📒 Files selected for processing (3)
  • src/commands/deploy/deploy.ts
  • src/utils/deploy/upload-source-zip.ts
  • tests/unit/utils/deploy/upload-source-zip.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • netlify/blueprints (manual)

Comment thread src/utils/deploy/upload-source-zip.ts Outdated
@davbree davbree force-pushed the empty-source-zip branch 2 times, most recently from 25b7a9c to d8094f0 Compare July 7, 2026 12:34

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/commands/deploy/deploy.ts (2)

1393-1431: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Same orphaned-zip pattern as runDeploy (Lines 609-637).

See the comment on Lines 609-637 for the full analysis — sourceZipPath built here at Line 1393 is only cleaned up if it reaches uploadSourceZip at Line 1425; if api.createSiteDeploy throws, or the response lacks upload metadata, the zip is left on disk with no cleanup path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/deploy/deploy.ts` around lines 1393 - 1431, The deploy flow
leaves the temporary source zip orphaned when `api.createSiteDeploy` fails or
returns no upload metadata, because `sourceZipPath` is only used in the
`uploadSourceZip` branch. Update the `runDeploy`/deploy logic around
`buildSourceZipOrExit`, `createSiteDeploy`, and `uploadSourceZip` to ensure the
zip is always cleaned up, typically by tracking the generated path and removing
it in a `finally` block or equivalent cleanup path even when deployment creation
or upload setup fails.

609-637: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clean up the temp source-zip directory when upload is skipped or fails. uploadSourceZip only unlinks the .zip, so if createSiteDeploy throws or returns no source_zip_upload_url/source_zip_filename, the mkdtemp directory from createSourceZip is left behind. Add a finally cleanup that removes the whole temp dir here; the same pattern appears later in this command.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/deploy/deploy.ts` around lines 609 - 637, The deploy flow in
deploy() leaves the temporary source-zip directory behind when createSiteDeploy
fails or when source_zip_upload_url/source_zip_filename is missing, because only
the zip file is cleaned up by uploadSourceZip. Add a finally-style cleanup
around the createSourceZip path here so the entire mkdtemp directory is removed
regardless of upload success or skip, and follow the same cleanup pattern used
later in this command. Reference the existing createSourceZip and
uploadSourceZip flow to place the cleanup correctly.
♻️ Duplicate comments (1)
src/utils/deploy/upload-source-zip.ts (1)

80-80: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Only the zip file is cleaned up, not its parent temp directory.

createSourceZip allocates a fresh mkdtemp directory (Line 80: join(temporaryDirectory(), 'source.zip')), but the cleanup in uploadSourceZip's finally block only unlinks zipPath (Lines 191-193), leaving the now-empty parent directory behind on every successful/failed upload. It also isn't cleaned when createSourceZip throws EmptySourceZipError (the directory is created before the zip command runs). This accumulates orphaned temp directories over repeated deploys.

🧹 Proposed fix
-import { readFile, unlink } from 'node:fs/promises'
-import { join } from 'node:path'
+import { readFile, rm } from 'node:fs/promises'
+import { dirname, join } from 'node:path'
   } finally {
-    // Best-effort cleanup of the temporary zip.
-    try {
-      await unlink(zipPath)
-    } catch {
-      // Ignore cleanup errors
-    }
+    // Best-effort cleanup of the temporary zip and its directory.
+    try {
+      await rm(dirname(zipPath), { recursive: true, force: true })
+    } catch {
+      // Ignore cleanup errors
+    }
   }

Also applies to: 158-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/deploy/upload-source-zip.ts` at line 80, The cleanup in
uploadSourceZip only removes zipPath and leaves the mkdtemp parent directory
behind, including when createSourceZip fails with EmptySourceZipError. Update
uploadSourceZip and createSourceZip so the temporary directory created via
temporaryDirectory() is always removed in the finally path, not just the file;
use the existing zipPath/temporary directory handling in uploadSourceZip to
clean up both the archive and its parent dir after upload or on error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/deploy/upload-source-zip.ts`:
- Around line 59-66: The JSDoc for the source zip helper is out of sync with the
exported API because it references uploadSourceZipFile and cleanupSourceZip,
which do not exist in this module. Update the comment on uploadSourceZip to
refer to the correct helper name or rephrase it so it says the caller uploads or
removes the temporary file itself, keeping the documentation aligned with the
actual exports.

---

Outside diff comments:
In `@src/commands/deploy/deploy.ts`:
- Around line 1393-1431: The deploy flow leaves the temporary source zip
orphaned when `api.createSiteDeploy` fails or returns no upload metadata,
because `sourceZipPath` is only used in the `uploadSourceZip` branch. Update the
`runDeploy`/deploy logic around `buildSourceZipOrExit`, `createSiteDeploy`, and
`uploadSourceZip` to ensure the zip is always cleaned up, typically by tracking
the generated path and removing it in a `finally` block or equivalent cleanup
path even when deployment creation or upload setup fails.
- Around line 609-637: The deploy flow in deploy() leaves the temporary
source-zip directory behind when createSiteDeploy fails or when
source_zip_upload_url/source_zip_filename is missing, because only the zip file
is cleaned up by uploadSourceZip. Add a finally-style cleanup around the
createSourceZip path here so the entire mkdtemp directory is removed regardless
of upload success or skip, and follow the same cleanup pattern used later in
this command. Reference the existing createSourceZip and uploadSourceZip flow to
place the cleanup correctly.

---

Duplicate comments:
In `@src/utils/deploy/upload-source-zip.ts`:
- Line 80: The cleanup in uploadSourceZip only removes zipPath and leaves the
mkdtemp parent directory behind, including when createSourceZip fails with
EmptySourceZipError. Update uploadSourceZip and createSourceZip so the temporary
directory created via temporaryDirectory() is always removed in the finally
path, not just the file; use the existing zipPath/temporary directory handling
in uploadSourceZip to clean up both the archive and its parent dir after upload
or on error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7326f10f-c159-4699-a1eb-5ab72cba90d6

📥 Commits

Reviewing files that changed from the base of the PR and between 10aa6b8 and d8094f0.

📒 Files selected for processing (3)
  • src/commands/deploy/deploy.ts
  • src/utils/deploy/upload-source-zip.ts
  • tests/unit/utils/deploy/upload-source-zip.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • netlify/blueprints (manual)

Comment thread src/utils/deploy/upload-source-zip.ts
When deploying with `--upload-source-zip`, the deploy record was created
before the source zip was built. If the zip came back empty ("zip error:
Nothing to do!" — everything matched an ignore pattern or the working tree
was empty), the deploy was left dangling in the UI (the build path didn't
even cancel it) and the command failed with a generic error.

Now the source zip is built *first*; the deploy is only created when it is
non-empty. When empty, the command exits with code 12 (mirroring zip's own
"nothing to do" exit code) so callers can detect this non-failure outcome.

`uploadSourceZip` is split into `createSourceZip` / `uploadSourceZipFile`
(+ `cleanupSourceZip`), and a typed `EmptySourceZipError` is introduced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@davbree davbree force-pushed the empty-source-zip branch from d8094f0 to d31e478 Compare July 7, 2026 12:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/unit/utils/deploy/upload-source-zip.test.ts (1)

125-142: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Windows test doesn't verify warn/cleanup, unlike the sibling failure test.

The Windows-platform failure goes through the same general-failure branch as 'reports and rethrows other zip failures' (calls warn and cleans up the temp dir), but this test only asserts the thrown message and statusCb. Given the temp-dir cleanup was previously a flagged bug, asserting fs.rm (and warn) here would guard against regressions on this specific early-exit path too.

✅ Suggested additional assertions
   test('throws on Windows platform', async () => {
     const mockOs = await import('os')
     vi.mocked(mockOs.platform).mockReturnValue('win32')

     const { createSourceZip } = await import('../../../../src/utils/deploy/upload-source-zip.js')
+    const mockCommandHelpers = await import('../../../../src/utils/command-helpers.js')
+    const mockFs = await import('fs/promises')

     const statusCb = vi.fn()
     await expect(createSourceZip({ sourceDir: '/test/source', statusCb })).rejects.toThrow(
       'Source zip upload is not supported on Windows',
     )
     expect(statusCb).toHaveBeenCalledWith(
       expect.objectContaining({
         type: 'source-zip-upload',
         phase: 'error',
         msg: 'Failed to create source zip: Source zip upload is not supported on Windows',
       }),
     )
+    expect(mockCommandHelpers.warn).toHaveBeenCalledWith(
+      'Failed to create source zip: Source zip upload is not supported on Windows',
+    )
+    expect(mockFs.rm).toHaveBeenCalledWith('/tmp/test-temp-dir', { recursive: true, force: true })
   })

As per coding guidelines, "Unit tests should test individual modules and utilities in tests/unit/ directory" - this strengthens coverage of the module's cleanup contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/utils/deploy/upload-source-zip.test.ts` around lines 125 - 142,
The Windows-path test for createSourceZip currently only checks the thrown error
and statusCb, but it should also cover the same cleanup/warning behavior as the
existing failure case. Update the createSourceZip Windows test to assert the
warn path is invoked and the temporary directory cleanup via fs.rm happens,
using the same mocked failure flow and symbols already present in
upload-source-zip.test.ts. This will verify the early-exit branch still performs
the expected cleanup contract on win32.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/deploy/upload-source-zip.ts`:
- Around line 78-93: Remove the redundant narrative comments in uploadSourceZip
that merely restate the next statements; keep the logic in platform() check,
DEFAULT_IGNORE_PATTERNS handling, and the zip command try block unchanged, and
rely on the code itself for clarity.
- Around line 59-132: The deploy flow currently leaves the temporary source zip
directory behind when `createSourceZip` succeeds but the `uploadSourceZip`
branch is skipped because the response lacks both `source_zip_upload_url` and
`source_zip_filename`, or an error occurs before upload. Add a caller-side
cleanup path in the deploy logic that owns `sourceZipPath` so the temp directory
is removed whenever no upload happens, while keeping `uploadSourceZip`
responsible for the successful upload case.

---

Nitpick comments:
In `@tests/unit/utils/deploy/upload-source-zip.test.ts`:
- Around line 125-142: The Windows-path test for createSourceZip currently only
checks the thrown error and statusCb, but it should also cover the same
cleanup/warning behavior as the existing failure case. Update the
createSourceZip Windows test to assert the warn path is invoked and the
temporary directory cleanup via fs.rm happens, using the same mocked failure
flow and symbols already present in upload-source-zip.test.ts. This will verify
the early-exit branch still performs the expected cleanup contract on win32.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db1eb6f1-1659-4e93-9bb6-f3e97cff9bbd

📥 Commits

Reviewing files that changed from the base of the PR and between d8094f0 and d31e478.

📒 Files selected for processing (3)
  • src/commands/deploy/deploy.ts
  • src/utils/deploy/upload-source-zip.ts
  • tests/unit/utils/deploy/upload-source-zip.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • netlify/blueprints (manual)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/commands/deploy/deploy.ts

Comment on lines +59 to +132
/**
* Creates the source zip in a temporary directory and returns its path. The
* caller uploads it via `uploadSourceZip`, which removes the temporary directory
* afterwards. On any failure (including an empty archive) the directory is
* removed here before throwing.
*
* Throws `EmptySourceZipError` when the archive would be empty, so the caller
* can avoid creating a deploy for a source that has nothing to upload.
*/
export const createSourceZip = async ({
sourceDir,
filename,
statusCb,
statusCb = () => {},
}: {
sourceDir: string
filename: string
statusCb: (status: DeployEvent) => void
}) => {
// Check for Windows - this feature is not supported on Windows
if (platform() === 'win32') {
throw new Error('Source zip upload is not supported on Windows')
}
statusCb?: (status: DeployEvent) => void
}): Promise<string> => {
const zipPath = join(temporaryDirectory(), 'source.zip')

const tmpDir = temporaryDirectory()
const zipPath = join(tmpDir, filename)
try {
// Check for Windows - this feature is not supported on Windows
if (platform() === 'win32') {
throw new Error('Source zip upload is not supported on Windows')
}

// 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...`,
phase: 'start',
})

statusCb({
type: 'source-zip-upload',
msg: `Creating source zip...`,
phase: 'start',
})
// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])

// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])
// Use system zip command to create the archive
try {
await execa('zip', ['-r', '-q', zipPath, '.', ...excludeArgs], {
all: true,
cwd: sourceDir,
stdio: ['ignore', 'pipe', 'pipe'],
})
} catch (baseErr) {
// An empty archive is an expected outcome, not a failure — let the caller decide.
if (isEmptyZipError(baseErr)) {
throw new EmptySourceZipError()
}

// Use system zip command to create the archive
try {
await execa('zip', ['-r', '-q', zipPath, '.', ...excludeArgs], {
all: true,
cwd: sourceDir,
stdio: ['ignore', 'pipe', 'pipe'],
})
} catch (_baseErr) {
let message = 'zip command failed'
if (_baseErr instanceof Error && 'command' in _baseErr) {
const baseErr = _baseErr as ExecaError
message = `${baseErr.shortMessage}\n\n${baseErr.all ?? ''}`
let message = 'zip command failed'
if (baseErr instanceof Error && 'command' in baseErr) {
const execaErr = baseErr as ExecaError
message = `${execaErr.shortMessage}\n\n${execaErr.all ?? ''}`
}
throw new Error(message, { cause: baseErr })
}

return zipPath
} catch (error) {
// Nothing usable was produced; drop the temp directory we created.
await removeSourceZipDir(zipPath)

// An empty source zip is not a reported failure.
if (error instanceof EmptySourceZipError) {
throw error
}
throw new Error(message, { cause: _baseErr })

const errorMsg = error instanceof Error ? error.message : String(error)
statusCb({
type: 'source-zip-upload',
msg: `Failed to create source zip: ${errorMsg}`,
phase: 'error',
})
warn(`Failed to create source zip: ${errorMsg}`)
throw error
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Relevant files ==\n'
fd -a 'upload-source-zip.ts|deploy.ts' src

printf '\n== Outline: src/utils/deploy/upload-source-zip.ts ==\n'
ast-grep outline src/utils/deploy/upload-source-zip.ts --view expanded

printf '\n== Outline: src/utils/deploy/deploy.ts ==\n'
ast-grep outline src/utils/deploy/deploy.ts --view expanded

printf '\n== Search for createSourceZip / uploadSourceZip usage ==\n'
rg -n "createSourceZip|uploadSourceZip|source_zip_upload_url|source_zip_filename|removeSourceZipDir|temporaryDirectory" src

printf '\n== Read upload-source-zip.ts (selected ranges) ==\n'
sed -n '1,240p' src/utils/deploy/upload-source-zip.ts

printf '\n== Read deploy.ts (selected ranges around source zip handling) ==\n'
rg -n -A40 -B20 "source_zip_upload_url|source_zip_filename|createSourceZip|uploadSourceZip" src/utils/deploy/deploy.ts

Repository: netlify/cli

Length of output: 10488


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== deploy.ts source-zip section ==\n'
sed -n '1380,1465p' src/commands/deploy/deploy.ts

printf '\n== createSourceZip callsites ==\n'
rg -n -A8 -B8 "createSourceZip\(" src/commands/deploy/deploy.ts src

printf '\n== cleanup/finally around deploy flow ==\n'
rg -n -A20 -B20 "finally|removeSourceZipDir|uploadSourceZip\(" src/commands/deploy/deploy.ts

Repository: netlify/cli

Length of output: 8756


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Any cleanup of source zip path elsewhere ==\n'
rg -n "removeSourceZipDir|rm\\(dirname\\(zipPath\\)|sourceZipPath|sourceZipFileName|uploadSourceZipResult" src/commands/deploy src/utils/deploy

printf '\n== buildSourceZipOrExit implementation ==\n'
sed -n '300,330p' src/commands/deploy/deploy.ts

Repository: netlify/cli

Length of output: 3144


Clean up sourceZipPath when the upload branch is skipped src/commands/deploy/deploy.ts:1424

If createSourceZip succeeds but the deploy response never provides both source_zip_upload_url and source_zip_filename (or fails before that branch runs), the temp directory stays on disk because only uploadSourceZip removes it. Add a caller-side cleanup path for the no-upload case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/deploy/upload-source-zip.ts` around lines 59 - 132, The deploy flow
currently leaves the temporary source zip directory behind when
`createSourceZip` succeeds but the `uploadSourceZip` branch is skipped because
the response lacks both `source_zip_upload_url` and `source_zip_filename`, or an
error occurs before upload. Add a caller-side cleanup path in the deploy logic
that owns `sourceZipPath` so the temp directory is removed whenever no upload
happens, while keeping `uploadSourceZip` responsible for the successful upload
case.

Comment on lines +78 to +93
// Check for Windows - this feature is not supported on Windows
if (platform() === 'win32') {
throw new Error('Source zip upload is not supported on Windows')
}

// 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...`,
phase: 'start',
})

statusCb({
type: 'source-zip-upload',
msg: `Creating source zip...`,
phase: 'start',
})
// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])

// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])
// Use system zip command to create the archive
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove comments that just restate the following line.

"Check for Windows - this feature is not supported on Windows", "Create exclusion list for zip command", and "Use system zip command to create the archive" merely narrate the code immediately below them and add no information the code doesn't already convey.

♻️ Proposed cleanup
-    // Check for Windows - this feature is not supported on Windows
     if (platform() === 'win32') {
       throw new Error('Source zip upload is not supported on Windows')
     }

     statusCb({
       type: 'source-zip-upload',
       msg: `Creating source zip...`,
       phase: 'start',
     })

-    // Create exclusion list for zip command
     const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])

-    // Use system zip command to create the archive
     try {
       await execa('zip', ['-r', '-q', zipPath, '.', ...excludeArgs], {

As per coding guidelines, "Never write comments on what the code does; make the code clean and self-explanatory instead."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check for Windows - this feature is not supported on Windows
if (platform() === 'win32') {
throw new Error('Source zip upload is not supported on Windows')
}
// 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...`,
phase: 'start',
})
statusCb({
type: 'source-zip-upload',
msg: `Creating source zip...`,
phase: 'start',
})
// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])
// Create exclusion list for zip command
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])
// Use system zip command to create the archive
try {
if (platform() === 'win32') {
throw new Error('Source zip upload is not supported on Windows')
}
statusCb({
type: 'source-zip-upload',
msg: `Creating source zip...`,
phase: 'start',
})
const excludeArgs = DEFAULT_IGNORE_PATTERNS.flatMap((pattern) => ['-x', pattern])
try {
await execa('zip', ['-r', '-q', zipPath, '.', ...excludeArgs], {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/deploy/upload-source-zip.ts` around lines 78 - 93, Remove the
redundant narrative comments in uploadSourceZip that merely restate the next
statements; keep the logic in platform() check, DEFAULT_IGNORE_PATTERNS
handling, and the zip command try block unchanged, and rely on the code itself
for clarity.

Source: Coding guidelines

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant