From 44ddd59fd674b1b320d106d914fb8e7e9043ddd2 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 13 Mar 2026 12:18:01 +0100 Subject: [PATCH 1/5] fix: pnpm v8+ --global flag and Homebrew 120s timeout in auto-upgrade (#22366) - cliInstallCommand(): pnpm now emits `pnpm add --global` (compatible with v8+ and older) - runCLIUpgrade(): Homebrew path uses a 120-second exec timeout; on timeout, prints a manual install reminder before re-throwing so autoUpgradeIfNeeded fires Bugsnag - ExecOptions gains an optional `timeout` field, passed through to execa Co-Authored-By: Claude Sonnet 4.6 --- .changeset/auto-upgrade-pnpm-homebrew.md | 5 +++ packages/cli-kit/src/public/node/system.ts | 3 ++ .../cli-kit/src/public/node/upgrade.test.ts | 42 +++++++++++++++++-- packages/cli-kit/src/public/node/upgrade.ts | 28 ++++++++++--- 4 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 .changeset/auto-upgrade-pnpm-homebrew.md diff --git a/.changeset/auto-upgrade-pnpm-homebrew.md b/.changeset/auto-upgrade-pnpm-homebrew.md new file mode 100644 index 00000000000..40f39a233d8 --- /dev/null +++ b/.changeset/auto-upgrade-pnpm-homebrew.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': patch +--- + +Fix pnpm v8+ auto-upgrade command (use `--global` instead of `-g`) and add a 120-second timeout guard for Homebrew upgrades. diff --git a/packages/cli-kit/src/public/node/system.ts b/packages/cli-kit/src/public/node/system.ts index 537275458e0..f0ecc6ad834 100644 --- a/packages/cli-kit/src/public/node/system.ts +++ b/packages/cli-kit/src/public/node/system.ts @@ -26,6 +26,8 @@ export interface ExecOptions { externalErrorHandler?: (error: unknown) => Promise // Ignored on Windows background?: boolean + // Maximum time in milliseconds to wait for the process to exit + timeout?: number } /** @@ -311,6 +313,7 @@ function buildExec( windowsHide: false, detached: options?.background, cleanup: !options?.background, + timeout: options?.timeout, ...execaOptions, }) outputDebug(`Running system process${options?.background ? ' in background' : ''}: diff --git a/packages/cli-kit/src/public/node/upgrade.test.ts b/packages/cli-kit/src/public/node/upgrade.test.ts index 83a5d3d8b99..7c81f6e9812 100644 --- a/packages/cli-kit/src/public/node/upgrade.test.ts +++ b/packages/cli-kit/src/public/node/upgrade.test.ts @@ -2,7 +2,8 @@ import {currentProcessIsGlobal, inferPackageManagerForGlobalCLI} from './is-glob import {checkForCachedNewVersion} from './node-package-manager.js' import {cliInstallCommand, versionToAutoUpgrade, runCLIUpgrade} from './upgrade.js' import {exec} from './system.js' -import {vi, describe, test, expect, beforeEach} from 'vitest' +import {mockAndCaptureOutput} from './testing/output.js' +import {vi, describe, test, expect, beforeEach, afterEach} from 'vitest' vi.mock('./is-global.js') vi.mock('./node-package-manager.js') @@ -45,16 +46,21 @@ describe('cliInstallCommand', () => { expect(got).toMatchInlineSnapshot(`"npm install -g @shopify/cli@latest"`) }) - test('returns pnpm add -g for pnpm', () => { + test('returns pnpm add --global for pnpm (v8+ compatible)', () => { vi.mocked(currentProcessIsGlobal).mockReturnValue(true) vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('pnpm') const got = cliInstallCommand() - expect(got).toMatchInlineSnapshot(`"pnpm add -g @shopify/cli@latest"`) + expect(got).toMatchInlineSnapshot(`"pnpm add --global @shopify/cli@latest"`) }) }) +afterEach(() => { + mockAndCaptureOutput().clear() + vi.unstubAllEnvs() +}) + describe('versionToAutoUpgrade', () => { beforeEach(() => { vi.unstubAllEnvs() @@ -98,7 +104,7 @@ describe('versionToAutoUpgrade', () => { }) describe('runCLIUpgrade', () => { - test('calls exec with the install command for global installs', async () => { + test('calls exec with the install command for global npm installs', async () => { vi.mocked(currentProcessIsGlobal).mockReturnValue(true) vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('npm') vi.mocked(exec).mockResolvedValue(undefined) @@ -107,4 +113,32 @@ describe('runCLIUpgrade', () => { expect(exec).toHaveBeenCalledWith('npm', ['install', '-g', '@shopify/cli@latest'], {stdio: 'inherit'}) }) + + test('calls exec with 120s timeout for homebrew installs', async () => { + vi.mocked(currentProcessIsGlobal).mockReturnValue(true) + vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('homebrew') + vi.mocked(exec).mockResolvedValue(undefined) + + await runCLIUpgrade() + + expect(exec).toHaveBeenCalledWith( + 'brew', + ['upgrade', 'shopify-cli'], + expect.objectContaining({stdio: 'inherit', timeout: 120_000}), + ) + }) + + test('warns and re-throws on homebrew timeout', async () => { + const outputMock = mockAndCaptureOutput() + vi.mocked(currentProcessIsGlobal).mockReturnValue(true) + vi.mocked(inferPackageManagerForGlobalCLI).mockReturnValue('homebrew') + + const timeoutError = Object.assign(new Error('timed out'), {timedOut: true}) + vi.mocked(exec).mockImplementation(async (_cmd, _args, opts) => { + if (opts?.externalErrorHandler) await opts.externalErrorHandler(timeoutError) + }) + + await expect(runCLIUpgrade()).rejects.toThrow() + expect(outputMock.warn()).toContain('timed out') + }) }) diff --git a/packages/cli-kit/src/public/node/upgrade.ts b/packages/cli-kit/src/public/node/upgrade.ts index b7dd8810166..234e73619d4 100644 --- a/packages/cli-kit/src/public/node/upgrade.ts +++ b/packages/cli-kit/src/public/node/upgrade.ts @@ -10,7 +10,7 @@ import { addNPMDependencies, getPackageManager, } from './node-package-manager.js' -import {outputContent, outputDebug, outputInfo, outputToken} from './output.js' +import {outputContent, outputDebug, outputInfo, outputToken, outputWarn} from './output.js' import {cwd, moduleDirectory, sniffForPath} from './path.js' import {exec, isCI} from './system.js' import {isPreReleaseVersion} from './version.js' @@ -29,10 +29,12 @@ export function cliInstallCommand(): string | undefined { if (packageManager === 'homebrew') { return 'brew upgrade shopify-cli' } else if (packageManager === 'yarn') { - return `${packageManager} global add @shopify/cli@latest` + return 'yarn global add @shopify/cli@latest' + } else if (packageManager === 'pnpm') { + // pnpm v8+ requires --global instead of -g + return 'pnpm add --global @shopify/cli@latest' } else { - const verb = packageManager === 'pnpm' ? 'add' : 'install' - return `${packageManager} ${verb} -g @shopify/cli@latest` + return `${packageManager} install -g @shopify/cli@latest` } } @@ -67,7 +69,23 @@ export async function runCLIUpgrade(): Promise { throw new Error('Could not determine the command to run') } outputInfo(outputContent`Upgrading Shopify CLI by running: ${outputToken.genericShellCommand(installCommand)}...`) - await exec(command, args, {stdio: 'inherit'}) + const packageManager = inferPackageManagerForGlobalCLI() + if (packageManager === 'homebrew') { + // Homebrew triggers `brew update` as a side effect, which can take 30–90 s on slow networks. + const HOMEBREW_TIMEOUT_MS = 120_000 + await exec(command, args, { + stdio: 'inherit', + timeout: HOMEBREW_TIMEOUT_MS, + externalErrorHandler: async (error: unknown) => { + if ((error as {timedOut?: boolean}).timedOut) { + outputWarn('Homebrew upgrade timed out. Run `brew upgrade shopify-cli` manually.') + } + throw error + }, + }) + } else { + await exec(command, args, {stdio: 'inherit'}) + } } else if (projectDir) { await upgradeLocalShopify(projectDir, CLI_KIT_VERSION) } else { From c5397ae4e4332e087165f8f5705c0c8b762175de Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 13 Mar 2026 12:50:35 +0100 Subject: [PATCH 2/5] fix: address lint errors in auto-upgrade implementation - Move HOMEBREW_TIMEOUT_MS constant to module scope (prefer-module-scope-constants) - Fix prettier trailing comma in postrun.test.ts - Use static imports in cli upgrade.test.ts to avoid NX enforce-module-boundaries violation Co-Authored-By: Claude Sonnet 4.6 --- packages/cli-kit/src/public/node/hooks/postrun.test.ts | 4 +++- packages/cli-kit/src/public/node/upgrade.ts | 5 +++-- packages/cli/src/cli/commands/upgrade.test.ts | 7 +++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/cli-kit/src/public/node/hooks/postrun.test.ts b/packages/cli-kit/src/public/node/hooks/postrun.test.ts index 4d2bc6ff6d6..a47e5a688ec 100644 --- a/packages/cli-kit/src/public/node/hooks/postrun.test.ts +++ b/packages/cli-kit/src/public/node/hooks/postrun.test.ts @@ -47,7 +47,9 @@ describe('autoUpgradeIfNeeded', () => { vi.mocked(versionToAutoUpgrade).mockReturnValue('3.100.0') vi.mocked(isMajorVersionChange).mockReturnValue(false) vi.mocked(runCLIUpgrade).mockRejectedValue(new Error('upgrade failed')) - vi.mocked(getOutputUpdateCLIReminder).mockReturnValue('💡 Version 3.100.0 available! Run `npm install -g @shopify/cli@latest`') + vi.mocked(getOutputUpdateCLIReminder).mockReturnValue( + '💡 Version 3.100.0 available! Run `npm install -g @shopify/cli@latest`', + ) await autoUpgradeIfNeeded() diff --git a/packages/cli-kit/src/public/node/upgrade.ts b/packages/cli-kit/src/public/node/upgrade.ts index 234e73619d4..142b8aee075 100644 --- a/packages/cli-kit/src/public/node/upgrade.ts +++ b/packages/cli-kit/src/public/node/upgrade.ts @@ -16,6 +16,9 @@ import {exec, isCI} from './system.js' import {isPreReleaseVersion} from './version.js' import {CLI_KIT_VERSION} from '../common/version.js' +// Homebrew triggers `brew update` as a side effect, which can take 30–90 s on slow networks. +const HOMEBREW_TIMEOUT_MS = 120_000 + /** * Utility function for generating an install command for the user to run * to install an updated version of Shopify CLI. @@ -71,8 +74,6 @@ export async function runCLIUpgrade(): Promise { outputInfo(outputContent`Upgrading Shopify CLI by running: ${outputToken.genericShellCommand(installCommand)}...`) const packageManager = inferPackageManagerForGlobalCLI() if (packageManager === 'homebrew') { - // Homebrew triggers `brew update` as a side effect, which can take 30–90 s on slow networks. - const HOMEBREW_TIMEOUT_MS = 120_000 await exec(command, args, { stdio: 'inherit', timeout: HOMEBREW_TIMEOUT_MS, diff --git a/packages/cli/src/cli/commands/upgrade.test.ts b/packages/cli/src/cli/commands/upgrade.test.ts index e0e72458a1d..890b87ae6d0 100644 --- a/packages/cli/src/cli/commands/upgrade.test.ts +++ b/packages/cli/src/cli/commands/upgrade.test.ts @@ -1,13 +1,12 @@ import Upgrade from './upgrade.js' +import {runCLIUpgrade} from '@shopify/cli-kit/node/upgrade' import {describe, test, vi, expect} from 'vitest' -vi.mock('@shopify/cli-kit/node/upgrade', () => ({ - runCLIUpgrade: vi.fn().mockResolvedValue(undefined), -})) +vi.mock('@shopify/cli-kit/node/upgrade') describe('upgrade command', () => { test('calls runCLIUpgrade', async () => { - const {runCLIUpgrade} = await import('@shopify/cli-kit/node/upgrade') + vi.mocked(runCLIUpgrade).mockResolvedValue(undefined) await Upgrade.run([], import.meta.url) From 56bd399e2967311faec4cbf236527e4a4975bc5d Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 13 Mar 2026 12:53:33 +0100 Subject: [PATCH 3/5] fix: update OCLIF manifest and README for upgraded upgrade command Update the command description and summary to reflect new behavior (runs upgrade instead of showing instructions). Co-Authored-By: Claude Sonnet 4.6 --- packages/cli/README.md | 6 +++--- packages/cli/oclif.manifest.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 0ef528b2e4d..07fb8dee74b 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -2765,16 +2765,16 @@ DESCRIPTION ## `shopify upgrade` -Shows details on how to upgrade Shopify CLI. +Upgrades Shopify CLI. ``` USAGE $ shopify upgrade DESCRIPTION - Shows details on how to upgrade Shopify CLI. + Upgrades Shopify CLI. - Shows details on how to upgrade Shopify CLI. + Upgrades Shopify CLI using your package manager. ``` ## `shopify version` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 8a053bd1ea3..46d246328f9 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -7822,8 +7822,8 @@ ], "args": { }, - "description": "Shows details on how to upgrade Shopify CLI.", - "descriptionWithMarkdown": "Shows details on how to upgrade Shopify CLI.", + "description": "Upgrades Shopify CLI.", + "descriptionWithMarkdown": "Upgrades Shopify CLI using your package manager.", "enableJsonFlag": false, "flags": { }, @@ -7835,7 +7835,7 @@ "pluginName": "@shopify/cli", "pluginType": "core", "strict": true, - "summary": "Shows details on how to upgrade Shopify CLI." + "summary": "Upgrades Shopify CLI." }, "version": { "aliases": [ From 83506ffc335941548b6af693d6d43918ec84fe73 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 13 Mar 2026 13:06:20 +0100 Subject: [PATCH 4/5] fix: remove unused beforeEach import in is-global.test.ts Co-Authored-By: Claude Sonnet 4.6 --- packages/cli-kit/src/public/node/is-global.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli-kit/src/public/node/is-global.test.ts b/packages/cli-kit/src/public/node/is-global.test.ts index 5142ba9d77b..5ed18d5186c 100644 --- a/packages/cli-kit/src/public/node/is-global.test.ts +++ b/packages/cli-kit/src/public/node/is-global.test.ts @@ -2,7 +2,7 @@ import {currentProcessIsGlobal, inferPackageManagerForGlobalCLI, installGlobalCL import {terminalSupportsPrompting} from './system.js' import {renderSelectPrompt} from './ui.js' import {globalCLIVersion} from './version.js' -import {beforeEach, describe, expect, test, vi, afterEach} from 'vitest' +import {describe, expect, test, vi, afterEach} from 'vitest' vi.mock('./system.js') vi.mock('./ui.js') From 89a03dbd3b53f2fbad38a8c266766a65e9145796 Mon Sep 17 00:00:00 2001 From: Alfonso Noriega Date: Fri, 13 Mar 2026 13:16:01 +0100 Subject: [PATCH 5/5] fix: correct description field in oclif manifest for upgrade command --- packages/cli/oclif.manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 46d246328f9..78675c99fcf 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -7822,7 +7822,7 @@ ], "args": { }, - "description": "Upgrades Shopify CLI.", + "description": "Upgrades Shopify CLI using your package manager.", "descriptionWithMarkdown": "Upgrades Shopify CLI using your package manager.", "enableJsonFlag": false, "flags": {