Skip to content
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
195 changes: 128 additions & 67 deletions src/commands/deploy/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,41 +373,46 @@ const prepareProductionDeploy = async ({ api, siteData, options, command }) => {
}
}

// @ts-expect-error TS(7006) FIXME: Parameter 'actual' implicitly has an 'any' type.
const hasErrorMessage = (actual, expected) => {
const hasErrorMessage = (actual: unknown, expected: string): boolean => {
if (typeof actual === 'string') {
return actual.includes(expected)
}
return false
}

// @ts-expect-error TS(7031) FIXME: Binding element 'error_' implicitly has an 'any' t... Remove this comment to see the full error message
const reportDeployError = ({ error_, failAndExit }) => {
interface DeployError extends Error {
json?: { message?: string }
status?: unknown
}
const reportDeployError = ({
error,
failAndExit,
}: {
error: DeployError
failAndExit: (err: unknown) => never
}): never => {
switch (true) {
case error_.name === 'JSONHTTPError': {
const message = error_?.json?.message ?? ''
case error.name === 'JSONHTTPError': {
const message = error.json?.message ?? ''
if (hasErrorMessage(message, 'Background Functions not allowed by team plan')) {
return failAndExit(`\n${BACKGROUND_FUNCTIONS_WARNING}`)
}
warn(`JSONHTTPError: ${message} ${error_.status}`)
warn(`\n${JSON.stringify(error_, null, ' ')}\n`)
failAndExit(error_)
return
warn(`JSONHTTPError: ${message} ${error.status}`)
warn(`\n${JSON.stringify(error, null, ' ')}\n`)
return failAndExit(error)
}
case error_.name === 'TextHTTPError': {
warn(`TextHTTPError: ${error_.status}`)
warn(`\n${error_}\n`)
failAndExit(error_)
return
case error.name === 'TextHTTPError': {
warn(`TextHTTPError: ${error.status}`)
warn(`\n${error}\n`)
return failAndExit(error)
}
case hasErrorMessage(error_.message, 'Invalid filename'): {
warn(error_.message)
failAndExit(error_)
return
case hasErrorMessage(error.message, 'Invalid filename'): {
warn(error.message)
return failAndExit(error)
}
default: {
warn(`\n${JSON.stringify(error_, null, ' ')}\n`)
failAndExit(error_)
warn(`\n${JSON.stringify(error, null, ' ')}\n`)
return failAndExit(error)
}
}
}
Expand Down Expand Up @@ -531,9 +536,11 @@ const runDeploy = async ({
skipFunctionsCache,
// @ts-expect-error TS(7031) FIXME: Binding element 'title' implicitly has an 'any' ty... Remove this comment to see the full error message
title,
deployId: existingDeployId,
}: {
functionsFolder?: string
command: BaseCommand
deployId?: string
}): Promise<{
siteId: string
siteName: string
Expand All @@ -546,28 +553,35 @@ const runDeploy = async ({
sourceZipFileName?: string
}> => {
let results
let deployId
let deployId = existingDeployId
let uploadSourceZipResult

try {
if (deployToProduction) {
await prepareProductionDeploy({ siteData, api, options, command })
}

const draft = options.draft || (!deployToProduction && !alias)
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip }

results = await api.createSiteDeploy({ siteId, title, body: createDeployBody })
deployId = results.id
// We won't have a deploy ID if we run the command with `--no-build`.
// In this case, we must create the deploy.
if (!deployId) {
if (deployToProduction) {
await prepareProductionDeploy({ siteData, api, options, command })
}

// Handle source zip upload if requested and URL provided
if (options.uploadSourceZip && results.source_zip_upload_url && results.source_zip_filename) {
uploadSourceZipResult = await uploadSourceZip({
sourceDir: site.root,
uploadUrl: results.source_zip_upload_url,
filename: results.source_zip_filename,
statusCb: silent ? () => {} : deployProgressCb(),
})
const draft = options.draft || (!deployToProduction && !alias)
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip }

const createDeployResponse = await api.createSiteDeploy({ siteId, title, body: createDeployBody })
deployId = createDeployResponse.id as string

if (
options.uploadSourceZip &&
createDeployResponse.source_zip_upload_url &&
createDeployResponse.source_zip_filename
) {
uploadSourceZipResult = await uploadSourceZip({
sourceDir: site.root,
uploadUrl: createDeployResponse.source_zip_upload_url,
filename: createDeployResponse.source_zip_filename,
statusCb: silent ? () => {} : deployProgressCb(),
})
}
}

const internalFunctionsFolder = await getInternalFunctionsDir({ base: site.root, packagePath, ensureExists: true })
Expand Down Expand Up @@ -628,11 +642,12 @@ const runDeploy = async ({
skipFunctionsCache,
siteRoot: site.root,
})
} catch (error_) {
} catch (error) {
if (deployId) {
await cancelDeploy({ api, deployId })
}
reportDeployError({ error_, failAndExit: logAndThrowError })

return reportDeployError({ error: error as DeployError, failAndExit: logAndThrowError })
}

const siteUrl = results.deploy.ssl_url || results.deploy.url
Expand Down Expand Up @@ -690,7 +705,7 @@ const handleBuild = async ({
})
const { configMutations, exitCode, newConfig, logs } = await runBuild(resolvedOptions)
// Without this, the deploy command fails silently
if (options.json && exitCode !== 0) {
if (exitCode !== 0) {
let message = ''

if (options.verbose && logs?.stdout.length) {
Expand All @@ -703,9 +718,6 @@ const handleBuild = async ({

logAndThrowError(`Error while running build${message}`)
}
if (exitCode !== 0) {
exit(exitCode)
}
return { newConfig, configMutations }
}

Expand Down Expand Up @@ -849,10 +861,12 @@ const prepAndRunDeploy = async ({
siteData,
siteId,
workingDir,
deployId,
}: {
options: DeployOptionValues
command: BaseCommand
workingDir: string
deployId?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FIXME(serhalp)
[key: string]: any
}) => {
Expand Down Expand Up @@ -933,6 +947,7 @@ const prepAndRunDeploy = async ({
siteId,
skipFunctionsCache: options.skipFunctionsCache,
title: options.message,
deployId,
})

return results
Expand Down Expand Up @@ -1080,29 +1095,75 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand)
let results = {} as Awaited<ReturnType<typeof prepAndRunDeploy>>

if (options.build) {
const settings = await detectFrameworkSettings(command, 'build')
await handleBuild({
packagePath: command.workspacePackage,
cachedConfig: command.netlify.cachedConfig,
defaultConfig: getDefaultConfig(settings),
currentDir: command.workingDir,
options,
deployHandler: async ({ netlifyConfig }: { netlifyConfig: NetlifyConfig }) => {
results = await prepAndRunDeploy({
command,
options,
workingDir,
api,
site,
config: netlifyConfig,
siteData,
siteId,
deployToProduction,
})
if (deployToProduction) {
await prepareProductionDeploy({ siteData, api, options, command })
}

return { newEnvChanges: { DEPLOY_ID: results.deployId, DEPLOY_URL: results.deployUrl } }
},
})
const draft = options.draft || (!deployToProduction && !alias)
const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip }

// TODO: Type this properly in `@netlify/api`.
const deployMetadata = (await api.createSiteDeploy({
siteId,
title: options.message,
body: createDeployBody,
})) as Awaited<ReturnType<typeof api.createSiteDeploy>> & {
source_zip_upload_url?: string
source_zip_filename?: string
}
const deployId = deployMetadata.id || ''
const deployUrl = deployMetadata.deploy_ssl_url || deployMetadata.deploy_url || ''

command.netlify.cachedConfig.env.DEPLOY_ID = { sources: ['internal'], value: deployId }
command.netlify.cachedConfig.env.DEPLOY_URL = { sources: ['internal'], value: deployUrl }

process.env.DEPLOY_ID = deployId
process.env.DEPLOY_URL = deployUrl
Comment on lines +1120 to +1121
Copy link
Member

Choose a reason for hiding this comment

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

🤔 What purpose does populating these in the CLI process serve? If it serves a purpose, could we leave a comment here explaining that? and is it possible to add test coverage?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is lets @netlify/config, which runs in the same process, grab the deploy details.

Copy link
Member

Choose a reason for hiding this comment

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

In that case, what is the purpose of the mutations to command.netlify.cachedConfig.env? and why can't we use the newEnvChanges mechanism? I think the why would be worth documenting in a comment in a follow-up PR!


if (
options.uploadSourceZip &&
deployMetadata.source_zip_upload_url &&
deployMetadata.source_zip_filename &&
site.root
) {
await uploadSourceZip({
sourceDir: site.root,
uploadUrl: deployMetadata.source_zip_upload_url,
filename: deployMetadata.source_zip_filename,
statusCb: options.json || options.silent ? () => {} : deployProgressCb(),
})
}
try {
const settings = await detectFrameworkSettings(command, 'build')
await handleBuild({
packagePath: command.workspacePackage,
cachedConfig: command.netlify.cachedConfig,
defaultConfig: getDefaultConfig(settings),
currentDir: command.workingDir,
options,
deployHandler: async ({ netlifyConfig }: { netlifyConfig: NetlifyConfig }) => {
results = await prepAndRunDeploy({
command,
options,
workingDir,
api,
site,
config: netlifyConfig,
siteData,
siteId,
deployToProduction,
deployId,
})

return {}
},
})
} catch (error) {
// The build has failed, so let's cancel the deploy we created.
await cancelDeploy({ api, deployId })

throw error
}
} else {
results = await prepAndRunDeploy({
command,
Expand Down
37 changes: 33 additions & 4 deletions tests/integration/commands/deploy/deploy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,12 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co

test('runs build command before deploy by default', async (t) => {
await withSiteBuilder(t, async (builder) => {
const content = '<h1>⊂◉‿◉つ</h1>'
const rootContent = '<h1>⊂◉‿◉つ</h1>'

builder
.withContentFile({
path: 'public/index.html',
content,
content: rootContent,
})
.withNetlifyToml({
config: {
Expand All @@ -386,13 +387,32 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co
.withBuildPlugin({
name: 'log-env',
plugin: {
async onPreBuild() {
const { DEPLOY_ID, DEPLOY_URL } = require('process').env
console.log(`DEPLOY_ID_PREBUILD: ${DEPLOY_ID}`)
console.log(`DEPLOY_URL_PREBUILD: ${DEPLOY_URL}`)
},
async onSuccess() {
const { DEPLOY_ID, DEPLOY_URL } = require('process').env
console.log(`DEPLOY_ID: ${DEPLOY_ID}`)
console.log(`DEPLOY_URL: ${DEPLOY_URL}`)
},
},
})
.withEdgeFunction({
handler: async () => new Response('Hello from edge function'),
name: 'edge',
config: {
path: '/edge-function',
},
})
.withFunction({
config: { path: '/function' },
path: 'hello.mjs',
pathPrefix: 'netlify/functions',
handler: async () => new Response('Hello from function'),
runtimeAPIVersion: 2,
})

await builder.build()

Expand All @@ -402,11 +422,20 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co
})

t.expect(output).toContain('Netlify Build completed in')
const [, deployIdPreBuild] = output.match(/DEPLOY_ID_PREBUILD: (\w+)/) ?? []
const [, deployURLPreBuild] = output.match(/DEPLOY_URL_PREBUILD: (.+)/) ?? []
const [, deployId] = output.match(/DEPLOY_ID: (\w+)/) ?? []
const [, deployURL] = output.match(/DEPLOY_URL: (.+)/) ?? []

t.expect(deployId).not.toEqual('0')
t.expect(deployURL).toContain(`https://${deployId}--`)
t.expect(deployIdPreBuild).toBeTruthy()
t.expect(deployIdPreBuild).not.toEqual('0')
t.expect(deployURLPreBuild).toContain(`https://${deployIdPreBuild}--`)
t.expect(deployId).toEqual(deployIdPreBuild)
t.expect(deployURL).toEqual(deployURLPreBuild)

await validateContent({ siteUrl: deployURL, path: '', content: rootContent })
await validateContent({ siteUrl: deployURL, path: '/edge-function', content: 'Hello from edge function' })
await validateContent({ siteUrl: deployURL, path: '/function', content: 'Hello from function' })
})
})

Expand Down
Loading