feat(deploy): add a --dry-run flag to the deploy command#1415
Conversation
📦 Bundle Stats —
|
| 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.
Coverage Delta
Comparing 11 changed files against main @ Overall Coverage
|
f51fbb0 to
4452111
Compare
f893b04 to
ea7158b
Compare
4452111 to
74ad092
Compare
ea7158b to
61fbd15
Compare
47c3197 to
d211f5f
Compare
61fbd15 to
892459f
Compare
5ac786b to
b0a6426
Compare
892459f to
bf39e53
Compare
b0a6426 to
0222427
Compare
snocorp
left a comment
There was a problem hiding this comment.
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.
bf39e53 to
e7375aa
Compare
c016b6b to
6eb86f5
Compare
e7375aa to
ab3c200
Compare
`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.
…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
6eb86f5 to
935c7cf
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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', |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 935c7cf. Configure here.


Description
Note
Third in the deploy stack — builds on #1407. This is the payoff the earlier PRs laid the foundation for.
sanity deploy --dry-runreports 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:
Each check carries its fix in a
solutionfield, 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:
The design property that matters: dry-run drives the same
runDeploysequence (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
deploymentPlanmodule 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
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
listDeploymentFilesandrenderDeploymentPlan(including the problems-with-fixes and warnings sections); integration tests rundeploy --dry-runfor 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 includingindex.html, surfaces the blocking problem with its fix, and reports the deprecated--auto-updatesflag as a warning — skipped in registry/smoke mode since--dry-runisn'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.runDeploybranches on the flag: a collecting reporter runs the sharedrunsequence,deploymentPlanlistsdistfiles 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-onlycheckStudioTarget/checkAppTarget(andcheckAppIdfor apps) instead offindUserApplication*.Deploy checks now carry optional
solutiontext appended in fail-fast errors and in the dry-run report.resolveAppIdIssuecentralizes deprecatedapp.idvsdeployment.appIdlogic for real and dry-run paths. The deploy command treats--dry-runlike unattended mode (no overwrite confirm;yes: truefor build/target steps).Reviewed by Cursor Bugbot for commit 935c7cf. Bugbot is set up for automated code reviews on this repo. Configure here.