From dcda7b3239a56b3ddbd7838dd2d8e981ffbfc00a Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:59:45 +0300 Subject: [PATCH 1/2] Build/Test Tools: Wait for the database before running WP-CLI commands. `npm run env:install` began with `wp config create`, which performs a database connectivity check. The script's only readiness wait ran six WP-CLI calls later, and waited on the web port rather than the database, so nothing actually confirmed the database was reachable before it was used. The `cli` service does depend on `mysql` being `service_healthy`, but that healthcheck runs `mysqladmin ping -h localhost` inside the mysql container, which connects over the unix socket. While a cold volume is initialising, that socket is served by the entrypoint's temporary server, which listens on socket only (`port: 0`). The healthcheck therefore reports healthy while the real server is not yet listening on TCP, and connections from the `cli` container are refused. Retry the `config create` call until it succeeds. Its built-in connectivity check exercises exactly the path that matters, cli container to host `mysql` over TCP, so retrying it doubles as the readiness probe without adding a separate wait or relying on a published host port. Failures that retrying cannot fix, such as the environment never having been started, still exit immediately, and the timeout reports the underlying error. --- tools/local-env/scripts/install.js | 75 ++++++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 038ecc3a67d5e..4411a64b9e219 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -9,8 +9,20 @@ const local_env_utils = require( './utils' ); dotenvExpand.expand( dotenv.config() ); -// Create wp-config.php. -wp_cli( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"` ); +// Create wp-config.php. This verifies the database connection, so retrying it doubles as the +// readiness probe: the mysql healthcheck pings the container's own socket, which the temporary +// server used to initialise a cold volume answers before the real server listens on TCP. +wp_cli_retry( + `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"`, + { + // Initialising a cold database volume takes well over the 3 second web server timeout + // used below, and longer still on a loaded CI runner. + timeout: 120000, + waiting: 'Waiting for the database to accept connections...', + failure: 'The database did not accept connections', + hint: `Check the container logs with 'npm run env:logs mysql'.`, + } +); // Add the debug settings to wp-config.php. // Windows requires this to be done as an additional step, rather than using the --extra-php option in the previous step. @@ -56,8 +68,61 @@ wait_on( { /** * Runs WP-CLI commands in the Docker environment. * - * @param {string} cmd The WP-CLI command to run. + * @param {string} cmd The WP-CLI command to run. + * @param {string} stdio How to handle the command's output. Defaults to 'inherit'. */ -function wp_cli( cmd ) { - execSync( `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); +function wp_cli( cmd, stdio = 'inherit' ) { + return execSync( `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio } ); +} + +/** + * Runs a WP-CLI command, retrying it until it succeeds or the timeout is reached. + * + * Exits with an error when the timeout is reached, or as soon as the failure is one that + * retrying cannot fix. + * + * @param {string} cmd The WP-CLI command to run. + * @param {Object} options + * @param {number} options.timeout How long to keep retrying for, in milliseconds. + * @param {string} options.waiting Message shown when the command has to be retried. + * @param {string} options.failure Reason reported when the timeout is reached. + * @param {string} options.hint Suggested next step when the timeout is reached. + */ +function wp_cli_retry( cmd, { timeout, waiting, failure, hint } ) { + const interval = 2000; + const deadline = Date.now() + timeout; + let notified = false; + + for ( ;; ) { + try { + process.stdout.write( wp_cli( cmd, 'pipe' ) ); + return; + } catch ( err ) { + const output = err.stderr ? err.stderr.toString() : err.message; + + // Retrying only helps while the environment is still starting up. A missing container + // means it was never started, so there is nothing to wait for. + if ( output.includes( 'is not running' ) ) { + console.error( output ); + console.error( `Error: It appears the development environment has not been started.` ); + console.error( `Did you forget to do 'npm run env:start'?` ); + process.exit( 1 ); + } + + if ( ! notified ) { + notified = true; + console.log( waiting ); + } + + if ( Date.now() >= deadline ) { + console.error( output ); + console.error( `Error: ${ failure } within ${ timeout / 1000 } seconds.` ); + console.error( hint ); + process.exit( 1 ); + } + + // Sleep synchronously, so the retries stay in front of the commands that follow. + Atomics.wait( new Int32Array( new SharedArrayBuffer( 4 ) ), 0, 0, interval ); + } + } } From 921d2a92850757905569b02f5dc2e9895c86d35f Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:27:16 +0300 Subject: [PATCH 2/2] Build/Test Tools: Report the underlying error when a WP-CLI retry fails. `err.stderr` is a Buffer, which is truthy even when empty, so an error with no stderr output resolved to an empty string rather than falling back to `err.message`. That discarded the underlying error at the end of the retry window, and also defeated the `is not running` check, so a missing container could retry for the full timeout and then report nothing. Use the first of stderr, stdout, and `err.message` that actually captured something. --- tools/local-env/scripts/install.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 4411a64b9e219..0578545c11fec 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -98,7 +98,11 @@ function wp_cli_retry( cmd, { timeout, waiting, failure, hint } ) { process.stdout.write( wp_cli( cmd, 'pipe' ) ); return; } catch ( err ) { - const output = err.stderr ? err.stderr.toString() : err.message; + // `stderr` and `stdout` are buffers, which are truthy even when empty, so use the + // first one that actually captured something. + const output = [ err.stderr, err.stdout, err.message ] + .map( ( value ) => ( value ? value.toString().trim() : '' ) ) + .find( ( value ) => value !== '' ) || 'No output was captured.'; // Retrying only helps while the environment is still starting up. A missing container // means it was never started, so there is nothing to wait for.