@@ -21,36 +21,62 @@ import {extractCoreAppManifest, resolveTitleUpdate} from '../manifest/extractCor
2121import { type CoreAppManifest } from '../manifest/types.js'
2222import { createUserApplicationForApp } from './createUserApplication.js'
2323import {
24+ checkAppTarget ,
2425 checkAutoUpdates ,
2526 checkBuild ,
27+ checkOutputDir ,
2628 checkPackageVersion ,
29+ createAggregatingChecks ,
2730 createFailFastChecks ,
2831 type DeployChecks ,
32+ type DeployTarget ,
2933 verifyOutputDir ,
3034} from './deployChecks.js'
3135import { deployDebug } from './deployDebug.js'
36+ import { type DryRunReport , isDeployable } from './dryRunReport.js'
3237import { findUserApplicationForApp } from './findUserApplication.js'
33- import { type DeployAppOptions } from './types.js'
38+ import { type DeployFileSummary } from './listDeploymentFiles.js'
39+ import { type DeployAppOptions , type DeployFlags } from './types.js'
3440
3541type Workbench = ReturnType < typeof getWorkbench >
3642
3743/**
38- * Builds and deploys a Sanity application.
44+ * Builds and deploys a Sanity application. With --dry-run, runs the same
45+ * sequence read-only and returns a report instead of shipping.
3946 *
4047 * @internal
4148 */
42- export async function deployApp ( options : DeployAppOptions ) : Promise < void > {
43- const { cliConfig, output} = options
49+ export async function deployApp ( options : DeployAppOptions ) : Promise < DryRunReport | undefined > {
50+ const { cliConfig, flags , output} = options
4451 const workbench = getWorkbench ( cliConfig )
4552
53+ if ( flags [ 'dry-run' ] ) {
54+ const checks = createAggregatingChecks ( )
55+ if ( workbench ) {
56+ try {
57+ workbench . assertDeployable ( )
58+ } catch ( err ) {
59+ checks . add ( { message : getErrorMessage ( err ) , name : 'app-deployable' , status : 'fail' } )
60+ }
61+ }
62+ const { files, target} = await createAppDeployment ( options , checks , workbench )
63+ return {
64+ checks : checks . all ( ) ,
65+ deployable : isDeployable ( checks . all ( ) ) ,
66+ dryRun : true ,
67+ files,
68+ target,
69+ }
70+ }
71+
4672 // A federated app with no entry, view or service would ship a remote with
4773 // nothing to load — fail before any prompts or API calls.
4874 if ( workbench ) {
4975 try {
5076 workbench . assertDeployable ( )
5177 } catch ( err ) {
5278 output . error ( getErrorMessage ( err ) , { exit : exitCodes . USAGE_ERROR } )
53- return
79+ return undefined
5480 }
5581 }
5682
@@ -61,28 +87,32 @@ export async function deployApp(options: DeployAppOptions): Promise<void> {
6187 // Don't throw a generic error when the user cancels a prompt
6288 if ( error . name === 'ExitPromptError' ) {
6389 output . error ( 'Deployment cancelled by user' , { exit : 1 } )
64- return
90+ return undefined
6591 }
6692 if ( error instanceof CLIError ) {
6793 const { message, ...errorOptions } = error
6894 output . error ( message , { ...errorOptions , exit : 1 } )
69- return
95+ return undefined
7096 }
7197 deployDebug ( 'Error deploying application' , error )
7298 output . error ( `Error deploying application: ${ error } ` , { exit : 1 } )
7399 }
100+ return undefined
74101}
75102
76103interface AppDeployment {
77104 application : UserApplication | null
105+ files : DeployFileSummary | null
78106 isAutoUpdating : boolean
79107 manifest : CoreAppManifest | undefined
108+ target : DeployTarget | null
80109 version : string | null
81110}
82111
83112/**
84113 * Validates the deploy, syncs the title from the manifest, and ships the build.
85- * Steps report through `checks`; a real deploy fails fast on the first problem.
114+ * Steps report through `checks` (fail fast for real deploys, aggregated for dry
115+ * runs); side-effecting steps and the upload branch on `--dry-run`.
86116 */
87117async function createAppDeployment (
88118 options : DeployAppOptions ,
@@ -92,6 +122,7 @@ async function createAppDeployment(
92122 const { cliConfig, flags, output, projectRoot, sourceDir} = options
93123 const workDir = projectRoot . directory
94124 const organizationId = cliConfig . app ?. organizationId
125+ const dryRun = ! ! flags [ 'dry-run' ]
95126
96127 const isAutoUpdating = checkAutoUpdates ( checks , { cliConfig, flags} )
97128
@@ -108,8 +139,14 @@ async function createAppDeployment(
108139 )
109140
110141 let application : UserApplication | null = null
142+ let target : DeployTarget | null = null
111143 if ( flags . external ) {
112144 checks . add ( { message : EXTERNAL_APP_NOT_SUPPORTED , name : 'target' , status : 'fail' } )
145+ } else if ( dryRun ) {
146+ ; ( { existingApp : application , target} = await checkAppTarget ( checks , {
147+ appId : getAppId ( cliConfig ) ,
148+ organizationId,
149+ } ) )
113150 } else {
114151 application = await resolveAppApplication ( options )
115152 }
@@ -120,7 +157,8 @@ async function createAppDeployment(
120157 autoUpdatesEnabled : isAutoUpdating ,
121158 calledFromDeploy : true ,
122159 cliConfig,
123- flags,
160+ // Dry runs never prompt
161+ flags : dryRun ? ( { ...flags , yes : true } as DeployFlags ) : flags ,
124162 outDir : sourceDir ,
125163 output,
126164 workDir,
@@ -131,10 +169,16 @@ async function createAppDeployment(
131169 successMessage : 'App built' ,
132170 } )
133171
134- await verifyOutputDir ( { output, sourceDir, workbench} )
172+ let files : DeployFileSummary | null = null
173+ if ( dryRun ) {
174+ files = await checkOutputDir ( checks , { sourceDir, workbench} )
175+ } else {
176+ await verifyOutputDir ( { output, sourceDir, workbench} )
177+ }
135178
136179 // Manifests aren't strictly essential, so a failure warns and continues
137180 let manifest : CoreAppManifest | undefined
181+ let manifestFailed = false
138182 try {
139183 manifest = await extractCoreAppManifest ( { workDir} )
140184 } catch ( err ) {
@@ -144,11 +188,26 @@ async function createAppDeployment(
144188 name : 'app-manifest' ,
145189 status : 'warn' ,
146190 } )
191+ manifestFailed = true
147192 }
148193
149194 // Sync the application title from the manifest when it has changed
150195 const titleUpdate = application ? resolveTitleUpdate ( manifest , application ) : null
151- if ( application && titleUpdate ) {
196+ if ( dryRun ) {
197+ if ( ! manifestFailed ) {
198+ checks . add ( {
199+ message : titleUpdate
200+ ? titleUpdate . from
201+ ? `Would update application title from "${ titleUpdate . from } " to "${ titleUpdate . to } "`
202+ : `Would set application title to "${ titleUpdate . to } "`
203+ : manifest
204+ ? 'App manifest extracted'
205+ : 'No app manifest (no icon or title in app configuration)' ,
206+ name : 'app-manifest' ,
207+ status : 'pass' ,
208+ } )
209+ }
210+ } else if ( application && titleUpdate ) {
152211 deployDebug ( 'Updating application title from manifest' , titleUpdate )
153212 output . log (
154213 titleUpdate . from
@@ -171,7 +230,9 @@ async function createAppDeployment(
171230 }
172231 }
173232
174- if ( ! application || ! version ) return { application, isAutoUpdating, manifest, version}
233+ if ( dryRun || ! application || ! version ) {
234+ return { application, files, isAutoUpdating, manifest, target, version}
235+ }
175236
176237 const parentDir = dirname ( sourceDir )
177238 const base = basename ( sourceDir )
@@ -192,7 +253,7 @@ async function createAppDeployment(
192253 }
193254 } catch ( err ) {
194255 output . error ( `Invalid view declaration: ${ getErrorMessage ( err ) } ` , { exit : 1 } )
195- return { application, isAutoUpdating, manifest, version}
256+ return { application, files , isAutoUpdating, manifest, target , version}
196257 }
197258 }
198259
@@ -234,7 +295,7 @@ ${styleText(
234295) } `)
235296 }
236297
237- return { application, isAutoUpdating, manifest, version}
298+ return { application, files , isAutoUpdating, manifest, target , version}
238299}
239300
240301/** Resolves the app's target application, creating one when none exists. */
0 commit comments