Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): don't create default apps on deploy #7328

Merged
merged 3 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jest.mock('../helpers')
jest.mock('../../build/buildAction')

type Helpers = typeof _helpers
const helpers = _helpers as {[K in keyof Helpers]: jest.Mock<Helpers[K]>}
const helpers = _helpers as unknown as {[K in keyof Helpers]: jest.Mock<Helpers[K]>}
const buildSanityStudioMock = buildSanityStudio as jest.Mock<typeof buildSanityStudio>
const tarPackMock = tar.pack as jest.Mock
const zlibCreateGzipMock = zlib.createGzip as jest.Mock
Expand Down Expand Up @@ -118,6 +118,60 @@ describe('deployStudioAction', () => {
expect(mockSpinner.succeed).toHaveBeenCalled()
})

it('builds and deploys the studio if the directory is empty and hostname in config', async () => {
const mockSpinner = mockContext.output.spinner('')
mockContext.cliConfig = {studioHost: 'app-host'}

// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
helpers.getOrCreateUserApplicationFromConfig.mockResolvedValueOnce(mockApplication)
helpers.createDeployment.mockResolvedValueOnce({location: 'https://app-host.sanity.studio'})
buildSanityStudioMock.mockResolvedValueOnce({didCompile: true})
tarPackMock.mockReturnValueOnce({pipe: jest.fn().mockReturnValue('tarball')})
zlibCreateGzipMock.mockReturnValue('gzipped')

await deployStudioAction(
{
argsWithoutOptions: ['customSourceDir'],
extOptions: {},
} as CliCommandArguments<DeployStudioActionFlags>,
mockContext,
)

// Check that buildSanityStudio was called
expect(buildSanityStudioMock).toHaveBeenCalledWith(
expect.objectContaining({
extOptions: {build: true},
argsWithoutOptions: ['customSourceDir'],
}),
mockContext,
{basePath: '/'},
)
expect(helpers.dirIsEmptyOrNonExistent).toHaveBeenCalledWith(
expect.stringContaining('customSourceDir'),
)
expect(helpers.getOrCreateUserApplicationFromConfig).toHaveBeenCalledWith(
expect.objectContaining({
client: expect.anything(),
context: expect.anything(),
appHost: 'app-host',
}),
)
expect(helpers.createDeployment).toHaveBeenCalledWith({
client: expect.anything(),
applicationId: 'app-id',
version: 'vX',
isAutoUpdating: false,
tarball: 'tarball',
})

expect(mockContext.output.print).toHaveBeenCalledWith(
'\nSuccess! Studio deployed to https://app-host.sanity.studio',
)
expect(mockSpinner.succeed).toHaveBeenCalled()
})

it('prompts the user if the directory is not empty', async () => {
const mockSpinner = mockContext.output.spinner('')

Expand Down Expand Up @@ -205,8 +259,6 @@ describe('deployStudioAction', () => {
})

it('returns an error if API responds with 402', async () => {
const mockSpinner = mockContext.output.spinner('')

// Mock utility functions
helpers.dirIsEmptyOrNonExistent.mockResolvedValueOnce(true)
helpers.getInstalledSanityVersion.mockResolvedValueOnce('vX')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {type Stats} from 'node:fs'
import fs from 'node:fs/promises'
import {Readable} from 'node:stream'
Expand All @@ -13,6 +14,7 @@ import {
deleteUserApplication,
dirIsEmptyOrNonExistent,
getOrCreateUserApplication,
getOrCreateUserApplicationFromConfig,
} from '../helpers'

jest.mock('node:fs/promises')
Expand All @@ -36,8 +38,12 @@ const mockOutput = {
warn: jest.fn(),
spinner: jest.fn(),
} as CliCommandContext['output']
const mockPrompt = {single: jest.fn()} as unknown as CliCommandContext['prompt']
const mockPrompt = {
single: jest.fn(),
Separator: jest.fn(),
} as unknown as CliCommandContext['prompt']
const mockSpinner = {
start: jest.fn(),
succeed: jest.fn(),
} as unknown as ReturnType<CliCommandContext['output']['spinner']>

Expand Down Expand Up @@ -68,23 +74,88 @@ describe('getOrCreateUserApplication', () => {
expect(result).toEqual({id: 'default-app'})
})

it('creates a user application by prompting the user for a name', async () => {
const newApp = {
id: 'default-app',
appHost: 'default.sanity.studio',
urlType: 'internal',
}
mockClientRequest.mockResolvedValueOnce(null) // Simulate no existing app
mockClientRequest.mockResolvedValueOnce([]) // Simulate no list of deployments
;(mockPrompt.single as jest.Mock<any>).mockImplementationOnce(
async ({validate}: Parameters<CliCommandContext['prompt']['single']>[0]) => {
// Simulate user input and validation
const appHost = 'default.sanity.studio'
await validate!(appHost)
return appHost
},
)
mockClientRequest.mockResolvedValueOnce(newApp)

const result = await getOrCreateUserApplication({
client: mockClient,
context,
spinner: mockSpinner,
})

expect(mockPrompt.single).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Studio hostname (<value>.sanity.studio):',
}),
)
expect(result).toEqual(newApp)
})

it('allows user to select a user application from a list', async () => {
const existingApp = {
id: 'default-app',
appHost: 'default.sanity.studio',
urlType: 'internal',
}
mockClientRequest.mockResolvedValueOnce(null) // Simulate no existing app
mockClientRequest.mockResolvedValueOnce([existingApp]) // Simulate no list of deployments
;(mockPrompt.single as jest.Mock<any>).mockImplementationOnce(async ({choices}: any) => {
// Simulate user input
return Promise.resolve(choices[2].value)
})

const result = await getOrCreateUserApplication({
client: mockClient,
context,
spinner: mockSpinner,
})

expect(mockPrompt.single).toHaveBeenCalledWith(
expect.objectContaining({
choices: expect.arrayContaining([expect.objectContaining({name: 'default.sanity.studio'})]),
}),
)
expect(result).toEqual(existingApp)
})
})

