Skip to content

[Phase 1] Port and productionize auto-upgrade POC to main#7104

Open
alfonso-noriega wants to merge 8 commits intomainfrom
03-25-phase1_port_auto_upgrade_poc_to_main
Open

[Phase 1] Port and productionize auto-upgrade POC to main#7104
alfonso-noriega wants to merge 8 commits intomainfrom
03-25-phase1_port_auto_upgrade_poc_to_main

Conversation

@alfonso-noriega
Copy link
Contributor

@alfonso-noriega alfonso-noriega commented Mar 25, 2026

WHY are these changes introduced?

Closes shop/issues-develop#22363

This is Phase 1 of the Semantic Versioning, Auto-Upgrade, and --force Deprecation project.

It ports and productionizes the auto-upgrade POC originally co-authored by @gonzaloriestra and @alfonso-noriega on the auto-upgrade branch, making it ready for production behind an opt-in flag.

WHAT is this pull request doing?

Adds automatic CLI self-upgrade support, gated behind an opt-in autoUpgradeEnabled config flag set via shopify upgrade:

  • shopify upgrade now prompts the user to enable auto-upgrade for future runs, then immediately runs the upgrade
  • prerun hook: fires a non-blocking background version check (cached 24h) — replaces the old daily "version available" warning
  • postrun hook: after every command (except upgrade and notifications), if a newer version is available and auto-upgrade is enabled, upgrades the CLI automatically — rate-limited to once per day
  • Major version changes are never auto-upgraded — a manual install reminder is shown instead
  • CI and pre-release versions are always skipped
  • Homebrew is now a first-class package manager: detected via SHOPIFY_HOMEBREW_FORMULA, Cellar path, and HOMEBREW_PREFIX; uses brew upgrade shopify-cli
  • Upgrade failures are caught, sent to Bugsnag with OS/package-manager/version context, and show a manual install reminder

Key guard conditions in versionToAutoUpgrade()

  1. No newer version in cache → skip
  2. SHOPIFY_CLI_FORCE_AUTO_UPGRADE=1 → force (bypasses all remaining guards, including rate limit)
  3. autoUpgradeEnabled not set → skip
  4. CI environment → skip
  5. Pre-release version → skip

SHOPIFY_CLI_FORCE_AUTO_UPGRADE is internal/test-only and must never appear in public docs or help text.

How to test your changes?

  1. Run shopify upgrade — confirm it prompts to enable auto-upgrade and then runs the upgrade
  2. Set autoUpgradeEnabled=true in the conf store and SHOPIFY_CLI_FORCE_AUTO_UPGRADE=1, then run any command — confirm auto-upgrade fires in postrun
  3. Confirm no auto-upgrade fires for shopify upgrade or shopify notifications commands
  4. Confirm auto-upgrade is skipped in CI (CI=1)

Measuring impact

  • n/a - this doesn't need measurement, e.g. a linting rule or a bug-fix
  • Existing analytics will cater for this addition
  • PR includes analytics changes to measure impact — upgrade failures reported to Bugsnag with package manager, OS, and CLI version metadata

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — Homebrew (Mac/Linux), npm, yarn, pnpm, bun all handled
  • I've considered possible documentation changes

Copy link
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 25, 2026

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 82.08% 15165/18476
🟡 Branches 74.63% 7485/10029
🟢 Functions 81.07% 3799/4686
🟢 Lines 82.47% 14338/17386

Test suite run success

4010 tests passing in 1530 suites.

Report generated by 🧪jest coverage report action from aa8b402

@alfonso-noriega alfonso-noriega force-pushed the 03-25-phase1_port_auto_upgrade_poc_to_main branch from 337e60c to f54eb89 Compare March 25, 2026 19:19
@gonzaloriestra gonzaloriestra mentioned this pull request Mar 26, 2026
5 tasks
Copy link
Contributor

@gonzaloriestra gonzaloriestra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about preventing CLI commands from other tabs while updating? Just in case it takes too long (it happens sometimes with brew).

@gonzaloriestra gonzaloriestra force-pushed the 03-25-phase1_port_auto_upgrade_poc_to_main branch from 9d1c051 to 90f54d2 Compare March 26, 2026 11:30
@alfonso-noriega alfonso-noriega force-pushed the 03-25-phase1_port_auto_upgrade_poc_to_main branch 2 times, most recently from 79a6716 to 9f31e82 Compare March 26, 2026 16:13
@alfonso-noriega
Copy link
Contributor Author

/snapit

@github-actions
Copy link
Contributor

🫰✨ Thanks @alfonso-noriega! Your snapshot has been published to npm.

Test the snapshot by installing your package globally:

npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260326162301

Caution

After installing, validate the version by running shopify version in your terminal.
If the versions don't match, you might have multiple global instances installed.
Use which shopify to find out which one you are running and uninstall it.

alfonso-noriega and others added 5 commits March 26, 2026 17:58
- Add auto-upgrade preference storage to conf-store (getAutoUpgradeEnabled/setAutoUpgradeEnabled)
- Add isMajorVersionChange to version.ts
- Add findPathUpSync to fs.ts
- Add homebrew package manager support to node-package-manager.ts, output.ts, and is-global.ts
- Replace execaSync with getProjectDir in currentProcessIsGlobal for safer detection
- Add Homebrew detection via SHOPIFY_HOMEBREW_FORMULA, Cellar path, and HOMEBREW_PREFIX
- Rewrite upgrade.ts with runCLIUpgrade, versionToAutoUpgrade, promptAutoUpgrade
- Replace warnOnAvailableUpgrade in prerun.ts with checkForNewVersionInBackground
- Add autoUpgradeIfNeeded to postrun.ts triggered after each command
- Update shopify upgrade command to prompt and then run upgrade
- Update tests for all changed modules

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap fallback outputWarn in runAtMinimumInterval (1 day) so failed
  upgrades don't spam the reminder on every subsequent command
- Attach packageManager, platform, and cliVersion to the Bugsnag error
  for better diagnostics

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only the failure reminder was rate-limited. Now the full
flow (major-version warning + upgrade attempt) runs at most once per
day via runAtMinimumInterval. SHOPIFY_CLI_FORCE_AUTO_UPGRADE=1 bypasses
the rate limit so tests and explicit forced upgrades always run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alfonso-noriega alfonso-noriega force-pushed the 03-25-phase1_port_auto_upgrade_poc_to_main branch from 9f31e82 to 4a8708a Compare March 26, 2026 17:06
alfonso-noriega and others added 3 commits March 26, 2026 18:14
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README: first two occurrences of upgrade description use the short
  summary ("Upgrades Shopify CLI."); only the body uses the full text
- postrun.test.ts: mock runAtMinimumInterval to always execute its
  callback so the daily rate limit doesn't cause tests to interfere
  with each other

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restore README to main's exact content except for the upgrade command
description, removing the accidentally introduced --json flag
description truncations that were not part of this PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@alfonso-noriega alfonso-noriega force-pushed the 03-25-phase1_port_auto_upgrade_poc_to_main branch from 0baf6d4 to aa8b402 Compare March 26, 2026 17:38
@github-actions
Copy link
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/cli-kit/dist/private/node/conf-store.d.ts
@@ -24,6 +24,7 @@ export interface ConfSchema {
     devSessionStore?: string;
     currentDevSessionId?: string;
     cache?: Cache;
+    autoUpgradeEnabled?: boolean;
 }
 /**
  * Get session.
@@ -125,6 +126,18 @@ interface RunWithRateLimitOptions {
  * @returns true, or undefined if the task was not run.
  */
 export declare function runWithRateLimit(options: RunWithRateLimitOptions, config?: LocalStorage<ConfSchema>): Promise<boolean>;
