@@ -23,75 +23,115 @@ import {createUserApplicationForApp} from './createUserApplicationForApp.js'
2323import {
2424 checkAutoUpdates ,
2525 checkBuild ,
26+ checkOutputDir ,
2627 checkPackageVersion ,
28+ createAggregatingChecks ,
2729 createFailFastChecks ,
2830 type DeployChecks ,
2931 verifyOutputDir ,
3032} from './deployChecks.js'
3133import { deployDebug } from './deployDebug.js'
34+ import { checkAppTarget , type DeployTarget } from './deployTargetChecks.js'
35+ import { type DryRunReport , isDeployable } from './dryRunReport.js'
3236import { findUserApplicationForApp } from './findUserApplicationForApp.js'
33- import { type DeployAppOptions } from './types.js'
37+ import { type DeployFileSummary } from './listDeploymentFiles.js'
38+ import { type DeployAppOptions , type DeployFlags } from './types.js'
3439
3540type Workbench = ReturnType < typeof getWorkbench >
3641
3742/**
38- * Builds and deploys a Sanity application: a plan phase validates and produces
39- * the deployable artifacts, then an execute phase ships them.
43+ * Builds and deploys a Sanity application. A plan phase validates and produces
44+ * the deployable artifacts; with `--dry-run` it runs read-only and returns a
45+ * report instead of shipping.
4046 *
4147 * @internal
4248 */
43- export async function deployApp ( options : DeployAppOptions ) : Promise < void > {
44- const { cliConfig, output} = options
49+ export async function deployApp ( options : DeployAppOptions ) : Promise < DryRunReport | undefined > {
50+ const { cliConfig, flags, output} = options
51+ const workbench = getWorkbench ( cliConfig )
52+
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 planAppDeployment ( options , checks , workbench , true )
63+ return {
64+ checks : checks . all ( ) ,
65+ deployable : isDeployable ( checks . all ( ) ) ,
66+ dryRun : true ,
67+ files,
68+ target,
69+ }
70+ }
4571
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.
48- const workbench = getWorkbench ( cliConfig )
4974 if ( workbench ) {
5075 try {
5176 workbench . assertDeployable ( )
5277 } catch ( err ) {
5378 output . error ( getErrorMessage ( err ) , { exit : exitCodes . USAGE_ERROR } )
54- return
79+ return undefined
5580 }
5681 }
5782
5883 try {
5984 const checks = createFailFastChecks ( output )
60- const plan = await planAppDeployment ( options , checks , workbench )
61- if ( plan ) await executeAppDeployment ( plan , options , workbench )
85+ const { application, isAutoUpdating, manifest, version} = await planAppDeployment (
86+ options ,
87+ checks ,
88+ workbench ,
89+ false ,
90+ )
91+ if ( application && version ) {
92+ await executeAppDeployment (
93+ { application, isAutoUpdating, manifest, version} ,
94+ options ,
95+ workbench ,
96+ )
97+ }
6298 } catch ( error ) {
6399 // Don't throw a generic error when the user cancels a prompt
64100 if ( error . name === 'ExitPromptError' ) {
65101 output . error ( 'Deployment cancelled by user' , { exit : 1 } )
66- return
102+ return undefined
67103 }
68104 if ( error instanceof CLIError ) {
69105 const { message, ...errorOptions } = error
70106 output . error ( message , { ...errorOptions , exit : 1 } )
71- return
107+ return undefined
72108 }
73109 deployDebug ( 'Error deploying application' , error )
74110 output . error ( `Error deploying application: ${ error } ` , { exit : 1 } )
75111 }
112+ return undefined
76113}
77114
78- interface AppDeploymentPlan {
79- application : UserApplication
115+ interface AppPlanResult {
116+ application : UserApplication | null
117+ files : DeployFileSummary | null
80118 isAutoUpdating : boolean
81119 manifest : CoreAppManifest | undefined
82- version : string
120+ target : DeployTarget | null
121+ version : string | null
83122}
84123
85124/**
86- * Validates the app deploy and produces everything a deploy needs: the target
87- * application (title synced from the manifest) and the resolved version. Steps
88- * report through `checks`; a real deploy fails fast .
125+ * The app deploy sequence, shared by real deploys and dry runs. Validations
126+ * report through `checks` (fail fast for real deploys, aggregated for dry runs);
127+ * side-effecting steps branch on `dryRun` .
89128 */
90129async function planAppDeployment (
91130 options : DeployAppOptions ,
92131 checks : DeployChecks ,
93132 workbench : Workbench ,
94- ) : Promise < AppDeploymentPlan | null > {
133+ dryRun : boolean ,
134+ ) : Promise < AppPlanResult > {
95135 const { cliConfig, flags, output, projectRoot, sourceDir} = options
96136 const workDir = projectRoot . directory
97137 const organizationId = cliConfig . app ?. organizationId
@@ -110,19 +150,27 @@ async function planAppDeployment(
110150 : { message : NO_ORGANIZATION_ID , name : 'organization-id' , status : 'fail' } ,
111151 )
112152
153+ let application : UserApplication | null = null
154+ let target : DeployTarget | null = null
113155 if ( flags . external ) {
114156 checks . add ( { message : EXTERNAL_APP_NOT_SUPPORTED , name : 'target' , status : 'fail' } )
157+ } else if ( dryRun ) {
158+ ; ( { existingApp : application , target} = await checkAppTarget ( checks , {
159+ appId : getAppId ( cliConfig ) ,
160+ organizationId,
161+ } ) )
162+ } else {
163+ application = await resolveAppApplication ( options )
115164 }
116165
117- let application = await resolveAppApplication ( options )
118-
119166 await checkBuild ( checks , {
120167 build : ( ) =>
121168 buildApp ( {
122169 autoUpdatesEnabled : isAutoUpdating ,
123170 calledFromDeploy : true ,
124171 cliConfig,
125- flags,
172+ // Dry runs never prompt
173+ flags : dryRun ? ( { ...flags , yes : true } as DeployFlags ) : flags ,
126174 outDir : sourceDir ,
127175 output,
128176 workDir,
@@ -133,17 +181,58 @@ async function planAppDeployment(
133181 successMessage : 'App built' ,
134182 } )
135183
136- await verifyOutputDir ( { output, sourceDir, workbench} )
184+ let files : DeployFileSummary | null = null
185+ if ( dryRun ) {
186+ files = await checkOutputDir ( checks , { sourceDir, workbench} )
187+ } else {
188+ await verifyOutputDir ( { output, sourceDir, workbench} )
189+ }
190+
191+ let manifest : CoreAppManifest | undefined
192+ let manifestFailed = false
193+ try {
194+ manifest = await extractCoreAppManifest ( { workDir} )
195+ } catch ( err ) {
196+ deployDebug ( 'Error extracting app manifest' , err )
197+ // Manifests aren't strictly essential, so a real deploy warns and continues
198+ checks . add ( {
199+ message : `Error extracting app manifest: ${ getErrorMessage ( err ) } ` ,
200+ name : 'app-manifest' ,
201+ status : 'warn' ,
202+ } )
203+ manifestFailed = true
204+ }
205+
206+ const titleUpdate = application ? resolveTitleUpdate ( manifest , application ) : null
137207
138- const manifest = await extractAppManifest ( checks , workDir )
139- application = await syncAppTitle ( { application, manifest, output} )
208+ if ( dryRun ) {
209+ if ( ! manifestFailed ) {
210+ checks . add ( {
211+ message : titleUpdate
212+ ? titleUpdate . from
213+ ? `Would update application title from "${ titleUpdate . from } " to "${ titleUpdate . to } "`
214+ : `Would set application title to "${ titleUpdate . to } "`
215+ : manifest
216+ ? 'App manifest extracted'
217+ : 'No app manifest (no icon or title in app configuration)' ,
218+ name : 'app-manifest' ,
219+ status : 'pass' ,
220+ } )
221+ }
222+ } else if ( application && titleUpdate ) {
223+ application = await syncAppTitle ( application , titleUpdate , output )
224+ }
140225
141- if ( ! application || ! version ) return null
142- return { application, isAutoUpdating, manifest, version}
226+ return { application, files, isAutoUpdating, manifest, target, version}
143227}
144228
145229async function executeAppDeployment (
146- plan : AppDeploymentPlan ,
230+ plan : {
231+ application : UserApplication
232+ isAutoUpdating : boolean
233+ manifest : CoreAppManifest | undefined
234+ version : string
235+ } ,
147236 options : DeployAppOptions ,
148237 workbench : Workbench ,
149238) : Promise < void > {
@@ -229,38 +318,12 @@ async function resolveAppApplication(options: DeployAppOptions): Promise<UserApp
229318 return application
230319}
231320
232- /** Extracts the app manifest; a failure warns and continues (it isn't essential). */
233- async function extractAppManifest (
234- checks : DeployChecks ,
235- workDir : string ,
236- ) : Promise < CoreAppManifest | undefined > {
237- try {
238- return await extractCoreAppManifest ( { workDir} )
239- } catch ( err ) {
240- deployDebug ( 'Error extracting app manifest' , err )
241- checks . add ( {
242- message : `Error extracting app manifest: ${ getErrorMessage ( err ) } ` ,
243- name : 'app-manifest' ,
244- status : 'warn' ,
245- } )
246- return undefined
247- }
248- }
249-
250- /** Syncs the application title from the manifest when it has changed. */
251- async function syncAppTitle ( {
252- application,
253- manifest,
254- output,
255- } : {
256- application : UserApplication | null
257- manifest : CoreAppManifest | undefined
258- output : DeployAppOptions [ 'output' ]
259- } ) : Promise < UserApplication | null > {
260- if ( ! application ) return application
261- const titleUpdate = resolveTitleUpdate ( manifest , application )
262- if ( ! titleUpdate ) return application
263-
321+ /** Syncs the application title from the manifest to the user application. */
322+ async function syncAppTitle (
323+ application : UserApplication ,
324+ titleUpdate : { from : string | null ; to : string } ,
325+ output : DeployAppOptions [ 'output' ] ,
326+ ) : Promise < UserApplication > {
264327 deployDebug ( 'Updating application title from manifest' , titleUpdate )
265328 output . log (
266329 titleUpdate . from
0 commit comments