Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/upgrade-non-interactive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-hydrogen': patch
---

Add `--non-interactive` flag (alias `-y`/`--yes`, env `SHOPIFY_HYDROGEN_FLAG_NON_INTERACTIVE`) to `shopify hydrogen upgrade` so it can run in CI and other non-TTY environments. Requires `--version` to be set, auto-accepts the confirmation prompt, and auto-overwrites any existing upgrade instructions file. When run without a TTY and without this flag, the resulting error now points to it.
Comment thread
z0n marked this conversation as resolved.
11 changes: 11 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1655,6 +1655,17 @@
"name": "force",
"allowNo": false,
"type": "boolean"
},
"non-interactive": {
"aliases": [
"yes"
],
"char": "y",
"description": "Run without any interactive prompts. Requires --version. Auto-accepts the upgrade confirmation and overwrites any existing upgrade instructions file.",
"env": "SHOPIFY_HYDROGEN_FLAG_NON_INTERACTIVE",
"name": "non-interactive",
"allowNo": false,
"type": "boolean"
}
},
"hasDynamicHelp": false,
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/hydrogen/upgrade-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* UPGRADE_TEST_TO=<version> - Test to specific version (default: latest)
* UPGRADE_TEST_LAST_N=<number> - Test last N versions
* FORCE_CHANGELOG_SOURCE=local - Read changelog from local file (set by tests)
* SHOPIFY_HYDROGEN_FLAG_FORCE=1 - Skip interactive prompts (set by tests)
* SHOPIFY_HYDROGEN_FLAG_FORCE=1 - Skip dirty git branch check (set by tests)
Comment thread
z0n marked this conversation as resolved.
* CI=1 - Enable CI mode (set by tests)
*
* Key design decisions:
Expand Down Expand Up @@ -72,6 +72,7 @@ vi.mock('@shopify/cli-kit/node/ui', async () => {

return {
...original,
isTTY: vi.fn(() => true),
renderTasks: vi.fn(async (tasks) => {
for (const task of tasks) {
if (task.task && typeof task.task === 'function') {
Expand Down
170 changes: 170 additions & 0 deletions packages/cli/src/commands/hydrogen/upgrade.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {describe, it, expect, vi, beforeEach} from 'vitest';
import {writeFile, fileExists} from '@shopify/cli-kit/node/fs';
import {joinPath} from '@shopify/cli-kit/node/path';
import {
isTTY,
renderSelectPrompt,
renderConfirmationPrompt,
renderTasks,
Expand Down Expand Up @@ -48,6 +49,7 @@ vi.mock('@shopify/cli-kit/node/ui', async () => {

return {
...original,
isTTY: vi.fn(() => true),
renderTasks: vi.fn(() => Promise.resolve()),
renderSelectPrompt: vi.fn(() => Promise.resolve()),
renderConfirmationPrompt: vi.fn(() => Promise.resolve(false)),
Expand Down Expand Up @@ -2001,6 +2003,174 @@ describe('upgrade', async () => {
expect(args).toEqual(result);
});
});

describe('non-interactive mode', () => {
it('throws when --non-interactive is set without --version', async () => {
await inTemporaryHydrogenRepo(
async (appPath) => {
await expect(
runUpgrade({appPath, nonInteractive: true}),
).rejects.toThrowError('--non-interactive flag requires --version');
},
{
cleanGitRepo: true,
packageJson: OUTDATED_HYDROGEN_PACKAGE_JSON,
},
);
});

it('throws with a hint pointing at --non-interactive when stdin is not a TTY', async () => {
vi.mocked(isTTY).mockReturnValueOnce(false);

await inTemporaryHydrogenRepo(
async (appPath) => {
await expect(runUpgrade({appPath})).rejects.toThrowError(
/requires an interactive terminal/,
);
},
{
cleanGitRepo: true,
packageJson: OUTDATED_HYDROGEN_PACKAGE_JSON,
},
);
});

it('bypasses the TTY check when --non-interactive is set', async () => {
Comment thread
z0n marked this conversation as resolved.
vi.mocked(isTTY).mockReturnValue(false);

await inTemporaryHydrogenRepo(
async (appPath) => {
// Without --version this should fail for the preconditions reason,
// not for the TTY reason — proving the TTY gate didn't trigger.
await expect(
runUpgrade({appPath, nonInteractive: true}),
).rejects.toThrowError('--non-interactive flag requires --version');
},
{
cleanGitRepo: true,
packageJson: OUTDATED_HYDROGEN_PACKAGE_JSON,
},
);
});

it('throws from getSelectedRelease when --version is not a known upgrade target', async () => {
await inTemporaryHydrogenRepo(
async (appPath) => {
const {releases} = await getChangelog();
const current = await getHydrogenVersion({appPath});
const {availableUpgrades} = getAvailableUpgrades({
...current,
releases,
});

await expect(
getSelectedRelease({
availableUpgrades,
currentVersion: current.currentVersion,
currentDependencies: current.currentDependencies,
targetVersion: '9999.99.99',
nonInteractive: true,
}),
).rejects.toThrowError(
/9999\.99\.99 is not an available Hydrogen upgrade target/,
);

expect(renderSelectPrompt).not.toHaveBeenCalled();
},
{
cleanGitRepo: true,
packageJson: OUTDATED_HYDROGEN_PACKAGE_JSON,
},
);
});

it('auto-confirms displayConfirmation when nonInteractive is set', async () => {
const selectedRelease = {
title: 'Test release',
version: '2025.7.0',
} as Release;

const result = await displayConfirmation({
cumulativeRelease: CUMULATIVE_RELEASE,
selectedRelease,
nonInteractive: true,
});

expect(result).toEqual(true);
expect(renderConfirmationPrompt).not.toHaveBeenCalled();
});

it('bypasses the TTY check when --version=next is passed', async () => {
vi.mocked(isTTY).mockReturnValue(false);

await inTemporaryHydrogenRepo(
async (appPath) => {
await expect(
runUpgrade({appPath, version: 'next', force: false}),
).resolves.toBeUndefined();
},
{
cleanGitRepo: true,
packageJson: {
dependencies: {
'@shopify/hydrogen': '2025.4.0',
},
},
},
);
});

it('overwrites an existing upgrade instructions file without prompting', async () => {
await inTemporaryHydrogenRepo(
async (appPath) => {
const {releases} = await getChangelog();

const selectedRelease = releases.find(
(release) => release.version === '2023.10.0',
) as (typeof releases)[0];

const releaseWithRemovals: CumulativeRelease = {
...CUMULATIVE_RELEASE,
removeDependencies: ['@remix-run/react'],
removeDevDependencies: ['@remix-run/dev'],
};

// First run creates the instructions file.
const firstPath = await generateUpgradeInstructionsFile({
appPath,
cumulativeRelease: releaseWithRemovals,
currentVersion: '2023.1.6',
selectedRelease,
});

expect(firstPath).toBeDefined();

// Second run, non-interactive, should overwrite without prompting.
const secondPath = await generateUpgradeInstructionsFile({
appPath,
cumulativeRelease: releaseWithRemovals,
currentVersion: '2023.1.6',
selectedRelease,
nonInteractive: true,
});

expect(secondPath).toBe(firstPath);
expect(renderConfirmationPrompt).not.toHaveBeenCalled();

const mdContent = await readFile(
joinPath(appPath, secondPath!),
'utf8',
);

expect(mdContent).toContain('## Removed packages');
},
{
cleanGitRepo: true,
packageJson: OUTDATED_HYDROGEN_PACKAGE_JSON,
},
);
});
});
});

// cumulative result when upgrading from 2023.1.6 (outdated) to 2023.4.1
Expand Down
Loading
Loading