Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs-shopify.dev/commands/upgrade.doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'

const data: ReferenceEntityTemplateSchema = {
name: 'upgrade',
description: `Shows details on how to upgrade Shopify CLI.`,
overviewPreviewDescription: `Shows details on how to upgrade Shopify CLI.`,
description: `Upgrades Shopify CLI using your package manager.`,
overviewPreviewDescription: `Upgrades Shopify CLI.`,
type: 'command',
isVisualComponent: false,
defaultExample: {
Expand Down
4 changes: 2 additions & 2 deletions docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -7815,8 +7815,8 @@
},
{
"name": "upgrade",
"description": "Shows details on how to upgrade Shopify CLI.",
"overviewPreviewDescription": "Shows details on how to upgrade Shopify CLI.",
"description": "Upgrades Shopify CLI using your package manager.",
"overviewPreviewDescription": "Upgrades Shopify CLI.",
"type": "command",
"isVisualComponent": false,
"defaultExample": {
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/cli/services/app/config/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ vi.mock('@shopify/cli-kit/node/ui')
vi.mock('../../context/partner-account-info.js')
vi.mock('../../context.js')
vi.mock('../select-app.js')
vi.mock('@shopify/cli-kit/node/is-global', () => ({
currentProcessIsGlobal: () => false,
}))

const DEFAULT_REMOTE_CONFIGURATION = {
name: 'app1',
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/cli/services/app/config/use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ vi.mock('../../local-storage.js')
vi.mock('../../../models/app/loader.js')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('../../context.js')
vi.mock('@shopify/cli-kit/node/is-global', () => ({
currentProcessIsGlobal: () => false,
}))

describe('use', () => {
test('clears currentConfiguration when reset is true', async () => {
Expand All @@ -31,6 +34,7 @@ describe('use', () => {
developerPlatformClient: testDeveloperPlatformClient(),
}
writeFileSync(joinPath(tmp, 'package.json'), '{}')
writeFileSync(joinPath(tmp, 'shopify.app.toml'), '')

// When
await use(options)
Expand Down
3 changes: 3 additions & 0 deletions packages/app/src/cli/services/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ vi.mock('../prompts/generate/extension.js')
vi.mock('../services/generate/extension.js')
vi.mock('../services/context.js')
vi.mock('./local-storage.js')
vi.mock('@shopify/cli-kit/node/is-global', () => ({
currentProcessIsGlobal: () => false,
}))

afterEach(() => {
mockAndCaptureOutput().clear()
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/cli/services/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ async function init(options: InitOptions) {
await appendFile(joinPath(templateScaffoldDir, '.npmrc'), `auto-install-peers=true\n`)
break
}
case 'homebrew':
case 'unknown':
throw new UnknownPackageManagerError()
}
Expand Down
1 change: 1 addition & 0 deletions packages/cli-kit/src/private/node/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export const environmentVariables = {
neverUsePartnersApi: 'SHOPIFY_CLI_NEVER_USE_PARTNERS_API',
skipNetworkLevelRetry: 'SHOPIFY_CLI_SKIP_NETWORK_LEVEL_RETRY',
maxRequestTimeForNetworkCalls: 'SHOPIFY_CLI_MAX_REQUEST_TIME_FOR_NETWORK_CALLS',
noAutoUpgrade: 'SHOPIFY_CLI_NO_AUTO_UPGRADE',
}

export const defaultThemeKitAccessDomain = 'theme-kit-access.shopifyapps.com'
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/node/context/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,4 +292,14 @@ export function opentelemetryDomain(env = process.env): string {
return isSet(domain) ? domain : 'https://otlp-http-production-cli.shopifysvc.com'
}

/**
* Returns true if the CLIshould not automatically upgrade.
*
* @param env - The environment variables from the environment of the current process.
* @returns True if the CLI should not automatically upgrade.
*/
export function noAutoUpgrade(env = process.env): boolean {
return isTruthy(env[environmentVariables.noAutoUpgrade])
}

export type CIMetadata = Metadata
19 changes: 18 additions & 1 deletion packages/cli-kit/src/public/node/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {

import {temporaryDirectory, temporaryDirectoryTask} from 'tempy'
import {sep, join} from 'pathe'
import {findUp as internalFindUp} from 'find-up'
import {findUp as internalFindUp, findUpSync as internalFindUpSync} from 'find-up'
import {minimatch} from 'minimatch'
import fastGlobLib from 'fast-glob'
import {
Expand Down Expand Up @@ -650,6 +650,23 @@ export async function findPathUp(
return got ? normalizePath(got) : undefined
}

/**
* Find a file by walking parent directories.
*
* @param matcher - A pattern or an array of patterns to match a file name.
* @param options - Options for the search.
* @returns The first path found that matches or `undefined` if none could be found.
*/
export function findPathUpSync(
matcher: OverloadParameters<typeof internalFindUp>[0],
options: OverloadParameters<typeof internalFindUp>[1],
): ReturnType<typeof internalFindUpSync> {
// findUp has odd typing
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const got = internalFindUpSync(matcher as any, options)
return got ? normalizePath(got) : undefined
}

export interface MatchGlobOptions {
matchBase: boolean
noglobstar: boolean
Expand Down
83 changes: 83 additions & 0 deletions packages/cli-kit/src/public/node/hooks/postrun.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {autoUpgradeIfNeeded} from './postrun.js'
import {mockAndCaptureOutput} from '../testing/output.js'
import {getOutputUpdateCLIReminder, runCLIUpgrade, versionToAutoUpgrade} from '../upgrade.js'
import {isMajorVersionChange} from '../version.js'
import {describe, expect, test, vi, afterEach} from 'vitest'

vi.mock('../upgrade.js', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
runCLIUpgrade: vi.fn(),
getOutputUpdateCLIReminder: vi.fn(),
versionToAutoUpgrade: vi.fn(),
}
})

vi.mock('../version.js', async (importOriginal) => {
const actual: any = await importOriginal()
return {
...actual,
isMajorVersionChange: vi.fn(),
}
})

afterEach(() => {
mockAndCaptureOutput().clear()
})

describe('autoUpgradeIfNeeded', () => {
test('runs the upgrade when versionToAutoUpgrade returns a version', async () => {
// Given
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.91.0')
vi.mocked(runCLIUpgrade).mockResolvedValue()

// When
await autoUpgradeIfNeeded()

// Then
expect(runCLIUpgrade).toHaveBeenCalled()
})

test('falls back to warning when the upgrade fails', async () => {
// Given
const outputMock = mockAndCaptureOutput()
vi.mocked(versionToAutoUpgrade).mockReturnValue('3.91.0')
vi.mocked(runCLIUpgrade).mockRejectedValue(new Error('upgrade failed'))
const installReminder = '💡 Version 3.91.0 available! Run `npm install @shopify/cli@latest`'
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue(installReminder)

// When
await autoUpgradeIfNeeded()

// Then
expect(outputMock.warn()).toMatch(installReminder)
})

test('does nothing when versionToAutoUpgrade returns undefined', async () => {
// Given
vi.mocked(versionToAutoUpgrade).mockReturnValue(undefined)

// When
await autoUpgradeIfNeeded()

// Then
expect(runCLIUpgrade).not.toHaveBeenCalled()
})

test('shows warning instead of upgrading for a major version change', async () => {
// Given
const outputMock = mockAndCaptureOutput()
vi.mocked(versionToAutoUpgrade).mockReturnValue('4.0.0')
vi.mocked(isMajorVersionChange).mockReturnValue(true)
const installReminder = '💡 Version 4.0.0 available! Run `npm install @shopify/cli@latest`'
vi.mocked(getOutputUpdateCLIReminder).mockReturnValue(installReminder)

// When
await autoUpgradeIfNeeded()

// Then
expect(runCLIUpgrade).not.toHaveBeenCalled()
expect(outputMock.warn()).toMatch(installReminder)
})
})
32 changes: 31 additions & 1 deletion packages/cli-kit/src/public/node/hooks/postrun.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {postrun as deprecationsHook} from './deprecations.js'
import {reportAnalyticsEvent} from '../analytics.js'
import {outputDebug} from '../../../public/node/output.js'
import {outputDebug, outputWarn} from '../../../public/node/output.js'
import {getOutputUpdateCLIReminder, runCLIUpgrade, versionToAutoUpgrade} from '../../../public/node/upgrade.js'
import BaseCommand from '../base-command.js'
import * as metadata from '../../../public/node/metadata.js'
import {CLI_KIT_VERSION} from '../../common/version.js'
import {isMajorVersionChange} from '../version.js'
import {Command, Hook} from '@oclif/core'

let postRunHookCompleted = false
Expand All @@ -25,6 +28,33 @@ export const hook: Hook.Postrun = async ({config, Command}) => {
const command = Command.id.replace(/:/g, ' ')
outputDebug(`Completed command ${command}`)
postRunHookCompleted = true

if (!command.includes('notifications')) await autoUpgradeIfNeeded()
}

/**
* Auto-upgrades the CLI after a command completes, if a newer version is available.
*
* @returns Resolves when the upgrade attempt (or fallback warning) is complete.
*/
export async function autoUpgradeIfNeeded(): Promise<void> {
const newerVersion = versionToAutoUpgrade()
if (!newerVersion) return
if (isMajorVersionChange(CLI_KIT_VERSION, newerVersion)) {
return outputWarn(getOutputUpdateCLIReminder(newerVersion))
}

try {
await runCLIUpgrade()
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (error) {
const errorMessage = `Auto-upgrade failed: ${error}`
outputDebug(errorMessage)
outputWarn(getOutputUpdateCLIReminder(newerVersion))
// Report to Observe as a handled error without showing anything extra to the user
const {sendErrorToBugsnag} = await import('../../../public/node/error-handler.js')
await sendErrorToBugsnag(new Error(errorMessage), 'expected_error')
}
}

/**
Expand Down
46 changes: 2 additions & 44 deletions packages/cli-kit/src/public/node/hooks/prerun.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,5 @@
import {parseCommandContent, warnOnAvailableUpgrade} from './prerun.js'
import {checkForCachedNewVersion, packageManagerFromUserAgent} from '../node-package-manager.js'
import {cacheClear} from '../../../private/node/conf-store.js'
import {mockAndCaptureOutput} from '../testing/output.js'
import {describe, expect, test, vi, afterEach, beforeEach} from 'vitest'

vi.mock('../node-package-manager')

beforeEach(() => {
cacheClear()
})

afterEach(() => {
mockAndCaptureOutput().clear()
cacheClear()
})

describe('warnOnAvailableUpgrade', () => {
test('displays latest version and an install command when a newer exists', async () => {
// Given
const outputMock = mockAndCaptureOutput()
vi.mocked(checkForCachedNewVersion).mockReturnValue('3.0.10')
vi.mocked(packageManagerFromUserAgent).mockReturnValue('npm')
const installReminder = '💡 Version 3.0.10 available! Run `npm install @shopify/cli@latest`'

// When
await warnOnAvailableUpgrade()

// Then
expect(outputMock.warn()).toMatch(installReminder)
})

test('displays nothing when no newer version exists', async () => {
// Given
const outputMock = mockAndCaptureOutput()
vi.mocked(checkForCachedNewVersion).mockReturnValue(undefined)

// When
await warnOnAvailableUpgrade()

// Then
expect(outputMock.warn()).toEqual('')
})
})
import {parseCommandContent} from './prerun.js'
import {describe, expect, test} from 'vitest'

describe('parseCommandContent', () => {
test('when a create command is used should return the correct command content', async () => {
Expand Down
29 changes: 8 additions & 21 deletions packages/cli-kit/src/public/node/hooks/prerun.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import {CLI_KIT_VERSION} from '../../common/version.js'
import {checkForNewVersion, checkForCachedNewVersion} from '../node-package-manager.js'
import {isPreReleaseVersion} from '../version.js'
import {checkForNewVersion} from '../node-package-manager.js'
import {startAnalytics} from '../../../private/node/analytics.js'
import {outputDebug, outputWarn} from '../../../public/node/output.js'
import {getOutputUpdateCLIReminder} from '../../../public/node/upgrade.js'
import {outputDebug} from '../../../public/node/output.js'
import Command from '../../../public/node/base-command.js'
import {runAtMinimumInterval} from '../../../private/node/conf-store.js'
import {fetchNotificationsInBackground} from '../notifications-system.js'
import {isPreReleaseVersion} from '../version.js'
import {Hook} from '@oclif/core'

export declare interface CommandContent {
Expand All @@ -22,7 +20,7 @@ export const hook: Hook.Prerun = async (options) => {
pluginAlias: options.Command.plugin?.alias,
})
const args = options.argv
await warnOnAvailableUpgrade()
checkForNewVersionInBackground()
outputDebug(`Running command ${commandContent.command}`)
await startAnalytics({commandContent, args, commandClass: options.Command as unknown as typeof Command})
fetchNotificationsInBackground(options.Command.id)
Expand Down Expand Up @@ -89,25 +87,14 @@ function findAlias(aliases: string[]) {
}

/**
* Warns the user if there is a new version of the CLI available
* Triggers a background check for a newer CLI version (non-blocking).
* The result is cached and consumed by the postrun hook for auto-upgrade.
*/
export async function warnOnAvailableUpgrade(): Promise<void> {
const cliDependency = '@shopify/cli'
export function checkForNewVersionInBackground(): void {
const currentVersion = CLI_KIT_VERSION
if (isPreReleaseVersion(currentVersion)) {
// This is a nightly/snapshot/experimental version, so we don't want to check for updates
return
}

// Check in the background, once daily
// eslint-disable-next-line no-void
void checkForNewVersion(cliDependency, currentVersion, {cacheExpiryInHours: 24})

// Warn if we previously found a new version
await runAtMinimumInterval('warn-on-available-upgrade', {days: 1}, async () => {
const newerVersion = checkForCachedNewVersion(cliDependency, currentVersion)
if (newerVersion) {
outputWarn(getOutputUpdateCLIReminder(newerVersion))
}
})
void checkForNewVersion('@shopify/cli', currentVersion, {cacheExpiryInHours: 24})
}
Loading
Loading