diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index 91b0eb8ac3d..436c323b125 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -544,7 +544,7 @@ const runDeploy = async ({ await prepareProductionDeploy({ siteData, api, options, command }) } - const draft = !deployToProduction && !alias + const draft = options.draft || (!deployToProduction && !alias) const createDeployBody = { draft, branch: alias, include_upload_url: options.uploadSourceZip } results = await api.createSiteDeploy({ siteId, title, body: createDeployBody }) @@ -1050,7 +1050,8 @@ export const deploy = async (options: DeployOptionValues, command: BaseCommand) return triggerDeploy({ api, options, siteData, siteId }) } - const deployToProduction = options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false)) + const deployToProduction = + !options.draft && (options.prod || (options.prodIfUnlocked && !(siteData.published_deploy?.locked ?? false))) let results = {} as Awaited> diff --git a/src/commands/deploy/index.ts b/src/commands/deploy/index.ts index ea10937707a..886de83367c 100644 --- a/src/commands/deploy/index.ts +++ b/src/commands/deploy/index.ts @@ -29,12 +29,18 @@ For detailed configuration options, see the Netlify documentation.`, .addOption( new Option('-p, --prod', 'Deploy to production') .default(false) - .conflicts(['alias', 'branch', 'prod-if-unlocked']), + .conflicts(['alias', 'branch', 'prod-if-unlocked', 'draft']), ) .addOption( new Option('--prod-if-unlocked', 'Deploy to production if unlocked, create a draft otherwise') .default(false) - .conflicts(['alias', 'branch', 'prod']), + .conflicts(['alias', 'branch', 'prod', 'draft']), + ) + .addOption( + new Option('--draft', 'Explicitly create a draft deploy') + .default(false) + .conflicts(['prod', 'prod-if-unlocked']) + .hideHelp(true), ) .option( '--alias ', diff --git a/src/commands/deploy/option_values.ts b/src/commands/deploy/option_values.ts index cc8f4cd8f57..4a9675d82d5 100644 --- a/src/commands/deploy/option_values.ts +++ b/src/commands/deploy/option_values.ts @@ -9,6 +9,7 @@ export type DeployOptionValues = BaseOptionValues & { context?: string createSite?: string | boolean dir?: string + draft: boolean functions?: string json: boolean message?: string diff --git a/tests/integration/commands/deploy/deploy.test.ts b/tests/integration/commands/deploy/deploy.test.ts index bd66d80b485..6b59acf24b0 100644 --- a/tests/integration/commands/deploy/deploy.test.ts +++ b/tests/integration/commands/deploy/deploy.test.ts @@ -1186,6 +1186,98 @@ describe.skipIf(process.env.NETLIFY_TEST_DISABLE_LIVE === 'true').concurrent('co }) }) + test('should deploy as draft when --draft flag is used', async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = '

Draft deploy test

' + builder.withContentFile({ + path: 'public/index.html', + content, + }) + + await builder.build() + + const deploy = await callCli(['deploy', '--json', '--no-build', '--dir', 'public', '--draft'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }).then((output: string) => JSON.parse(output)) + + await validateDeploy({ deploy, siteName: SITE_NAME, content }) + expect(deploy).toHaveProperty( + 'function_logs', + `https://app.netlify.com/projects/${SITE_NAME}/logs/functions?scope=deploy:${deploy.deploy_id}`, + ) + expect(deploy).toHaveProperty( + 'edge_function_logs', + `https://app.netlify.com/projects/${SITE_NAME}/logs/edge-functions?scope=deployid:${deploy.deploy_id}`, + ) + }) + }) + + test('should not run deploy with --draft and --prod flags together', async (t) => { + await withSiteBuilder(t, async (builder) => { + await builder.build() + try { + await callCli(['deploy', '--no-build', '--draft', '--prod'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }) + } catch (error) { + expect(error).toHaveProperty( + 'stderr', + expect.stringContaining(`Error: option '-p, --prod' cannot be used with option '--draft'`), + ) + } + }) + }) + + test('should not run deploy with --draft and --prod-if-unlocked flags together', async (t) => { + await withSiteBuilder(t, async (builder) => { + await builder.build() + try { + await callCli(['deploy', '--no-build', '--draft', '--prod-if-unlocked'], { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }) + } catch (error) { + expect(error).toHaveProperty( + 'stderr', + expect.stringContaining(`Error: option '--prod-if-unlocked' cannot be used with option '--draft'`), + ) + } + }) + }) + + test('should deploy as draft when --draft flag is used with --alias and --no-build', async (t) => { + await withSiteBuilder(t, async (builder) => { + const content = '

Draft deploy with alias test

' + builder.withContentFile({ + path: 'public/index.html', + content, + }) + + await builder.build() + + const deploy = await callCli( + ['deploy', '--json', '--no-build', '--dir', 'public', '--draft', '--alias', 'test-branch'], + { + cwd: builder.directory, + env: { NETLIFY_SITE_ID: context.siteId }, + }, + ).then((output: string) => JSON.parse(output)) + + await validateDeploy({ deploy, siteName: SITE_NAME, content }) + expect(deploy).toHaveProperty( + 'function_logs', + `https://app.netlify.com/projects/${SITE_NAME}/logs/functions?scope=deploy:${deploy.deploy_id}`, + ) + expect(deploy).toHaveProperty( + 'edge_function_logs', + `https://app.netlify.com/projects/${SITE_NAME}/logs/edge-functions?scope=deployid:${deploy.deploy_id}`, + ) + expect(deploy.deploy_url).toContain('test-branch--') + }) + }) + test('should include source_zip_filename in JSON output when --upload-source-zip flag is used', async (t) => { await withSiteBuilder(t, async (builder) => { const content = '

Source zip test

'