describe('getOrCreateUserApplicationFromConfig', () => {
beforeEach(() => {
jest.clearAllMocks()
})

it('gets an existing user application if a `studioHost` is provided in the config', async () => {
mockClientRequest.mockResolvedValueOnce({
id: 'existing-app',
appHost: 'example.sanity.studio',
urlType: 'internal',
})

const result = await getOrCreateUserApplication({
const result = await getOrCreateUserApplicationFromConfig({
client: mockClient,
spinner: mockSpinner,
cliConfig: {studioHost: 'example.sanity.studio'},
context,
appHost: 'example',
})

expect(mockClientRequest).toHaveBeenCalledWith({
uri: '/user-applications',
query: {appHost: 'example.sanity.studio'},
query: {appHost: 'example'},
})
expect(result).toEqual({
id: 'existing-app',
Expand All @@ -102,11 +173,11 @@ describe('getOrCreateUserApplication', () => {
mockClientRequest.mockResolvedValueOnce(null) // Simulate no existing app
mockClientRequest.mockResolvedValueOnce(newApp)

const result = await getOrCreateUserApplication({
const result = await getOrCreateUserApplicationFromConfig({
client: mockClient,
spinner: mockSpinner,
cliConfig: {studioHost: 'newhost'},
context,
appHost: 'newhost',
})

expect(mockClientRequest).toHaveBeenCalledTimes(2)
Expand All @@ -121,38 +192,6 @@ describe('getOrCreateUserApplication', () => {
})
expect(result).toEqual(newApp)
})

it('creates a default user application by prompting the user for a name', async () => {
const newApp = {
id: 'default-app',
appHost: 'default.sanity.studio',
urlType: 'internal',
isDefaultForDeployment: true,
}
mockClientRequest.mockResolvedValueOnce(null) // Simulate no existing app
;(mockPrompt.single as jest.Mock<any>).mockImplementationOnce(
async ({validate}: Parameters<CliCommandContext['prompt']['single']>[0]) => {
// Simulate user input and validation
const appHost = 'default.sanity.studio'
await validate!(appHost)
return appHost
},
)
mockClientRequest.mockResolvedValueOnce(newApp)

const result = await getOrCreateUserApplication({
client: mockClient,
context,
spinner: mockSpinner,
})

expect(mockPrompt.single).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Studio hostname (<value>.sanity.studio):',
}),
)
expect(result).toEqual(newApp)
})
})

describe('createDeployment', () => {
Expand Down
30 changes: 19 additions & 11 deletions packages/sanity/src/_internal/cli/actions/deploy/deployAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ import zlib from 'node:zlib'
import {type CliCommandArguments, type CliCommandContext} from '@sanity/cli'
import tar from 'tar-fs'

import {debug as debugIt} from '../../debug'
import buildSanityStudio, {type BuildSanityStudioCommandFlags} from '../build/buildAction'
import {
checkDir,
createDeployment,
debug,
dirIsEmptyOrNonExistent,
getInstalledSanityVersion,
getOrCreateUserApplication,
getOrCreateUserApplicationFromConfig,
type UserApplication,
} from './helpers'

const debug = debugIt.extend('deploy')

export interface DeployStudioActionFlags extends BuildSanityStudioCommandFlags {
build?: boolean
}
Expand Down Expand Up @@ -70,16 +70,24 @@ export default async function deployStudioAction(
// Check that the project has a studio hostname
let spinner = output.spinner('Checking project info').start()

let userApplication
let userApplication: UserApplication

try {
userApplication = await getOrCreateUserApplication({
client,
context,
spinner,
// ensures only v3 configs with `studioHost` are sent
...(cliConfig && 'studioHost' in cliConfig && {cliConfig}),
})
// If the user has provided a studioHost in the config, use that
if (cliConfig && 'studioHost' in cliConfig && cliConfig.studioHost) {
userApplication = await getOrCreateUserApplicationFromConfig({
client,
context,
spinner,
appHost: cliConfig.studioHost,
})
} else {
userApplication = await getOrCreateUserApplication({
client,
context,
spinner,
})
}
} catch (err) {
if (err.message) {
output.error(chalk.red(err.message))
Expand Down
Loading
Loading