Skip to content

Commit 40cf609

Browse files
Add install count warning for safer deploys
1 parent 1ab6618 commit 40cf609

File tree

7 files changed

+96
-0
lines changed

7 files changed

+96
-0
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/* eslint-disable @typescript-eslint/consistent-type-definitions */
2+
import * as Types from './types.js'
3+
4+
import {TypedDocumentNode as DocumentNode} from '@graphql-typed-document-node/core'
5+
6+
export type AppInstallCountQueryVariables = Types.Exact<{
7+
appId: Types.Scalars['ID']['input']
8+
}>
9+
10+
export type AppInstallCountQuery = {
11+
app: {
12+
installCount: number
13+
}
14+
}
15+
16+
export const AppInstallCount = {
17+
kind: 'Document',
18+
definitions: [
19+
{
20+
kind: 'OperationDefinition',
21+
operation: 'query',
22+
name: {kind: 'Name', value: 'AppInstallCount'},
23+
variableDefinitions: [
24+
{
25+
kind: 'VariableDefinition',
26+
variable: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
27+
type: {kind: 'NonNullType', type: {kind: 'NamedType', name: {kind: 'Name', value: 'ID'}}},
28+
},
29+
],
30+
selectionSet: {
31+
kind: 'SelectionSet',
32+
selections: [
33+
{
34+
kind: 'Field',
35+
name: {kind: 'Name', value: 'app'},
36+
arguments: [
37+
{
38+
kind: 'Argument',
39+
name: {kind: 'Name', value: 'id'},
40+
value: {kind: 'Variable', name: {kind: 'Name', value: 'appId'}},
41+
},
42+
],
43+
selectionSet: {
44+
kind: 'SelectionSet',
45+
selections: [
46+
{kind: 'Field', name: {kind: 'Name', value: 'installCount'}},
47+
{kind: 'Field', name: {kind: 'Name', value: '__typename'}},
48+
],
49+
},
50+
},
51+
],
52+
},
53+
},
54+
],
55+
} as unknown as DocumentNode<AppInstallCountQuery, AppInstallCountQueryVariables>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
query AppInstallCount($appId: ID!) {
2+
app(id: $appId) {
3+
installCount
4+
}
5+
}

packages/app/src/cli/prompts/deploy-release.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ interface DeployOrReleaseConfirmationPromptOptions {
2424
/** If true, allow removing extensions and configuration without user confirmation */
2525
allowDeletes?: boolean
2626
showConfig?: boolean
27+
installCount?: number
2728
}
2829

2930
interface DeployConfirmationPromptOptions {
@@ -36,6 +37,7 @@ interface DeployConfirmationPromptOptions {
3637
configInfoTable: InfoTableSection
3738
}
3839
release: boolean
40+
installCount?: number
3941
}
4042

4143
/**
@@ -97,6 +99,7 @@ export async function deployOrReleaseConfirmationPrompt({
9799
configExtensionIdentifiersBreakdown,
98100
appTitle,
99101
release,
102+
installCount,
100103
}: DeployOrReleaseConfirmationPromptOptions): Promise<boolean> {
101104
await metadata.addPublicMetadata(() => buildConfigurationBreakdownMetadata(configExtensionIdentifiersBreakdown))
102105

@@ -117,6 +120,7 @@ export async function deployOrReleaseConfirmationPrompt({
117120
extensionsContentPrompt,
118121
configContentPrompt,
119122
release,
123+
installCount,
120124
})
121125
}
122126

@@ -125,6 +129,7 @@ async function deployConfirmationPrompt({
125129
extensionsContentPrompt: {extensionsInfoTable, hasDeletedExtensions},
126130
configContentPrompt,
127131
release,
132+
installCount,
128133
}: DeployConfirmationPromptOptions): Promise<boolean> {
129134
const timeBeforeConfirmationMs = new Date().valueOf()
130135
let confirmationResponse = true
@@ -149,11 +154,17 @@ async function deployConfirmationPrompt({
149154
}
150155

151156
const question = `${release ? 'Release' : 'Create'} a new version${appTitle ? ` of ${appTitle}` : ''}?`
157+
const showInstallCountWarning = hasDeletedExtensions && installCount !== undefined && installCount > 0
152158
if (isDangerous) {
153159
confirmationResponse = await renderDangerousConfirmationPrompt({
154160
message: question,
155161
infoTable,
156162
confirmation: appTitle,
163+
...(showInstallCountWarning
164+
? {
165+
body: `This release removes extensions and related data from ${installCount} app installations.\nUse caution as this may include production data on live stores.`,
166+
}
167+
: {}),
157168
})
158169
} else {
159170
confirmationResponse = await renderConfirmationPrompt({

packages/app/src/cli/services/context/identifiers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,16 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr
5858
activeAppVersion: options.activeAppVersion,
5959
})
6060

61+
let installCount: number | undefined
62+
if (extensionIdentifiersBreakdown.onlyRemote.length > 0) {
63+
try {
64+
installCount = await options.developerPlatformClient.appInstallCount({id: options.appId})
65+
// eslint-disable-next-line no-catch-all/no-catch-all
66+
} catch (_error) {
67+
installCount = undefined
68+
}
69+
}
70+
6171
const confirmed = await deployOrReleaseConfirmationPrompt({
6272
extensionIdentifiersBreakdown,
6373
configExtensionIdentifiersBreakdown,
@@ -66,6 +76,7 @@ export async function ensureDeploymentIdsPresence(options: EnsureDeploymentIdsPr
6676
force: options.force,
6777
allowUpdates: options.allowUpdates,
6878
allowDeletes: options.allowDeletes,
79+
installCount,
6980
})
7081
if (!confirmed) throw new AbortSilentError()
7182

packages/app/src/cli/utilities/developer-platform-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ export interface DeveloperPlatformClient {
260260
activeAppVersion?: AppVersion,
261261
) => Promise<AllAppExtensionRegistrationsQuerySchema>
262262
appVersions: (app: OrganizationApp) => Promise<AppVersionsQuerySchema>
263+
appInstallCount: (app: MinimalAppIdentifiers) => Promise<number>
263264
activeAppVersion: (app: MinimalAppIdentifiers) => Promise<AppVersion | undefined>
264265
appVersionByTag: (app: MinimalOrganizationApp, tag: string) => Promise<AppVersionWithContext>
265266
appVersionsDiff: (app: MinimalOrganizationApp, version: AppVersionIdentifiers) => Promise<AppVersionsDiffSchema>

packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ import {
117117
import {CreateAssetUrl} from '../../api/graphql/app-management/generated/create-asset-url.js'
118118
import {AppVersionById} from '../../api/graphql/app-management/generated/app-version-by-id.js'
119119
import {AppVersions} from '../../api/graphql/app-management/generated/app-versions.js'
120+
import {AppInstallCount} from '../../api/graphql/app-management/generated/app-install-count.js'
120121
import {CreateApp, CreateAppMutationVariables} from '../../api/graphql/app-management/generated/create-app.js'
121122
import {FetchSpecifications} from '../../api/graphql/app-management/generated/specifications.js'
122123
import {ListApps} from '../../api/graphql/app-management/generated/apps.js'
@@ -652,6 +653,13 @@ export class AppManagementClient implements DeveloperPlatformClient {
652653
}
653654
}
654655

656+
async appInstallCount({id}: MinimalAppIdentifiers): Promise<number> {
657+
const query = AppInstallCount
658+
const variables = {appId: id}
659+
const result = await this.appManagementRequest({query, variables})
660+
return result.app.installCount
661+
}
662+
655663
async appVersionByTag(
656664
{id: appId, organizationId}: MinimalOrganizationApp,
657665
versionTag: string,

packages/app/src/cli/utilities/developer-platform-client/partners-client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,11 @@ export class PartnersClient implements DeveloperPlatformClient {
430430
return this.request(AppVersionsQuery, variables)
431431
}
432432

433+
async appInstallCount(_app: MinimalAppIdentifiers): Promise<number> {
434+
// Install count is not supported in partners client.
435+
throw new Error('Unsupported operation')
436+
}
437+
433438
async appVersionByTag({apiKey}: MinimalOrganizationApp, versionTag: string): Promise<AppVersionWithContext> {
434439
const input: AppVersionByTagVariables = {apiKey, versionTag}
435440
const result: AppVersionByTagSchema = await this.request(AppVersionByTagQuery, input)

0 commit comments

Comments
 (0)