Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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