From b19f3fc16ae0ef6f0e2836a0d3a77d56245908aa Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Fri, 9 Jan 2026 15:25:23 +0100 Subject: [PATCH 1/2] Add --allow-updates and --allow-delete flags to deploy and release --- .changeset/lucky-dots-watch.md | 5 + .../interfaces/app-deploy.interface.ts | 14 +- .../interfaces/app-release.interface.ts | 14 +- .../generated/generated_docs_data.json | 44 +++++- packages/app/src/cli/commands/app/deploy.ts | 35 ++++- packages/app/src/cli/commands/app/release.ts | 28 +++- .../src/cli/prompts/deploy-release.test.ts | 143 +++++++++++++++++- .../app/src/cli/prompts/deploy-release.ts | 81 +++++++++- packages/app/src/cli/services/context.ts | 2 + .../src/cli/services/context/identifiers.ts | 6 + packages/app/src/cli/services/deploy.ts | 12 +- packages/app/src/cli/services/release.ts | 10 +- packages/cli/README.md | 18 ++- packages/cli/oclif.manifest.json | 36 ++++- 14 files changed, 420 insertions(+), 28 deletions(-) create mode 100644 .changeset/lucky-dots-watch.md diff --git a/.changeset/lucky-dots-watch.md b/.changeset/lucky-dots-watch.md new file mode 100644 index 00000000000..7cfa73254d6 --- /dev/null +++ b/.changeset/lucky-dots-watch.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Add --allow-updates and --allow-delete flags to deploy and release diff --git a/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts b/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts index 5a2e0f7e850..f9fbe66cd92 100644 --- a/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts +++ b/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts @@ -1,5 +1,17 @@ // This is an autogenerated file. Don't edit this file manually. export interface appdeploy { + /** + * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions. + * @environment SHOPIFY_FLAG_ALLOW_DELETES + */ + '--allow-deletes'?: '' + + /** + * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release. + * @environment SHOPIFY_FLAG_ALLOW_UPDATES + */ + '--allow-updates'?: '' + /** * The Client ID of your app. * @environment SHOPIFY_FLAG_CLIENT_ID @@ -13,7 +25,7 @@ export interface appdeploy { '-c, --config '?: string /** - * Deploy without asking for confirmation. + * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. * @environment SHOPIFY_FLAG_FORCE */ '-f, --force'?: '' diff --git a/docs-shopify.dev/commands/interfaces/app-release.interface.ts b/docs-shopify.dev/commands/interfaces/app-release.interface.ts index f057da4d92a..6b722ff4177 100644 --- a/docs-shopify.dev/commands/interfaces/app-release.interface.ts +++ b/docs-shopify.dev/commands/interfaces/app-release.interface.ts @@ -1,5 +1,17 @@ // This is an autogenerated file. Don't edit this file manually. export interface apprelease { + /** + * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions. + * @environment SHOPIFY_FLAG_ALLOW_DELETES + */ + '--allow-deletes'?: '' + + /** + * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release. + * @environment SHOPIFY_FLAG_ALLOW_UPDATES + */ + '--allow-updates'?: '' + /** * The Client ID of your app. * @environment SHOPIFY_FLAG_CLIENT_ID @@ -13,7 +25,7 @@ export interface apprelease { '-c, --config '?: string /** - * Release without asking for confirmation. + * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. * @environment SHOPIFY_FLAG_FORCE */ '-f, --force'?: '' diff --git a/docs-shopify.dev/generated/generated_docs_data.json b/docs-shopify.dev/generated/generated_docs_data.json index 0d1e110a135..112c6a07910 100644 --- a/docs-shopify.dev/generated/generated_docs_data.json +++ b/docs-shopify.dev/generated/generated_docs_data.json @@ -396,6 +396,24 @@ "name": "appdeploy", "description": "", "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/app-deploy.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--allow-deletes", + "value": "\"\"", + "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_ALLOW_DELETES" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-deploy.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--allow-updates", + "value": "\"\"", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_ALLOW_UPDATES" + }, { "filePath": "docs-shopify.dev/commands/interfaces/app-deploy.interface.ts", "syntaxKind": "PropertySignature", @@ -500,12 +518,12 @@ "syntaxKind": "PropertySignature", "name": "-f, --force", "value": "\"\"", - "description": "Deploy without asking for confirmation.", + "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_FORCE" } ], - "value": "export interface appdeploy {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Deploy without asking for confirmation.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message '?: string\n\n /**\n * Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.\n * @environment SHOPIFY_FLAG_NO_BUILD\n */\n '--no-build'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n}" + "value": "export interface appdeploy {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message '?: string\n\n /**\n * Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.\n * @environment SHOPIFY_FLAG_NO_BUILD\n */\n '--no-build'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n}" } } } @@ -2421,6 +2439,24 @@ "name": "apprelease", "description": "", "members": [ + { + "filePath": "docs-shopify.dev/commands/interfaces/app-release.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--allow-deletes", + "value": "\"\"", + "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_ALLOW_DELETES" + }, + { + "filePath": "docs-shopify.dev/commands/interfaces/app-release.interface.ts", + "syntaxKind": "PropertySignature", + "name": "--allow-updates", + "value": "\"\"", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_ALLOW_UPDATES" + }, { "filePath": "docs-shopify.dev/commands/interfaces/app-release.interface.ts", "syntaxKind": "PropertySignature", @@ -2488,12 +2524,12 @@ "syntaxKind": "PropertySignature", "name": "-f, --force", "value": "\"\"", - "description": "Release without asking for confirmation.", + "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_FORCE" } ], - "value": "export interface apprelease {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Release without asking for confirmation.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The name of the app version to release.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version ': string\n}" + "value": "export interface apprelease {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The name of the app version to release.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version ': string\n}" } } } diff --git a/packages/app/src/cli/commands/app/deploy.ts b/packages/app/src/cli/commands/app/deploy.ts index 1ea49402070..6d08855cce0 100644 --- a/packages/app/src/cli/commands/app/deploy.ts +++ b/packages/app/src/cli/commands/app/deploy.ts @@ -1,6 +1,5 @@ import {appFlags} from '../../flags.js' import {deploy} from '../../services/deploy.js' -import {getAppConfigurationState} from '../../models/app/loader.js' import {validateVersion} from '../../validations/version-name.js' import {validateMessage} from '../../validations/message.js' import metadata from '../../metadata.js' @@ -27,10 +26,22 @@ export default class Deploy extends AppLinkedCommand { ...appFlags, force: Flags.boolean({ hidden: false, - description: 'Deploy without asking for confirmation.', + description: 'Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.', env: 'SHOPIFY_FLAG_FORCE', char: 'f', }), + 'allow-updates': Flags.boolean({ + hidden: false, + description: + 'Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.', + env: 'SHOPIFY_FLAG_ALLOW_UPDATES', + }), + 'allow-deletes': Flags.boolean({ + hidden: false, + description: + 'Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.', + env: 'SHOPIFY_FLAG_ALLOW_DELETES', + }), 'no-release': Flags.boolean({ hidden: false, description: "Creates a version but doesn't release it - it's not made available to merchants.", @@ -80,12 +91,17 @@ export default class Deploy extends AppLinkedCommand { cmd_app_reset_used: flags.reset, })) - const requiredNonTTYFlags = ['force'] - const configurationState = await getAppConfigurationState(flags.path, flags.config) - if (configurationState.state === 'template-only' && !clientId) { - requiredNonTTYFlags.push('client-id') + // When using --no-release, we don't require --force or --allow-updates/--allow-deletes for non-TTY + // because we're just creating a version, not releasing it. Validation happens at release time. + // When releasing (no --no-release), we require either --force or --allow-updates for non-TTY. + // The --allow-deletes flag is only required when there are actual deletions (validated at runtime). + const requiredNonTTYFlags: string[] = [] + if (!flags['no-release']) { + const hasAnyForceFlags = flags.force || flags['allow-updates'] || flags['allow-deletes'] + if (!hasAnyForceFlags) { + requiredNonTTYFlags.push('force') + } } - this.failMissingNonTTYFlags(flags, requiredNonTTYFlags) const {app, remoteApp, developerPlatformClient, organization} = await linkedAppContext({ directory: flags.path, @@ -94,6 +110,9 @@ export default class Deploy extends AppLinkedCommand { userProvidedConfigName: flags.config, }) + const allowUpdates = flags.force || flags['allow-updates'] + const allowDeletes = flags.force || flags['allow-deletes'] + const result = await deploy({ app, remoteApp, @@ -101,6 +120,8 @@ export default class Deploy extends AppLinkedCommand { developerPlatformClient, reset: flags.reset, force: flags.force, + allowUpdates, + allowDeletes, noRelease: flags['no-release'], message: flags.message, version: flags.version, diff --git a/packages/app/src/cli/commands/app/release.ts b/packages/app/src/cli/commands/app/release.ts index 8fb6d419980..ec3aaeb6f6a 100644 --- a/packages/app/src/cli/commands/app/release.ts +++ b/packages/app/src/cli/commands/app/release.ts @@ -21,10 +21,22 @@ export default class Release extends AppLinkedCommand { ...appFlags, force: Flags.boolean({ hidden: false, - description: 'Release without asking for confirmation.', + description: 'Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.', env: 'SHOPIFY_FLAG_FORCE', char: 'f', }), + 'allow-updates': Flags.boolean({ + hidden: false, + description: + 'Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.', + env: 'SHOPIFY_FLAG_ALLOW_UPDATES', + }), + 'allow-deletes': Flags.boolean({ + hidden: false, + description: + 'Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.', + env: 'SHOPIFY_FLAG_ALLOW_DELETES', + }), version: Flags.string({ hidden: false, description: 'The name of the app version to release.', @@ -41,7 +53,13 @@ export default class Release extends AppLinkedCommand { cmd_app_reset_used: flags.reset, })) - const requiredNonTTYFlags = ['force'] + // For releases, require either --force or --allow-updates for non-TTY + // The --allow-deletes flag is only required when there are actual deletions (validated at runtime). + const requiredNonTTYFlags: string[] = [] + const hasAnyForceFlags = flags.force || flags['allow-updates'] || flags['allow-deletes'] + if (!hasAnyForceFlags) { + requiredNonTTYFlags.push('force') + } const configurationState = await getAppConfigurationState(flags.path, flags.config) if (configurationState.state === 'template-only' && !clientId) { requiredNonTTYFlags.push('client-id') @@ -55,11 +73,17 @@ export default class Release extends AppLinkedCommand { userProvidedConfigName: flags.config, }) + // --force is a synonym for --allow-updates --allow-deletes + const allowUpdates = flags.force || flags['allow-updates'] + const allowDeletes = flags.force || flags['allow-deletes'] + await release({ app, remoteApp, developerPlatformClient, force: flags.force, + allowUpdates, + allowDeletes, version: flags.version, }) diff --git a/packages/app/src/cli/prompts/deploy-release.test.ts b/packages/app/src/cli/prompts/deploy-release.test.ts index 4eff2440102..1f3331c14e9 100644 --- a/packages/app/src/cli/prompts/deploy-release.test.ts +++ b/packages/app/src/cli/prompts/deploy-release.test.ts @@ -6,11 +6,16 @@ import { buildDashboardBreakdownInfo, buildExtensionBreakdownInfo, } from '../services/context/breakdown-extensions.js' -import {MockInstance, describe, expect, test, vi} from 'vitest' +import {MockInstance, beforeEach, describe, expect, test, vi} from 'vitest' import * as ui from '@shopify/cli-kit/node/ui' vi.mock('@shopify/cli-kit/node/context/local') +beforeEach(() => { + // Mock isTTY to return true so prompts are shown instead of errors in tests + vi.spyOn(ui, 'isTTY').mockReturnValue(true) +}) + describe('deployOrReleaseConfirmationPrompt', () => { describe('when release', () => { test('and force no prompt should be displayed and true returned', async () => { @@ -408,6 +413,142 @@ describe('deployOrReleaseConfirmationPrompt', () => { expect(result).toBe(true) }) }) + + describe('allow-updates and allow-deletes flags', () => { + test('allowUpdates and allowDeletes together should skip prompt and return true', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + const renderConfirmationPromptSpyOn = vi.spyOn(ui, 'renderConfirmationPrompt') + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: true, + allowDeletes: true, + }) + + // Then + expect(renderConfirmationPromptSpyOn).not.toHaveBeenCalled() + expect(result).toBe(true) + }) + + test('allowUpdates without deletes should skip prompt and return true', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + // Remove all deletions + breakdownInfo.extensionIdentifiersBreakdown.onlyRemote = [] + breakdownInfo.configExtensionIdentifiersBreakdown!.deletedFieldNames = [] + + const renderConfirmationPromptSpyOn = vi.spyOn(ui, 'renderConfirmationPrompt') + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: true, + allowDeletes: false, + }) + + // Then + expect(renderConfirmationPromptSpyOn).not.toHaveBeenCalled() + expect(result).toBe(true) + }) + + test('allowUpdates with deletes should show prompt in TTY', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + + const renderDangerousConfirmationPromptSpyOn = vi + .spyOn(ui, 'renderDangerousConfirmationPrompt') + .mockResolvedValue(true) + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + vi.spyOn(ui, 'isTTY').mockReturnValue(true) + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: true, + allowDeletes: false, + }) + + // Then + expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalled() + expect(result).toBe(true) + }) + + test('allowDeletes without updates should skip prompt and return true', async () => { + // Given + const breakdownInfo = buildEmptyBreakdownInfo() + // Add only deletions + breakdownInfo.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('remote', 'uid')] + breakdownInfo.configExtensionIdentifiersBreakdown!.deletedFieldNames = ['deleted field'] + + const renderConfirmationPromptSpyOn = vi.spyOn(ui, 'renderConfirmationPrompt') + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: false, + allowDeletes: true, + }) + + // Then + expect(renderConfirmationPromptSpyOn).not.toHaveBeenCalled() + expect(result).toBe(true) + }) + + test('non-TTY with deletes and without allowDeletes should throw error', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + vi.spyOn(ui, 'isTTY').mockReturnValue(false) + + // When/Then + await expect( + deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: true, + allowDeletes: false, + }), + ).rejects.toThrow('This deployment includes changes that require confirmation.') + }) + + test('non-TTY with updates and without allowUpdates should throw error', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + vi.spyOn(ui, 'isTTY').mockReturnValue(false) + + // When/Then + await expect( + deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle: 'app title', + release: true, + force: false, + allowUpdates: false, + allowDeletes: true, + }), + ).rejects.toThrow('This deployment includes changes that require confirmation.') + }) + }) }) interface RenderConfirmationPromptContentOptions { diff --git a/packages/app/src/cli/prompts/deploy-release.ts b/packages/app/src/cli/prompts/deploy-release.ts index 11379ce7591..e3707f455b0 100644 --- a/packages/app/src/cli/prompts/deploy-release.ts +++ b/packages/app/src/cli/prompts/deploy-release.ts @@ -5,7 +5,13 @@ import { ExtensionIdentifierBreakdownInfo, ExtensionIdentifiersBreakdown, } from '../services/context/breakdown-extensions.js' -import {InfoTableSection, renderConfirmationPrompt, renderDangerousConfirmationPrompt} from '@shopify/cli-kit/node/ui' +import { + InfoTableSection, + renderConfirmationPrompt, + renderDangerousConfirmationPrompt, + isTTY, +} from '@shopify/cli-kit/node/ui' +import {AbortError} from '@shopify/cli-kit/node/error' interface DeployOrReleaseConfirmationPromptOptions { extensionIdentifiersBreakdown: ExtensionIdentifiersBreakdown @@ -13,6 +19,10 @@ interface DeployOrReleaseConfirmationPromptOptions { appTitle?: string release: boolean force: boolean + /** If true, allow adding and updating extensions and configuration without user confirmation */ + allowUpdates?: boolean + /** If true, allow removing extensions and configuration without user confirmation */ + allowDeletes?: boolean showConfig?: boolean } @@ -28,15 +38,80 @@ interface DeployConfirmationPromptOptions { release: boolean } +/** + * Determines whether the confirmation prompt can be skipped based on the allow flags and change types. + * Returns true if the prompt should be skipped, false if the prompt should be shown. + * Throws an error if in non-TTY mode and there are changes that require confirmation. + */ +function shouldSkipConfirmationPrompt({ + force, + allowUpdates, + allowDeletes, + extensionIdentifiersBreakdown, + configExtensionIdentifiersBreakdown, +}: { + force: boolean + allowUpdates?: boolean + allowDeletes?: boolean + extensionIdentifiersBreakdown: ExtensionIdentifiersBreakdown + configExtensionIdentifiersBreakdown?: ConfigExtensionIdentifiersBreakdown +}): boolean { + // --force is equivalent to --allow-updates --allow-deletes + if (force || (allowUpdates && allowDeletes)) return true + + const hasDeletedExtensions = extensionIdentifiersBreakdown.onlyRemote.length > 0 + const hasDeletedConfig = (configExtensionIdentifiersBreakdown?.deletedFieldNames.length ?? 0) > 0 + const hasDeletes = hasDeletedExtensions || hasDeletedConfig + + const hasNewOrUpdatedExtensions = + extensionIdentifiersBreakdown.toCreate.length > 0 || extensionIdentifiersBreakdown.toUpdate.length > 0 + const hasNewOrUpdatedConfig = + (configExtensionIdentifiersBreakdown?.newFieldNames.length ?? 0) > 0 || + (configExtensionIdentifiersBreakdown?.existingUpdatedFieldNames.length ?? 0) > 0 + const hasUpdates = hasNewOrUpdatedExtensions || hasNewOrUpdatedConfig + + // If we only have updates (no deletes) and allowUpdates is true, skip prompt + if (allowUpdates && !hasDeletes) return true + + // If we only have deletes (no updates) and allowDeletes is true, skip prompt + if (allowDeletes && !hasUpdates) return true + + // If we're in non-TTY mode and there are changes that require confirmation, throw an error + if (!isTTY() && (hasDeletes || hasUpdates)) { + throw new AbortError('This deployment includes changes that require confirmation.', [ + 'Run the command with', + {command: '--force'}, + 'or', + {command: '--allow-updates'}, + 'or', + {command: '--allow-deletes'}, + 'to deploy without confirmation.', + ]) + } + + return false +} + export async function deployOrReleaseConfirmationPrompt({ force, + allowUpdates, + allowDeletes, extensionIdentifiersBreakdown, configExtensionIdentifiersBreakdown, appTitle, release, -}: DeployOrReleaseConfirmationPromptOptions) { +}: DeployOrReleaseConfirmationPromptOptions): Promise { await metadata.addPublicMetadata(() => buildConfigurationBreakdownMetadata(configExtensionIdentifiersBreakdown)) - if (force) return true + + const shouldSkip = shouldSkipConfirmationPrompt({ + force, + allowUpdates, + allowDeletes, + extensionIdentifiersBreakdown, + configExtensionIdentifiersBreakdown, + }) + if (shouldSkip) return true + const extensionsContentPrompt = await buildExtensionsContentPrompt(extensionIdentifiersBreakdown) const configContentPrompt = await buildConfigContentPrompt(release, configExtensionIdentifiersBreakdown) diff --git a/packages/app/src/cli/services/context.ts b/packages/app/src/cli/services/context.ts index 7ae262f5342..a89a30efbca 100644 --- a/packages/app/src/cli/services/context.ts +++ b/packages/app/src/cli/services/context.ts @@ -173,6 +173,8 @@ export async function ensureDeployContext(options: DeployOptions): Promise force: boolean + /** If true, allow adding and updating extensions and configuration without user confirmation */ + allowUpdates?: boolean + /** If true, allow removing extensions and configuration without user confirmation */ + allowDeletes?: boolean release: boolean remoteApp: PartnersAppForIdentifierMatching includeDraftExtensions?: boolean @@ -60,6 +64,8 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr appTitle: options.remoteApp?.title, release: options.release, force: options.force, + allowUpdates: options.allowUpdates, + allowDeletes: options.allowDeletes, }) if (!confirmed) throw new AbortSilentError() diff --git a/packages/app/src/cli/services/deploy.ts b/packages/app/src/cli/services/deploy.ts index 00b89f0807a..5cda1effbf9 100644 --- a/packages/app/src/cli/services/deploy.ts +++ b/packages/app/src/cli/services/deploy.ts @@ -35,9 +35,15 @@ export interface DeployOptions { /** If true, ignore any cached appId or extensionId */ reset: boolean - /** If true, proceed with deploy without asking for confirmation */ + /** If true, proceed with deploy without asking for confirmation (equivalent to allowUpdates && allowDeletes) */ force: boolean + /** If true, allow adding and updating extensions and configuration without user confirmation */ + allowUpdates?: boolean + + /** If true, allow removing extensions and configuration without user confirmation */ + allowDeletes?: boolean + /** If true, deploy app without releasing it to the users */ noRelease: boolean @@ -171,7 +177,7 @@ export async function importExtensionsIfNeeded(options: ImportExtensionsIfNeeded } export async function deploy(options: DeployOptions) { - const {remoteApp, developerPlatformClient, noRelease, force} = options + const {remoteApp, developerPlatformClient, noRelease, force, allowUpdates, allowDeletes} = options const app = await importExtensionsIfNeeded({ app: options.app, @@ -184,6 +190,8 @@ export async function deploy(options: DeployOptions) { ...options, app, developerPlatformClient, + allowUpdates, + allowDeletes, }) const release = !noRelease diff --git a/packages/app/src/cli/services/release.ts b/packages/app/src/cli/services/release.ts index 80e5ecd4d4f..cd0e50d4578 100644 --- a/packages/app/src/cli/services/release.ts +++ b/packages/app/src/cli/services/release.ts @@ -20,9 +20,15 @@ interface ReleaseOptions { /** The developer platform client */ developerPlatformClient: DeveloperPlatformClient - /** If true, proceed with deploy without asking for confirmation */ + /** If true, proceed with deploy without asking for confirmation (equivalent to allowUpdates && allowDeletes) */ force: boolean + /** If true, allow adding and updating extensions and configuration without user confirmation */ + allowUpdates?: boolean + + /** If true, allow removing extensions and configuration without user confirmation */ + allowDeletes?: boolean + /** App version tag */ version: string } @@ -51,6 +57,8 @@ export async function release(options: ReleaseOptions) { appTitle: remoteApp.title, release: true, force: options.force, + allowUpdates: options.allowUpdates, + allowDeletes: options.allowDeletes, }) if (!confirmed) throw new AbortSilentError() interface Context { diff --git a/packages/cli/README.md b/packages/cli/README.md index c01aaaa8d26..501ad0460bc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -200,12 +200,18 @@ Deploy your Shopify app. ``` USAGE - $ shopify app deploy [--client-id | -c ] [-f] [--message ] [--no-build] [--no-color] - [--no-release] [--path ] [--reset | ] [--source-control-url ] [--verbose] [--version ] + $ shopify app deploy [--allow-deletes] [--allow-updates] [--client-id | -c ] [-f] [--message + ] [--no-build] [--no-color] [--no-release] [--path ] [--reset | ] [--source-control-url ] + [--verbose] [--version ] FLAGS -c, --config= The name of the app configuration. - -f, --force Deploy without asking for confirmation. + -f, --force Deploy without asking for confirmation. Equivalent to --allow-updates + --allow-deletes. + --allow-deletes Allows removing extensions and configuration without requiring user confirmation. + Required for non-interactive release that removes any configuration or extensions. + --allow-updates Allows adding and updating extensions and configuration without requiring user + confirmation. Required for non-interactive release. --client-id= The Client ID of your app. --message= Optional message that will be associated with this version. This is for internal use only and won't be available externally. @@ -720,7 +726,11 @@ USAGE FLAGS -c, --config= The name of the app configuration. - -f, --force Release without asking for confirmation. + -f, --force Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. + --allow-deletes Allows removing extensions and configuration without requiring user confirmation. Required + for non-interactive release that removes any configuration or extensions. + --allow-updates Allows adding and updating extensions and configuration without requiring user confirmation. + Required for non-interactive release. --client-id= The Client ID of your app. --no-color Disable color output. --path= The path to your app directory. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 14650db5415..f5226ab146c 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -669,6 +669,22 @@ "description": "\"Builds the app\" (https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your \"web app\" (https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to \"deploy your web app\" (https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", "descriptionWithMarkdown": "[Builds the app](https://shopify.dev/docs/api/shopify-cli/app/app-build), then deploys your app configuration and extensions.\n\n This command creates an app version, which is a snapshot of your app configuration and all extensions. This version is then released to users.\n\n This command doesn't deploy your [web app](https://shopify.dev/docs/apps/tools/cli/structure#web-components). You need to [deploy your web app](https://shopify.dev/docs/apps/deployment/web) to your own hosting solution.\n ", "flags": { + "allow-deletes": { + "allowNo": false, + "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "env": "SHOPIFY_FLAG_ALLOW_DELETES", + "hidden": false, + "name": "allow-deletes", + "type": "boolean" + }, + "allow-updates": { + "allowNo": false, + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "hidden": false, + "name": "allow-updates", + "type": "boolean" + }, "client-id": { "description": "The Client ID of your app.", "env": "SHOPIFY_FLAG_CLIENT_ID", @@ -694,7 +710,7 @@ "force": { "allowNo": false, "char": "f", - "description": "Deploy without asking for confirmation.", + "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", "env": "SHOPIFY_FLAG_FORCE", "hidden": false, "name": "force", @@ -2715,6 +2731,22 @@ "description": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", "descriptionWithMarkdown": "Releases an existing app version. Pass the name of the version that you want to release using the `--version` flag.", "flags": { + "allow-deletes": { + "allowNo": false, + "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "env": "SHOPIFY_FLAG_ALLOW_DELETES", + "hidden": false, + "name": "allow-deletes", + "type": "boolean" + }, + "allow-updates": { + "allowNo": false, + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "env": "SHOPIFY_FLAG_ALLOW_UPDATES", + "hidden": false, + "name": "allow-updates", + "type": "boolean" + }, "client-id": { "description": "The Client ID of your app.", "env": "SHOPIFY_FLAG_CLIENT_ID", @@ -2740,7 +2772,7 @@ "force": { "allowNo": false, "char": "f", - "description": "Release without asking for confirmation.", + "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", "env": "SHOPIFY_FLAG_FORCE", "hidden": false, "name": "force", From 68279b0b913c3f0461ecff80006dc331ece49e67 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Mon, 12 Jan 2026 13:24:41 +0100 Subject: [PATCH 2/2] Recommend --allow-updates and improve errors on CI/CD --- .../interfaces/app-deploy.interface.ts | 8 ++--- .../interfaces/app-release.interface.ts | 6 ++-- .../generated/generated_docs_data.json | 18 +++++------ packages/app/src/cli/commands/app/deploy.ts | 32 +++++++++---------- packages/app/src/cli/commands/app/release.ts | 13 ++++---- .../app/src/cli/prompts/deploy-release.ts | 13 +++----- packages/cli/README.md | 16 ++++++---- packages/cli/oclif.manifest.json | 14 ++++---- 8 files changed, 59 insertions(+), 61 deletions(-) diff --git a/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts b/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts index f9fbe66cd92..08078c3575c 100644 --- a/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts +++ b/docs-shopify.dev/commands/interfaces/app-deploy.interface.ts @@ -1,13 +1,13 @@ // This is an autogenerated file. Don't edit this file manually. export interface appdeploy { /** - * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions. + * Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates. * @environment SHOPIFY_FLAG_ALLOW_DELETES */ '--allow-deletes'?: '' /** - * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release. + * Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments. * @environment SHOPIFY_FLAG_ALLOW_UPDATES */ '--allow-updates'?: '' @@ -25,7 +25,7 @@ export interface appdeploy { '-c, --config '?: string /** - * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. + * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates. * @environment SHOPIFY_FLAG_FORCE */ '-f, --force'?: '' @@ -49,7 +49,7 @@ export interface appdeploy { '--no-color'?: '' /** - * Creates a version but doesn't release it - it's not made available to merchants. + * Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required. * @environment SHOPIFY_FLAG_NO_RELEASE */ '--no-release'?: '' diff --git a/docs-shopify.dev/commands/interfaces/app-release.interface.ts b/docs-shopify.dev/commands/interfaces/app-release.interface.ts index 6b722ff4177..ad01162281e 100644 --- a/docs-shopify.dev/commands/interfaces/app-release.interface.ts +++ b/docs-shopify.dev/commands/interfaces/app-release.interface.ts @@ -1,13 +1,13 @@ // This is an autogenerated file. Don't edit this file manually. export interface apprelease { /** - * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions. + * Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates. * @environment SHOPIFY_FLAG_ALLOW_DELETES */ '--allow-deletes'?: '' /** - * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release. + * Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments. * @environment SHOPIFY_FLAG_ALLOW_UPDATES */ '--allow-updates'?: '' @@ -25,7 +25,7 @@ export interface apprelease { '-c, --config '?: string /** - * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. + * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates. * @environment SHOPIFY_FLAG_FORCE */ '-f, --force'?: '' diff --git a/docs-shopify.dev/generated/generated_docs_data.json b/docs-shopify.dev/generated/generated_docs_data.json index 112c6a07910..eb04908faa7 100644 --- a/docs-shopify.dev/generated/generated_docs_data.json +++ b/docs-shopify.dev/generated/generated_docs_data.json @@ -401,7 +401,7 @@ "syntaxKind": "PropertySignature", "name": "--allow-deletes", "value": "\"\"", - "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ALLOW_DELETES" }, @@ -410,7 +410,7 @@ "syntaxKind": "PropertySignature", "name": "--allow-updates", "value": "\"\"", - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ALLOW_UPDATES" }, @@ -455,7 +455,7 @@ "syntaxKind": "PropertySignature", "name": "--no-release", "value": "\"\"", - "description": "Creates a version but doesn't release it - it's not made available to merchants.", + "description": "Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_NO_RELEASE" }, @@ -518,12 +518,12 @@ "syntaxKind": "PropertySignature", "name": "-f, --force", "value": "\"\"", - "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", + "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_FORCE" } ], - "value": "export interface appdeploy {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message '?: string\n\n /**\n * Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.\n * @environment SHOPIFY_FLAG_NO_BUILD\n */\n '--no-build'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n}" + "value": "export interface appdeploy {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Optional message that will be associated with this version. This is for internal use only and won't be available externally.\n * @environment SHOPIFY_FLAG_MESSAGE\n */\n '--message '?: string\n\n /**\n * Use with caution: Skips building any elements of the app that require building. You should ensure your app has been prepared in advance, such as by running `shopify app build` or by caching build artifacts.\n * @environment SHOPIFY_FLAG_NO_BUILD\n */\n '--no-build'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.\n * @environment SHOPIFY_FLAG_NO_RELEASE\n */\n '--no-release'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * URL associated with the new app version.\n * @environment SHOPIFY_FLAG_SOURCE_CONTROL_URL\n */\n '--source-control-url '?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Optional version tag that will be associated with this app version. If not provided, an auto-generated identifier will be generated for this app version.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version '?: string\n}" } } } @@ -2444,7 +2444,7 @@ "syntaxKind": "PropertySignature", "name": "--allow-deletes", "value": "\"\"", - "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ALLOW_DELETES" }, @@ -2453,7 +2453,7 @@ "syntaxKind": "PropertySignature", "name": "--allow-updates", "value": "\"\"", - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ALLOW_UPDATES" }, @@ -2524,12 +2524,12 @@ "syntaxKind": "PropertySignature", "name": "-f, --force", "value": "\"\"", - "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", + "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.", "isOptional": true, "environmentValue": "SHOPIFY_FLAG_FORCE" } ], - "value": "export interface apprelease {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The name of the app version to release.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version ': string\n}" + "value": "export interface apprelease {\n /**\n * Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.\n * @environment SHOPIFY_FLAG_ALLOW_DELETES\n */\n '--allow-deletes'?: ''\n\n /**\n * Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.\n * @environment SHOPIFY_FLAG_ALLOW_UPDATES\n */\n '--allow-updates'?: ''\n\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id '?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config '?: string\n\n /**\n * Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '-f, --force'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * The name of the app version to release.\n * @environment SHOPIFY_FLAG_VERSION\n */\n '--version ': string\n}" } } } diff --git a/packages/app/src/cli/commands/app/deploy.ts b/packages/app/src/cli/commands/app/deploy.ts index 6d08855cce0..1f169c0b98d 100644 --- a/packages/app/src/cli/commands/app/deploy.ts +++ b/packages/app/src/cli/commands/app/deploy.ts @@ -26,25 +26,27 @@ export default class Deploy extends AppLinkedCommand { ...appFlags, force: Flags.boolean({ hidden: false, - description: 'Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.', + description: + 'Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.', env: 'SHOPIFY_FLAG_FORCE', char: 'f', }), 'allow-updates': Flags.boolean({ hidden: false, description: - 'Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.', + 'Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.', env: 'SHOPIFY_FLAG_ALLOW_UPDATES', }), 'allow-deletes': Flags.boolean({ hidden: false, description: - 'Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.', + 'Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.', env: 'SHOPIFY_FLAG_ALLOW_DELETES', }), 'no-release': Flags.boolean({ hidden: false, - description: "Creates a version but doesn't release it - it's not made available to merchants.", + description: + "Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.", env: 'SHOPIFY_FLAG_NO_RELEASE', default: false, }), @@ -91,17 +93,15 @@ export default class Deploy extends AppLinkedCommand { cmd_app_reset_used: flags.reset, })) - // When using --no-release, we don't require --force or --allow-updates/--allow-deletes for non-TTY - // because we're just creating a version, not releasing it. Validation happens at release time. - // When releasing (no --no-release), we require either --force or --allow-updates for non-TTY. - // The --allow-deletes flag is only required when there are actual deletions (validated at runtime). + const force = flags.force || flags['no-release'] + + // When releasing, we require --force or --allow-updates or --allow-deletes for non-TTY. const requiredNonTTYFlags: string[] = [] - if (!flags['no-release']) { - const hasAnyForceFlags = flags.force || flags['allow-updates'] || flags['allow-deletes'] - if (!hasAnyForceFlags) { - requiredNonTTYFlags.push('force') - } + const hasAnyForceFlags = force || flags['allow-updates'] || flags['allow-deletes'] + if (!hasAnyForceFlags) { + requiredNonTTYFlags.push('allow-updates') } + this.failMissingNonTTYFlags(flags, requiredNonTTYFlags) const {app, remoteApp, developerPlatformClient, organization} = await linkedAppContext({ directory: flags.path, @@ -110,8 +110,8 @@ export default class Deploy extends AppLinkedCommand { userProvidedConfigName: flags.config, }) - const allowUpdates = flags.force || flags['allow-updates'] - const allowDeletes = flags.force || flags['allow-deletes'] + const allowUpdates = force || flags['allow-updates'] + const allowDeletes = force || flags['allow-deletes'] const result = await deploy({ app, @@ -119,7 +119,7 @@ export default class Deploy extends AppLinkedCommand { organization, developerPlatformClient, reset: flags.reset, - force: flags.force, + force, allowUpdates, allowDeletes, noRelease: flags['no-release'], diff --git a/packages/app/src/cli/commands/app/release.ts b/packages/app/src/cli/commands/app/release.ts index ec3aaeb6f6a..6fe89496ed1 100644 --- a/packages/app/src/cli/commands/app/release.ts +++ b/packages/app/src/cli/commands/app/release.ts @@ -21,20 +21,21 @@ export default class Release extends AppLinkedCommand { ...appFlags, force: Flags.boolean({ hidden: false, - description: 'Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.', + description: + 'Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.', env: 'SHOPIFY_FLAG_FORCE', char: 'f', }), 'allow-updates': Flags.boolean({ hidden: false, description: - 'Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.', + 'Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.', env: 'SHOPIFY_FLAG_ALLOW_UPDATES', }), 'allow-deletes': Flags.boolean({ hidden: false, description: - 'Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.', + 'Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.', env: 'SHOPIFY_FLAG_ALLOW_DELETES', }), version: Flags.string({ @@ -53,12 +54,11 @@ export default class Release extends AppLinkedCommand { cmd_app_reset_used: flags.reset, })) - // For releases, require either --force or --allow-updates for non-TTY - // The --allow-deletes flag is only required when there are actual deletions (validated at runtime). + // We require --force or --allow-updates or --allow-deletes for non-TTY. const requiredNonTTYFlags: string[] = [] const hasAnyForceFlags = flags.force || flags['allow-updates'] || flags['allow-deletes'] if (!hasAnyForceFlags) { - requiredNonTTYFlags.push('force') + requiredNonTTYFlags.push('allow-updates') } const configurationState = await getAppConfigurationState(flags.path, flags.config) if (configurationState.state === 'template-only' && !clientId) { @@ -73,7 +73,6 @@ export default class Release extends AppLinkedCommand { userProvidedConfigName: flags.config, }) - // --force is a synonym for --allow-updates --allow-deletes const allowUpdates = flags.force || flags['allow-updates'] const allowDeletes = flags.force || flags['allow-deletes'] diff --git a/packages/app/src/cli/prompts/deploy-release.ts b/packages/app/src/cli/prompts/deploy-release.ts index e3707f455b0..5bb428a5109 100644 --- a/packages/app/src/cli/prompts/deploy-release.ts +++ b/packages/app/src/cli/prompts/deploy-release.ts @@ -70,21 +70,18 @@ function shouldSkipConfirmationPrompt({ (configExtensionIdentifiersBreakdown?.existingUpdatedFieldNames.length ?? 0) > 0 const hasUpdates = hasNewOrUpdatedExtensions || hasNewOrUpdatedConfig - // If we only have updates (no deletes) and allowUpdates is true, skip prompt + // Skip prompt if we only have updates or deletes and the corresponding allow flag is true if (allowUpdates && !hasDeletes) return true - - // If we only have deletes (no updates) and allowDeletes is true, skip prompt if (allowDeletes && !hasUpdates) return true // If we're in non-TTY mode and there are changes that require confirmation, throw an error if (!isTTY() && (hasDeletes || hasUpdates)) { + let suggestedFlag = '--force' + if (hasUpdates && !hasDeletes) suggestedFlag = '--allow-updates' + if (hasDeletes && !hasUpdates) suggestedFlag = '--allow-deletes' throw new AbortError('This deployment includes changes that require confirmation.', [ 'Run the command with', - {command: '--force'}, - 'or', - {command: '--allow-updates'}, - 'or', - {command: '--allow-deletes'}, + {command: suggestedFlag}, 'to deploy without confirmation.', ]) } diff --git a/packages/cli/README.md b/packages/cli/README.md index 501ad0460bc..407a03ae468 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -207,11 +207,11 @@ USAGE FLAGS -c, --config= The name of the app configuration. -f, --force Deploy without asking for confirmation. Equivalent to --allow-updates - --allow-deletes. + --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates. --allow-deletes Allows removing extensions and configuration without requiring user confirmation. - Required for non-interactive release that removes any configuration or extensions. + For CI/CD environments, the recommended flag is --allow-updates. --allow-updates Allows adding and updating extensions and configuration without requiring user - confirmation. Required for non-interactive release. + confirmation. Recommended option for CI/CD environments. --client-id= The Client ID of your app. --message= Optional message that will be associated with this version. This is for internal use only and won't be available externally. @@ -220,6 +220,7 @@ FLAGS build` or by caching build artifacts. --no-color Disable color output. --no-release Creates a version but doesn't release it - it's not made available to merchants. + With this flag, a user confirmation is not required. --path= The path to your app directory. --reset Reset all your settings. --source-control-url= URL associated with the new app version. @@ -726,11 +727,12 @@ USAGE FLAGS -c, --config= The name of the app configuration. - -f, --force Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. - --allow-deletes Allows removing extensions and configuration without requiring user confirmation. Required - for non-interactive release that removes any configuration or extensions. + -f, --force Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For + CI/CD environments, the recommended flag is --allow-updates. + --allow-deletes Allows removing extensions and configuration without requiring user confirmation. For CI/CD + environments, the recommended flag is --allow-updates. --allow-updates Allows adding and updating extensions and configuration without requiring user confirmation. - Required for non-interactive release. + Recommended option for CI/CD environments. --client-id= The Client ID of your app. --no-color Disable color output. --path= The path to your app directory. diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index f5226ab146c..887b8eb591e 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -671,7 +671,7 @@ "flags": { "allow-deletes": { "allowNo": false, - "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", "env": "SHOPIFY_FLAG_ALLOW_DELETES", "hidden": false, "name": "allow-deletes", @@ -679,7 +679,7 @@ }, "allow-updates": { "allowNo": false, - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", "env": "SHOPIFY_FLAG_ALLOW_UPDATES", "hidden": false, "name": "allow-updates", @@ -710,7 +710,7 @@ "force": { "allowNo": false, "char": "f", - "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", + "description": "Deploy without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.", "env": "SHOPIFY_FLAG_FORCE", "hidden": false, "name": "force", @@ -742,7 +742,7 @@ }, "no-release": { "allowNo": false, - "description": "Creates a version but doesn't release it - it's not made available to merchants.", + "description": "Creates a version but doesn't release it - it's not made available to merchants. With this flag, a user confirmation is not required.", "env": "SHOPIFY_FLAG_NO_RELEASE", "hidden": false, "name": "no-release", @@ -2733,7 +2733,7 @@ "flags": { "allow-deletes": { "allowNo": false, - "description": "Allows removing extensions and configuration without requiring user confirmation. Required for non-interactive release that removes any configuration or extensions.", + "description": "Allows removing extensions and configuration without requiring user confirmation. For CI/CD environments, the recommended flag is --allow-updates.", "env": "SHOPIFY_FLAG_ALLOW_DELETES", "hidden": false, "name": "allow-deletes", @@ -2741,7 +2741,7 @@ }, "allow-updates": { "allowNo": false, - "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Required for non-interactive release.", + "description": "Allows adding and updating extensions and configuration without requiring user confirmation. Recommended option for CI/CD environments.", "env": "SHOPIFY_FLAG_ALLOW_UPDATES", "hidden": false, "name": "allow-updates", @@ -2772,7 +2772,7 @@ "force": { "allowNo": false, "char": "f", - "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes.", + "description": "Release without asking for confirmation. Equivalent to --allow-updates --allow-deletes. For CI/CD environments, the recommended flag is --allow-updates.", "env": "SHOPIFY_FLAG_FORCE", "hidden": false, "name": "force",