diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index 185a843b69c..a2e535c412c 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -1,9 +1,9 @@ -import { Stats } from 'fs' +import { type Stats } from 'fs' import { stat } from 'fs/promises' import { basename, resolve } from 'path' -import { runCoreSteps } from '@netlify/build' -import { OptionValues } from 'commander' +import { type NetlifyConfig, runCoreSteps } from '@netlify/build' +import { type OptionValues } from 'commander' import inquirer from 'inquirer' import isEmpty from 'lodash/isEmpty.js' import isObject from 'lodash/isObject.js' @@ -34,7 +34,6 @@ import { import { DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.js' import { deploySite } from '../../utils/deploy/deploy-site.js' import { getEnvelopeEnv } from '../../utils/env/index.js' -import { getFrameworksAPIPaths } from '../../utils/frameworks-api.js' import { getFunctionsManifestPath, getInternalFunctionsDir } from '../../utils/functions/index.js' import openBrowser from '../../utils/open-browser.js' import BaseCommand from '../base-command.js' @@ -446,6 +445,7 @@ const runDeploy = async ({ deployUrl: string logsUrl: string functionLogsUrl: string + edgeFunctionLogsUrl: string }> => { let results let deployId @@ -532,9 +532,11 @@ const runDeploy = async ({ const logsUrl = `${results.deploy.admin_url}/deploys/${results.deploy.id}` let functionLogsUrl = `${results.deploy.admin_url}/logs/functions` + let edgeFunctionLogsUrl = `${results.deploy.admin_url}/logs/edge-functions` if (!deployToProduction) { functionLogsUrl += `?scope=deploy:${deployId}` + edgeFunctionLogsUrl += `?scope=deploy:${deployId}` } return { @@ -545,6 +547,7 @@ const runDeploy = async ({ deployUrl, logsUrl, functionLogsUrl, + edgeFunctionLogsUrl, } } @@ -624,30 +627,40 @@ const bundleEdgeFunctions = async (options, command: BaseCommand) => { }) } -/** - * - * @param {object} config - * @param {boolean} config.deployToProduction - * @param {boolean} config.isIntegrationDeploy If the user ran netlify integration:deploy instead of just netlify deploy - * @param {boolean} config.json If the result should be printed as json message - * @param {boolean} config.runBuildCommand If the build command should be run - * @param {object} config.results - * @returns {void} - */ -// @ts-expect-error TS(7031) FIXME: Binding element 'deployToProduction' implicitly ha... Remove this comment to see the full error message -const printResults = ({ deployToProduction, isIntegrationDeploy, json, results, runBuildCommand }) => { - const msgData = { +interface JsonData { + site_id: string + site_name: string + deploy_id: string + deploy_url: string + logs: string + function_logs: string + edge_function_logs: string + url?: string +} + +const printResults = ({ + deployToProduction, + isIntegrationDeploy, + json, + results, + runBuildCommand, +}: { + deployToProduction: boolean + isIntegrationDeploy: boolean + json: boolean + results: Awaited> + runBuildCommand: boolean +}): void => { + const msgData: Record = { 'Build logs': results.logsUrl, 'Function logs': results.functionLogsUrl, + 'Edge function Logs': results.edgeFunctionLogsUrl, } if (deployToProduction) { - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message msgData['Unique deploy URL'] = results.deployUrl - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message msgData['Website URL'] = results.siteUrl } else { - // @ts-expect-error TS(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message msgData['Website draft URL'] = results.deployUrl } @@ -656,16 +669,16 @@ const printResults = ({ deployToProduction, isIntegrationDeploy, json, results, // Json response for piping commands if (json) { - const jsonData = { - name: results.name, - site_id: results.site_id, + const jsonData: JsonData = { + site_id: results.siteId, site_name: results.siteName, deploy_id: results.deployId, deploy_url: results.deployUrl, logs: results.logsUrl, + function_logs: results.functionLogsUrl, + edge_function_logs: results.edgeFunctionLogsUrl, } if (deployToProduction) { - // @ts-expect-error TS(2339) FIXME: Property 'url' does not exist on type '{ name: any... Remove this comment to see the full error message jsonData.url = results.siteUrl } @@ -853,8 +866,7 @@ export const deploy = async (options: OptionValues, command: BaseCommand) => { defaultConfig: getDefaultConfig(settings), currentDir: command.workingDir, options, - // @ts-expect-error TS(7031) FIXME: Binding element 'netlifyConfig' implicitly has an ... Remove this comment to see the full error message - deployHandler: async ({ netlifyConfig }) => { + deployHandler: async ({ netlifyConfig }: { netlifyConfig: NetlifyConfig }) => { results = await prepAndRunDeploy({ command, options, diff --git a/tests/integration/commands/deploy/deploy.test.ts b/tests/integration/commands/deploy/deploy.test.ts index 99cdb5f32ec..de4f0b95e1a 100644 --- a/tests/integration/commands/deploy/deploy.test.ts +++ b/tests/integration/commands/deploy/deploy.test.ts @@ -38,12 +38,23 @@ const validateDeploy = async ({ contentMessage?: string siteName: string content?: string - deploy: { site_name: string; deploy_url: string; deploy_id: string; logs: string } + deploy: { + site_id: string + site_name: string + deploy_url: string + deploy_id: string + logs: string + function_logs: string + edge_function_logs: string + } }) => { + expect(deploy.site_id).toBeTruthy() expect(deploy.site_name).toBeTruthy() expect(deploy.deploy_url).toBeTruthy() expect(deploy.deploy_id).toBeTruthy() expect(deploy.logs).toBeTruthy() + expect(deploy.function_logs).toBeTruthy() + expect(deploy.edge_function_logs).toBeTruthy() expect(deploy.site_name, contentMessage).toEqual(siteName) await validateContent({ siteUrl: deploy.deploy_url, path: '', content }) @@ -263,6 +274,57 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co }) }) + test('should print deploy-scoped URLs for build logs, function logs, and edge function logs', async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = '

Why Next.js is perfect, an essay

' + builder.withContentFile({ + path: 'public/index.html', + content, + }) + await builder.build() + + const deploy = await callCli(['deploy', '--json', '--dir', 'public'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }).then((output) => JSON.parse(output)) + + await validateDeploy({ deploy, siteName: SITE_NAME, content }) + expect(deploy).toHaveProperty('logs', `https://app.netlify.com/sites/${SITE_NAME}/deploys/${deploy.deploy_id}`) + expect(deploy).toHaveProperty( + 'function_logs', + `https://app.netlify.com/sites/${SITE_NAME}/logs/functions?scope=deploy:${deploy.deploy_id}`, + ) + expect(deploy).toHaveProperty( + 'edge_function_logs', + `https://app.netlify.com/sites/${SITE_NAME}/logs/edge-functions?scope=deploy:${deploy.deploy_id}`, + ) + }) + }) + + test('should print production URLs for build logs, function logs, and edge function logs when --prod is passed', async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = '

Why Next.js is perfect, a novella

' + builder.withContentFile({ + path: 'public/index.html', + content, + }) + await builder.build() + + const deploy = await callCli(['deploy', '--json', '--dir', 'public', '--prod'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }).then((output) => JSON.parse(output)) + + await validateDeploy({ deploy, siteName: SITE_NAME, content }) + expect(deploy).toHaveProperty('logs', `https://app.netlify.com/sites/${SITE_NAME}/deploys/${deploy.deploy_id}`) + expect(deploy).toHaveProperty('function_logs', `https://app.netlify.com/sites/${SITE_NAME}/logs/functions`) + expect(deploy).toHaveProperty( + 'edge_function_logs', + `https://app.netlify.com/sites/${SITE_NAME}/logs/edge-functions`, + ) + }) + }) + test('should return valid json when both --build and --json are passed', async (t) => { await withSiteBuilder(t, async (builder) => { const content = '

⊂◉‿◉つ

'