Skip to content

feat(deploy): add --json flag#1416

Open
gu-stav wants to merge 21 commits into
sdk-1784-add-dry-run-support-to-deploy-commandfrom
feat/deploy-json
Open

feat(deploy): add --json flag#1416
gu-stav wants to merge 21 commits into
sdk-1784-add-dry-run-support-to-deploy-commandfrom
feat/deploy-json

Conversation

@gu-stav

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

Copy link
Copy Markdown
Member

Description

Note

Stacked on #1415 — adds --json on top of --dry-run.

sanity deploy --json prints machine-readable JSON instead of the human report, independent of --dry-run:

  • deploy --dry-run --json → the plan (applicationType, applicationVersion, isDeployable, errors, warnings, files, totalBytes)
  • deploy --json → the real deploy result (deployed, applicationId, applicationType, applicationVersion, studio location)

Both the human report and the JSON derive from the same checks, so they can't drift. Blocking problems come back as errors — a map of problem → fix — with warnings as a list of messages and a top-level isDeployable flag, so an agent gets the same "why it can't deploy" the human report shows.

In JSON mode the run's stdout is routed to stderr so a sub-step that prints progress straight to stdout (the build, the schema upload) can't corrupt the payload — only the JSON reaches stdout.

Follows the existing --json convention (doctor, tokens list, schemas list): an explicit flag, no auto-detection.

What to review

  • deployRunner.ts — the JSON branch and the redirectStdoutToStderr guard that keeps stdout clean.
  • deploymentPlanToJson in deploymentPlan.ts — shares the plan with the human renderer.

Testing

Unit covers deploymentPlanToJson; integration runs deploy --dry-run --json and a real deploy --json, parsing the payload and asserting no human text leaks to stdout.


Note

Low Risk
CLI output and non-interactive behavior only; deploy mutations and API calls are unchanged aside from returning structured metadata after success.

Overview
Adds sanity deploy --json (-j) so CI and tooling can consume deploy output as JSON instead of the human report.

With --dry-run --json, stdout is a plan derived from the same checks as the text dry-run: applicationType, applicationVersion, isDeployable, errors (message → fix), warnings, files, and totalBytes. A real deploy --json prints { deployed: true, applicationId, applicationType, applicationVersion, location } (studio URL or null for core apps). Studio and app deploy paths now return a shared DeployResult from their run functions.

runDeploy branches on the flag: human mode is unchanged aside from shared collectPlan / exitIfBlocked. In JSON mode, process.stdout is temporarily redirected to stderr so build/schema progress cannot mix with the payload; only the final JSON is logged to stdout. The deploy command also skips overwrite prompts and the “Building to …” line when --json is set.

deploymentPlanToJson and isDeployable centralize the machine-readable dry-run shape; plans include the installed framework version via packageName on DeploySpec.

Reviewed by Cursor Bugbot for commit 939cb15. 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 sdk-1784-add-dry-run-support-to-deploy-command (c016b6be)

@sanity/cli

Metric Value vs sdk-1784-add-dry-run-support-to-deploy-command (c016b6b)
Internal (raw) 2.7 KB -
Internal (gzip) 1.0 KB -
Bundled (raw) 11.16 MB -
Bundled (gzip) 2.10 MB -
Import time 817ms -16ms, -1.9%

bin:sanity

Metric Value vs sdk-1784-add-dry-run-support-to-deploy-command (c016b6b)
Internal (raw) 782 B -
Internal (gzip) 423 B -
Bundled (raw) 9.87 MB -
Bundled (gzip) 1.78 MB -
Import time 2.13s -45ms, -2.1%

🗺️ 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 sdk-1784-add-dry-run-support-to-deploy-command (c016b6be)

Metric Value vs sdk-1784-add-dry-run-support-to-deploy-command (c016b6b)
Internal (raw) 106.7 KB -
Internal (gzip) 26.7 KB -
Bundled (raw) 21.72 MB -
Bundled (gzip) 3.46 MB -
Import time 715ms -22ms, -3.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 sdk-1784-add-dry-run-support-to-deploy-command (c016b6be)

Metric Value vs sdk-1784-add-dry-run-support-to-deploy-command (c016b6b)
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.

