Skip to content

Commit f3d32ce

Browse files
On app selecetion keep retrying when failing but avoid capturing unrelated errors
1 parent 2e41507 commit f3d32ce

4 files changed

Lines changed: 64 additions & 44 deletions

File tree

packages/app/src/cli/commands/app/init.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ async function selectAppOrNewAppName(
167167
return {result: 'new', name, org}
168168
} else {
169169
const app = await selectAppPrompt(searchForAppsByNameFactory(developerPlatformClient, org.id), apps, hasMorePages)
170+
if (!app) throw new AbortError('Unable to select an app: the selection prompt was interrupted.')
170171

171172
const fullSelectedApp = await developerPlatformClient.appFromIdentifiers(app.apiKey)
172173
if (!fullSelectedApp) throw new AbortError(`App with id ${app.id} not found`)

packages/app/src/cli/prompts/dev.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export async function selectAppPrompt(
3232
options?: {
3333
directory?: string
3434
},
35-
): Promise<MinimalAppIdentifiers> {
35+
): Promise<MinimalAppIdentifiers | undefined> {
3636
const tomls = await getTomls(options?.directory)
3737

3838
if (tomls) setCachedCommandTomlMap(tomls)
@@ -64,15 +64,7 @@ export async function selectAppPrompt(
6464
},
6565
})
6666

67-
const appChoice = currentAppChoices.find((app) => app.apiKey === apiKey)!
68-
69-
if (!appChoice) {
70-
throw new Error(
71-
`Unable to select an app: the selection prompt was interrupted multiple times./n
72-
Api key ${apiKey} was selected but not found in ${currentAppChoices.map((app) => app.apiKey).join(', ')}`,
73-
)
74-
}
75-
return appChoice
67+
return currentAppChoices.find((app) => app.apiKey === apiKey)
7668
}
7769

