From 21ec8807ae99c48c2085304dfaf56903c202ab6f Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 15:42:11 +0530 Subject: [PATCH 01/12] Media: support strict stream-wrapper directory stats --- src/wp-admin/includes/file.php | 2 +- src/wp-includes/class-wp-image-editor-gd.php | 5 +- .../class-wp-image-editor-imagick.php | 5 +- src/wp-includes/functions.php | 25 +++- .../class-wp-test-strict-dir-stream.php | 115 ++++++++++++++++++ tests/phpunit/tests/image/editorGd.php | 31 +++++ tests/phpunit/tests/image/editorImagick.php | 34 +++++- tests/phpunit/tests/upload.php | 40 +++++- 8 files changed, 248 insertions(+), 9 deletions(-) create mode 100644 tests/phpunit/includes/class-wp-test-strict-dir-stream.php diff --git a/src/wp-admin/includes/file.php b/src/wp-admin/includes/file.php index 0c6d968ea02d3..50ecc039f3b01 100644 --- a/src/wp-admin/includes/file.php +++ b/src/wp-admin/includes/file.php @@ -1041,7 +1041,7 @@ function wp_handle_upload_error( &$file, $message ) { } // Set correct file permissions. - $stat = stat( dirname( $new_file ) ); + $stat = stat( _wp_get_dir_perms_stat_path( $new_file ) ); $perms = $stat['mode'] & 0000666; chmod( $new_file, $perms ); diff --git a/src/wp-includes/class-wp-image-editor-gd.php b/src/wp-includes/class-wp-image-editor-gd.php index d89d366d71baf..bac45050f0e54 100644 --- a/src/wp-includes/class-wp-image-editor-gd.php +++ b/src/wp-includes/class-wp-image-editor-gd.php @@ -573,8 +573,9 @@ protected function _save( $image, $filename = null, $mime_type = null ) { } // Set correct file permissions. - $stat = stat( dirname( $filename ) ); - $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. + $stat = stat( _wp_get_dir_perms_stat_path( $filename ) ); + $perms = $stat['mode'] & 0000666; + // Same permissions as parent folder, strip off the executable bits. chmod( $filename, $perms ); return array( diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index 19b27ba12e2ae..304673da3ec59 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -963,8 +963,9 @@ protected function _save( $image, $filename = null, $mime_type = null ) { } // Set correct file permissions. - $stat = stat( dirname( $filename ) ); - $perms = $stat['mode'] & 0000666; // Same permissions as parent folder, strip off the executable bits. + $stat = stat( _wp_get_dir_perms_stat_path( $filename ) ); + $perms = $stat['mode'] & 0000666; + // Same permissions as parent folder, strip off the executable bits. chmod( $filename, $perms ); return array( diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 7d71c8c56963d..6dd271614cf8f 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -2972,7 +2972,7 @@ function wp_upload_bits( $name, $deprecated, $bits, $time = null ) { clearstatcache(); // Set correct file permissions. - $stat = @ stat( dirname( $new_file ) ); + $stat = @ stat( _wp_get_dir_perms_stat_path( $new_file ) ); $perms = $stat['mode'] & 0007777; $perms = $perms & 0000666; chmod( $new_file, $perms ); @@ -7412,8 +7412,8 @@ function _device_can_upload() { * @return bool True if the path is a stream URL. */ function wp_is_stream( $path ) { - $scheme_separator = strpos( $path, '://' ); + $scheme_separator = strpos( $path, '://' ); if ( false === $scheme_separator ) { // $path isn't a stream. return false; @@ -7424,6 +7424,27 @@ function wp_is_stream( $path ) { return in_array( $stream, stream_get_wrappers(), true ); } +/** + * Gets the directory path used to inherit permissions for a file path. + * + * Stream wrappers that model directories as paths ending in a slash can require + * the trailing slash for `stat()` to resolve the parent directory. + * + * @since 6.9.0 + * + * @param string $path File path. + * @return string Directory path to use with `stat()`. + */ +function _wp_get_dir_perms_stat_path( $path ) { + + $dir = dirname( $path ); + + if ( wp_is_stream( $dir ) ) { + $dir = trailingslashit( $dir ); + } + + return $dir; +} /** * Tests if the supplied date is valid for the Gregorian calendar. * diff --git a/tests/phpunit/includes/class-wp-test-strict-dir-stream.php b/tests/phpunit/includes/class-wp-test-strict-dir-stream.php new file mode 100644 index 0000000000000..8b5f7c2de9627 --- /dev/null +++ b/tests/phpunit/includes/class-wp-test-strict-dir-stream.php @@ -0,0 +1,115 @@ + '', + 'path' => '', + ), + parse_url( $url ) + ); + + $this->bucket = $components['host']; + $this->file = $components['path'] ? $components['path'] : '/'; + + if ( empty( $this->bucket ) ) { + throw new Exception( 'Cannot use an empty bucket name' ); + } + + if ( ! isset( WP_Test_Stream::$data[ $this->bucket ] ) ) { + WP_Test_Stream::$data[ $this->bucket ] = array(); + } + + $this->data_ref = null; + if ( array_key_exists( $this->file, WP_Test_Stream::$data[ $this->bucket ] ) ) { + $this->data_ref =& WP_Test_Stream::$data[ $this->bucket ][ $this->file ]; + } + + $this->position = 0; + } + + /** + * Creates a file metadata object, with defaults. + * + * @param array $stats Partial file metadata. + * @return array Complete file metadata. + */ + private function make_strict_stat( $stats ) { + $defaults = array( + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ); + + return array_merge( $defaults, $stats ); + } + + /** + * Retrieves information about a file. + * + * @see WP_Test_Stream::stream_stat() + * + * @return array|false File stats on success, false on failure. + */ + public function stream_stat() { + if ( '/' === substr( $this->file, -1 ) ) { + if ( ! isset( WP_Test_Stream::$data[ $this->bucket ][ $this->file ] ) ) { + return false; + } + + return $this->make_strict_stat( + array( + 'mode' => WP_Test_Stream::DIRECTORY_MODE, + ) + ); + } + + if ( ! isset( $this->data_ref ) ) { + return false; + } + + return $this->make_strict_stat( + array( + 'size' => strlen( $this->data_ref ), + 'mode' => WP_Test_Stream::FILE_MODE, + ) + ); + } + + /** + * Retrieves information about a file. + * + * @see WP_Test_Stream::url_stat() + * + * @param string $path Path to get information about. + * @param int $flags Bitmask of STREAM_URL_STAT_* constants. + * @return array|false File stats on success, false on failure. + */ + public function url_stat( $path, $flags ) { + $this->open_strict( $path ); + return $this->stream_stat(); + } +} diff --git a/tests/phpunit/tests/image/editorGd.php b/tests/phpunit/tests/image/editorGd.php index ac0e8268390c2..4fb81443fd626 100644 --- a/tests/phpunit/tests/image/editorGd.php +++ b/tests/phpunit/tests/image/editorGd.php @@ -8,6 +8,8 @@ * @group wp-image-editor-gd */ require_once __DIR__ . '/base.php'; +require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php'; +require_once DIR_TESTROOT . '/includes/class-wp-test-strict-dir-stream.php'; class Tests_Image_Editor_GD extends WP_Image_UnitTestCase { @@ -51,6 +53,35 @@ public function test_supports_mime_type_gif() { $this->assertSame( $expected, $gd_image_editor->supports_mime_type( 'image/gif' ) ); } + /** + * @ticket 42838 + * @requires function imagejpeg + */ + public function test_save_to_nested_stream_path() { + stream_wrapper_register( 'wptestgddir', 'WP_Test_Strict_Dir_Stream' ); + WP_Test_Stream::$data = array( + 'Tests_Image_Editor_GD' => array( + '/read.jpg' => file_get_contents( DIR_TESTDATA . '/images/waffles.jpg' ), + '/nested-path/' => 'DIRECTORY', + ), + ); + + $file = 'wptestgddir://Tests_Image_Editor_GD/read.jpg'; + $destination = 'wptestgddir://Tests_Image_Editor_GD/nested-path/write.jpg'; + $gd_image_editor = new WP_Image_Editor_GD( $file ); + + $loaded = $gd_image_editor->load(); + $this->assertNotWPError( $loaded ); + + $saved = $gd_image_editor->save( $destination ); + + stream_wrapper_unregister( 'wptestgddir' ); + + $this->assertNotWPError( $saved ); + $this->assertSame( $destination, $saved['path'] ); + $this->assertArrayHasKey( '/nested-path/write.jpg', WP_Test_Stream::$data['Tests_Image_Editor_GD'] ); + } + /** * Tests resizing an image, not using crop. * diff --git a/tests/phpunit/tests/image/editorImagick.php b/tests/phpunit/tests/image/editorImagick.php index e120c32502ad5..6ee96d2e1731b 100644 --- a/tests/phpunit/tests/image/editorImagick.php +++ b/tests/phpunit/tests/image/editorImagick.php @@ -14,10 +14,11 @@ class Tests_Image_Editor_Imagick extends WP_Image_UnitTestCase { public $editor_engine = 'WP_Image_Editor_Imagick'; public function set_up() { + require_once ABSPATH . WPINC . '/class-wp-image-editor.php'; require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php'; require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php'; - + require_once DIR_TESTROOT . '/includes/class-wp-test-strict-dir-stream.php'; // This needs to come after the mock image editor class is loaded. parent::set_up(); } @@ -621,6 +622,37 @@ public function test_streams() { $this->assertSame( $temp_file, $saved['path'] ); } + /** + * @ticket 42838 + */ + public function test_nested_streams() { + stream_wrapper_register( 'wptestdir', 'WP_Test_Strict_Dir_Stream' ); + WP_Test_Stream::$data = array( + 'Tests_Image_Editor_Imagick' => array( + '/read.jpg' => file_get_contents( DIR_TESTDATA . '/images/waffles.jpg' ), + '/nested-path/' => 'DIRECTORY', + ), + ); + + $file = 'wptestdir://Tests_Image_Editor_Imagick/read.jpg'; + $imagick_image_editor = new WP_Image_Editor_Imagick( $file ); + + $loaded = $imagick_image_editor->load(); + $this->assertNotWPError( $loaded ); + + $temp_file = 'wptestdir://Tests_Image_Editor_Imagick/nested-path/write.jpg'; + $saved = $imagick_image_editor->save( $temp_file ); + + if ( $temp_file !== $saved['path'] ) { + unlink( $saved['path'] ); + } + + stream_wrapper_unregister( 'wptestdir' ); + + $this->assertNotWPError( $saved ); + $this->assertSame( $temp_file, $saved['path'] ); + $this->assertArrayHasKey( '/nested-path/write.jpg', WP_Test_Stream::$data['Tests_Image_Editor_Imagick'] ); + } /** * @ticket 51665 */ diff --git a/tests/phpunit/tests/upload.php b/tests/phpunit/tests/upload.php index 46fcea7099097..8fbe22f56a13e 100644 --- a/tests/phpunit/tests/upload.php +++ b/tests/phpunit/tests/upload.php @@ -4,7 +4,6 @@ * @group media */ class Tests_Upload extends WP_UnitTestCase { - public $siteurl; public function set_up() { @@ -19,6 +18,16 @@ private function reset_options() { update_option( 'uploads_use_yearmonth_folders', 1 ); } + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php'; + require_once DIR_TESTROOT . '/includes/class-wp-test-strict-dir-stream.php'; + stream_wrapper_register( 'wptestdir', 'WP_Test_Strict_Dir_Stream' ); + } + + public static function wpTearDownAfterClass() { + stream_wrapper_unregister( 'wptestdir' ); + } + public function test_upload_dir_default() { // wp_upload_dir() with default parameters. $info = wp_upload_dir(); @@ -105,4 +114,33 @@ public function test_upload_dir_empty() { $this->assertSame( $subdir, $info['subdir'] ); $this->assertFalse( $info['error'] ); } + + /** + * @ticket 42838 + */ + public function test_wp_upload_bits_should_support_stream_wrapper_directories() { + $filter = static function ( $uploads ) { + $uploads['path'] = 'wptestdir://uploads-test/uploads'; + $uploads['basedir'] = 'wptestdir://uploads-test/uploads'; + $uploads['subdir'] = ''; + $uploads['url'] = 'https://example.org/uploads'; + $uploads['baseurl'] = 'https://example.org/uploads'; + + return $uploads; + }; + + WP_Test_Stream::$data = array( + 'uploads-test' => array(), + ); + + add_filter( 'upload_dir', $filter ); + + $upload = wp_upload_bits( 'stream.txt', null, 'stream wrapper contents' ); + + remove_filter( 'upload_dir', $filter ); + + $this->assertSame( 'stream wrapper contents', WP_Test_Stream::$data['uploads-test']['/uploads/stream.txt'] ); + $this->assertSame( 'https://example.org/uploads/stream.txt', $upload['url'] ); + $this->assertFalse( $upload['error'] ); + } } From 90b4c57ecc4e105a7cb640d9a59f2e8d78f6eba7 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 16:00:59 +0530 Subject: [PATCH 02/12] Tests: precreate strict stream upload directory --- tests/phpunit/tests/upload.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/phpunit/tests/upload.php b/tests/phpunit/tests/upload.php index 8fbe22f56a13e..750c859564b53 100644 --- a/tests/phpunit/tests/upload.php +++ b/tests/phpunit/tests/upload.php @@ -130,7 +130,9 @@ public function test_wp_upload_bits_should_support_stream_wrapper_directories() }; WP_Test_Stream::$data = array( - 'uploads-test' => array(), + 'uploads-test' => array( + '/uploads/' => 'DIRECTORY', + ), ); add_filter( 'upload_dir', $filter ); From a4164dece491fd8e552adb57fbbc805f6ed39938 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 18:43:54 +0530 Subject: [PATCH 03/12] Media: normalize stream directory stat paths --- src/wp-includes/functions.php | 43 +++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index bae0b7fd284e6..323c74c1212fd 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -2070,10 +2070,11 @@ function wp_mkdir_p( $target ) { $target = '/'; } - if ( file_exists( $target ) ) { - return @is_dir( $target ); - } + $stat_target = _wp_normalize_directory_stat_path( $target ); + if ( file_exists( $stat_target ) ) { + return @is_dir( $stat_target ); + } // Do not allow path traversals. if ( str_contains( $target, '../' ) || str_contains( $target, '..' . DIRECTORY_SEPARATOR ) ) { return false; @@ -2081,12 +2082,12 @@ function wp_mkdir_p( $target ) { // We need to find the permissions of the parent folder that exists and inherit that. $target_parent = dirname( $target ); - while ( '.' !== $target_parent && ! is_dir( $target_parent ) && dirname( $target_parent ) !== $target_parent ) { + while ( '.' !== $target_parent && ! is_dir( _wp_normalize_directory_stat_path( $target_parent ) ) && dirname( $target_parent ) !== $target_parent ) { $target_parent = dirname( $target_parent ); } // Get the permission bits. - $stat = @stat( $target_parent ); + $stat = @stat( _wp_normalize_directory_stat_path( $target_parent ) ); if ( $stat ) { $dir_perms = $stat['mode'] & 0007777; } else { @@ -7414,6 +7415,7 @@ function _validate_cache_id( $object_id ) { * @return bool Whether the device is able to upload files. */ function _device_can_upload() { + if ( ! wp_is_mobile() ) { return true; } @@ -7423,7 +7425,7 @@ function _device_can_upload() { if ( str_contains( $ua, 'iPhone' ) || str_contains( $ua, 'iPad' ) || str_contains( $ua, 'iPod' ) ) { - return preg_match( '#OS ([\d_]+) like Mac OS X#', $ua, $version ) && version_compare( $version[1], '6', '>=' ); + return preg_match( '#OS ([\d_]+) like Mac OS X#', $ua, $version ) && version_compare( $version[1], '6', '>=' ); } return true; @@ -7438,7 +7440,6 @@ function _device_can_upload() { * @return bool True if the path is a stream URL. */ function wp_is_stream( $path ) { - $scheme_separator = strpos( $path, '://' ); if ( false === $scheme_separator ) { // $path isn't a stream. @@ -7450,6 +7451,26 @@ function wp_is_stream( $path ) { return in_array( $stream, stream_get_wrappers(), true ); } +/** + * Normalizes a directory path for stat-style filesystem checks. + * + * Stream wrappers that model directories as paths ending in a slash can require + * the trailing slash for existence and metadata checks to resolve correctly. + * + * @since 6.9.0 + * + * @param string $path Directory path. + * @return string Directory path to use with stat-style checks. + */ +function _wp_normalize_directory_stat_path( $path ) { + + if ( wp_is_stream( $path ) ) { + $path = trailingslashit( $path ); + } + + return $path; +} + /** * Gets the directory path used to inherit permissions for a file path. * @@ -7463,13 +7484,7 @@ function wp_is_stream( $path ) { */ function _wp_get_dir_perms_stat_path( $path ) { - $dir = dirname( $path ); - - if ( wp_is_stream( $dir ) ) { - $dir = trailingslashit( $dir ); - } - - return $dir; + return _wp_normalize_directory_stat_path( dirname( $path ) ); } /** * Tests if the supplied date is valid for the Gregorian calendar. From d4966905b6a696ea0e3f184cfc6f12770c7c0d95 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 18:54:01 +0530 Subject: [PATCH 04/12] CI: retrigger PR checks From a80ff8a822e666b3ef3429d55e3d41140c702c28 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:03:15 +0530 Subject: [PATCH 05/12] Build/Test Tools: Fix memcached performance drop-in path --- .github/workflows/reusable-performance-test-v2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index c0279c37fe64b..72861b6d0dd0d 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -196,7 +196,7 @@ jobs: - name: Install object cache drop-in if: ${{ inputs.memcached }} - run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php + run: cp tests/phpunit/includes/object-cache.php build/wp-content/object-cache.php - name: Log running Docker containers run: docker ps -a From f7808a64951c36b69887d569bde88d5b9b0d22d2 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:08:31 +0530 Subject: [PATCH 06/12] Build/Test Tools: Retry WP-CLI install commands --- tools/local-env/scripts/install.js | 54 +++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 038ecc3a67d5e..f090c977bcdff 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -59,5 +59,57 @@ wait_on( { * @param {string} cmd The WP-CLI command to run. */ function wp_cli( cmd ) { - execSync( `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`, { stdio: 'inherit' } ); + const wp_cli_command = `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`; + + for ( let attempt = 1; attempt <= 5; attempt++ ) { + try { + const output = execSync( wp_cli_command, { + encoding: 'utf8', + stdio: [ 'ignore', 'pipe', 'pipe' ], + } ); + + if ( output ) { + process.stdout.write( output ); + } + + return; + } catch ( error ) { + if ( error.stdout ) { + process.stdout.write( error.stdout.toString() ); + } + + if ( error.stderr ) { + process.stderr.write( error.stderr.toString() ); + } + + if ( 5 === attempt || ! wp_cli_container_is_starting( error ) ) { + throw error; + } + + const delay = attempt * 1000; + console.warn( `The WP-CLI container is still starting, retrying in ${ delay / 1000 } second(s)...` ); + sleep( delay ); + } + } +} + +/** + * Determines whether the WP-CLI container is still starting. + * + * @param {Error & {stdout?: Buffer|string, stderr?: Buffer|string}} error Error thrown by execSync(). + * @return {boolean} Whether the CLI container is still starting up. + */ +function wp_cli_container_is_starting( error ) { + const output = `${ error.stdout || '' }\n${ error.stderr || '' }\n${ error.message || '' }`; + + return output.includes( 'service "cli" is not running' ); +} + +/** + * Sleeps synchronously for a specified number of milliseconds. + * + * @param {number} milliseconds The number of milliseconds to sleep. + */ +function sleep( milliseconds ) { + Atomics.wait( new Int32Array( new SharedArrayBuffer( 4 ) ), 0, 0, milliseconds ); } From 7791c8e80a6c191fee76ac55f16a08eab073e0f9 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:16:41 +0530 Subject: [PATCH 07/12] Build/Test Tools: Allow longer WP-CLI startup retries --- tools/local-env/scripts/install.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index f090c977bcdff..5b183cf2f214c 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -61,7 +61,7 @@ wait_on( { function wp_cli( cmd ) { const wp_cli_command = `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`; - for ( let attempt = 1; attempt <= 5; attempt++ ) { + for ( let attempt = 1; attempt <= 12; attempt++ ) { try { const output = execSync( wp_cli_command, { encoding: 'utf8', @@ -82,11 +82,13 @@ function wp_cli( cmd ) { process.stderr.write( error.stderr.toString() ); } - if ( 5 === attempt || ! wp_cli_container_is_starting( error ) ) { + if ( 12 === attempt || ! wp_cli_container_is_starting( error ) ) { throw error; } - const delay = attempt * 1000; + // The Docker-backed WP-CLI container can lag behind on slower runners while images + // are still starting up, especially across the broader DB/version matrix. + const delay = Math.min( attempt, 5 ) * 1000; console.warn( `The WP-CLI container is still starting, retrying in ${ delay / 1000 } second(s)...` ); sleep( delay ); } From 94e57439126c4920141d0384110243eda189dbc0 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:26:19 +0530 Subject: [PATCH 08/12] Build/Test Tools: Run WP-CLI commands on demand --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 429e0469dd491..20ea5fc94d130 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "env:clean": "node ./tools/local-env/scripts/docker.js down -v --remove-orphans", "env:reset": "node ./tools/local-env/scripts/docker.js down --rmi all -v --remove-orphans", "env:install": "node ./tools/local-env/scripts/install.js", - "env:cli": "node ./tools/local-env/scripts/docker.js exec --user wp_php cli wp", + "env:cli": "node ./tools/local-env/scripts/docker.js run --rm --user wp_php cli wp", "env:composer": "node ./tools/local-env/scripts/docker.js run -T --rm php composer", "env:logs": "node ./tools/local-env/scripts/docker.js logs", "env:pull": "node ./tools/local-env/scripts/docker.js pull", From b3f804761696c1f9c8d877c8407f6dfd24893181 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:32:50 +0530 Subject: [PATCH 09/12] Build/Test Tools: Run installer WP-CLI commands on demand --- package.json | 2 +- tools/local-env/scripts/install.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 20ea5fc94d130..429e0469dd491 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "env:clean": "node ./tools/local-env/scripts/docker.js down -v --remove-orphans", "env:reset": "node ./tools/local-env/scripts/docker.js down --rmi all -v --remove-orphans", "env:install": "node ./tools/local-env/scripts/install.js", - "env:cli": "node ./tools/local-env/scripts/docker.js run --rm --user wp_php cli wp", + "env:cli": "node ./tools/local-env/scripts/docker.js exec --user wp_php cli wp", "env:composer": "node ./tools/local-env/scripts/docker.js run -T --rm php composer", "env:logs": "node ./tools/local-env/scripts/docker.js logs", "env:pull": "node ./tools/local-env/scripts/docker.js pull", diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 5b183cf2f214c..ec879cc02384c 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -59,7 +59,7 @@ wait_on( { * @param {string} cmd The WP-CLI command to run. */ function wp_cli( cmd ) { - const wp_cli_command = `npm --silent run env:cli -- ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`; + const wp_cli_command = `node ./tools/local-env/scripts/docker.js run --rm cli wp ${cmd} --path=/var/www/${process.env.LOCAL_DIR}`; for ( let attempt = 1; attempt <= 12; attempt++ ) { try { From ca31c71fc93af670540ec19e0071261cd80ac263 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:41:08 +0530 Subject: [PATCH 10/12] Build/Test Tools: Wait longer for local env startup --- tools/local-env/scripts/install.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index ec879cc02384c..f9e230bfc161f 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -35,7 +35,9 @@ writeFileSync( 'wp-tests-config.php', testConfig ); // Once the site is available, install WordPress! wait_on( { resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`], - timeout: 3000, + // CI runners can take noticeably longer than local machines before the forwarded + // web port begins accepting connections after `env:start` returns. + timeout: 60000, } ) .catch( err => { console.error( `Error: It appears the development environment has not been started. Message: ${ err.message }` ); From 2c2981a64d73cafb6605eafe156f4926297e496f Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:49:02 +0530 Subject: [PATCH 11/12] Build/Test Tools: Retry installer startup dependencies --- tools/local-env/scripts/install.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index f9e230bfc161f..c9766a0aae983 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -84,29 +84,30 @@ function wp_cli( cmd ) { process.stderr.write( error.stderr.toString() ); } - if ( 12 === attempt || ! wp_cli_container_is_starting( error ) ) { + if ( 12 === attempt || ! wp_cli_dependency_is_starting( error ) ) { throw error; } - // The Docker-backed WP-CLI container can lag behind on slower runners while images - // are still starting up, especially across the broader DB/version matrix. + // The Docker-backed WP-CLI container and database can lag behind on slower runners + // while images are still starting up, especially across the broader DB/version matrix. const delay = Math.min( attempt, 5 ) * 1000; - console.warn( `The WP-CLI container is still starting, retrying in ${ delay / 1000 } second(s)...` ); + console.warn( `The local environment is still starting, retrying in ${ delay / 1000 } second(s)...` ); sleep( delay ); } } } /** - * Determines whether the WP-CLI container is still starting. + * Determines whether a transient local-env startup dependency is still initializing. * * @param {Error & {stdout?: Buffer|string, stderr?: Buffer|string}} error Error thrown by execSync(). - * @return {boolean} Whether the CLI container is still starting up. + * @return {boolean} Whether the local environment is still starting up. */ -function wp_cli_container_is_starting( error ) { +function wp_cli_dependency_is_starting( error ) { const output = `${ error.stdout || '' }\n${ error.stderr || '' }\n${ error.message || '' }`; - return output.includes( 'service "cli" is not running' ); + return output.includes( 'service "cli" is not running' ) || + output.includes( 'Database connection error (2002) Connection refused' ); } /** From c3b24212ff7ac0cb454d71bfd1d7c8ed9d567b08 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 22 Jun 2026 19:59:59 +0530 Subject: [PATCH 12/12] Build/Test Tools: Allow longer local env startup --- tools/local-env/scripts/install.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index c9766a0aae983..df2e3d6256754 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -36,8 +36,9 @@ writeFileSync( 'wp-tests-config.php', testConfig ); wait_on( { resources: [ `tcp:localhost:${process.env.LOCAL_PORT}`], // CI runners can take noticeably longer than local machines before the forwarded - // web port begins accepting connections after `env:start` returns. - timeout: 60000, + // web port begins accepting connections after `env:start` returns, especially + // on slower DB/image combinations in the broader matrix. + timeout: 180000, } ) .catch( err => { console.error( `Error: It appears the development environment has not been started. Message: ${ err.message }` );