+/**
+ * Get auto-upgrade preference.
+ *
+ * @returns Whether auto-upgrade is enabled, or undefined if never set.
+ */
+export declare function getAutoUpgradeEnabled(config?: LocalStorage<ConfSchema>): boolean | undefined;
+/**
+ * Set auto-upgrade preference.
+ *
+ * @param enabled - Whether auto-upgrade should be enabled.
+ */
+export declare function setAutoUpgradeEnabled(enabled: boolean, config?: LocalStorage<ConfSchema>): void;
 export declare function getConfigStoreForPartnerStatus(): LocalStorage<Record<string, {
     status: true;
     checkedAt: string;
packages/cli-kit/dist/public/node/fs.d.ts
@@ -1,6 +1,6 @@
 import { OverloadParameters } from '../../private/common/ts/overloaded-parameters.js';
 import { RandomNameFamily } from '../common/string.js';
-import { findUp as internalFindUp } from 'find-up';
+import { findUp as internalFindUp, findUpSync as internalFindUpSync } from 'find-up';
 import { ReadStream, WriteStream } from 'fs';
 import type { Pattern, Options as GlobOptions } from 'fast-glob';
 /**
@@ -335,6 +335,14 @@ export declare function defaultEOL(): EOL;
  * @returns The first path found that matches or  if none could be found.
  */
 export declare function findPathUp(matcher: OverloadParameters<typeof internalFindUp>[0], options: OverloadParameters<typeof internalFindUp>[1]): ReturnType<typeof internalFindUp>;
+/**
+ * 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  if none could be found.
+ */
+export declare function findPathUpSync(matcher: OverloadParameters<typeof internalFindUp>[0], options: OverloadParameters<typeof internalFindUp>[1]): ReturnType<typeof internalFindUpSync>;
 export interface MatchGlobOptions {
     matchBase: boolean;
     noglobstar: boolean;
packages/cli-kit/dist/public/node/is-global.d.ts
@@ -26,6 +26,14 @@ export declare function installGlobalCLIPrompt(): Promise<InstallGlobalCLIPrompt
  * Infers the package manager used by the global CLI.
  *
  * @param argv - The arguments passed to the process.
+ * @param env - The environment variables of the process.
  * @returns The package manager used by the global CLI.
  */
-export declare function inferPackageManagerForGlobalCLI(argv?: string[]): PackageManager;
\ No newline at end of file
+export declare function inferPackageManagerForGlobalCLI(argv?: string[], env?: NodeJS.ProcessEnv): PackageManager;
+/**
+ * Returns the project directory for the given path.
+ *
+ * @param directory - The path to search upward from.
+ * @returns The project root directory, or undefined if not found.
+ */
+export declare function getProjectDir(directory: string): string | undefined;
\ No newline at end of file
packages/cli-kit/dist/public/node/node-package-manager.d.ts
@@ -25,7 +25,7 @@ export type DependencyType = 'dev' | 'prod' | 'peer';
 /**
  * A union that represents the package managers available.
  */
-export declare const packageManager: readonly ["yarn", "npm", "pnpm", "bun", "unknown"];
+export declare const packageManager: readonly ["yarn", "npm", "pnpm", "bun", "homebrew", "unknown"];
 export type PackageManager = (typeof packageManager)[number];
 /**
  * Returns an abort error that's thrown when the package manager can't be determined.
packages/cli-kit/dist/public/node/upgrade.d.ts
@@ -4,11 +4,32 @@
  *
  * @returns A string with the command to run.
  */
-export declare function cliInstallCommand(): string;
+export declare function cliInstallCommand(): string | undefined;
+/**
+ * Runs the CLI upgrade using the appropriate package manager.
+ * Determines the install command and executes it.
+ *
+ * @throws AbortError if the package manager or command cannot be determined.
+ */
+export declare function runCLIUpgrade(): Promise<void>;
+/**
+ * Returns the version to auto-upgrade to, or undefined if auto-upgrade should be skipped.
+ * Auto-upgrade is disabled by default and must be enabled via .
+ * Also skips for CI, pre-release versions, or when no newer version is available.
+ *
+ * @returns The version string to upgrade to, or undefined if no upgrade should happen.
+ */
+export declare function versionToAutoUpgrade(): string | undefined;
 /**
  * Generates a message to remind the user to update the CLI.
  *
  * @param version - The version to update to.
  * @returns The message to remind the user to update the CLI.
  */
-export declare function getOutputUpdateCLIReminder(version: string): string;
\ No newline at end of file
+export declare function getOutputUpdateCLIReminder(version: string): string;
+/**
+ * Prompts the user to enable or disable automatic upgrades, then persists their choice.
+ *
+ * @returns Whether the user chose to enable auto-upgrade.
+ */
+export declare function promptAutoUpgrade(): Promise<boolean>;
\ No newline at end of file
packages/cli-kit/dist/public/node/version.d.ts
@@ -18,4 +18,13 @@ export declare function globalCLIVersion(): Promise<string | undefined>;
  * @param version - The version to check.
  * @returns True if the version is a pre-release version.
  */
-export declare function isPreReleaseVersion(version: string): boolean;
\ No newline at end of file
+export declare function isPreReleaseVersion(version: string): boolean;
+/**
+ * Checks if there is a major version change between two versions.
+ * Pre-release versions (0.0.0-*) are treated as not having a major version change.
+ *
+ * @param currentVersion - The current version.
+ * @param newerVersion - The newer version to compare against.
+ * @returns True if there is a major version change.
+ */
+export declare function isMajorVersionChange(currentVersion: string, newerVersion: string): boolean;
\ No newline at end of file
packages/cli-kit/dist/public/node/hooks/postrun.d.ts
@@ -5,4 +5,11 @@ import { Hook } from '@oclif/core';
  * @returns Whether post run hook has completed.
  */
 export declare function postRunHookHasCompleted(): boolean;
-export declare const hook: Hook.Postrun;
\ No newline at end of file
+export declare const hook: Hook.Postrun;
+/**
+ * Auto-upgrades the CLI after a command completes, if a newer version is available.
+ * The entire flow is rate-limited to once per day unless forced via SHOPIFY_CLI_FORCE_AUTO_UPGRADE.
+ *
+ * @returns Resolves when the upgrade attempt (or fallback warning) is complete.
+ */
+export declare function autoUpgradeIfNeeded(): Promise<void>;
\ No newline at end of file
packages/cli-kit/dist/public/node/hooks/prerun.d.ts
@@ -11,6 +11,7 @@ export declare function parseCommandContent(cmdInfo: {
     pluginAlias?: string;
 }): CommandContent;
 /**
- * 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 declare function warnOnAvailableUpgrade(): Promise<void>;
\ No newline at end of file
+export declare function checkForNewVersionInBackground(): void;
\ No newline at end of file

@alfonso-noriega alfonso-noriega marked this pull request as ready for review March 26, 2026 17:45
@alfonso-noriega alfonso-noriega requested review from a team as code owners March 26, 2026 17:45
@github-actions
Copy link
Contributor

We detected some changes at packages/*/src and there are no updates in the .changeset.
If the changes are user-facing, run pnpm changeset add to track your changes and include them in the next release CHANGELOG.

Caution

DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants