Skip to content

Show daily new-major notices alongside automatic CLI updates - #9037

Open
isaacroldan wants to merge 1 commit into
codex/homebrew-auto-upgrade-guardfrom
codex/auto-upgrade-major-notice
Open

isaacroldan wants to merge 1 commit into
codex/homebrew-auto-upgrade-guardfrom
codex/auto-upgrade-major-notice

Conversation

@isaacroldan

@isaacroldan isaacroldan commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

WHY are these changes introduced?

Same-major automatic updates no longer reach the old major-version warning. Users with auto-upgrade enabled therefore stop seeing that a newer major is available, even though they must choose that upgrade manually.

Stacked on #9032, above #9034 and #9028.

WHAT is this pull request doing?

Show a daily new-major notice with release notes and an explicit manual upgrade command, using the existing latest cache. Check notices independently of whether a same-major update is available or its daily install attempt can run.

Keep same-major notices for users who disable auto-upgrade. Notices remain suppressed in CI and for prerelease builds. The notice adds no registry request and uses its own existing daily interval, separate from automatic install attempts.

Validation: 74 focused tests and the cli-kit type check pass. Six regression tests fail against the parent PR. The daily-limit test uses a real temporary config store and verifies that a recent auto-upgrade attempt does not suppress the notice. Changed-file lint passes with the Nx boundary rule skipped because no cached project graph is available.

How to manually test your changes?

Use macOS with Bash or Zsh, Node 22.12+ and pnpm 10.11.1. Start in a clean checkout of this repository.

  1. Check out this PR and build the CLI:
gh pr checkout 9037 --detach
pnpm install --frozen-lockfile
pnpm nx build cli
  1. Copy and run the whole block below. This simulates a CLI already on the newest release in its current major, with version 5 available. No install should run.
Copy-and-paste scenario
(
set -eu
export CLI_QA_REPO="$PWD"
export CLI_QA_DIR="$(mktemp -d /tmp/shopify-cli-review.XXXXXX)"
export CLI_QA_VERSION_FILE="$CLI_QA_REPO/packages/cli-kit/dist/public/common/version.js"
cp "$CLI_QA_VERSION_FILE" "$CLI_QA_DIR/original-version.js"
trap 'cp "$CLI_QA_DIR/original-version.js" "$CLI_QA_VERSION_FILE"; rm -rf "$CLI_QA_DIR"' EXIT

# Keep CLI preferences inside the temporary directory.
cat > "$CLI_QA_DIR/private-home.cjs" <<'JS'
const os = require('node:os')
os.homedir = () => process.env.CLI_QA_DIR
// Keep notification helpers attached until the temporary files can be removed.
const processes = require('node:child_process')
const spawn = processes.spawn
processes.spawn = function (command, args, options) {
  const child = spawn.call(this, command, args, options)
  if (Array.isArray(args) && args.includes('notifications') && args.includes('--ignore-errors')) child.unref = () => child
  return child
}
require('node:module').syncBuiltinESMExports()
JS
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require=$CLI_QA_DIR/private-home.cjs"
export CLI_QA_CONFIG="$CLI_QA_DIR/Library/Preferences/shopify-cli-kit-nodejs/config.json"
export SHOPIFY_CLI_ENV=production
export SHOPIFY_CLI_NO_ANALYTICS=1
unset NPM_CONFIG_PREFIX NPM_CONFIG_CACHE NPM_CONFIG_USERCONFIG NPM_CONFIG_GLOBALCONFIG NPM_CONFIG_REGISTRY
export npm_config_prefix="$CLI_QA_DIR/npm"
export npm_config_cache="$CLI_QA_DIR/npm-cache"
export npm_config_userconfig="$CLI_QA_DIR/npmrc"
export npm_config_globalconfig="$CLI_QA_DIR/global-npmrc"
export npm_config_registry=https://registry.npmjs.org/
: > "$npm_config_userconfig"
: > "$npm_config_globalconfig"
unset SHOPIFY_HOMEBREW_FORMULA SHOPIFY_UNIT_TEST CI
mkdir -p "$CLI_QA_DIR/npm/bin" "$CLI_QA_DIR/npm/lib/node_modules/@shopify" "$(dirname "$CLI_QA_CONFIG")"
ln -s "$CLI_QA_REPO/packages/cli" "$CLI_QA_DIR/npm/lib/node_modules/@shopify/cli"
ln -s ../lib/node_modules/@shopify/cli/bin/run.js "$CLI_QA_DIR/npm/bin/shopify"
export PATH="$CLI_QA_DIR/npm/bin:$PATH"

# Simulate an older stable CLI without changing tracked source files.
node -e 'require("node:fs").writeFileSync(process.env.CLI_QA_VERSION_FILE, "export const CLI_KIT_VERSION = \"3.59.0\"\n")'

node <<'JS'
const fs = require('node:fs')
fs.writeFileSync(process.env.CLI_QA_CONFIG, JSON.stringify({
  autoUpgradeEnabled: true,
  cache: {
    'npm-package-@shopify/cli': {value: '5.0.0', timestamp: Date.now()},
    'npm-package-@shopify/cli@3.x': {value: '3.59.0', timestamp: Date.now()},
  },
}))
JS
printf '\nFIRST RUN: expect the new-major warning\n'
shopify version
printf '\nSECOND RUN: expect no repeat of that warning\n'
shopify version
node <<'JS'
const fs = require('node:fs')
const path = process.env.CLI_QA_CONFIG
const config = JSON.parse(fs.readFileSync(path, 'utf8'))
config.cache['most-recent-occurrence-warn-on-available-upgrade'].timestamp = Date.now() - 86400001
fs.writeFileSync(path, JSON.stringify(config))
JS
printf '\nTHIRD RUN: expect the new-major warning again\n'
shopify version
)
  1. Check the terminal output:
  • First run: a new-major warning includes npm install -g @shopify/cli@5.0.0, This is a major version, and https://github.com/Shopify/cli/releases/tag/5.0.0.
  • Second run: the same new-major warning does not appear.
  • Third run: after the block moves the notice timestamp back by 24 hours, the warning appears again.
  • None of the three runs should print Now upgrading by running.
  1. Cleanup is automatic when the block exits: temporary preferences, packages and stubs are removed, and the original compiled version file is restored. Your normal Shopify CLI installation and preferences are not changed.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows)
  • I've considered possible documentation changes
  • I've considered analytics changes to measure impact
  • The change is user-facing — I've identified the correct bump type (patch) and added a changeset

