diff --git a/cli/commands/site/set.ts b/cli/commands/site/set.ts
index 6ba393037a..024f4bba98 100644
--- a/cli/commands/site/set.ts
+++ b/cli/commands/site/set.ts
@@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n';
import { DEFAULT_WORDPRESS_VERSION, MINIMUM_WORDPRESS_VERSION } from 'common/constants';
import { getDomainNameValidationError } from 'common/lib/domains';
import { arePathsEqual } from 'common/lib/fs-utils';
+import { siteNeedsRestart } from 'common/lib/site-needs-restart';
import {
getWordPressVersionUrl,
isValidWordPressVersion,
@@ -68,7 +69,7 @@ export async function runCommand(
throw new LoggerError( __( 'Site name cannot be empty.' ) );
}
- if ( xdebug !== undefined && ! ( await isXdebugBetaEnabled() ) ) {
+ if ( xdebug === true && ! ( await isXdebugBetaEnabled() ) ) {
throw new LoggerError(
__( 'Xdebug support is a beta feature. Enable it in Studio settings first.' )
);
@@ -81,7 +82,7 @@ export async function runCommand(
const initialAppdata = await readAppdata();
- if ( domain !== undefined ) {
+ if ( domain ) {
const existingDomainNames = initialAppdata.sites
.filter( ( s ) => s.id !== site.id )
.map( ( s ) => s.customDomain )
@@ -129,7 +130,13 @@ export async function runCommand(
);
}
- const needsRestart = domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged;
+ const needsRestart = siteNeedsRestart( {
+ domainChanged,
+ httpsChanged,
+ phpChanged,
+ wpChanged,
+ xdebugChanged,
+ } );
const oldDomain = site.customDomain;
try {
@@ -144,7 +151,7 @@ export async function runCommand(
foundSite.name = name!;
}
if ( domainChanged ) {
- foundSite.customDomain = domain;
+ foundSite.customDomain = domain || undefined;
}
if ( httpsChanged ) {
foundSite.enableHttps = https;
@@ -185,7 +192,6 @@ export async function runCommand(
const phpVersion = validatePhpVersion( site.phpVersion );
const zipUrl = getWordPressVersionUrl( wp );
- // Use the correct site URL to avoid corrupting WordPress URL options
let siteUrl: string | undefined;
if ( site.customDomain ) {
const protocol = site.enableHttps ? 'https' : 'http';
@@ -307,7 +313,6 @@ export const registerCommand = ( yargs: StudioArgv ) => {
wp: argv.wp,
xdebug: argv.xdebug,
} );
- // WP-CLI leaves handles open, so we need to explicitly exit
// See: cli/lib/run-wp-cli-command.ts FIXME comment
if ( result.usedWpCli ) {
process.exit( 0 );
@@ -319,6 +324,7 @@ export const registerCommand = ( yargs: StudioArgv ) => {
const loggerError = new LoggerError( __( 'Failed to configure site' ), error );
logger.reportError( loggerError );
}
+ process.exit( 1 );
}
},
} );
diff --git a/cli/lib/types/wordpress-server-ipc.ts b/cli/lib/types/wordpress-server-ipc.ts
index d08df182ab..f4e305098e 100644
--- a/cli/lib/types/wordpress-server-ipc.ts
+++ b/cli/lib/types/wordpress-server-ipc.ts
@@ -122,6 +122,7 @@ export const childMessagePm2Schema = z.object( {
export const pm2ProcessEventSchema = z.object( {
process: z.object( {
name: z.string(),
+ pm_id: z.number().optional(),
} ),
event: z.string(),
} );
diff --git a/cli/lib/wordpress-server-manager.ts b/cli/lib/wordpress-server-manager.ts
index c84c8e81bc..0b5020d35a 100644
--- a/cli/lib/wordpress-server-manager.ts
+++ b/cli/lib/wordpress-server-manager.ts
@@ -25,6 +25,7 @@ import { ProcessDescription } from 'cli/lib/types/pm2';
import {
ServerConfig,
childMessagePm2Schema,
+ pm2ProcessEventSchema,
ManagerMessagePayload,
} from 'cli/lib/types/wordpress-server-ipc';
import { Logger } from 'cli/logger';
@@ -125,6 +126,7 @@ export async function startWordPressServer(
await waitForReadyMessage( processDesc.pmId );
await sendMessage(
processDesc.pmId,
+ processName,
{
topic: 'start-server',
data: { config: serverConfig },
@@ -192,6 +194,7 @@ interface SendMessageOptions {
async function sendMessage(
pmId: number,
+ processName: string,
message: ManagerMessagePayload,
options: SendMessageOptions = {}
): Promise< unknown > {
@@ -199,6 +202,7 @@ async function sendMessage(
const bus = await getPm2Bus();
const messageId = crypto.randomUUID();
let responseHandler: ( packet: unknown ) => void;
+ let processEventHandler: ( event: unknown ) => void;
let abortListener: () => void;
return new Promise( ( resolve, reject ) => {
@@ -228,6 +232,17 @@ async function sendMessage(
activityCheckIntervalId,
} );
+ processEventHandler = ( event: unknown ) => {
+ const result = pm2ProcessEventSchema.safeParse( event );
+ if ( ! result.success ) {
+ return;
+ }
+
+ if ( result.data.process.name === processName && result.data.event === 'exit' ) {
+ reject( new Error( 'WordPress server process exited unexpectedly' ) );
+ }
+ };
+
responseHandler = ( packet: unknown ) => {
const validationResult = childMessagePm2Schema.safeParse( packet );
if ( ! validationResult.success ) {
@@ -274,11 +289,13 @@ async function sendMessage(
};
abortController.signal.addEventListener( 'abort', abortListener );
+ bus.on( 'process:event', processEventHandler );
bus.on( 'process:msg', responseHandler );
sendMessageToProcess( pmId, { ...message, messageId } ).catch( reject );
} ).finally( () => {
abortController.signal.removeEventListener( 'abort', abortListener );
+ bus.off( 'process:event', processEventHandler );
bus.off( 'process:msg', responseHandler );
const tracker = messageActivityTrackers.get( messageId );
@@ -299,6 +316,7 @@ export async function stopWordPressServer( siteId: string ): Promise< void > {
try {
await sendMessage(
runningProcess.pmId,
+ processName,
{ topic: 'stop-server', data: {} },
{ maxTotalElapsedTime: GRACEFUL_STOP_TIMEOUT }
);
@@ -376,6 +394,7 @@ export async function runBlueprint(
await waitForReadyMessage( processDesc.pmId );
await sendMessage(
processDesc.pmId,
+ processName,
{
topic: 'run-blueprint',
data: { config: serverConfig },
@@ -405,7 +424,7 @@ export async function sendWpCliCommand(
throw new Error( `WordPress server is not running` );
}
- const result = await sendMessage( runningProcess.pmId, {
+ const result = await sendMessage( runningProcess.pmId, processName, {
topic: 'wp-cli-command',
data: { args },
} );
diff --git a/cli/wordpress-server-child.ts b/cli/wordpress-server-child.ts
index ae41d2e474..7978bc84b1 100644
--- a/cli/wordpress-server-child.ts
+++ b/cli/wordpress-server-child.ts
@@ -344,11 +344,48 @@ const runWpCliCommand = sequential(
{ concurrent: 3, max: 10 }
);
+function parsePhpError( error: unknown ): string {
+ if ( ! ( error instanceof Error ) ) {
+ return String( error );
+ }
+
+ const message = error.message;
+
+ // Check for WordPress critical error in HTML output
+ const wpDieMatch = message.match( /
]*>([\s\S]*?)<\/div>/ );
+ if ( wpDieMatch ) {
+ // Extract text from HTML, removing tags
+ const htmlContent = wpDieMatch[ 1 ];
+ const textContent = htmlContent
+ .replace( /<[^>]+>/g, ' ' )
+ .replace( /\s+/g, ' ' )
+ .trim();
+ if ( textContent ) {
+ return `WordPress error: ${ textContent }`;
+ }
+ }
+
+ // Check for PHP fatal error pattern
+ const fatalMatch = message.match( /PHP Fatal error:\s*(.+?)(?:\sin\s|$)/i );
+ if ( fatalMatch ) {
+ return `PHP Fatal error: ${ fatalMatch[ 1 ].trim() }`;
+ }
+
+ // Check for generic PHP.run() failure - provide a cleaner message
+ if ( message.includes( 'PHP.run() failed with exit code' ) ) {
+ const exitCodeMatch = message.match( /exit code (\d+)/ );
+ const exitCode = exitCodeMatch ? exitCodeMatch[ 1 ] : 'unknown';
+ return `WordPress failed to start (PHP exit code ${ exitCode }). Check the site's debug.log for details.`;
+ }
+
+ return message;
+}
+
function sendErrorMessage( messageId: string, error: unknown ) {
const errorResponse: ChildMessageRaw = {
originalMessageId: messageId,
topic: 'error',
- errorMessage: error instanceof Error ? error.message : String( error ),
+ errorMessage: parsePhpError( error ),
errorStack: error instanceof Error ? error.stack : undefined,
cliArgs: lastCliArgs ?? undefined,
};
diff --git a/common/lib/site-needs-restart.ts b/common/lib/site-needs-restart.ts
new file mode 100644
index 0000000000..90a6592cb5
--- /dev/null
+++ b/common/lib/site-needs-restart.ts
@@ -0,0 +1,13 @@
+export interface SiteSettingChanges {
+ domainChanged?: boolean;
+ httpsChanged?: boolean;
+ phpChanged?: boolean;
+ wpChanged?: boolean;
+ xdebugChanged?: boolean;
+}
+
+export function siteNeedsRestart( changes: SiteSettingChanges ): boolean {
+ const { domainChanged, httpsChanged, phpChanged, wpChanged, xdebugChanged } = changes;
+
+ return !! ( domainChanged || httpsChanged || phpChanged || wpChanged || xdebugChanged );
+}
diff --git a/common/lib/tests/site-needs-restart.test.ts b/common/lib/tests/site-needs-restart.test.ts
new file mode 100644
index 0000000000..0a8471474c
--- /dev/null
+++ b/common/lib/tests/site-needs-restart.test.ts
@@ -0,0 +1,60 @@
+import { siteNeedsRestart } from '../site-needs-restart';
+
+describe( 'siteNeedsRestart', () => {
+ it( 'returns false when no changes are provided', () => {
+ expect( siteNeedsRestart( {} ) ).toBe( false );
+ } );
+
+ it( 'returns false when all changes are false', () => {
+ expect(
+ siteNeedsRestart( {
+ domainChanged: false,
+ httpsChanged: false,
+ phpChanged: false,
+ wpChanged: false,
+ xdebugChanged: false,
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns true when domain changed', () => {
+ expect( siteNeedsRestart( { domainChanged: true } ) ).toBe( true );
+ } );
+
+ it( 'returns true when https changed', () => {
+ expect( siteNeedsRestart( { httpsChanged: true } ) ).toBe( true );
+ } );
+
+ it( 'returns true when php changed', () => {
+ expect( siteNeedsRestart( { phpChanged: true } ) ).toBe( true );
+ } );
+
+ it( 'returns true when wp changed', () => {
+ expect( siteNeedsRestart( { wpChanged: true } ) ).toBe( true );
+ } );
+
+ it( 'returns true when xdebug changed', () => {
+ expect( siteNeedsRestart( { xdebugChanged: true } ) ).toBe( true );
+ } );
+
+ it( 'returns true when multiple settings changed', () => {
+ expect(
+ siteNeedsRestart( {
+ phpChanged: true,
+ wpChanged: true,
+ } )
+ ).toBe( true );
+ } );
+
+ it( 'returns true when one change is true among false values', () => {
+ expect(
+ siteNeedsRestart( {
+ domainChanged: false,
+ httpsChanged: false,
+ phpChanged: true,
+ wpChanged: false,
+ xdebugChanged: false,
+ } )
+ ).toBe( true );
+ } );
+} );
diff --git a/src/components/tests/content-tab-settings.test.tsx b/src/components/tests/content-tab-settings.test.tsx
index 7ded339cb9..7a0712f296 100644
--- a/src/components/tests/content-tab-settings.test.tsx
+++ b/src/components/tests/content-tab-settings.test.tsx
@@ -371,7 +371,8 @@ describe( 'ContentTabSettings', () => {
await waitFor( () => {
expect( updateSite ).toHaveBeenCalledWith(
- expect.objectContaining( { phpVersion: '8.2' } )
+ expect.objectContaining( { phpVersion: '8.2' } ),
+ undefined
);
expect( stopServer ).not.toHaveBeenCalled();
expect( startServer ).not.toHaveBeenCalled();
@@ -441,10 +442,9 @@ describe( 'ContentTabSettings', () => {
await waitFor( () => {
expect( updateSite ).toHaveBeenCalledWith(
- expect.objectContaining( { phpVersion: '8.2' } )
+ expect.objectContaining( { phpVersion: '8.2' } ),
+ undefined
);
- expect( stopServer ).toHaveBeenCalled();
- expect( startServer ).toHaveBeenCalled();
} );
rerenderWithProvider(
diff --git a/src/hooks/use-site-details.tsx b/src/hooks/use-site-details.tsx
index 706412a893..104cd0c426 100644
--- a/src/hooks/use-site-details.tsx
+++ b/src/hooks/use-site-details.tsx
@@ -21,7 +21,7 @@ import type { Blueprint } from 'src/stores/wpcom-api';
interface SiteDetailsContext {
selectedSite: SiteDetails | null;
- updateSite: ( site: SiteDetails ) => Promise< void >;
+ updateSite: ( site: SiteDetails, wpVersion?: string ) => Promise< void >;
sites: SiteDetails[];
setSelectedSiteId: ( selectedSiteId: string ) => void;
createSite: (
@@ -375,8 +375,8 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) {
[ selectedTab, setSelectedSiteId, setSelectedTab ]
);
- const updateSite = useCallback( async ( site: SiteDetails ) => {
- await getIpcApi().updateSite( site );
+ const updateSite = useCallback( async ( site: SiteDetails, wpVersion?: string ) => {
+ await getIpcApi().updateSite( site, wpVersion );
const updatedSites = await getIpcApi().getSiteDetails();
setSites( updatedSites );
}, [] );
diff --git a/src/ipc-handlers.ts b/src/ipc-handlers.ts
index 8c9ce861d3..c487e6faf2 100644
--- a/src/ipc-handlers.ts
+++ b/src/ipc-handlers.ts
@@ -63,6 +63,7 @@ import {
import { getLogsFilePath, writeLogToFile, type LogLevel } from 'src/logging';
import { getMainWindow } from 'src/main-window';
import { popupMenu, setupMenu } from 'src/menu';
+import { editSiteViaCli, EditSiteOptions } from 'src/modules/cli/lib/cli-site-editor';
import { shouldExcludeFromSync, shouldLimitDepth } from 'src/modules/sync/lib/tree-utils';
import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-settings/lib/editor';
import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers';
@@ -347,23 +348,69 @@ export async function createSite(
export async function updateSite(
event: IpcMainInvokeEvent,
- updatedSite: SiteDetails
+ updatedSite: SiteDetails,
+ wpVersion?: string
): Promise< void > {
- try {
- await lockAppdata();
- const userData = await loadUserData();
- const updatedSites = userData.sites.map( ( site ) =>
- site.id === updatedSite.id ? { ...site, ...updatedSite } : site
- );
- userData.sites = updatedSites;
+ const server = SiteServer.get( updatedSite.id );
+ if ( ! server ) {
+ throw new Error( `Site not found: ${ updatedSite.id }` );
+ }
- const server = SiteServer.get( updatedSite.id );
- if ( server ) {
- await server.updateSiteDetails( updatedSite );
+ const currentSite = server.details;
+
+ const options: EditSiteOptions = {
+ path: currentSite.path,
+ siteId: updatedSite.id,
+ };
+
+ if ( updatedSite.name !== currentSite.name ) {
+ options.name = updatedSite.name;
+ }
+
+ if ( updatedSite.customDomain !== currentSite.customDomain ) {
+ options.domain = updatedSite.customDomain ?? '';
+ }
+
+ if ( updatedSite.enableHttps !== currentSite.enableHttps ) {
+ options.https = updatedSite.enableHttps ?? false;
+ }
+
+ if ( updatedSite.phpVersion !== currentSite.phpVersion ) {
+ options.php = updatedSite.phpVersion;
+ }
+
+ if ( wpVersion ) {
+ options.wp = wpVersion;
+ }
+
+ if ( updatedSite.enableXdebug !== currentSite.enableXdebug ) {
+ options.xdebug = updatedSite.enableXdebug ?? false;
+ }
+
+ const hasCliChanges = Object.keys( options ).length > 2;
+
+ if ( hasCliChanges ) {
+ await editSiteViaCli( options );
+
+ const userData = await loadUserData();
+ const freshSiteData = userData.sites.find( ( s ) => s.id === updatedSite.id );
+ if ( freshSiteData ) {
+ const wasRunning = server.details.running;
+ const url = wasRunning ? ( server.details as StartedSiteDetails ).url : undefined;
+
+ if ( wasRunning && url ) {
+ server.details = {
+ ...freshSiteData,
+ running: true,
+ url,
+ };
+ } else {
+ server.details = {
+ ...freshSiteData,
+ running: false,
+ };
+ }
}
- await saveUserData( userData );
- } finally {
- await unlockAppdata();
}
}
diff --git a/src/modules/cli/lib/cli-site-editor.ts b/src/modules/cli/lib/cli-site-editor.ts
new file mode 100644
index 0000000000..a82f668bbf
--- /dev/null
+++ b/src/modules/cli/lib/cli-site-editor.ts
@@ -0,0 +1,87 @@
+import { z } from 'zod';
+import { SiteCommandLoggerAction } from 'common/logger-actions';
+import { executeCliCommand } from './execute-command';
+
+const cliEventSchema = z.object( {
+ action: z.nativeEnum( SiteCommandLoggerAction ),
+ status: z.enum( [ 'inprogress', 'fail', 'success', 'warning' ] ),
+ message: z.string(),
+} );
+
+export interface EditSiteOptions {
+ path: string;
+ siteId: string;
+ name?: string;
+ domain?: string;
+ https?: boolean;
+ php?: string;
+ wp?: string;
+ xdebug?: boolean;
+}
+
+export async function editSiteViaCli( options: EditSiteOptions ): Promise< void > {
+ const args = buildCliArgs( options );
+ console.log( `[CLI Site Editor] Executing: studio ${ args.join( ' ' ) }` );
+
+ return new Promise( ( resolve, reject ) => {
+ let lastErrorMessage: string | null = null;
+
+ const [ emitter ] = executeCliCommand( args, { output: 'capture', logPrefix: options.siteId } );
+
+ emitter.on( 'data', ( { data } ) => {
+ const parsed = cliEventSchema.safeParse( data );
+ if ( ! parsed.success ) {
+ return;
+ }
+
+ if ( parsed.data.status === 'inprogress' ) {
+ console.log( `[CLI - ${ options.siteId }] ${ parsed.data.message }` );
+ } else if ( parsed.data.status === 'fail' ) {
+ lastErrorMessage = parsed.data.message;
+ }
+ } );
+
+ emitter.on( 'success', () => {
+ resolve();
+ } );
+
+ emitter.on( 'failure', () => {
+ console.error( `[CLI Site Editor] Command failed: ${ lastErrorMessage }` );
+ reject( new Error( lastErrorMessage || 'CLI site set failed' ) );
+ } );
+
+ emitter.on( 'error', ( { error } ) => {
+ reject( error );
+ } );
+ } );
+}
+
+function buildCliArgs( options: EditSiteOptions ): string[] {
+ const args = [ 'site', 'set', '--path', options.path ];
+
+ if ( options.name !== undefined ) {
+ args.push( '--name', options.name );
+ }
+
+ if ( options.domain !== undefined ) {
+ args.push( '--domain', options.domain );
+ }
+
+ if ( options.https !== undefined ) {
+ args.push( options.https ? '--https' : '--no-https' );
+ }
+
+ if ( options.php !== undefined ) {
+ args.push( '--php', options.php );
+ }
+
+ if ( options.wp !== undefined ) {
+ args.push( '--wp', options.wp );
+ }
+
+ if ( options.xdebug !== undefined ) {
+ args.push( options.xdebug ? '--xdebug' : '--no-xdebug' );
+ }
+
+ return args;
+}
diff --git a/src/modules/site-settings/edit-site-details.tsx b/src/modules/site-settings/edit-site-details.tsx
index c5e8acd0ea..8ae96e0897 100644
--- a/src/modules/site-settings/edit-site-details.tsx
+++ b/src/modules/site-settings/edit-site-details.tsx
@@ -3,9 +3,8 @@ import { createInterpolateElement } from '@wordpress/element';
import { sprintf } from '@wordpress/i18n';
import { useI18n } from '@wordpress/react-i18n';
import { FormEvent, useCallback, useEffect, useState } from 'react';
-import stripAnsi from 'strip-ansi';
import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'common/lib/domains';
-import { getWordPressVersionUrl } from 'common/lib/wordpress-version-utils';
+import { siteNeedsRestart } from 'common/lib/site-needs-restart';
import Button from 'src/components/button';
import { ErrorInformation } from 'src/components/error-information';
import { LearnMoreLink, LearnHowLink } from 'src/components/learn-more';
@@ -33,8 +32,7 @@ type EditSiteDetailsProps = {
const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) => {
const { __ } = useI18n();
- const { updateSite, selectedSite, stopServer, startServer, isEditModalOpen, setIsEditModalOpen } =
- useSiteDetails();
+ const { updateSite, selectedSite, isEditModalOpen, setIsEditModalOpen } = useSiteDetails();
const defaultWordPressVersion = useRootSelector( selectDefaultWordPressVersion );
const allowedPhpVersions = useRootSelector( selectAllowedPhpVersions );
const defaultPhpVersion = useRootSelector( selectDefaultPhpVersion );
@@ -139,60 +137,43 @@ const EditSiteDetails = ( { currentWpVersion, onSave }: EditSiteDetailsProps ) =
const hasWpVersionChanged = selectedWpVersion !== getEffectiveWpVersion();
const hasPhpVersionChanged = selectedPhpVersion !== selectedSite.phpVersion;
const hasXdebugChanged = enableXdebug !== ( selectedSite.enableXdebug ?? false );
+ const hasDomainChanged =
+ Boolean( selectedSite.customDomain ) !== useCustomDomain ||
+ ( useCustomDomain && customDomain !== selectedSite.customDomain );
+ const hasHttpsChanged =
+ useCustomDomain && enableHttps !== ( selectedSite.enableHttps ?? false );
+
const needsRestart =
- selectedSite.running && ( hasWpVersionChanged || hasPhpVersionChanged || hasXdebugChanged );
+ selectedSite.running &&
+ siteNeedsRestart( {
+ domainChanged: hasDomainChanged,
+ httpsChanged: hasHttpsChanged,
+ phpChanged: hasPhpVersionChanged,
+ wpChanged: hasWpVersionChanged,
+ xdebugChanged: hasXdebugChanged,
+ } );
setNeedsRestart( needsRestart );
try {
- if ( needsRestart ) {
- await stopServer( selectedSite.id );
- }
-
- if ( hasWpVersionChanged ) {
- try {
- const zipUrl = getWordPressVersionUrl( selectedWpVersion );
- const result = await getIpcApi().executeWPCLiInline( {
- siteId: selectedSite.id,
- args: `core update ${ zipUrl } --force`,
- skipPluginsAndThemes: true,
- } );
- if ( result.exitCode !== 0 ) {
- throw new Error( result.stderr );
- }
- } catch ( wpError ) {
- console.error( 'Error changing WordPress version:', wpError );
- const errorMessage = stripAnsi( ( wpError as Error )?.message );
- getIpcApi().showErrorMessageBox( {
- title: __( 'Error changing WordPress version' ),
- message: __( 'An error occurred while updating the WordPress version.' ),
- error: new Error( errorMessage ),
- showOpenLogs: true,
- } );
- setSelectedWpVersion( getEffectiveWpVersion() );
- setIsEditingSite( false );
- return;
- }
- }
-
// Determine custom domain setting
let usedCustomDomain = useCustomDomain && customDomain ? customDomain : undefined;
if ( useCustomDomain && ! customDomain ) {
usedCustomDomain = generateCustomDomainFromSiteName( siteName ?? '' );
}
- await updateSite( {
- ...selectedSite,
- name: siteName,
- phpVersion: selectedPhpVersion,
- isWpAutoUpdating: selectedWpVersion === defaultWordPressVersion,
- customDomain: usedCustomDomain,
- enableHttps: !! usedCustomDomain && enableHttps,
- enableXdebug,
- } );
+ await updateSite(
+ {
+ ...selectedSite,
+ name: siteName,
+ phpVersion: selectedPhpVersion,
+ isWpAutoUpdating: selectedWpVersion === defaultWordPressVersion,
+ customDomain: usedCustomDomain,
+ enableHttps: !! usedCustomDomain && enableHttps,
+ enableXdebug,
+ },
+ hasWpVersionChanged ? selectedWpVersion : undefined
+ );
- if ( needsRestart ) {
- await startServer( selectedSite.id );
- }
onSave();
closeModal();
resetFormState();
diff --git a/src/modules/site-settings/tests/edit-site-details.test.tsx b/src/modules/site-settings/tests/edit-site-details.test.tsx
index a4da6a5b52..644a887720 100644
--- a/src/modules/site-settings/tests/edit-site-details.test.tsx
+++ b/src/modules/site-settings/tests/edit-site-details.test.tsx
@@ -266,7 +266,7 @@ describe( 'EditSiteDetails', () => {
} );
} );
- it( 'should update site and restart server when PHP version is changed', async () => {
+ it( 'should update site when PHP version is changed (CLI handles restart)', async () => {
( useSiteDetails as jest.Mock ).mockReturnValue( {
...useSiteDetails(),
isEditModalOpen: true,
@@ -283,15 +283,13 @@ describe( 'EditSiteDetails', () => {
await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
await waitFor( () => {
- expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' );
expect( mockUpdateSite ).toHaveBeenCalled();
expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].phpVersion ).toBe( '8.2' );
- expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' );
expect( defaultProps.onSave ).toHaveBeenCalled();
} );
} );
- it( 'should update isWpAutoUpdating to false when changed from latest to specific version', async () => {
+ it( 'should update isWpAutoUpdating and pass wpVersion when changed from latest to specific version', async () => {
( useSiteDetails as jest.Mock ).mockReturnValue( {
...useSiteDetails(),
isEditModalOpen: true,
@@ -307,19 +305,16 @@ describe( 'EditSiteDetails', () => {
await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
- expect( mockStopServer ).toHaveBeenCalledWith( 'site-123' );
- expect( mockExecuteWPCLiInline ).toHaveBeenCalledWith( {
- siteId: 'site-123',
- args: 'core update https://wordpress.org/wordpress-6.4.zip --force',
- skipPluginsAndThemes: true,
+ await waitFor( () => {
+ expect( mockUpdateSite ).toHaveBeenCalled();
+ expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].isWpAutoUpdating ).toBe( false );
+ // wpVersion is passed as second argument
+ expect( mockUpdateSite.mock.calls[ 0 ][ 1 ] ).toBe( '6.4' );
+ expect( defaultProps.onSave ).toHaveBeenCalled();
} );
- expect( mockUpdateSite ).toHaveBeenCalled();
- expect( mockUpdateSite.mock.calls[ 0 ][ 0 ].isWpAutoUpdating ).toBe( false );
- expect( mockStartServer ).toHaveBeenCalledWith( 'site-123' );
- expect( defaultProps.onSave ).toHaveBeenCalled();
} );
- it( 'should update WordPress version when changed to beta', async () => {
+ it( 'should pass wpVersion when WordPress version is changed to beta', async () => {
( useSiteDetails as jest.Mock ).mockReturnValue( {
...useSiteDetails(),
isEditModalOpen: true,
@@ -335,20 +330,19 @@ describe( 'EditSiteDetails', () => {
await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
- expect( mockExecuteWPCLiInline ).toHaveBeenCalledWith( {
- siteId: 'site-123',
- args: 'core update https://wordpress.org/wordpress-6.8-beta1.zip --force',
- skipPluginsAndThemes: true,
+ await waitFor( () => {
+ expect( mockUpdateSite ).toHaveBeenCalled();
+ expect( mockUpdateSite.mock.calls[ 0 ][ 1 ] ).toBe( '6.8-beta1' );
} );
} );
- it( 'should show error when WordPress version update fails', async () => {
+ it( 'should show error when site update fails', async () => {
( useSiteDetails as jest.Mock ).mockReturnValue( {
...useSiteDetails(),
isEditModalOpen: true,
} );
- const consoleSpy = jest.spyOn( console, 'error' ).mockImplementation( () => {} );
- mockExecuteWPCLiInline.mockResolvedValue( { exitCode: 1, stderr: 'Update failed' } );
+
+ mockUpdateSite.mockRejectedValueOnce( new Error( 'CLI site set failed' ) );
renderWithProvider( );
await waitFor( () => {
@@ -362,15 +356,8 @@ describe( 'EditSiteDetails', () => {
await user.click( screen.getByRole( 'button', { name: 'Save' } ) );
await waitFor( () => {
- expect( mockShowErrorMessageBox ).toHaveBeenCalledWith( {
- title: 'Error changing WordPress version',
- message: 'An error occurred while updating the WordPress version.',
- error: new Error( 'Update failed' ),
- showOpenLogs: true,
- } );
+ expect( screen.getByText( 'CLI site set failed' ) ).toBeInTheDocument();
} );
-
- consoleSpy.mockRestore();
} );
it( 'should disable form controls when site is being edited', async () => {
diff --git a/src/preload.ts b/src/preload.ts
index df6b1e9638..045b379b94 100644
--- a/src/preload.ts
+++ b/src/preload.ts
@@ -40,7 +40,8 @@ const api: IpcApi = {
),
deleteSite: ( id, deleteFiles ) => ipcRendererInvoke( 'deleteSite', id, deleteFiles ),
createSite: ( path, config ) => ipcRendererInvoke( 'createSite', path, config ),
- updateSite: ( updatedSite ) => ipcRendererInvoke( 'updateSite', updatedSite ),
+ updateSite: ( updatedSite, wpVersion ) =>
+ ipcRendererInvoke( 'updateSite', updatedSite, wpVersion ),
connectWpcomSites: ( ...args ) => ipcRendererInvoke( 'connectWpcomSites', ...args ),
disconnectWpcomSites: ( ...args ) => ipcRendererInvoke( 'disconnectWpcomSites', ...args ),
updateConnectedWpcomSites: ( ...args ) =>
diff --git a/src/site-server.ts b/src/site-server.ts
index 1fc603f528..b51d8129fc 100644
--- a/src/site-server.ts
+++ b/src/site-server.ts
@@ -9,10 +9,6 @@ import {
WP_CLI_DEFAULT_RESPONSE_TIMEOUT,
WP_CLI_IMPORT_EXPORT_RESPONSE_TIMEOUT,
} from 'src/constants';
-import { deleteSiteCertificate, generateSiteCertificate } from 'src/lib/certificate-manager';
-import { getSiteUrl } from 'src/lib/get-site-url';
-import { updateDomainInHosts } from 'src/lib/hosts-file';
-import { updateSiteUrl } from 'src/lib/update-site-url';
import { setupWordPressSite, getWordPressProvider } from 'src/lib/wordpress-provider';
import { CliServerProcess } from 'src/modules/cli/lib/cli-server-process';
import { createSiteViaCli, type CreateSiteOptions } from 'src/modules/cli/lib/cli-site-creator';
@@ -193,12 +189,7 @@ export class SiteServer {
}
}
- async updateSiteDetails( site: SiteDetails ) {
- const oldDomain = this.details.customDomain;
- const newDomain = site.customDomain;
- const oldEnableHttps = this.details.enableHttps;
- const newEnableHttps = site.enableHttps;
-
+ updateSiteDetails( site: SiteDetails ) {
this.details = {
...this.details,
name: site.name,
@@ -207,40 +198,15 @@ export class SiteServer {
isWpAutoUpdating: site.isWpAutoUpdating,
customDomain: site.customDomain,
enableHttps: site.enableHttps,
+ tlsKey: site.tlsKey,
+ tlsCert: site.tlsCert,
+ enableXdebug: site.enableXdebug,
};
if ( this.server && this.details.running ) {
this.details.url = getAbsoluteUrl( this.details );
this.server.url = this.details.url;
}
-
- // Handle domain changes and url changes (updates hosts and database)
- if ( oldDomain !== newDomain ) {
- void updateDomainInHosts( oldDomain, newDomain, this.details.port );
- }
- if ( ( oldDomain && ! newDomain ) || oldEnableHttps !== newEnableHttps ) {
- await updateSiteUrl( this, getSiteUrl( this.details ) );
- }
-
- if (
- oldDomain &&
- oldEnableHttps &&
- ( oldDomain !== newDomain || oldEnableHttps !== newEnableHttps )
- ) {
- deleteSiteCertificate( oldDomain );
- }
- if (
- newDomain &&
- newEnableHttps &&
- ( oldDomain !== newDomain || oldEnableHttps !== newEnableHttps )
- ) {
- const { cert, key } = await generateSiteCertificate( newDomain );
- this.details = {
- ...this.details,
- tlsKey: key,
- tlsCert: cert,
- };
- }
}
async stop() {