diff --git a/packages/cli-kit/src/public/node/hooks/postrun.ts b/packages/cli-kit/src/public/node/hooks/postrun.ts index a21bf49f89..52d600cdcb 100644 --- a/packages/cli-kit/src/public/node/hooks/postrun.ts +++ b/packages/cli-kit/src/public/node/hooks/postrun.ts @@ -96,7 +96,7 @@ async function performAutoUpgrade(newerVersion: string): Promise { } try { - await runCLIUpgrade() + await runCLIUpgrade({autoupgrade: true}) await metadata.addPublicMetadata(() => ({env_auto_upgrade_success: true})) // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { diff --git a/packages/cli-kit/src/public/node/upgrade.test.ts b/packages/cli-kit/src/public/node/upgrade.test.ts index eaa672632c..29f9128899 100644 --- a/packages/cli-kit/src/public/node/upgrade.test.ts +++ b/packages/cli-kit/src/public/node/upgrade.test.ts @@ -208,6 +208,17 @@ describe('runCLIUpgrade', () => { // Then expect(exec).not.toHaveBeenCalled() }) + + test('skips project-local upgrade when called from the auto-upgrade postrun hook', async () => { + // Given + vi.mocked(currentProcessIsGlobal).mockReturnValue(false) + + // When + await runCLIUpgrade({autoupgrade: true}) + + // Then + expect(exec).not.toHaveBeenCalled() + }) }) describe('versionToAutoUpgrade', () => { diff --git a/packages/cli-kit/src/public/node/upgrade.ts b/packages/cli-kit/src/public/node/upgrade.ts index 00916066bc..979a1af2ac 100644 --- a/packages/cli-kit/src/public/node/upgrade.ts +++ b/packages/cli-kit/src/public/node/upgrade.ts @@ -41,13 +41,27 @@ export function cliInstallCommand(): string | undefined { } } +/** + * Options for {@link runCLIUpgrade}. + */ +export interface RunCLIUpgradeOptions { + /** + * `true` when the upgrade is being triggered by the automatic postrun hook. + * In that case we skip project-local upgrades — those should only happen when the + * user explicitly runs `shopify upgrade` so we don't surprise them by mutating + * their `package.json` / lockfile in the background. + */ + autoupgrade?: boolean +} + /** * Runs the CLI upgrade using the appropriate package manager. * Determines the install command and executes it. * + * @param options - See {@link RunCLIUpgradeOptions}. * @throws AbortError if the package manager or command cannot be determined. */ -export async function runCLIUpgrade(): Promise { +export async function runCLIUpgrade(options: RunCLIUpgradeOptions = {}): Promise { // Path where the current project is (app/hydrogen) const path = sniffForPath() ?? cwd() const projectDir = getProjectDir(path) @@ -61,6 +75,14 @@ export async function runCLIUpgrade(): Promise { return } + // When triggered by the automatic postrun hook, skip project-local upgrades. + // Bumping `package.json` / lockfile silently in the background would surprise users + // and produce noisy diffs; explicit `shopify upgrade` invocations still upgrade the + // local project. + if (options.autoupgrade && !isGlobal) { + return + } + // Generate the install command for the global CLI and execute it if (isGlobal) { const installCommand = cliInstallCommand()