Skip to content

Commit 75f75ba

Browse files
Add failsafe and improve loging on unexepcete select app error
1 parent c8829ea commit 75f75ba

File tree

3 files changed

+102
-19
lines changed

3 files changed

+102
-19
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,16 @@ export async function selectAppPrompt(
6464
}
6565
},
6666
})
67-
return currentAppChoices.find((app) => app.apiKey === apiKey)!
67+
68+
const appChoice = currentAppChoices.find((app) => app.apiKey === apiKey)!
69+
70+
if (!appChoice) {
71+
throw new Error(
72+
`Unable to select an app: the selection prompt was interrupted multiple times./n
73+
Api key ${apiKey} was selected but not found in ${currentAppChoices.map((app) => app.apiKey).join(', ')}`,
74+
)
75+
}
76+
return appChoice
6877
}
6978

7079
interface SelectStorePromptOptions {

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

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import {Organization, MinimalOrganizationApp, OrganizationApp} from '../../model
44
import {getCachedCommandInfo, setCachedCommandTomlPreference} from '../local-storage.js'
55
import {CreateAppOptions, DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
66
import {AppConfigurationFileName} from '../../models/app/loader.js'
7-
import {BugError} from '@shopify/cli-kit/node/error'
7+
import {AbortError, BugError} from '@shopify/cli-kit/node/error'
8+
import {outputInfo, outputDebug} from '@shopify/cli-kit/node/output'
9+
10+
const MAX_PROMPT_RETRIES = 2
811

912
/**
1013
* Select an app from env, list or create a new one:
@@ -30,25 +33,89 @@ export async function selectOrCreateApp(
3033
const name = await appNamePrompt(options.name)
3134
return developerPlatformClient.createApp(org, {...options, name})
3235
} else {
33-
const app = await selectAppPrompt(searchForAppsByNameFactory(developerPlatformClient, org.id), apps, hasMorePages, {
34-
directory: options.directory,
35-
})
36+
// Capture app selection context
37+
const cachedData = getCachedCommandInfo()
38+
const tomls = (cachedData?.tomls as {[key: string]: AppConfigurationFileName}) ?? {}
39+
40+
for (let attempt = 0; attempt < MAX_PROMPT_RETRIES; attempt++) {
41+
try {
42+
// eslint-disable-next-line no-await-in-loop
43+
const app = await selectAppPrompt(
44+
searchForAppsByNameFactory(developerPlatformClient, org.id),
45+
apps,
46+
hasMorePages,
47+
{directory: options.directory},
48+
)
49+
50+
const selectedToml = tomls[app.apiKey]
51+
if (selectedToml) setCachedCommandTomlPreference(selectedToml)
52+
53+
// eslint-disable-next-line no-await-in-loop
54+
const fullSelectedApp = await developerPlatformClient.appFromIdentifiers(app.apiKey)
55+
56+
if (!fullSelectedApp) {
57+
throw new BugError(
58+
`Unable to fetch app ${app.apiKey} from Shopify`,
59+
'Try running `shopify app config link` to connect to an app you have access to.',
60+
)
61+
}
3662

37-
const data = getCachedCommandInfo()
38-
const tomls = (data?.tomls as {[key: string]: AppConfigurationFileName}) ?? {}
39-
const selectedToml = tomls[app.apiKey]
63+
return fullSelectedApp
64+
} catch (error) {
65+
// Don't retry BugError - those indicate actual bugs, not transient issues
66+
if (error instanceof BugError) {
67+
throw error
68+
}
4069

41-
if (selectedToml) setCachedCommandTomlPreference(selectedToml)
70+
const errorObj = error as Error
4271

43-
const fullSelectedApp = await developerPlatformClient.appFromIdentifiers(app.apiKey)
72+
// Log each attempt for observability
73+
outputDebug(`App selection attempt ${attempt + 1}/${MAX_PROMPT_RETRIES} failed: ${errorObj.message}`)
4474

45-
if (!fullSelectedApp) {
46-
// This is unlikely, and a bug. But we still want a nice user facing message plus appropriate context logged.
47-
throw new BugError(
48-
`Unable to fetch app ${app.apiKey} from Shopify`,
49-
'Try running `shopify app config link` to connect to an app you have access to.',
50-
)
75+
// If we have retries left, inform user and retry
76+
if (attempt < MAX_PROMPT_RETRIES - 1) {
77+
outputInfo('App selection was interrupted. Retrying...')
78+
} else {
79+
const tryMessage = [
80+
'This may happen if:',
81+
' • The process received a signal (SIGINT, SIGTERM) or was terminated',
82+
' • Running in an unstable environment (container restart, resource limits)',
83+
' • Network interruption during app fetching',
84+
'',
85+
'Try running the command again. If the issue persists:',
86+
' • Check system resources and stability',
87+
' • Try running outside of containers/WSL if applicable',
88+
' • Report this issue with the error detailsmad a verbose log',
89+
]
90+
.filter(Boolean)
91+
.join('\n')
92+
93+
throw new BugError(errorObj.message, tryMessage)
94+
}
95+
}
5196
}
52-
return fullSelectedApp
97+
98+
// User-facing error message with key diagnostic info
99+
const errorMessage = [
100+
'Unable to select an app: the selection prompt was interrupted multiple times.',
101+
'',
102+
`Available apps: ${apps.length}`,
103+
].join('\n')
104+
105+
const tryMessage = [
106+
'This may happen if:',
107+
' • The process received a signal (SIGINT, SIGTERM) or was terminated',
108+
' • Running in an unstable environment (container restart, resource limits)',
109+
' • Network interruption during app fetching',
110+
'',
111+
'Try running the command again. If the issue persists:',
112+
' • Check system resources and stability',
113+
' • Try running outside of containers/WSL if applicable',
114+
' • Report this issue with the error details',
115+
]
116+
.filter(Boolean)
117+
.join('\n')
118+
119+
throw new BugError(errorMessage, tryMessage)
53120
}
54121
}

packages/cli-kit/src/public/node/ui.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ export async function renderAutocompletePrompt<T>(
426426
}
427427

428428
return runWithTimer('cmd_all_timing_prompts_ms')(async () => {
429-
let selectedValue: T
429+
let selectedValue: T | undefined
430430
await render(
431431
<AutocompletePrompt
432432
{...newProps}
@@ -439,7 +439,14 @@ export async function renderAutocompletePrompt<T>(
439439
exitOnCtrlC: false,
440440
},
441441
)
442-
return selectedValue!
442+
443+
if (selectedValue === undefined) {
444+
throw new Error(
445+
'Prompt was interrupted before a selection was made. This can happen if the process received a signal, was terminated, or the prompt was aborted.',
446+
)
447+
}
448+
449+
return selectedValue
443450
})
444451
}
445452

0 commit comments

Comments
 (0)