Skip to content

feat: pull DB migrations#8139

Merged
paulo merged 4 commits intomainfrom
pa/pull-migrations
Apr 8, 2026
Merged

feat: pull DB migrations#8139
paulo merged 4 commits intomainfrom
pa/pull-migrations

Conversation

@paulo
Copy link
Copy Markdown
Contributor

@paulo paulo commented Apr 7, 2026

Summary

Add netlify db migrations pull command that fetches migrations from the API and overwrites the local migrations directory

  • netlify db migrations pull — pulls production migrations, prompts for confirmation
  • netlify db migrations pull --force — skips confirmation
  • netlify db migrations pull --branch staging — pulls migrations for the staging branch
  • netlify db migrations pull --branch — auto-detects local git branch
  • netlify db migrations pull --json — outputs JSON

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 7, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added an experimental CLI command to pull remote DB migration files into your local project: supports --branch (auto-detect or specify), --force to skip confirmation, and --json output.
  • Safety & UX

    • Safer pulls with path validation (prevents traversal/absolute paths), branch resolution checks, confirmation before overwrite (can be skipped), and clearer error messages.
  • Tests

    • Added unit tests covering fetch behavior, branch resolution, path safety, overwrite prompts, error handling, and JSON output.

Walkthrough

Adds an experimental CLI subcommand netlify db migrations pull that fetches migration files from a linked Netlify site and writes them into the local migrations directory. The command accepts --branch (string, true to auto-detect from git, or omitted), --force to skip confirmation, and --json for machine output. It validates project linkage and auth, resolves the source branch, requests migrations from the Netlify API, validates and sanitizes migration paths, replaces the local migrations directory, writes migration files, and emits either JSON or human-readable summaries. New unit tests cover errors, validation, prompts, and success cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: pull DB migrations' clearly and concisely describes the main feature being added—a command to pull database migrations. It is directly related to the changeset and provides meaningful context about the primary change.
Description check ✅ Passed The description provides a comprehensive overview of the new command, detailing its functionality, usage examples, and supported flags. It is clearly related to the changeset and explains the intent and behavior of the implemented feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 pa/pull-migrations

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

📊 Benchmark results

Comparing with c28ffa3

  • Dependency count: 1,059 (no change)
  • Package size: 354 MB ⬆️ 0.00% increase vs. c28ffa3
  • Number of ts-expect-error directives: 356 (no change)

@paulo paulo force-pushed the pa/pull-migrations branch from 08f5e37 to ae85de3 Compare April 7, 2026 10:13
@paulo paulo requested review from eduardoboucas and kathmbeck April 7, 2026 12:41
@paulo paulo marked this pull request as ready for review April 7, 2026 12:41
@paulo paulo requested a review from a team as a code owner April 7, 2026 12:41
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 (2)
tests/unit/commands/database/migration-pull.test.ts (1)

219-229: Consider adding tests for additional error scenarios.

