Skip to content

feat: move migration topic into CLI#1322

Open
rexxars wants to merge 16 commits into
mainfrom
feat/move-migration
Open

feat: move migration topic into CLI#1322
rexxars wants to merge 16 commits into
mainfrom
feat/move-migration

Conversation

@rexxars

@rexxars rexxars commented Jun 17, 2026

Copy link
Copy Markdown
Member

Description

Currently the @sanity/migrate module contains both the migration logic as well as CLI logic. This last part is a bit tedious, especially due to peer dependency version conflicts whenever we release a new major of @sanity/cli-core etc.

This PR moves the CLI logic into the CLI instead of using it as an oclif plugin. This is backwards compatible, but once sanity-io/migrate#92 is released we should upgrade to v8 of that to avoid @sanity/cli-core dependencies of different versions etc.


Note

High Risk
Live migration run --no-dry-run applies mutations to configured datasets; regressions in script resolution or migrate v8 integration could cause failed or unintended content changes despite dry-run defaults and confirm prompts.

Overview
Content migration commands are now first-class in @sanity/cli instead of registering through the @sanity/migrate oclif plugin. oclif.config.js drops that plugin (with an explicit note to avoid duplicate command IDs) and adds a migrations topic; migration is wired as a topic alias.

New surface: sanity migration create, list, and run live under commands/migrations/, backed by actions/migration/ for discovering scripts under migrations/, scaffolding templates, dry-run mutation formatting, and API version normalization. @sanity/migrate is bumped to ^8.0.0 and used only as the engine (dryRun / run); execution still defaults to dry-run, requires confirmation for live runs, and supports export-based dry runs.

Tests cover create/list/run flows, duplicate migration ID resolution, and a smoke test that exercises the real migrate engine against a local export fixture.

Reviewed by Cursor Bugbot for commit 398c44c. Bugbot is set up for automated code reviews on this repo. Configure here.

@rexxars rexxars requested review from binoy14 and runeb June 17, 2026 22:27
@rexxars rexxars requested a review from a team as a code owner June 17, 2026 22:27
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @sanity/cli

Compared against main (987a6226)

@sanity/cli

Metric Value vs main (987a622)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 11.13 MB -
Bundled (gzip) 2.10 MB -
Import time 846ms -3ms, -0.4%

bin:sanity

Metric Value vs main (987a622)
Internal (raw) 782 B -
Internal (gzip) 423 B -
Bundled (raw) 9.87 MB -
Bundled (gzip) 1.77 MB -
Import time 1.96s +38ms, +2.0%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (987a6226)

Metric Value vs main (987a622)
Internal (raw) 98.2 KB -
Internal (gzip) 23.3 KB -
Bundled (raw) 21.70 MB -
Bundled (gzip) 3.45 MB -
Import time 754ms -1ms, -0.2%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (987a6226)

Metric Value vs main (987a622)
Internal (raw) 908 B -
Internal (gzip) 483 B -
Bundled (raw) 931 B -
Bundled (gzip) 491 B -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

Comment thread packages/@sanity/cli/src/actions/migration/resolveMigrations.ts
Comment thread packages/@sanity/cli/src/commands/migrations/create.ts
Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Coverage Delta

File Statements
packages/@sanity/cli/src/actions/migration/constants.ts 100.0% (new)
packages/@sanity/cli/src/actions/migration/ensureApiVersionFormat.ts 80.0% (new)
packages/@sanity/cli/src/actions/migration/fileExists.ts 0.0% (new)
packages/@sanity/cli/src/actions/migration/prettyMutationFormatter.ts 64.8% (new)
packages/@sanity/cli/src/actions/migration/resolveMigrationScript.ts 17.4% (new)
packages/@sanity/cli/src/actions/migration/resolveMigrations.ts 100.0% (new)
packages/@sanity/cli/src/actions/migration/tree.ts 83.3% (new)
packages/@sanity/cli/src/commands/migrations/create.ts 94.7% (new)
packages/@sanity/cli/src/commands/migrations/list.ts 100.0% (new)
packages/@sanity/cli/src/commands/migrations/run.ts 95.1% (new)
packages/@sanity/cli/src/topicAliases.ts 100.0% (±0%)

Comparing 11 changed files against main @ 813a9b6f8b1280c2828e45ae76a70e8d3f8e8ff7

Overall Coverage

Metric Coverage
Statements 77.7% (+ 0.2%)
Branches 68.7% (+ 0.0%)
Functions 73.4% (+ 0.2%)
Lines 78.2% (+ 0.2%)

Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
Comment thread packages/@sanity/cli/src/commands/migrations/run.ts
Comment thread packages/@sanity/cli/src/actions/migration/resolveMigrations.ts
@runeb

runeb commented Jul 1, 2026

Copy link
Copy Markdown
Member

Also deferring to @binoy14 here who reviewed the other corresponding PR

@runeb runeb removed their request for review July 1, 2026 21:28
@rexxars rexxars force-pushed the feat/move-migration branch from e432ed5 to ea2ddcc Compare July 15, 2026 18:49
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @sanity/cli

Compared against main (813a9b6f)

@sanity/cli

Metric Value vs main (813a9b6)
Internal (raw) 2.2 KB -
Internal (gzip) 838 B -
Bundled (raw) 11.16 MB -
Bundled (gzip) 2.10 MB -
Import time 869ms -3ms, -0.3%

bin:sanity

Metric Value vs main (813a9b6)
Internal (raw) 782 B -
Internal (gzip) 423 B -
Bundled (raw) 9.87 MB -
Bundled (gzip) 1.78 MB -
Import time 2.29s +34ms, +1.5%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (813a9b6f)

