Skip to content

feat(deploy): add a --dry-run flag to the deploy command#1415

Open
gu-stav wants to merge 11 commits into
refactor/deploy-studiofrom
sdk-1784-add-dry-run-support-to-deploy-command
Open

feat(deploy): add a --dry-run flag to the deploy command#1415
gu-stav wants to merge 11 commits into
refactor/deploy-studiofrom
sdk-1784-add-dry-run-support-to-deploy-command

Conversation

@gu-stav

@gu-stav gu-stav commented Jul 1, 2026

Copy link
Copy Markdown
Member

Description

Note

Third in the deploy stack — builds on #1407. This is the payoff the earlier PRs laid the foundation for.

sanity deploy --dry-run reports whether the studio or app can be deployed and lists the files a deploy would upload (with sizes) — without uploading, creating, or prompting for anything. When something blocks the deploy it lists every problem with a fix, and surfaces warnings.

A blocked studio:

Dry run — no changes made.

  ✓ Using sanity 3.99.0
  ✓ Studio built

This studio can't be deployed.

Problems to fix:
  ✗ No studio hostname configured: Set `studioHost` in sanity.cli.ts, or pass a hostname with --url

Warnings:
  ⚠ The autoUpdates config has moved to deployment.autoUpdates.

Files to deploy (0.50 MB):
  dist/index.html (0.50 MB)

Each check carries its fix in a solution field, so the same fix renders in both modes — appended after the problem in the dry-run report and in a real deploy's error (instead of baking the fix into the message).

A deployable one just lists the files:

Dry run — no changes made.

  ✓ Using sanity 3.99.0
  ✓ Project: my-project
  ✓ Deploys to existing studio https://my-studio.sanity.studio
  ✓ Studio built

This studio can be deployed.

Files to deploy (1.53 MB):
  dist/assets/app.js (1.53 MB)
  dist/index.html (0.00 MB)