The test suite covers the happy path and API errors well. Consider adding tests for:

  1. Filesystem write failures (e.g., mockWriteFile.mockRejectedValue(new Error('EACCES')))
  2. Network failures (e.g., mockFetch.mockRejectedValue(new Error('Network error')))
  3. Path traversal in migration.path (if the security fix is implemented)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/commands/database/migration-pull.test.ts` around lines 219 - 229,
Add unit tests to migration-pull.test.ts that cover three additional failure
scenarios: (1) simulate filesystem write failures by making
mockWriteFile.mockRejectedValue(new Error('EACCES')) and assert migrationPull
rejects with the expected error, (2) simulate network failures by making
mockFetch.mockRejectedValue(new Error('Network error')) and assert migrationPull
rejects with that error, and (3) if migration.path sanitization/path-traversal
protection was added, add a test that supplies a migration with a malicious path
(e.g., '../etc/passwd') and assert migrationPull either rejects or sanitizes the
path (use migrationPull and createMockCommand() to invoke and check behavior).
src/commands/database/migration-pull.ts (1)

66-70: Consider adding a timeout to the fetch request.

The fetch call has no timeout configured. If the API is slow or unresponsive, the command will hang indefinitely, degrading user experience.

♻️ Suggested improvement using AbortController
+  const controller = new AbortController()
+  const timeoutId = setTimeout(() => controller.abort(), 30000)
+
   const response = await fetch(url, {
     headers: {
       Authorization: `Bearer ${token}`,
     },
+    signal: controller.signal,
   })
+
+  clearTimeout(timeoutId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/database/migration-pull.ts` around lines 66 - 70, The fetch call
that assigns to the variable response (fetch(url, { headers: { Authorization:
`Bearer ${token}` } })) has no timeout and can hang; wrap the request with an
AbortController, set a reasonable timeout (e.g., 5–15s) that calls
controller.abort() via setTimeout, pass signal: controller.signal into fetch,
and clear the timeout on success; also catch AbortError to produce a
user-friendly timeout message and ensure any existing response.ok handling still
runs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/database/database.ts`:
- Line 157: Update the help text for the CLI option definition .option('-b,
--branch [branch]', ...) to accurately describe the real behavior: state that
omission defaults to 'production' and that passing --branch without a value uses
the local git branch; e.g. change the description to something like "Pull
migrations for a specific branch (defaults to 'production'; pass --branch with
no value to use local git branch)". Locate the option call in the database
command where .option('-b, --branch [branch]', ...) is defined and replace the
misleading text accordingly.

In `@src/commands/database/migration-pull.ts`:
- Around line 117-123: Ensure paths are validated to prevent directory traversal
and accidental deletes: resolve migrationsDirectory to an absolute canonical
path (resolve/realpath) and verify it is not a dangerous root/home/system dir
before calling rm; for each migration, reject or sanitize migration.path if it's
absolute or contains ".." segments, then build the candidate path, resolve it to
an absolute/real path and verify it starts with the canonical
migrationsDirectory before calling mkdir/join/writeFile; reference the symbols
migrationsDirectory, migration.path, rm, join, dirname, writeFile and
resolveMigrationsDirectory when applying these checks.

---

Nitpick comments:
In `@src/commands/database/migration-pull.ts`:
- Around line 66-70: The fetch call that assigns to the variable response
(fetch(url, { headers: { Authorization: `Bearer ${token}` } })) has no timeout
and can hang; wrap the request with an AbortController, set a reasonable timeout
(e.g., 5–15s) that calls controller.abort() via setTimeout, pass signal:
controller.signal into fetch, and clear the timeout on success; also catch
AbortError to produce a user-friendly timeout message and ensure any existing
response.ok handling still runs.

In `@tests/unit/commands/database/migration-pull.test.ts`:
- Around line 219-229: Add unit tests to migration-pull.test.ts that cover three
additional failure scenarios: (1) simulate filesystem write failures by making
mockWriteFile.mockRejectedValue(new Error('EACCES')) and assert migrationPull
rejects with the expected error, (2) simulate network failures by making
mockFetch.mockRejectedValue(new Error('Network error')) and assert migrationPull
rejects with that error, and (3) if migration.path sanitization/path-traversal
protection was added, add a test that supplies a migration with a malicious path
(e.g., '../etc/passwd') and assert migrationPull either rejects or sanitizes the
path (use migrationPull and createMockCommand() to invoke and check behavior).
🪄 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: 63856767-19b1-496a-ab30-8caf221471b8

📥 Commits

Reviewing files that changed from the base of the PR and between c28ffa3 and ae85de3.

📒 Files selected for processing (3)
  • src/commands/database/database.ts
  • src/commands/database/migration-pull.ts
  • tests/unit/commands/database/migration-pull.test.ts

Comment thread src/commands/database/database.ts Outdated
Comment thread src/commands/database/migration-pull.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/commands/database/migration-pull.ts (1)

117-133: ⚠️ Potential issue | 🟠 Major

Validate all migration paths before deleting local migrations.

The current implementation deletes the local migrations directory (line 119) before validating individual migration paths (lines 122-129). If any migration from the API has an invalid path, the function throws after the local directory is already deleted, causing data loss.

Move path validation to occur before the rm call:

🛠️ Proposed fix
   const canonicalMigrationsDir = resolve(migrationsDirectory)

+  for (const migration of migrations) {
+    if (isAbsolute(migration.path) || migration.path.split(/[/\\]/).includes('..')) {
+      throw new Error(`Migration path "${migration.path}" contains invalid path segments.`)
+    }
+    const filePath = resolve(canonicalMigrationsDir, migration.path)
+    if (!filePath.startsWith(canonicalMigrationsDir + '/') && filePath !== canonicalMigrationsDir) {
+      throw new Error(`Migration path "${migration.path}" resolves outside the migrations directory.`)
+    }
+  }
+
   await rm(canonicalMigrationsDir, { recursive: true, force: true })

   for (const migration of migrations) {
-    if (isAbsolute(migration.path) || migration.path.split(/[/\\]/).includes('..')) {
-      throw new Error(`Migration path "${migration.path}" contains invalid path segments.`)
-    }
-
     const filePath = resolve(canonicalMigrationsDir, migration.path)
-    if (!filePath.startsWith(canonicalMigrationsDir)) {
-      throw new Error(`Migration path "${migration.path}" resolves outside the migrations directory.`)
-    }
-
     await mkdir(dirname(filePath), { recursive: true })
     await writeFile(filePath, migration.content)
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/database/migration-pull.ts` around lines 117 - 133, The code
currently calls rm(canonicalMigrationsDir) before validating each migration.path
which can delete local data if any API migration path is invalid; move the
entire validation block (the checks using isAbsolute(migration.path),
path.split(...).includes('..'), and the resolve+startsWith check for filePath vs
canonicalMigrationsDir) to run for all migrations before invoking
rm(canonicalMigrationsDir), so that you validate every migration in migrations
first and only after all pass call rm, then proceed to mkdir(dirname(filePath))
and writeFile(filePath) for each migration; reference the variables/functions
canonicalMigrationsDir, migrationsDirectory, migrations, isAbsolute, resolve,
dirname, rm, mkdir, and writeFile to locate the relevant code.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/commands/database/migration-pull.ts`:
- Around line 117-133: The code currently calls rm(canonicalMigrationsDir)
before validating each migration.path which can delete local data if any API
migration path is invalid; move the entire validation block (the checks using
isAbsolute(migration.path), path.split(...).includes('..'), and the
resolve+startsWith check for filePath vs canonicalMigrationsDir) to run for all
migrations before invoking rm(canonicalMigrationsDir), so that you validate
every migration in migrations first and only after all pass call rm, then
proceed to mkdir(dirname(filePath)) and writeFile(filePath) for each migration;
reference the variables/functions canonicalMigrationsDir, migrationsDirectory,
migrations, isAbsolute, resolve, dirname, rm, mkdir, and writeFile to locate the
relevant code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 659f1748-d20c-4536-8ee2-bf56e0db1986

📥 Commits

Reviewing files that changed from the base of the PR and between ae85de3 and 4678bba.

📒 Files selected for processing (3)
  • src/commands/database/database.ts
  • src/commands/database/migration-pull.ts
  • tests/unit/commands/database/migration-pull.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/commands/database/database.ts
  • tests/unit/commands/database/migration-pull.test.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/unit/commands/database/migration-pull.test.ts`:
