From baee1cfa9cb1a5df8220e5fc0aab2ffcbffffee3 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Tue, 24 Mar 2026 11:46:14 +0100 Subject: [PATCH 1/5] Add install count warning for safer deploys --- .../generated/app-install-count.ts | 51 +++++++++++++++++++ .../queries/app-install-count.graphql | 5 ++ .../app/src/cli/models/app/app.test-data.ts | 1 + .../app/src/cli/prompts/deploy-release.ts | 11 ++++ .../src/cli/services/context/identifiers.ts | 15 ++++++ .../utilities/developer-platform-client.ts | 1 + .../app-management-client.ts | 8 +++ .../partners-client.ts | 5 ++ 8 files changed, 97 insertions(+) create mode 100644 packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts create mode 100644 packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql diff --git a/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts b/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts new file mode 100644 index 00000000000..b9a478c01a2 --- /dev/null +++ b/packages/app/src/cli/api/graphql/app-management/generated/app-install-count.ts @@ -0,0 +1,51 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ +import * as Types from './types.js' + +import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core' + +export type AppInstallCountQueryVariables = Types.Exact<{ + appId: Types.Scalars['ID']['input'] +}> + +export type AppInstallCountQuery = {app: {installCount?: number | null}} + +export const AppInstallCount = { + kind: 'Document', + definitions: [ + { + kind: 'OperationDefinition', + operation: 'query', + name: {kind: 'Name', value: 'AppInstallCount'}, + variableDefinitions: [ + { + kind: 'VariableDefinition', + variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + { + kind: 'Field', + name: {kind: 'Name', value: 'app'}, + arguments: [ + { + kind: 'Argument', + name: {kind: 'Name', value: 'id'}, + value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}}, + }, + ], + selectionSet: { + kind: 'SelectionSet', + selections: [ + {kind: 'Field', name: {kind: 'Name', value: 'installCount'}}, + {kind: 'Field', name: {kind: 'Name', value: '__typename'}}, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode diff --git a/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql b/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql new file mode 100644 index 00000000000..e5495a3fff4 --- /dev/null +++ b/packages/app/src/cli/api/graphql/app-management/queries/app-install-count.graphql @@ -0,0 +1,5 @@ +query AppInstallCount($appId: ID!) { + app(id: $appId) { + installCount + } +} diff --git a/packages/app/src/cli/models/app/app.test-data.ts b/packages/app/src/cli/models/app/app.test-data.ts index 838f8b12d20..e13448952f6 100644 --- a/packages/app/src/cli/models/app/app.test-data.ts +++ b/packages/app/src/cli/models/app/app.test-data.ts @@ -1376,6 +1376,7 @@ export function testDeveloperPlatformClient(stubs: Partial Promise.resolve(testOrganization()), appsForOrg: (_organizationId: string) => Promise.resolve({apps: [testOrganizationApp()], hasMorePages: false}), specifications: (_app: MinimalAppIdentifiers) => Promise.resolve(testRemoteSpecifications), + appInstallCount: (_app: MinimalAppIdentifiers) => Promise.resolve(0), templateSpecifications: (_app: MinimalAppIdentifiers) => Promise.resolve({templates: testRemoteExtensionTemplates, groupOrder: []}), orgAndApps: (_orgId: string) => diff --git a/packages/app/src/cli/prompts/deploy-release.ts b/packages/app/src/cli/prompts/deploy-release.ts index 5198b205e25..d91a00b4468 100644 --- a/packages/app/src/cli/prompts/deploy-release.ts +++ b/packages/app/src/cli/prompts/deploy-release.ts @@ -24,6 +24,7 @@ interface DeployOrReleaseConfirmationPromptOptions { /** If true, allow removing extensions and configuration without user confirmation */ allowDeletes?: boolean showConfig?: boolean + installCount?: number } interface DeployConfirmationPromptOptions { @@ -36,6 +37,7 @@ interface DeployConfirmationPromptOptions { configInfoTable: InfoTableSection } release: boolean + installCount?: number } /** @@ -97,6 +99,7 @@ export async function deployOrReleaseConfirmationPrompt({ configExtensionIdentifiersBreakdown, appTitle, release, + installCount, }: DeployOrReleaseConfirmationPromptOptions): Promise { await metadata.addPublicMetadata(() => buildConfigurationBreakdownMetadata(configExtensionIdentifiersBreakdown)) @@ -117,6 +120,7 @@ export async function deployOrReleaseConfirmationPrompt({ extensionsContentPrompt, configContentPrompt, release, + installCount, }) } @@ -125,6 +129,7 @@ async function deployConfirmationPrompt({ extensionsContentPrompt: {extensionsInfoTable, hasDeletedExtensions}, configContentPrompt, release, + installCount, }: DeployConfirmationPromptOptions): Promise { const timeBeforeConfirmationMs = new Date().valueOf() let confirmationResponse = true @@ -149,11 +154,17 @@ async function deployConfirmationPrompt({ } const question = `${release ? 'Release' : 'Create'} a new version${appTitle ? ` of ${appTitle}` : ''}?` + const showInstallCountWarning = hasDeletedExtensions && installCount !== undefined && installCount > 0 if (isDangerous) { confirmationResponse = await renderDangerousConfirmationPrompt({ message: question, infoTable, confirmation: appTitle, + ...(showInstallCountWarning + ? { + body: `This release removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`, + } + : {}), }) } else { confirmationResponse = await renderConfirmationPrompt({ diff --git a/packages/app/src/cli/services/context/identifiers.ts b/packages/app/src/cli/services/context/identifiers.ts index 247ab71bfef..6aa9dba461f 100644 --- a/packages/app/src/cli/services/context/identifiers.ts +++ b/packages/app/src/cli/services/context/identifiers.ts @@ -58,6 +58,20 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr activeAppVersion: options.activeAppVersion, }) + let installCount: number | undefined + if (extensionIdentifiersBreakdown.onlyRemote.length > 0) { + try { + installCount = await options.developerPlatformClient.appInstallCount({ + id: options.appId, + apiKey: options.remoteApp.apiKey, + organizationId: options.remoteApp.organizationId, + }) + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (_error) { + installCount = undefined + } + } + const confirmed = await deployOrReleaseConfirmationPrompt({ extensionIdentifiersBreakdown, configExtensionIdentifiersBreakdown, @@ -66,6 +80,7 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr force: options.force, allowUpdates: options.allowUpdates, allowDeletes: options.allowDeletes, + installCount, }) if (!confirmed) throw new AbortSilentError() diff --git a/packages/app/src/cli/utilities/developer-platform-client.ts b/packages/app/src/cli/utilities/developer-platform-client.ts index 8b27e3b501a..e8d792f0de1 100644 --- a/packages/app/src/cli/utilities/developer-platform-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client.ts @@ -260,6 +260,7 @@ export interface DeveloperPlatformClient { activeAppVersion?: AppVersion, ) => Promise appVersions: (app: OrganizationApp) => Promise + appInstallCount: (app: MinimalAppIdentifiers) => Promise activeAppVersion: (app: MinimalAppIdentifiers) => Promise appVersionByTag: (app: MinimalOrganizationApp, tag: string) => Promise appVersionsDiff: (app: MinimalOrganizationApp, version: AppVersionIdentifiers) => Promise diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index fcf8c5232b8..65d43386920 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -114,6 +114,7 @@ import { import {CreateAssetUrl} from '../../api/graphql/app-management/generated/create-asset-url.js' import {AppVersionById} from '../../api/graphql/app-management/generated/app-version-by-id.js' import {AppVersions} from '../../api/graphql/app-management/generated/app-versions.js' +import {AppInstallCount} from '../../api/graphql/app-management/generated/app-install-count.js' import {CreateApp, CreateAppMutationVariables} from '../../api/graphql/app-management/generated/create-app.js' import {FetchSpecifications} from '../../api/graphql/app-management/generated/specifications.js' import {ListApps} from '../../api/graphql/app-management/generated/apps.js' @@ -644,6 +645,13 @@ export class AppManagementClient implements DeveloperPlatformClient { } } + async appInstallCount({id}: MinimalAppIdentifiers): Promise { + const query = AppInstallCount + const variables = {appId: id} + const result = await this.appManagementRequest({query, variables}) + return result.app.installCount + } + async appVersionByTag( {id: appId, organizationId}: MinimalOrganizationApp, versionTag: string, diff --git a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts index 9f54e23a50e..08c73410fa1 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/partners-client.ts @@ -428,6 +428,11 @@ export class PartnersClient implements DeveloperPlatformClient { return this.request(AppVersionsQuery, variables) } + async appInstallCount(_app: MinimalAppIdentifiers): Promise { + // Install count is not supported in partners client. + throw new Error('Unsupported operation') + } + async appVersionByTag({apiKey}: MinimalOrganizationApp, versionTag: string): Promise { const input: AppVersionByTagVariables = {apiKey, versionTag} const result: AppVersionByTagSchema = await this.request(AppVersionByTagQuery, input) From cc2c52d703b5c8544e16134e09791ff2cb6525fa Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Thu, 9 Apr 2026 21:06:27 +0200 Subject: [PATCH 2/5] Fix TypeScript type error: handle null/undefined installCount --- .../developer-platform-client/app-management-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 65d43386920..1ab8d93dbef 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -649,7 +649,7 @@ export class AppManagementClient implements DeveloperPlatformClient { const query = AppInstallCount const variables = {appId: id} const result = await this.appManagementRequest({query, variables}) - return result.app.installCount + return result.app.installCount ?? 0 } async appVersionByTag( From 040a4302fd000ce606895530ff8e83e6eaf48b5d Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Thu, 9 Apr 2026 21:29:47 +0200 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=203=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Warning body now says 'version' instead of 'release' on deploy-without-release path 2. Skip appInstallCount API call when prompt will be skipped (--force / --allow-updates+--allow-deletes) 3. Pass remoteApp.id (Management API app ID) instead of options.appId (API key) to appInstallCount 4. Add nullish coalesce for installCount in AppManagementClient (type safety) --- packages/app/src/cli/prompts/deploy-release.ts | 2 +- packages/app/src/cli/services/context/identifiers.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/app/src/cli/prompts/deploy-release.ts b/packages/app/src/cli/prompts/deploy-release.ts index d91a00b4468..687f6dfe1b5 100644 --- a/packages/app/src/cli/prompts/deploy-release.ts +++ b/packages/app/src/cli/prompts/deploy-release.ts @@ -162,7 +162,7 @@ async function deployConfirmationPrompt({ confirmation: appTitle, ...(showInstallCountWarning ? { - body: `This release removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`, + body: `This ${release ? 'release' : 'version'} removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`, } : {}), }) diff --git a/packages/app/src/cli/services/context/identifiers.ts b/packages/app/src/cli/services/context/identifiers.ts index 6aa9dba461f..4584c66ed0b 100644 --- a/packages/app/src/cli/services/context/identifiers.ts +++ b/packages/app/src/cli/services/context/identifiers.ts @@ -58,11 +58,16 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr activeAppVersion: options.activeAppVersion, }) + const shouldFetchInstallCount = + extensionIdentifiersBreakdown.onlyRemote.length > 0 && + !options.force && + !(options.allowUpdates && options.allowDeletes) + let installCount: number | undefined - if (extensionIdentifiersBreakdown.onlyRemote.length > 0) { + if (shouldFetchInstallCount) { try { installCount = await options.developerPlatformClient.appInstallCount({ - id: options.appId, + id: options.remoteApp.id, apiKey: options.remoteApp.apiKey, organizationId: options.remoteApp.organizationId, }) From 8420572493e45a0f8b7a987e2d4b403cc09e2286 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 10 Apr 2026 14:36:57 +0200 Subject: [PATCH 4/5] test: add coverage for installCount in deploy prompt and identifiers - deploy-release: verify installCount is passed to danger prompt when > 0 - deploy-release: verify installCount is omitted when 0 - identifiers: verify appInstallCount called with remoteApp.id (not appId) - identifiers: verify appInstallCount skipped when --force - identifiers: verify appInstallCount skipped when --allow-updates + --allow-deletes - identifiers: verify graceful degradation when appInstallCount throws --- .../src/cli/prompts/deploy-release.test.ts | 54 ++++++ .../app/src/cli/prompts/deploy-release.ts | 6 +- .../cli/services/context/identifiers.test.ts | 155 +++++++++++++++++- .../DangerousConfirmationPrompt.tsx | 8 + 4 files changed, 220 insertions(+), 3 deletions(-) diff --git a/packages/app/src/cli/prompts/deploy-release.test.ts b/packages/app/src/cli/prompts/deploy-release.test.ts index e7286c555f6..68822bbca9d 100644 --- a/packages/app/src/cli/prompts/deploy-release.test.ts +++ b/packages/app/src/cli/prompts/deploy-release.test.ts @@ -206,6 +206,60 @@ describe('deployOrReleaseConfirmationPrompt', () => { expect(result).toBe(true) }) + test('and no force with deleted extensions and installCount should pass installCount to danger prompt', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + const renderDangerousConfirmationPromptSpyOn = vi + .spyOn(ui, 'renderDangerousConfirmationPrompt') + .mockResolvedValue(true) + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + const appTitle = 'app title' + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle, + release: true, + force: false, + installCount: 1243, + }) + + // Then + expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith( + expect.objectContaining({ + installCount: 1243, + }), + ) + expect(result).toBe(true) + }) + + test('and no force with deleted extensions but installCount 0 should not pass installCount to danger prompt', async () => { + // Given + const breakdownInfo = buildCompleteBreakdownInfo() + const renderDangerousConfirmationPromptSpyOn = vi + .spyOn(ui, 'renderDangerousConfirmationPrompt') + .mockResolvedValue(true) + vi.spyOn(metadata, 'addPublicMetadata').mockImplementation(async () => {}) + const appTitle = 'app title' + + // When + const result = await deployOrReleaseConfirmationPrompt({ + ...breakdownInfo, + appTitle, + release: true, + force: false, + installCount: 0, + }) + + // Then + expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith( + expect.not.objectContaining({ + installCount: expect.anything(), + }), + ) + expect(result).toBe(true) + }) + test('and no force with deleted extensions but without app title should display the complete confirmation prompt', async () => { // Given const breakdownInfo = buildCompleteBreakdownInfo() diff --git a/packages/app/src/cli/prompts/deploy-release.ts b/packages/app/src/cli/prompts/deploy-release.ts index 687f6dfe1b5..40b91d5b57a 100644 --- a/packages/app/src/cli/prompts/deploy-release.ts +++ b/packages/app/src/cli/prompts/deploy-release.ts @@ -162,7 +162,11 @@ async function deployConfirmationPrompt({ confirmation: appTitle, ...(showInstallCountWarning ? { - body: `This ${release ? 'release' : 'version'} removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`, + warningItem: [ + 'This release removes extensions and related data from', + {error: installCount.toString()}, + 'app installations.\nUse caution as this may include production data on live stores.', + ], } : {}), }) diff --git a/packages/app/src/cli/services/context/identifiers.test.ts b/packages/app/src/cli/services/context/identifiers.test.ts index 97e0351c489..b24fb50ccdf 100644 --- a/packages/app/src/cli/services/context/identifiers.test.ts +++ b/packages/app/src/cli/services/context/identifiers.test.ts @@ -1,4 +1,9 @@ -import {configExtensionsIdentifiersBreakdown, extensionsIdentifiersDeployBreakdown} from './breakdown-extensions.js' +import { + buildExtensionBreakdownInfo, + configExtensionsIdentifiersBreakdown, + ExtensionIdentifierBreakdownInfo, + extensionsIdentifiersDeployBreakdown, +} from './breakdown-extensions.js' import {ensureDeploymentIdsPresence} from './identifiers.js' import {deployConfirmed} from './identifiers-extensions.js' import {deployOrReleaseConfirmationPrompt} from '../../prompts/deploy-release.js' @@ -34,6 +39,152 @@ describe('ensureDeploymentIdsPresence', () => { await expect(ensureDeploymentIdsPresence(params)).rejects.toThrow(AbortSilentError) }) + test('when there are remote-only extensions and not forced, appInstallCount is called with remoteApp.id', async () => { + // Given + const breakdown = buildExtensionsBreakdown() + breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')] + vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown) + vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown()) + vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(false) + + const remoteApp = testOrganizationApp({id: 'real-app-id', apiKey: 'api-key-different'}) + const client = testDeveloperPlatformClient({ + appInstallCount: vi.fn().mockResolvedValue(42), + }) + + const params = { + app: testApp(), + developerPlatformClient: client, + appId: 'api-key-different', + appName: 'appName', + remoteApp, + envIdentifiers: {}, + force: false, + release: true, + } + + // When + await expect(ensureDeploymentIdsPresence(params)).rejects.toThrow() + + // Then + expect(client.appInstallCount).toHaveBeenCalledWith({ + id: 'real-app-id', + apiKey: 'api-key-different', + organizationId: remoteApp.organizationId, + }) + }) + + test('when force is true, appInstallCount is not called even with remote-only extensions', async () => { + // Given + const breakdown = buildExtensionsBreakdown() + breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')] + vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown) + vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown()) + vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true) + vi.mocked(deployConfirmed).mockResolvedValue({ + extensions: {}, + extensionIds: {}, + extensionsNonUuidManaged: {}, + }) + + const client = testDeveloperPlatformClient({ + appInstallCount: vi.fn().mockResolvedValue(42), + }) + + const params = { + app: testApp(), + developerPlatformClient: client, + appId: 'appId', + appName: 'appName', + remoteApp: testOrganizationApp(), + envIdentifiers: {}, + force: true, + release: true, + } + + // When + await ensureDeploymentIdsPresence(params) + + // Then + expect(client.appInstallCount).not.toHaveBeenCalled() + }) + + test('when allowUpdates and allowDeletes are both true, appInstallCount is not called', async () => { + // Given + const breakdown = buildExtensionsBreakdown() + breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')] + vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown) + vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown()) + vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true) + vi.mocked(deployConfirmed).mockResolvedValue({ + extensions: {}, + extensionIds: {}, + extensionsNonUuidManaged: {}, + }) + + const client = testDeveloperPlatformClient({ + appInstallCount: vi.fn().mockResolvedValue(42), + }) + + const params = { + app: testApp(), + developerPlatformClient: client, + appId: 'appId', + appName: 'appName', + remoteApp: testOrganizationApp(), + envIdentifiers: {}, + force: false, + allowUpdates: true, + allowDeletes: true, + release: true, + } + + // When + await ensureDeploymentIdsPresence(params) + + // Then + expect(client.appInstallCount).not.toHaveBeenCalled() + }) + + test('when appInstallCount throws, installCount is undefined and deploy proceeds', async () => { + // Given + const breakdown = buildExtensionsBreakdown() + breakdown.extensionIdentifiersBreakdown.onlyRemote = [buildExtensionBreakdownInfo('removed', 'uuid-1')] + vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(breakdown) + vi.mocked(configExtensionsIdentifiersBreakdown).mockResolvedValue(buildConfigBreakdown()) + vi.mocked(deployOrReleaseConfirmationPrompt).mockResolvedValue(true) + vi.mocked(deployConfirmed).mockResolvedValue({ + extensions: {}, + extensionIds: {}, + extensionsNonUuidManaged: {}, + }) + + const client = testDeveloperPlatformClient({ + appInstallCount: vi.fn().mockRejectedValue(new Error('API error')), + }) + + const params = { + app: testApp(), + developerPlatformClient: client, + appId: 'appId', + appName: 'appName', + remoteApp: testOrganizationApp(), + envIdentifiers: {}, + force: false, + release: true, + } + + // When + await ensureDeploymentIdsPresence(params) + + // Then - installCount should be undefined in the prompt call + expect(deployOrReleaseConfirmationPrompt).toHaveBeenCalledWith( + expect.objectContaining({ + installCount: undefined, + }), + ) + }) + test('when the prompt is confirmed post-confirmation actions as run and the result is returned', async () => { // Given vi.mocked(extensionsIdentifiersDeployBreakdown).mockResolvedValue(buildExtensionsBreakdown()) @@ -75,7 +226,7 @@ describe('ensureDeploymentIdsPresence', () => { function buildExtensionsBreakdown() { return { extensionIdentifiersBreakdown: { - onlyRemote: [], + onlyRemote: [] as ExtensionIdentifierBreakdownInfo[], toCreate: [], toUpdate: [], fromDashboard: [], diff --git a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx index 533b20ac8c3..2a31e5756ad 100644 --- a/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx +++ b/packages/cli-kit/src/private/node/ui/components/DangerousConfirmationPrompt.tsx @@ -16,6 +16,7 @@ export interface DangerousConfirmationPromptProps { message: string confirmation: string infoTable?: InfoTableProps['table'] + warningItem?: TokenItem onSubmit: (value: boolean) => void abortSignal?: AbortSignal } @@ -24,6 +25,7 @@ const DangerousConfirmationPrompt: FunctionComponent { @@ -103,6 +105,12 @@ const DangerousConfirmationPrompt: FunctionComponent ) : null} + {warningItem ? ( + + {figures.warning} WARNING + + + ) : null} From c4f4b2b1b71ec7a253b26924b62da4541e7d66a2 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 10 Apr 2026 18:08:29 +0200 Subject: [PATCH 5/5] fix: align test assertions with warningItem prop (was installCount) --- packages/app/src/cli/prompts/deploy-release.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/app/src/cli/prompts/deploy-release.test.ts b/packages/app/src/cli/prompts/deploy-release.test.ts index 68822bbca9d..a0121f6c176 100644 --- a/packages/app/src/cli/prompts/deploy-release.test.ts +++ b/packages/app/src/cli/prompts/deploy-release.test.ts @@ -227,13 +227,13 @@ describe('deployOrReleaseConfirmationPrompt', () => { // Then expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith( expect.objectContaining({ - installCount: 1243, + warningItem: expect.arrayContaining([{error: '1243'}]), }), ) expect(result).toBe(true) }) - test('and no force with deleted extensions but installCount 0 should not pass installCount to danger prompt', async () => { + test('and no force with deleted extensions but installCount 0 should not pass warningItem to danger prompt', async () => { // Given const breakdownInfo = buildCompleteBreakdownInfo() const renderDangerousConfirmationPromptSpyOn = vi @@ -254,7 +254,7 @@ describe('deployOrReleaseConfirmationPrompt', () => { // Then expect(renderDangerousConfirmationPromptSpyOn).toHaveBeenCalledWith( expect.not.objectContaining({ - installCount: expect.anything(), + warningItem: expect.anything(), }), ) expect(result).toBe(true)