7870
interface SelectStorePromptOptions {

packages/app/src/cli/services/dev/select-app.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import {AppInterface, WebType} from '../../models/app/app.js'
33
import {Organization, OrganizationSource} from '../../models/organization.js'
44
import {appNamePrompt, createAsNewAppPrompt, selectAppPrompt} from '../../prompts/dev.js'
55
import {testApp, testOrganizationApp, testDeveloperPlatformClient} from '../../models/app/app.test-data.js'
6+
import {BugError} from '@shopify/cli-kit/node/error'
67
import {describe, expect, vi, test} from 'vitest'
78

89
vi.mock('../../prompts/dev')
10+
vi.mock('@shopify/cli-kit/node/output')
911

1012
const LOCAL_APP: AppInterface = testApp({
1113
directory: '',
@@ -82,4 +84,48 @@ describe('selectOrCreateApp', () => {
8284
expect(appNamePrompt).toHaveBeenCalledWith(LOCAL_APP.name)
8385
expect(developerPlatformClient.createApp).toHaveBeenCalledWith(ORG1, {name: 'app-name'})
8486
})
87+
88+
test('retries when selectAppPrompt returns undefined and succeeds on next attempt', async () => {
89+
// Given
90+
vi.mocked(selectAppPrompt).mockResolvedValueOnce(undefined).mockResolvedValueOnce(APPS[0])
91+
vi.mocked(createAsNewAppPrompt).mockResolvedValue(false)
92+
93+
// When
94+
const {developerPlatformClient} = mockDeveloperPlatformClient()
95+
const got = await selectOrCreateApp(APPS, false, ORG1, developerPlatformClient, {name: LOCAL_APP.name})
96+
97+
// Then
98+
expect(got).toEqual(APP1)
99+
expect(selectAppPrompt).toHaveBeenCalledTimes(2)
100+
})
101+
102+
test('throws BugError when selectAppPrompt returns undefined for all attempts', async () => {
103+
// Given
104+
vi.mocked(selectAppPrompt).mockResolvedValue(undefined)
105+
vi.mocked(createAsNewAppPrompt).mockResolvedValue(false)
106+
107+
// When/Then
108+
const {developerPlatformClient} = mockDeveloperPlatformClient()
109+
await expect(selectOrCreateApp(APPS, false, ORG1, developerPlatformClient, {name: LOCAL_APP.name})).rejects.toThrow(
110+
BugError,
111+
)
112+
expect(selectAppPrompt).toHaveBeenCalledTimes(2)
113+
})
114+
115+
test('throws BugError when appFromIdentifiers returns undefined for all attempts', async () => {
116+
// Given
117+
vi.mocked(selectAppPrompt).mockResolvedValue(APPS[0])
118+
vi.mocked(createAsNewAppPrompt).mockResolvedValue(false)
119+
const developerPlatformClient = testDeveloperPlatformClient({
120+
async appFromIdentifiers(_apiKey) {
121+
return undefined
122+
},
123+
})
124+
125+
// When/Then
126+
await expect(selectOrCreateApp(APPS, false, ORG1, developerPlatformClient, {name: LOCAL_APP.name})).rejects.toThrow(
127+
BugError,
128+
)
129+
expect(selectAppPrompt).toHaveBeenCalledTimes(2)
130+
})
85131
})

packages/app/src/cli/services/dev/select-app.ts

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {getCachedCommandInfo, setCachedCommandTomlPreference} from '../local-sto
55
import {CreateAppOptions, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
66
import {AppConfigurationFileName} from '../../models/app/loader.js'
77
import {BugError} from '@shopify/cli-kit/node/error'
8-
import {outputInfo, outputDebug} from '@shopify/cli-kit/node/output'
8+
import {outputInfo} from '@shopify/cli-kit/node/output'
99

1010
const MAX_PROMPT_RETRIES = 2
1111

@@ -51,52 +51,33 @@ export async function selectOrCreateApp(
5151
const tomls = (cachedData?.tomls as {[key: string]: AppConfigurationFileName}) ?? {}
5252

5353
for (let attempt = 0; attempt < MAX_PROMPT_RETRIES; attempt++) {
54-
try {
55-
// eslint-disable-next-line no-await-in-loop
56-
const app = await selectAppPrompt(
57-
searchForAppsByNameFactory(developerPlatformClient, org.id),
58-
apps,
59-
hasMorePages,
60-
{directory: options.directory},
61-
)
54+
// eslint-disable-next-line no-await-in-loop
55+
const app = await selectAppPrompt(
56+
searchForAppsByNameFactory(developerPlatformClient, org.id),
57+
apps,
58+
hasMorePages,
59+
{directory: options.directory},
60+
)
6261

62+
if (!app) {
63+
if (attempt < MAX_PROMPT_RETRIES - 1) outputInfo('App selection failed. Retrying...')
64+
continue
65+
} else {
6366
const selectedToml = tomls[app.apiKey]
6467
if (selectedToml) setCachedCommandTomlPreference(selectedToml)
6568

6669
// eslint-disable-next-line no-await-in-loop
6770
const fullSelectedApp = await developerPlatformClient.appFromIdentifiers(app.apiKey)
6871

69-
if (!fullSelectedApp) {
70-
throw new BugError(
71-
`Unable to fetch app ${app.apiKey} from Shopify`,
72-
'Try running `shopify app config link` to connect to an app you have access to.',
73-
)
74-
}
75-
76-
return fullSelectedApp
77-
} catch (error) {
78-
// Don't retry BugError - those indicate actual bugs, not transient issues
79-
if (error instanceof BugError) {
80-
throw error
81-
}
82-
83-
const errorObj = error as Error
84-
85-
// Log each attempt for observability
86-
outputDebug(`App selection attempt ${attempt + 1}/${MAX_PROMPT_RETRIES} failed: ${errorObj.message}`)
87-
88-
// If we have retries left, inform user and retry
89-
if (attempt < MAX_PROMPT_RETRIES - 1) {
90-
outputInfo('App selection failed. Retrying...')
91-
} else {
92-
throw new BugError(errorObj.message, TRY_MESSAGE)
72+
if (fullSelectedApp) {
73+
return fullSelectedApp
9374
}
9475
}
9576
}
9677

9778
// User-facing error message with key diagnostic info
9879
const errorMessage = [
99-
'Unable to select an app: the selection prompt was interrupted multiple times.',
80+
'Unable to select an app: the selection prompt failed multiple times.',
10081
'',
10182
`Available apps: ${apps.length}`,
10283
].join('\n')

0 commit comments

Comments
 (0)