@gu-stav gu-stav changed the title feat(deploy): add a --json flag feat(deploy): add --json flag Jul 1, 2026
@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/deploymentPlan.ts 98.1% (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 10 changed files against main @ a99e40e39566b75e0163d0a20f33aa28b4e2d5dc

Overall Coverage

Metric Coverage
Statements 74.3% (+ 0.0%)
Branches 64.3% (+ 0.1%)
Functions 69.0% (+ 0.3%)
Lines 74.9% (+ 0.0%)

gu-stav added 8 commits July 1, 2026 15:14
Builds on the core app refactor: deployStudio now runs through a single
createStudioDeployment using the shared checks seam, with studio target
resolution (resolveStudioDeployTarget) and the studio target check added to the
shared modules.

With both deploy paths refactored, the two interactive adapters merge into one
findUserApplication module and the two creators into createUserApplication, and
cannotPromptForStudioHost gets one home. deployStudio carries the tightened
seams through: it asks getWorkbench for an isWorkbenchApp boolean, and the
studio target models isExternal as its own field.

No behavior change — the integration deploy tests pass unchanged.
…cation

The switch's own output.error({exit}) calls were thrown inside the broad
try/catch, which re-wrapped them via output.error(error) with oclif's default
exit 2 — masking the intended exit codes. Scope the try to just the deploy
target fetch (mirroring findUserApplicationForStudio) so intentional exits
surface as thrown.
Both flows now drive a single runDeploy skeleton (fail-fast reporter + one
normalized error handler) and read as the same top-to-bottom sequence of
reported steps, with the ship/title/schema work pulled into named helpers.
Removes the drift between the two deploy paths and their error handling.
Populates `solution` for the remaining blockers: missing organization/project id,
external-host misuse, an unresolved studio/app target, and a non-deployable
federated app. The dry-run report lists each under its problem.
`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.
@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 force-pushed the feat/deploy-json branch from 054d3e7 to 570cf0c Compare July 1, 2026 13:26
@gu-stav gu-stav force-pushed the feat/deploy-json branch from 570cf0c to 8bb2b82 Compare July 1, 2026 13:51
… 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.
@gu-stav gu-stav force-pushed the feat/deploy-json branch from 8bb2b82 to c427c3f Compare July 1, 2026 14:20
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.
@gu-stav gu-stav force-pushed the feat/deploy-json branch from c427c3f to 37199d7 Compare July 1, 2026 14:37
@gu-stav gu-stav marked this pull request as ready for review July 1, 2026 15:32
@gu-stav gu-stav requested a review from a team as a code owner July 1, 2026 15:32
Comment thread packages/@sanity/cli/src/commands/deploy.ts
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).
@gu-stav gu-stav force-pushed the feat/deploy-json branch from 3df8d2e to d935e04 Compare July 1, 2026 15:44
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.
@gu-stav gu-stav force-pushed the feat/deploy-json branch from d935e04 to af3287c Compare July 1, 2026 15:53
gu-stav added 3 commits July 1, 2026 18:02
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
Prints the deploy result — or the --dry-run plan — as machine-readable JSON,
built from the same data object the human report renders, so the two can't
drift. In JSON mode the run's stdout is routed to stderr so sub-steps (build,
schema upload) can't corrupt the payload.
gu-stav added 4 commits July 1, 2026 18:08
Add the resolved sanity/@sanity/sdk-react version as `applicationVersion`, and
name the deploy-kind field `applicationType`, so the JSON keys read clearly on
their own. Internal plan/spec keep the shorter `type`/`version` names.
…s list

Replace the flat `checks` array with `errors` (a map of blocking problem → fix)
and `warnings` (messages), dropping informational pass/skip lines. A consumer
acts on the fixes directly instead of filtering checks by status.
The "Building to …" line and the overwrite prompt from the custom source-dir
path ran before runDeploy installs its stdout redirect, so the JSON payload was
no longer the only thing on stdout. Skip both in --json mode.
@gu-stav gu-stav force-pushed the feat/deploy-json branch from af3287c to 939cb15 Compare July 1, 2026 16:09

@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 939cb15. Configure here.


this.output.log(`Building to ${relativeOutput}\n`)
// Keep stdout clean for --json; this human line would otherwise precede the payload
if (!flags.json) this.output.log(`Building to ${relativeOutput}\n`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

JSON deploy skips unattended flag

Medium Severity

The deploy command treats --json as non-interactive for the output-directory overwrite prompt and the “Building to …” log, but deployFlags only forces yes: true for --dry-run. A real deploy --json run can still hit build prerelease prompts and studio host selection because buildStudio and findUserApplicationForStudio only see flags.yes when the user also passes --yes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 939cb15. Configure here.

@runeb runeb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Adding more json output from cli commands is excellent, and where we want to go. But the bot review points at something I think we need to consider, non-interactive mode. Does it make sense to support interactive prompts when the output requested is json? We could treat json output as non interactive and error early on required params the same way we do -y.

@gu-stav gu-stav force-pushed the sdk-1784-add-dry-run-support-to-deploy-command branch 2 times, most recently from 6eb86f5 to 935c7cf Compare July 2, 2026 08:12
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