- Line 48: Remove the unused import symbol "join" from the import statement that
currently reads "import { join, resolve } from 'path'"; update the import to
only include "resolve" so the test file no longer imports an unused identifier
and the pipeline error is resolved.
🪄 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: 8eb9281d-e921-45ff-abfd-3de495f03708

📥 Commits

Reviewing files that changed from the base of the PR and between 4678bba and 886a87b.

📒 Files selected for processing (1)
  • tests/unit/commands/database/migration-pull.test.ts

Comment thread tests/unit/commands/database/migration-pull.test.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/unit/commands/database/migration-pull.test.ts (1)

211-218: Consider a more explicit pluralization assertion.

The assertion on line 217 checks that "migrations" (plural) doesn't appear in the message. While this works with the current test setup, it could become fragile if the branch name ever contained "migrations". Consider matching the exact expected message pattern instead.

Alternative assertion approach
   test('handles single migration with correct pluralization', async () => {
     mockFetchResponse([sampleMigrations[0]])

     await migrationPull({ force: true }, createMockCommand())

-    expect(logMessages[0]).toContain('Pulled 1 migration from production')
-    expect(logMessages[0]).not.toContain('migrations')
+    expect(logMessages[0]).toMatch(/Pulled 1 migration from/)
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/commands/database/migration-pull.test.ts` around lines 211 - 218,
The test handles single migration pluralization but uses a negative contains
check which is fragile; update the assertion in the test 'handles single
migration with correct pluralization' to assert the exact expected message (or
use a strict regex) against logMessages[0] so it matches "Pulled 1 migration
from production" exactly (e.g., replace the .toContain/.not.toContain checks
with a single equality or .toMatch that matches the full string), targeting the
migrationPull invocation and logMessages[0].
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/unit/commands/database/migration-pull.test.ts`:
- Around line 211-218: The test handles single migration pluralization but uses
a negative contains check which is fragile; update the assertion in the test
'handles single migration with correct pluralization' to assert the exact
expected message (or use a strict regex) against logMessages[0] so it matches
"Pulled 1 migration from production" exactly (e.g., replace the
.toContain/.not.toContain checks with a single equality or .toMatch that matches
the full string), targeting the migrationPull invocation and logMessages[0].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58af3de1-d8d5-40aa-bb3b-5364eb8f10e2

📥 Commits

Reviewing files that changed from the base of the PR and between 886a87b and a51aa5a.

📒 Files selected for processing (1)
  • tests/unit/commands/database/migration-pull.test.ts

@paulo paulo merged commit 2a8f7c0 into main Apr 8, 2026
71 checks passed
@paulo paulo deleted the pa/pull-migrations branch April 8, 2026 16:20
eduardoboucas pushed a commit that referenced this pull request Apr 9, 2026
🤖 I have created a release *beep* *boop*
---


## [24.11.0](v24.10.0...v24.11.0)
(2026-04-09)


### Features

* load connection string from env var
([#8142](#8142))
([e522b7e](e522b7e))
* pull DB migrations
([#8139](#8139))
([2a8f7c0](2a8f7c0))
* re-structure db commands
([#8137](#8137))
([c28ffa3](c28ffa3))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: token-generator-app[bot] <82042599+token-generator-app[bot]@users.noreply.github.com>
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.

2 participants