Skip to content

Commit 1a67719

Browse files
committed
fix(deploy): respect unattended mode in core-app deploy
`sanity deploy --yes` (or CI) for a core app could still block: the app-target resolver prompted to pick or name an application, and the custom output-dir overwrite confirm ignored --yes. Thread `unattended` into findUserApplicationForApp (error with guidance instead of prompting) and gate the overwrite prompt on --yes. Also addresses review feedback: patch @sanity/cli in the changeset, and adopt the vi.mock(import(...)) style for the mocks that spread the original module.
1 parent 3e24b06 commit 1a67719

5 files changed

Lines changed: 107 additions & 31 deletions

File tree

.changeset/refactor-core-app-deploy.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
---
2+
"@sanity/cli": patch
23
"@sanity/workbench-cli": patch
34
---
45

packages/@sanity/cli/src/actions/deploy/deployApp.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,14 @@ ${styleText(
225225

226226
/** Resolves the app's target application, creating one when none exists. */
227227
async function resolveAppApplication(options: DeployAppOptions): Promise<UserApplication | null> {
228-
const {cliConfig, output} = options
228+
const {cliConfig, flags, output} = options
229229
const organizationId = cliConfig.app?.organizationId ?? ''
230-
let application = await findUserApplicationForApp({cliConfig, organizationId, output})
230+
let application = await findUserApplicationForApp({
231+
cliConfig,
232+
organizationId,
233+
output,
234+
unattended: !!flags.yes,
235+
})
231236
deployDebug('User application found', application)
232237

233238
if (!application) {

packages/@sanity/cli/src/actions/deploy/findUserApplicationForApp.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* that turns the resolveAppDeployTarget verdicts into prompts and exits.
44
*/
55

6-
import {type CliConfig, type Output} from '@sanity/cli-core'
6+
import {type CliConfig, exitCodes, type Output} from '@sanity/cli-core'
77
import {select, Separator, spinner} from '@sanity/cli-core/ux'
88

99
import {type UserApplication} from '../../services/userApplications.js'
@@ -16,12 +16,14 @@ interface FindUserApplicationForAppOptions {
1616
cliConfig: CliConfig
1717
organizationId: string
1818
output: Output
19+
20+
unattended?: boolean
1921
}
2022

2123
export async function findUserApplicationForApp(
2224
options: FindUserApplicationForAppOptions,
2325
): Promise<UserApplication | null> {
24-
const {cliConfig, organizationId, output} = options
26+
const {cliConfig, organizationId, output, unattended = false} = options
2527

2628
const spin = spinner('Checking application info...').start()
2729

@@ -54,11 +56,27 @@ export async function findUserApplicationForApp(
5456
}
5557
case 'needs-input': {
5658
spin.info('No application ID configured')
59+
// Nobody to prompt in unattended mode — a real deploy would hang otherwise
60+
if (unattended) {
61+
output.error(
62+
'No `deployment.appId` configured. Set it in sanity.cli.ts to deploy without prompting.',
63+
{exit: exitCodes.USAGE_ERROR},
64+
)
65+
return null
66+
}
5767
return promptForExistingApp(resolution.existing)
5868
}
5969
// No appId configured and no existing applications — the deploy creates one
6070
case 'would-create': {
6171
spin.info('No application ID configured')
72+
// Creating one needs an interactive title prompt, which unattended can't answer
73+
if (unattended) {
74+
output.error(
75+
'No application to deploy to. Run `sanity deploy` interactively once to create one.',
76+
{exit: exitCodes.USAGE_ERROR},
77+
)
78+
return null
79+
}
6280
return null
6381
}
6482
}

packages/@sanity/cli/src/commands/deploy.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,19 @@ export class DeployCommand extends SanityCommand<typeof DeployCommand> {
103103
relativeOutput = `./${relativeOutput}`
104104
}
105105

106-
const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)
107-
// Prompt to delete the directory if it's not empty
108-
const shouldProceed =
109-
isEmpty ||
110-
(await confirm({
111-
default: false,
112-
message: `"${relativeOutput}" is not empty, do you want to proceed?`,
113-
}))
106+
// Unattended (--yes) proceeds without the overwrite prompt
107+
if (!flags.yes) {
108+
const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)
109+
const shouldProceed =
110+
isEmpty ||
111+
(await confirm({
112+
default: false,
113+
message: `"${relativeOutput}" is not empty, do you want to proceed?`,
114+
}))
114115

115-
if (!shouldProceed) {
116-
this.output.error('Cancelled.', {exit: 1})
116+
if (!shouldProceed) {
117+
this.output.error('Cancelled.', {exit: 1})
118+
}
117119
}
118120

119121
this.output.log(`Building to ${relativeOutput}\n`)

packages/@sanity/cli/test/integration/commands/deploy.app.test.ts

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,25 @@ vi.mock('../../../src/actions/deploy/checkDir.js', () => ({
3232

3333
// `getWorkbench`/`assertDeployable` stay real — pure config the no-interfaces
3434
// test exercises; only the fs-touching `checkBuiltOutput` is stubbed.
35-
vi.mock('@sanity/workbench-cli/deploy', async (importOriginal) => ({
36-
...(await importOriginal<typeof import('@sanity/workbench-cli/deploy')>()),
35+
vi.mock(import('@sanity/workbench-cli/deploy'), async (importOriginal) => ({
36+
...(await importOriginal()),
3737
checkBuiltOutput: mockCheckBuiltOutput,
3838
}))
3939

40-
vi.mock('../../../src/actions/manifest/extractCoreAppManifest.js', async (importOriginal) => ({
41-
...(await importOriginal<
42-
typeof import('../../../src/actions/manifest/extractCoreAppManifest.js')
43-
>()),
44-
extractCoreAppManifest: vi.fn(),
45-
}))
40+
vi.mock(
41+
import('../../../src/actions/manifest/extractCoreAppManifest.js'),
42+
async (importOriginal) => ({
43+
...(await importOriginal()),
44+
extractCoreAppManifest: vi.fn(),
45+
}),
46+
)
4647

47-
vi.mock('@sanity/cli-core/ux', async () => {
48-
const actual = await vi.importActual<typeof import('@sanity/cli-core/ux')>('@sanity/cli-core/ux')
49-
return {
50-
...actual,
51-
confirm: vi.fn(),
52-
input: vi.fn(),
53-
select: vi.fn(),
54-
}
55-
})
48+
vi.mock(import('@sanity/cli-core/ux'), async (importOriginal) => ({
49+
...(await importOriginal()),
50+
confirm: vi.fn(),
51+
input: vi.fn(),
52+
select: vi.fn(),
53+
}))
5654

5755
vi.mock('../../../src/util/dirIsEmptyOrNonExistent.js', () => ({
5856
dirIsEmptyOrNonExistent: vi.fn(() => true),
@@ -502,6 +500,58 @@ describe('#deploy app', () => {
502500
expect(stderr).toContain('No application ID configured')
503501
})
504502

503+
test('--yes errors instead of prompting to pick an existing application', async () => {
504+
const cwd = await testFixture('basic-app')
505+
process.cwd = () => cwd
506+
507+
// Existing apps but no configured appId → needs-input; unattended can't pick one.
508+
mockApi({
509+
apiVersion: USER_APPLICATIONS_API_VERSION,
510+
query: {appType: 'coreApp', organizationId},
511+
uri: `/user-applications`,
512+
}).reply(200, [
513+
{
514+
appHost: 'existing-host',
515+
createdAt: '2024-01-01T00:00:00Z',
516+
id: 'existing-app-id',
517+
organizationId,
518+
projectId: null,
519+
title: 'Existing App',
520+
type: 'coreApp',
521+
updatedAt: '2024-01-01T00:00:00Z',
522+
urlType: 'internal',
523+
},
524+
])
525+
526+
const {error} = await testCommand(DeployCommand, ['--yes'], {
527+
config: {root: cwd},
528+
mocks: {cliConfig: {app: {organizationId}}},
529+
})
530+
531+
expect(error).toBeInstanceOf(Error)
532+
expect(mockSelect).not.toHaveBeenCalled()
533+
})
534+
535+
test('--yes errors instead of prompting for a new application title', async () => {
536+
const cwd = await testFixture('basic-app')
537+
process.cwd = () => cwd
538+
539+
// No existing apps and no appId → would-create; unattended can't prompt for a title.
540+
mockApi({
541+
apiVersion: USER_APPLICATIONS_API_VERSION,
542+
query: {appType: 'coreApp', organizationId},
543+
uri: `/user-applications`,
544+
}).reply(200, [])
545+
546+
const {error} = await testCommand(DeployCommand, ['--yes'], {
547+
config: {root: cwd},
548+
mocks: {cliConfig: {app: {organizationId}}},
549+
})
550+
551+
expect(error).toBeInstanceOf(Error)
552+
expect(mockInput).not.toHaveBeenCalled()
553+
})
554+
505555
test('should skip build when --no-build flag is used', async () => {
506556
const cwd = await testFixture('basic-app')
507557
process.cwd = () => cwd

0 commit comments

Comments
 (0)