From 5e8f17736e8642b829effb9284ee09b7e05e0a1a Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 1 Sep 2026 19:11:36 +0300 Subject: [PATCH] Build/Test Tools: Correct local environment failure handling. Backport of [63416] and [62871] to the 7.0 branch. `start.js` and `docker.js` report success when Docker Compose fails to start, exits non-zero, or is killed. Propagate the exit status, and keep SIGINT as cancellation for interactive commands. Copy `.env.example` synchronously, so `.env` exists before `dotenv.config()` reads it. --- tools/local-env/scripts/docker.js | 11 ++++++++++- tools/local-env/scripts/start.js | 21 ++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index c7b11f0058424..ce71a4c075cd6 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -38,4 +38,13 @@ const returns = spawnSync( { stdio: 'inherit' } ); -process.exit( returns.status ); +if ( returns.error ) { + console.error( `Could not run Docker Compose. ${ returns.error.message }` ); +} else if ( returns.signal && returns.signal !== 'SIGINT' ) { + console.error( `Docker Compose was terminated by ${ returns.signal }.` ); +} + +// `status` is null when Docker could not be spawned at all, or was killed by a signal. SIGINT is +// how a long-running command such as `env:logs` is normally ended, so it is not a failure worth an +// npm error block. Every other signal means the command was killed before it finished. +process.exit( returns.signal === 'SIGINT' ? 0 : ( returns.status ?? 1 ) ); diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index 66559d4c10b85..98d1a8109fc21 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -4,12 +4,12 @@ const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); const { execSync, spawnSync } = require( 'child_process' ); const local_env_utils = require( './utils' ); -const { constants, copyFile } = require( 'node:fs' ); +const { copyFileSync, existsSync } = require( 'node:fs' ); // Copy the default .env file when one is not present. -copyFile( '.env.example', '.env', constants.COPYFILE_EXCL, () => { - console.log( '.env file already exists. .env.example was not copied.' ); -}); +if ( ! existsSync( '.env' ) ) { + copyFileSync( '.env.example', '.env' ); +} dotenvExpand.expand( dotenv.config() ); @@ -32,7 +32,7 @@ if ( process.env.LOCAL_PHP_MEMCACHED === 'true' ) { containers.push( 'memcached' ); } -spawnSync( +const up = spawnSync( 'docker', [ 'compose', @@ -45,6 +45,17 @@ spawnSync( { stdio: 'inherit' } ); +// No signal is exempt here, unlike in `docker.js`: `env:start` runs `composer update -W` next, and +// that must not run against containers that never came up. +if ( up.status !== 0 ) { + const reason = up.signal ? `It was terminated by ${ up.signal }.` : up.error?.message ?? ''; + + console.error( `Could not start the Docker containers. ${ reason }`.trim() ); + + // `status` is null when Docker could not be spawned at all, or was killed by a signal. + process.exit( up.status ?? 1 ); +} + // If Docker Toolbox is being used, we need to manually forward LOCAL_PORT to the Docker VM. if ( process.env.DOCKER_TOOLBOX_INSTALL_PATH ) { // VBoxManage is added to the PATH on every platform except Windows.