Skip to content

feat(deploy): add --dry-run flag for instant deployability feedback#1255

Draft
gu-stav wants to merge 7 commits into
refactor/deploy-target-resolutionfrom
feat/deploy-dry-run
Draft

feat(deploy): add --dry-run flag for instant deployability feedback#1255
gu-stav wants to merge 7 commits into
refactor/deploy-target-resolutionfrom
feat/deploy-dry-run

Conversation

@gu-stav

@gu-stav gu-stav commented Jun 11, 2026

Copy link
Copy Markdown
Member

Description

sanity deploy --dry-run answers "can this deploy" without deploying — a fast feedback loop for agents and CI.

  • Runs every deploy check read-only: target resolution (the verdicts from refactor(deploy): extract deploy target resolution and auto-update rules #1254), build, schema extraction and validation, output directory. No user application is created, no schemas are uploaded, no tarball is sent.
  • Aggregates failures instead of bailing on the first, so one run reports every problem.
  • Lists the files that would be uploaded, with sizes.
  • Never prompts — would-prompt situations become failed checks with the fix in the message.
  • Exits 2 when not deployable.
  • --json emits the report ({deployable, checks, target, files}) with stable check names, via oclif's enableJsonFlag. The exit code is set without throwing so the full report still reaches stdout on failure.

The schema worker's dryRun mode stops after extraction and validation (skips lexicon and schema-store uploads) but still writes the create-manifest locally, so the file summary matches what a real deploy would put in the tarball.

There is no separate dry-run implementation: each deploy type has one step sequence (createStudioDeployment / createAppDeployment) shared by real deploys and dry runs. Validations report through a checks seam — fail-fast in real deploys, aggregated in dry runs — so a real deploy fails with the same message its dry run reports, and a step added to the sequence is covered by both modes automatically.

Example output

$ sanity deploy --dry-run

✓ Using sanity 4.22.1
✓ Project: abc123de
✓ Deploys to existing studio https://my-studio.sanity.studio
✓ Studio built
✓ Schema valid (2 workspaces, 34 types)
✓ Output directory: /Users/dev/studio/dist
⚠ Add deployment.appId: 'h2f9k3xq8w' to sanity.cli.ts to skip hostname lookups and prompts on deploy.

Would upload 7 files (3.0 MB):
  favicon.ico 15.1 kB
  index.html 1.2 kB
  static/create-manifest.json 4.0 kB
  static/favicon.svg 871 B
  static/sanity-5xj2m1.js 2.8 MB
  static/sanity-5xj2m1.css 38.1 kB
  static/schema-default.json 102 kB

Ready to deploy. Dry run — nothing was deployed.

And when checks fail (exit code 2, every problem in one pass):

$ sanity deploy --dry-run

✓ Using sanity 4.22.1
✗ sanity.cli.ts does not contain a project identifier ("api.projectId"), which is required for the Sanity CLI to communicate with the Sanity API
✗ Cannot prompt for studio hostname in unattended mode. Use --url to specify the studio hostname.
✓ Studio built
✗ Schema validation failed:
  ✖ Error: workspace "default": document type 'post' — field 'author' references unknown type 'person'
✗ Directory "/Users/dev/studio/dist" does not exist

Not deployable — 4 checks failed. Dry run — nothing was deployed.

Stacked on #1254 — will retarget to main once that merges; only the last commit is new here.

What to review

  • The step sequences in deployStudio.ts/deployApp.ts against the previous deploy flow — real-deploy behavior (prompts, spinners, error messages, exit codes) should be unchanged.
  • The two DeployChecks implementations in deployChecks.ts: dry runs aggregate (a throwing step becomes a fail check), real deploys fail fast with the same message.
  • Check names and report shape — they're a machine contract for --json consumers.

Testing

Command-level tests cover both deploy types: all-checks-pass with file listing, would-create hostname, aggregated failures with exit code 2, --json output, app title sync, and the would-prompt failure when no appId is configured.


Note

Low Risk
CLI-only validation path; no production deploy or upload behavior changes when the flag is not used. Dry-run still may run builds and write local manifest files.

Overview
Adds sanity deploy --dry-run so studio and app deploys can be validated without creating applications, uploading schemas, or sending tarballs.

The deploy command branches early into dryRunDeployStudio / dryRunDeployApp, which run the same steps as a real deploy (target resolution via shared resolve*DeployTarget, optional build with yes: true, schema extraction, output dir) through a createDryRunChecks collector that turns thrown steps into fail checks instead of aborting. Results are rendered with renderDryRunReport (pass/fail/skip/warn, file list with sizes) and a deployable flag; failures set exit code 2 without throwing so --json still returns the full {checks, target, files} report.

Schema deployment gains a dryRun path in the worker: validate and optionally write create-manifest locally, skip lexicon/schema-store uploads and telemetry, and return per-workspace summaries for the schema check.

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

Agents and CI need a fast answer to "can this deploy" without uploading
anything. --dry-run runs every deploy check read-only — target resolution,
build, schema validation, output directory — aggregated instead of
fail-fast so one run reports every problem, lists the files that would be
uploaded, and exits non-zero when not deployable. Pairs with --json for
machine-readable output via the report's stable check names.
@gu-stav gu-stav requested a review from a team as a code owner June 11, 2026 09:45
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Stats — @sanity/cli

Compared against refactor/deploy-target-resolution (faad45d5)

@sanity/cli

Metric Value vs refactor/deploy-target-resolution (faad45d)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 11.13 MB -
Bundled (gzip) 2.10 MB -
Import time 849ms +0ms, +0.1%

bin:sanity

Metric Value vs refactor/deploy-target-resolution (faad45d)
Internal (raw) 1023 B -
Internal (gzip) 486 B -
Bundled (raw) 9.87 MB -
Bundled (gzip) 1.77 MB -
Import time 1.91s -3ms, -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 — @sanity/cli-core

Compared against refactor/deploy-target-resolution (faad45d5)

Metric Value vs refactor/deploy-target-resolution (faad45d)
Internal (raw) 96.3 KB -
Internal (gzip) 22.7 KB -
Bundled (raw) 21.70 MB -
Bundled (gzip) 3.45 MB -
Import time 754ms -0ms, -0.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 — create-sanity

Compared against refactor/deploy-target-resolution (faad45d5)

Metric Value vs refactor/deploy-target-resolution (faad45d)
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.

@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 2 potential issues.

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 fa77286. Configure here.


static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'

static override enableJsonFlag = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-dry-run deploy exposes json flag

Medium Severity

enableJsonFlag applies to every sanity deploy invocation, but only the dry-run path returns a DryRunReport. A real deploy finishes with undefined, so --json advertises machine-readable output that is not defined for actual deployments.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fa77286. Configure here.

}