Metric Value vs main (813a9b6)
Internal (raw) 108.1 KB -
Internal (gzip) 27.0 KB -
Bundled (raw) 21.73 MB -
Bundled (gzip) 3.46 MB -
Import time 772ms -3ms, -0.4%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-build

Compared against main (813a9b6f)

@sanity/cli-build/_internal/build

Metric Value vs main (813a9b6)
Internal (raw) 113.0 KB -
Internal (gzip) 28.4 KB -
Bundled (raw) 18.03 MB -
Bundled (gzip) 3.62 MB -
Import time 1.35s -2ms, -0.1%

@sanity/cli-build/_internal/env

Metric Value vs main (813a9b6)
Internal (raw) 1.8 KB -
Internal (gzip) 644 B -
Bundled (raw) 1.31 MB -
Bundled (gzip) 333.3 KB -
Import time 127ms +2ms, +1.6%

@sanity/cli-build/_internal/extract

Metric Value vs main (813a9b6)
Internal (raw) 8.6 KB -
Internal (gzip) 2.7 KB -
Bundled (raw) 151.2 KB -
Bundled (gzip) 38.3 KB -
Import time 244ms -1ms, -0.4%

🗺️ ./_internal/env · ./_internal/extract · @sanity/cli-build:./_internal/build treemap too large to embed · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (813a9b6f)

Metric Value vs main (813a9b6)
Internal (raw) 908 B -
Internal (gzip) 483 B -
Bundled (raw) 931 B -
Bundled (gzip) 491 B -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@socket-security

socket-security Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​sanity/​migrate@​8.0.0991007796100

View full report

Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
Comment thread packages/@sanity/cli/src/commands/migrations/run.ts Outdated
rexxars added a commit that referenced this pull request Jul 15, 2026
…rt error

Addresses two open Cursor Bugbot comments on PR #1322:
- Wrap the live migration run() in try/finally so the spinner is always
  stopped, even when run() throws, instead of animating after failure.
- Reword the named up/down export error, which previously claimed "up"
  migrations are supported while rejecting exactly those exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rexxars added a commit that referenced this pull request Jul 15, 2026
…rt error

Addresses two open Cursor Bugbot comments on PR #1322:
- Wrap the live migration run() in try/finally so the spinner is always
  stopped, even when run() throws, instead of animating after failure.
- Reword the named up/down export error, which previously claimed "up"
  migrations are supported while rejecting exactly those exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rexxars rexxars force-pushed the feat/move-migration branch from b796194 to 50cb6a7 Compare July 15, 2026 19:59
rexxars and others added 12 commits July 15, 2026 14:28
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…igrate plugin

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ke test

Add boundary assertions verifying the CLI hands @sanity/migrate the full API
config (incl. token and apiVersion), concurrency, and the resolved migration.

Add an offline smoke test that exercises the real @sanity/migrate dryRun engine
against a local export archive, so a runtime contract break (e.g. after a
dependency upgrade) is caught even when TypeScript types still line up.

This surfaced a latent bug: dryRunHandler was not awaited, so dry-run output
could be truncated if the process exits before the async work completes. Await it.
- resolveMigrations: dedupe so a migration id is listed only once, even when
  multiple loadable candidates resolve or a file and directory share a base
  name (no more duplicate rows in `migrations list` / run-without-id table)
- create: error instead of writing index.ts into the migrations folder itself
  when the title slugifies to an empty name (e.g. punctuation-only titles)
- run: show friendly guidance instead of crashing with ENOENT when invoked
  without an id and the migrations folder is missing

Each fix is covered by a test that fails without it (TDD).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…id-run

- run: pass deprecatedFlagName 'project' to getProjectId so `--project` is
  honored even when sanity.cli.js has no api.projectId. Previously the command
  failed with "Unable to determine project ID" even when valid --project and
  --dataset flags were supplied.
- run: only finalize the progress spinner (stopAndPersist) once the migration
  reports done. While running it now updates the spinner text instead of
  persisting a success line on every in-progress callback, which repeatedly
  finalized the spinner.

Covered by tests (the run.test.ts spinner tests now assert on the spinner mock
rather than incidental stderr output).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rexxars and others added 4 commits July 15, 2026 14:29
- run: move the no-id listing branch ahead of the api.projectId/dataset
  resolution and validation, so `sanity migrations run` (no id) lists available
  migrations with only a project root, matching `sanity migrations list`
- run: surface a clear CLI error when listing fails for a non-ENOENT reason
  (e.g. a syntax error in a migration file) instead of rethrowing the raw error

Both covered by tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rt error

Addresses two open Cursor Bugbot comments on PR #1322:
- Wrap the live migration run() in try/finally so the spinner is always
  stopped, even when run() throws, instead of animating after failure.
- Reword the named up/down export error, which previously claimed "up"
  migrations are supported while rejecting exactly those exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rexxars rexxars force-pushed the feat/move-migration branch from 50cb6a7 to 398c44c Compare July 15, 2026 21:35

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 398c44c. Configure here.


if ((dataset && !project) || (project && !dataset)) {
this.error('If either --dataset or --project is provided, both must be provided', {exit: 1})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config project dataset override blocked

Medium Severity

The pairing check treats a lone --dataset or --project flag as invalid even when the other value is already available from sanity.cli.js via getProjectId or api.dataset. A run like sanity migration run &lt;id&gt; --dataset staging with api.projectId configured fails with “both must be provided” instead of using the configured project.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 398c44c. Configure here.

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