The design property that matters: dry-run drives the same runDeploy sequence (runAppDeployment / runStudioDeployment, harmonized in #1407) as a real deploy, so the two can't drift. The only difference is the mode — a collecting reporter instead of the fail-fast one, and every network mutation gated off: the target is resolved read-only (checkAppTarget / checkStudioTarget, the same verdict resolver the real path uses), and the title update, schema upload, and deployment upload sit below a single dry-run stop. The build still runs locally, so the file list is accurate.

A small deploymentPlan module owns the plan shape and the report — a deep seam that's easy to extend later (schema diff, more per-file detail, …).

Deliberately not covered yet: schema validation doesn't run in a dry run, since it's coupled to the upload step. Straightforward follow-up on the same seam.

What to review

  • The dry-run stop + read-only target resolution in runAppDeployment / runStudioDeployment — confirm a dry run can't reach any POST or prompt.
  • deploymentPlan.ts (plan shape + render) and that the command stays a thin caller.

Testing

Unit tests cover listDeploymentFiles and renderDeploymentPlan (including the problems-with-fixes and warnings sections); integration tests run deploy --dry-run for a studio and an app and assert the report renders with no deployment POST (none is mocked, so a ship would fail). E2E tests (deploy.test.ts) drive the real CLI against both a studio and a core app, asserting the plan lists real built files including index.html, surfaces the blocking problem with its fix, and reports the deprecated --auto-updates flag as a warning — skipped in registry/smoke mode since --dry-run isn't on the published CLI yet.


Note

Medium Risk
Changes the shared deploy runner and studio/app resolution paths; mutations are gated and covered by tests, but incorrect gating could still allow side effects or drift from real deploy behavior.

Overview
Adds sanity deploy --dry-run, which runs the same studio/app deploy validation and local build as a real deploy, then prints whether deployment is possible, blocking problems (with fixes), warnings, and files that would be uploaded (paths and sizes)—without creating apps, uploading, or prompting.

runDeploy branches on the flag: a collecting reporter runs the shared run sequence, deploymentPlan lists dist files and renders the report, and the process exits non-zero when any check fails (matching real deploy exit codes for CI gating). Studio and app flows return before schema upload, title sync, and deployment POSTs; dry runs resolve targets via read-only checkStudioTarget / checkAppTarget (and checkAppId for apps) instead of findUserApplication*.

Deploy checks now carry optional solution text appended in fail-fast errors and in the dry-run report. resolveAppIdIssue centralizes deprecated app.id vs deployment.appId logic for real and dry-run paths. The deploy command treats --dry-run like unattended mode (no overwrite confirm; yes: true for build/target steps).

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

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @sanity/cli

Compared against refactor/deploy-studio (ab3c2003)

@sanity/cli

Metric Value vs refactor/deploy-studio (ab3c200)
Internal (raw) 2.7 KB -
Internal (gzip) 1.0 KB -
Bundled (raw) 11.16 MB -
Bundled (gzip) 2.10 MB -
Import time 885ms +3ms, +0.3%

bin:sanity

Metric Value vs refactor/deploy-studio (ab3c200)
Internal (raw) 782 B -
Internal (gzip) 423 B -
Bundled (raw) 9.87 MB -
Bundled (gzip) 1.78 MB -
Import time 2.27s -8ms, -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-core

Compared against refactor/deploy-studio (ab3c2003)

Metric Value vs refactor/deploy-studio (ab3c200)
Internal (raw) 106.7 KB -
Internal (gzip) 26.7 KB -
Bundled (raw) 21.72 MB -
Bundled (gzip) 3.46 MB -
Import time 786ms +5ms, +0.6%

🗺️ 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 refactor/deploy-studio (ab3c2003)

Metric Value vs refactor/deploy-studio (ab3c200)
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.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Coverage Delta

File Statements
packages/@sanity/cli/src/actions/build/shouldAutoUpdate.ts 100.0% (±0%)
packages/@sanity/cli/src/actions/deploy/deployChecks.ts 73.3% (new)
packages/@sanity/cli/src/actions/deploy/deployRunner.ts 47.8% (new)
packages/@sanity/cli/src/actions/deploy/deploymentPlan.ts 97.7% (new)
packages/@sanity/cli/src/actions/deploy/resolveDeployTarget.ts 57.1% (new)
packages/@sanity/cli/src/actions/manifest/extractCoreAppManifest.ts 94.4% (+ 0.5%)
packages/@sanity/cli/src/util/appId.ts 76.2% (+ 7.8%)
packages/@sanity/cli/src/util/errorMessages.ts 71.4% (- 28.6%)
packages/@sanity/workbench-cli/src/actions/deploy/checkBuiltOutput.ts 100.0% (new)
packages/@sanity/workbench-cli/src/actions/deploy/getWorkbench.ts 87.5% (- 12.5%)
packages/@sanity/workbench-cli/src/actions/deploy/viewDeployment.ts 66.7% (new)

Comparing 11 changed files against main @ e56340aea13282b039ed01625801c5268fee9964

Overall Coverage

Metric Coverage
Statements 74.3% (- 0.2%)
Branches 64.2% (- 0.1%)
Functions 68.9% (+ 0.0%)
Lines 74.8% (- 0.2%)

@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch 2 times, most recently from f51fbb0 to 4452111 Compare July 1, 2026 10:41
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from f893b04 to ea7158b Compare July 1, 2026 11:04
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch from 4452111 to 74ad092 Compare July 1, 2026 11:09
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from ea7158b to 61fbd15 Compare July 1, 2026 11:21
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch 3 times, most recently from 47c3197 to d211f5f Compare July 1, 2026 11:38
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from 61fbd15 to 892459f Compare July 1, 2026 12:01
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch 3 times, most recently from 5ac786b to b0a6426 Compare July 1, 2026 13:03
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from 892459f to bf39e53 Compare July 1, 2026 13:18
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch from b0a6426 to 0222427 Compare July 1, 2026 13:22
@gu-stav gu-stav marked this pull request as ready for review July 1, 2026 15:29
@gu-stav gu-stav requested a review from a team as a code owner July 1, 2026 15:29
@gu-stav gu-stav requested review from joshuaellis and snocorp July 1, 2026 15:29
Comment thread packages/@sanity/cli/src/actions/deploy/deployApp.ts
Comment thread packages/@sanity/cli/src/commands/deploy.ts
Comment thread packages/@sanity/cli/src/commands/deploy.ts
Comment thread packages/@sanity/cli/src/actions/deploy/deployRunner.ts Outdated

@snocorp snocorp 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.

Only one potential issue with the logic around rendering the deployment plan. Also requesting a few changes to the testing approach to avoid heavy integration tests as much as possible. I think it's worth the extra effort and will give us better confidence in our changes in the long run.

Comment thread packages/@sanity/cli/test/integration/commands/deploy.app.test.ts
Comment thread packages/@sanity/cli/src/actions/deploy/__tests__/deploymentPlan.test.ts Outdated
Comment thread packages/@sanity/cli/src/actions/deploy/deploymentPlan.ts Outdated
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from bf39e53 to e7375aa Compare July 2, 2026 07:13
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch from c016b6b to 6eb86f5 Compare July 2, 2026 07:28
Comment thread packages/@sanity/cli/src/actions/deploy/deployChecks.ts
@gu-stav gu-stav force-pushed the refactor/deploy-studio branch from e7375aa to ab3c200 Compare July 2, 2026 08:07
`sanity deploy --dry-run` reports whether the studio or app can be deployed and
lists the files a deploy would upload — without uploading or creating anything.

To keep it from drifting, dry-run runs the same createAppDeployment /
createStudioDeployment sequence as a real deploy, with a collecting reporter and
every network mutation gated off: the target is resolved read-only (checkAppTarget
/ checkStudioTarget), and the title update, schema upload and deployment upload
are skipped. The build still runs locally so the file list is accurate. A new
deploymentPlan module owns the plan shape and the human-readable report.
gu-stav added 10 commits July 2, 2026 10:10
…y-run report

When a dry run can't deploy, the report now groups the failing checks under
"Problems to fix" — each with its solution — and lists warnings in their own
section, instead of a flat mixed list.
… report

The fail-fast reporter now renders a failing (or warning) check's solution
beneath its message, so a real deploy is as actionable as the dry-run report.
With fixes shown in both modes, the dry-run studio-host problem drops the inline
--url hint from its message and carries it in solution instead.
Render the fix on the same line as the problem (`problem: fix`) in both the
dry-run report and a real deploy's error, rather than on an indented line.
The dry-run app path resolves the target via checkAppTarget and skipped
checkForDeprecatedAppId (which only runs in the real findUserApplication path),
so a dry run reported an app as deployable when both app.id and deployment.appId
are set — a real deploy exits on that. Share the decision via resolveAppIdIssue
and report it as a dry-run check (conflict fails, deprecated-only warns).
A dry run is a non-interactive preview, but a custom non-empty output directory
still triggered the confirm prompt, so `sanity deploy --dry-run <dir>` could
block waiting for input before the plan ran.
Two dry-run gaps a real deploy doesn't have:
- the build (buildApp/buildStudio) could still prompt for prerelease/version
  choices, so run it unattended in a dry run
- a blocking check with a custom exitCode (e.g. USAGE_ERROR) exited 1 instead of
  the code a real fail-fast deploy uses
- omit the "Files to deploy" section when a blocked plan has no files
- mock the fs in the listDeploymentFiles unit test instead of real i/o
- cover the runDeploy dry-run exit code in a unit test and drop the equivalent
  studio integration test
@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch from 6eb86f5 to 935c7cf Compare July 2, 2026 08:12

@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 935c7cf. Configure here.

: 'No studio hostname configured',
solution: isExternal
? 'Set `studioHost` in sanity.cli.ts, or pass the full URL with --url'
: 'Set `studioHost` in sanity.cli.ts, or pass a hostname with --url',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Dry run exit codes drift

Medium Severity

Several dry-run target checks report fail without an exitCode, so runDeploy exits with 1 via failed.exitCode ?? 1. The same situations in a real deploy use exitCodes.USAGE_ERROR (2), for example missing studio hostname under unattended/--yes and invalid studio host resolution.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 935c7cf. 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