return report
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Custom output dir prompt skipped

Medium Severity

--dry-run returns before the logic that warns when a non-default sourceDir exists and is non-empty and would prompt to proceed. Dry-run can report deployable while a normal deploy would block or cancel on that confirmation.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit fa77286. Configure here.

@gu-stav gu-stav marked this pull request as draft June 11, 2026 09:52
@github-actions

github-actions Bot commented Jun 11, 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/deployApp.ts 81.5% (+ 0.9%)
packages/@sanity/cli/src/actions/deploy/deployChecks.ts 89.7% (new)
packages/@sanity/cli/src/actions/deploy/deployStudio.ts 94.0% (+ 1.5%)
packages/@sanity/cli/src/actions/deploy/deployStudioSchemasAndManifests.ts 100.0% (±0%)
packages/@sanity/cli/src/actions/deploy/deployStudioSchemasAndManifests.worker.ts 0.0% (±0%)
packages/@sanity/cli/src/actions/deploy/dryRunReport.ts 71.0% (new)
packages/@sanity/cli/src/actions/deploy/findUserApplicationForApp.ts 83.3% (- 9.5%)
packages/@sanity/cli/src/actions/deploy/findUserApplicationForStudio.ts 86.2% (- 10.8%)
packages/@sanity/cli/src/actions/deploy/listDeploymentFiles.ts 100.0% (new)
packages/@sanity/cli/src/actions/deploy/resolveDeployTarget.ts 94.6% (new)
packages/@sanity/cli/src/actions/deploy/types.ts 0.0% (±0%)
packages/@sanity/cli/src/actions/manifest/extractAppManifest.ts 93.8% (+ 0.7%)
packages/@sanity/cli/src/commands/deploy.ts 100.0% (±0%)
packages/@sanity/cli/src/util/errorMessages.ts 100.0% (±0%)

Comparing 15 changed files against main @ fbcbace2e07a97da5415d6e557c102f8128f0805

Overall Coverage

Metric Coverage
Statements 79.9% (+ 0.0%)
Branches 71.7% (+ 0.0%)
Functions 79.0% (+ 0.3%)
Lines 80.3% (+ 0.0%)

gu-stav added 5 commits June 11, 2026 12:04
…validations

Dry-run checks invented their own wording for constraints the deploy
already enforces. Messages now come from the same place the real deploy
gets them — errorMessages constants and a shared autoUpdateIssueMessage —
so the dry run states existing constraints instead of introducing new
copy that could drift.
… run

The title comparison and the cannot-prompt-unattended constraint were the
last validation logic stated twice. The unattended error now lives where
the needs-input verdict is handled, and the title diff is computed by one
function both the deploy and the dry run consume.
…d dry runs

The step order and skip conditions were the last thing stated twice. Each
deploy type now has one sequence; validations report through a checks seam
(fail-fast in real deploys, aggregated in dry runs) and side effects branch
on dryRun. A real deploy now fails with the same message its dry run would
report, and a step added to the sequence is automatically covered by both.
listDeployFiles -> listDeploymentFiles, runStudioDeploySteps ->
createStudioDeployment, runAppDeploySteps -> createAppDeployment.
The four deploy test files each restated the user-applications endpoints
and full application response literals — ~1000 lines of setup noise that
buried what each test was actually about. Shared fixtures with defaults
(deployTestHelpers.ts) reduce a scenario to one or two expressive lines,
and encode the endpoint shapes once.
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