@isaacroldan
isaacroldan requested a review from a team as a code owner October 28, 2026 17:06
@isaacroldan
isaacroldan added this pull request to stack #9035 October 28, 2026 17:06
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Oct 28, 2026
@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

Copy link
Copy Markdown
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/public/node/is-global.d.ts
@@ -12,6 +12,13 @@ export declare function currentProcessIsGlobal(argv?: string[]): boolean;
  * @param packageManager - The package manager to use.
  */
 export declare function installGlobalShopifyCLI(packageManager: PackageManager): Promise<void>;
+/**
+ * Returns the Homebrew formula that installed the running CLI.
+ *
+ * @param env - Environment variables set by the Homebrew wrapper.
+ * @returns The wrapper's formula name, or the unversioned formula for older installs.
+ */
+export declare function getHomebrewCLIFormula(env?: NodeJS.ProcessEnv): string;
 export interface InstallGlobalCLIPromptResult {
     install: boolean;
     alreadyInstalled: boolean;
packages/cli-kit/dist/public/node/node-package-manager.d.ts
@@ -137,10 +137,12 @@ export declare function usesWorkspaces(appDirectory: string): Promise<boolean>;
  * @param currentVersion - The current version.
  * @param cacheExpiryInHours - If the last check was done more than this amount of hours ago, it will
  * refresh the cache. Defaults to always refreshing.
+ * @param versionRange - An optional semver range that limits the versions considered.
  * @returns A promise that resolves with a more recent version or undefined if there's no more recent version.
  */
-export declare function checkForNewVersion(dependency: string, currentVersion: string, { cacheExpiryInHours }?: {
-    cacheExpiryInHours?: number | undefined;
+export declare function checkForNewVersion(dependency: string, currentVersion: string, { cacheExpiryInHours, versionRange }?: {
+    cacheExpiryInHours?: number;
+    versionRange?: string;
 }): Promise<string | undefined>;
 /**
  * Given an NPM dependency, it checks if there's a cached more recent version, and if there is, it returns its value.
packages/cli-kit/dist/public/node/upgrade.d.ts
@@ -5,9 +5,11 @@ export { getAutoUpgradeEnabled, setAutoUpgradeEnabled };
  * Utility function for generating an install command for the user to run
  * to install an updated version of Shopify CLI.
  *
+ * @param version - The version or dist-tag to install. Homebrew can't
+ * install a specific version, so it always upgrades to the formula's current version.
  * @returns A string with the command to run, or undefined if the package manager cannot be determined.
  */
-export declare function cliInstallCommand(): string | undefined;
+export declare function cliInstallCommand(version: string): string | undefined;
 /**
  * Options for {@link runCLIUpgrade}.
  */
@@ -19,6 +21,13 @@ export interface RunCLIUpgradeOptions {
      * their `package.json` / lockfile in the background.
      */
     autoupgrade?: boolean;
+    /**
+     * The exact version to install. The auto-upgrade hook passes the version it already
+     * vetted, such as one that isn't a major version change, so the install can't drift to
+     * whatever `latest` points at by the time the package manager runs. When omitted, the
+     * CLI installs `latest`.
+     */
+    version?: string;
 }
 /**
  * Runs the CLI upgrade using the appropriate package manager.
@@ -36,13 +45,13 @@ export declare function runCLIUpgrade(options?: RunCLIUpgradeOptions): Promise<v
  */
 export declare function upgradeCLI(options?: RunCLIUpgradeOptions): Promise<UpgradeResult>;
 /**
- * Returns the version to auto-upgrade to, or undefined if auto-upgrade should be skipped.
+ * Finds the newest stable release in the current major, or skips automatic upgrades.
  * Auto-upgrade is enabled by default and can be disabled via `setAutoUpgradeEnabled(false)`.
  * 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;
+export declare function versionToAutoUpgrade(): Promise<string | undefined>;
 /**
  * Checks the freshly fetched notifications feed for a kill-switch notification that
  * disables auto-upgrade. A blocking notification is one with `surface: "autoupgrade"`,
@@ -56,9 +65,9 @@ export declare function versionToAutoUpgrade(): string | undefined;
  */
 export declare function hasBlockingAutoUpgradeNotification(): Promise<boolean>;
 /**
- * Shows a daily upgrade-available warning for users who have not enabled auto-upgrade.
- * Skipped in CI and for pre-release versions. When auto-upgrade is enabled this is a no-op
- * because the postrun hook will handle the upgrade directly.
+ * Shows a daily notice for a new major, independently of automatic same-major upgrades.
+ * When auto-upgrade is disabled, also shows notices for minor and patch releases.
+ * Skipped in CI and for pre-release versions.
  */
 export declare function warnIfUpgradeAvailable(): Promise<void>;
 /**
packages/cli-kit/dist/public/node/version.d.ts
@@ -8,9 +8,16 @@ export declare function localCLIVersion(directory: string): Promise<string | und
 /**
  * Returns the version of the globally installed CLI, only if it's greater than 3.59.0 (when the global CLI was introduced).
  *
+ * @param binary - The CLI command to locate and run.
  * @returns The version of the CLI if it is globally installed or undefined.
  */
-export declare function globalCLIVersion(): Promise<string | undefined>;
+export declare function globalCLIVersion(binary?: string): Promise<string | undefined>;
+/**
+ * Refreshes Homebrew metadata and returns the stable version of the running CLI's formula.
+ *
+ * @returns The available version, or undefined when the formula cannot be read.
+ */
+export declare function availableHomebrewCLIVersion(): Promise<string | undefined>;
 /**
  * Returns true if the given version is a pre-release version.
  * Meaning is a `nightly`, `snapshot`, or `experimental` version.
packages/cli-kit/dist/public/node/hooks/postrun.d.ts
@@ -22,8 +22,9 @@ export declare function postRunHookHasCompleted(): boolean;
 export declare function waitForPostRunHookAndExit(): void;
 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.
+ * Checks release notices and auto-upgrades the CLI after a command completes.
+ * Notices have their own daily limit. Upgrade attempts run once per day unless forced
+ * via SHOPIFY_CLI_FORCE_AUTO_UPGRADE.
  *
  * @returns Resolves when the upgrade attempt (or fallback warning) is complete.
  */
packages/cli-kit/dist/public/node/hooks/prerun.d.ts
@@ -12,6 +12,7 @@ export declare function parseCommandContent(cmdInfo: {
 }): CommandContent;
 /**
  * Triggers a background check for a newer CLI version (non-blocking).
- * The result is cached and consumed by the postrun hook for auto-upgrade.
+ * Caches the result for release notices and manual-upgrade hints.
+ * Automatic upgrades use a separate lookup restricted to the current major version.
  */
 export declare function checkForNewVersionInBackground(): Promise<void>;
\ No newline at end of file
packages/cli-kit/dist/public/node/upgrade/output.d.ts
@@ -1,3 +1,4 @@
+import { type ExecOptions } from '../system.js';
 import { Writable } from 'node:stream';
 /**
  * Keeps package-manager output off the result channel in JSON mode.
@@ -13,5 +14,6 @@ export declare function upgradeOutputStreams(): {
  *
  * @param command - The package-manager executable.
  * @param args - The install arguments.
+ * @param options - Environment overrides for the package manager.
  */
-export declare function execUpgradeCommand(command: string, args: string[]): Promise<void>;
\ No newline at end of file
+export declare function execUpgradeCommand(command: string, args: string[], options?: Pick<ExecOptions, 'env'>): Promise<void>;
\ No newline at end of file

@github-actions

Copy link
Copy Markdown
Contributor

The snapshot requested by @isaacroldan for 209a8fdf8297327fe65583532cdf28ca5502238c could not be published. View the workflow run.

@isaacroldan

Copy link
Copy Markdown
Contributor Author

/snapit

@github-actions

Copy link
Copy Markdown
Contributor

🫰✨ Thanks @isaacroldan! Your snapshot has been published to npm.

Built from 209a8fdf8297327fe65583532cdf28ca5502238c. Workflow run.

Test the snapshot by installing your package globally:

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

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.

This branch has not been deployed

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

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant