From f12250b80f0ece99e45b216cc15163a2443b2cb4 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:11:49 +0300 Subject: [PATCH 1/4] Build/Test Tools: Propagate the exit status of the local environment commands. `start.js` runs `docker compose up` through `spawnSync` and never inspects the result, so a failed pull does not fail the script. Execution continues into `composer update -W` with containers that may not exist, and the error surfaces later and in the wrong place. `docker.js` calls `process.exit( returns.status )`, and `status` is `null` when Docker cannot be spawned. `process.exit( null )` exits 0, so `npm run env:pull` reports success when the Docker CLI is missing. Reproduce with: LOCAL_PHP=this-tag-does-not-exist npm run env:start; echo $? See #65745. --- tools/local-env/scripts/docker.js | 8 +++++++- tools/local-env/scripts/start.js | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index c7b11f0058424..5eaead5fd09d4 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -38,4 +38,10 @@ const returns = spawnSync( { stdio: 'inherit' } ); -process.exit( returns.status ); +if ( returns.error ) { + console.error( `Could not run Docker Compose. ${ returns.error.message }` ); +} + +// `status` is null when Docker could not be spawned at all, or was killed by a signal. Ctrl+C on +// a long-running command such as `env:logs` is not a failure worth an npm error block. +process.exit( returns.signal ? 0 : ( returns.status ?? 1 ) ); diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index 66559d4c10b85..110201c74d4bb 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -32,7 +32,7 @@ if ( process.env.LOCAL_PHP_MEMCACHED === 'true' ) { containers.push( 'memcached' ); } -spawnSync( +const up = spawnSync( 'docker', [ 'compose', @@ -45,6 +45,13 @@ spawnSync( { stdio: 'inherit' } ); +if ( up.status !== 0 ) { + console.error( `Could not start the Docker containers.${ up.error ? ` ${ up.error.message }` : '' }` ); + + // `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. From a88479c520c417cd6a9f5fbd710625ea46ef9133 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:49:54 +0300 Subject: [PATCH 2/4] Build/Test Tools: Only treat SIGINT as a clean exit of a local environment command. `docker.js` exited 0 for any signal, so a command killed by SIGTERM or SIGKILL reported success. That reintroduced the false success this ticket set out to remove: the retry loop in `reusable-phpunit-tests-v3.yml` branches on the status of `npm run env:pull` and would treat a killed pull as a completed one. Restrict the exemption to SIGINT, which is how a long-running command such as `env:logs` is normally ended. Report every other signal and exit non-zero. `start.js` exempts no signal, because `env:start` runs `composer update -W` next and that must not run against containers that never came up. Say so in the comment, so the difference between the two files is deliberate. Also fold the unreachable `up.error` branch in `start.js` into the failure message. The `docker info` check above it already throws when the Docker CLI is missing or the daemon is down. --- tools/local-env/scripts/docker.js | 9 ++++++--- tools/local-env/scripts/start.js | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index 5eaead5fd09d4..ce71a4c075cd6 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -40,8 +40,11 @@ const returns = spawnSync( 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. Ctrl+C on -// a long-running command such as `env:logs` is not a failure worth an npm error block. -process.exit( returns.signal ? 0 : ( returns.status ?? 1 ) ); +// `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 110201c74d4bb..a8e5dc868bf9e 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -45,8 +45,12 @@ const up = 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 ) { - console.error( `Could not start the Docker containers.${ up.error ? ` ${ up.error.message }` : '' }` ); + 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 ); From c7fe0eeb039d908bb98eee1ef55183997f7dc5a8 Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:13:05 +0300 Subject: [PATCH 3/4] Build/Test Tools: Retry the Docker environment setup in CI. `Start Docker environment` failed in 85 runs across 69 branches over 30 days. Of 92 sampled failures, 63 were Docker Hub and 20 were repo.packagist.org. `env:pull` was already retried in `reusable-phpunit-tests-v3.yml`. Extend that to the five remaining workflows, and retry `npm run env:start` as well, since it also runs `composer update -W`. The retry stays in the workflows rather than the npm scripts. Released branches call these reusable workflows at `@trunk` but check out their own `tools/local-env/` and `package.json`, so a retry in the scripts would only protect trunk. Keeping it in CI also means a contributor working offline gets an immediate error instead of waiting through the backoff. Add `ensure_env_file()` so that every Compose command resolves the same image tags. The pull step runs before `start.js` created the `.env` file, which made `env:pull` fall back to the Compose defaults. The synchronous copy also removes a race: the previous `copyFile` was asynchronous, so `dotenv.config()` could read a `.env` that was not written yet. See #65745. --- .../workflows/reusable-end-to-end-tests.yml | 37 +++++++++++++++++- .../reusable-performance-test-v2.yml | 38 +++++++++++++++++- .../workflows/reusable-phpunit-tests-v1.yml | 37 +++++++++++++++++- .../workflows/reusable-phpunit-tests-v2.yml | 39 ++++++++++++++++++- .../workflows/reusable-phpunit-tests-v3.yml | 21 +++++++++- ...sable-test-local-docker-environment-v1.yml | 37 +++++++++++++++++- tools/local-env/scripts/docker.js | 2 + tools/local-env/scripts/install.js | 2 + tools/local-env/scripts/start.js | 6 +-- tools/local-env/scripts/utils.js | 21 +++++++++- 10 files changed, 222 insertions(+), 18 deletions(-) diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 36a71d1711d2f..48e42da1b20ef 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -105,9 +105,42 @@ jobs: - name: Build WordPress run: npm run build - - name: Start Docker environment + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. + - name: Pull Docker images (with retry) run: | - npm run env:start + for attempt in 1 2 3; do + if npm run env:pull; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:pull failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:pull failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done + + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: Log running Docker containers run: docker ps -a diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index a9b911e718cba..374a9abe841a0 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -150,8 +150,42 @@ jobs: - name: Install Playwright browsers run: npx playwright install --with-deps chromium - - name: Start Docker environment - run: npm run env:start + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. + - name: Pull Docker images (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:pull; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:pull failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:pull failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done + + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: Build WordPress run: npm run build diff --git a/.github/workflows/reusable-phpunit-tests-v1.yml b/.github/workflows/reusable-phpunit-tests-v1.yml index bd720d7da30ca..0f532ab3157fc 100644 --- a/.github/workflows/reusable-phpunit-tests-v1.yml +++ b/.github/workflows/reusable-phpunit-tests-v1.yml @@ -128,6 +128,25 @@ jobs: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. + - name: Pull Docker images (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:pull; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:pull failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:pull failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done + - name: Install Composer dependencies if: ${{ env.COMPOSER_INSTALL == true }} run: | @@ -139,9 +158,23 @@ jobs: docker -v docker compose -v - - name: Start Docker environment + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) run: | - npm run env:start + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done # The memcached server needs to start after the Docker network has been set up with `npm run env:start`. - name: Start the Memcached server. diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml index 84c05862d4a43..d131151fa3383 100644 --- a/.github/workflows/reusable-phpunit-tests-v2.yml +++ b/.github/workflows/reusable-phpunit-tests-v2.yml @@ -125,6 +125,27 @@ jobs: path: ${{ steps.composer-cache.outputs.composer_dir }} key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. + # + # This runs before the Composer step below, which is the first thing to pull the PHP image. + - name: Pull Docker images (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:pull; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:pull failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:pull failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done + - name: Install Composer dependencies run: | docker compose run --rm php composer --version @@ -150,9 +171,23 @@ jobs: docker -v docker compose -v - - name: Start Docker environment + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) run: | - npm run env:start + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: General debug information run: | diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 8c9a2aa9703c7..097493962c316 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -205,6 +205,9 @@ jobs: run: | docker -v + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do @@ -221,9 +224,23 @@ jobs: sleep $(( attempt * 10 )) done - - name: Start Docker environment + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) run: | - npm run env:start + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: Log running Docker containers run: docker ps -a diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml index 71c8a9a739f66..3db53d3c2fb94 100644 --- a/.github/workflows/reusable-test-local-docker-environment-v1.yml +++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml @@ -136,9 +136,42 @@ jobs: run: | docker -v - - name: Start Docker environment + # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips + # without holding a runner for minutes when the failure is a bad tag, not the network. + - name: Pull Docker images (with retry) run: | - npm run env:start + for attempt in 1 2 3; do + if npm run env:pull; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:pull failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:pull failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done + + # Retried for the same reason: `env:start` reaches the registry, and on branches where it + # also runs `composer update`, repo.packagist.org. + - name: Start Docker environment (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:start; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:start failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:start failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: Log running Docker containers run: docker ps -a diff --git a/tools/local-env/scripts/docker.js b/tools/local-env/scripts/docker.js index ce71a4c075cd6..fd1577dd9e21f 100644 --- a/tools/local-env/scripts/docker.js +++ b/tools/local-env/scripts/docker.js @@ -5,6 +5,8 @@ const dotenvExpand = require( 'dotenv-expand' ); const { spawnSync } = require( 'child_process' ); const local_env_utils = require( './utils' ); +local_env_utils.ensure_env_file(); + dotenvExpand.expand( dotenv.config() ); const composeFiles = local_env_utils.get_compose_files(); diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 038ecc3a67d5e..cecd1000d4451 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -7,6 +7,8 @@ const { execSync } = require( 'child_process' ); const { readFileSync, writeFileSync } = require( 'fs' ); const local_env_utils = require( './utils' ); +local_env_utils.ensure_env_file(); + dotenvExpand.expand( dotenv.config() ); // Create wp-config.php. diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index a8e5dc868bf9e..d419a08a59bf7 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -4,12 +4,8 @@ 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' ); -// 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.' ); -}); +local_env_utils.ensure_env_file(); dotenvExpand.expand( dotenv.config() ); diff --git a/tools/local-env/scripts/utils.js b/tools/local-env/scripts/utils.js index 51f02e32a1d2d..708ca397d9ea3 100644 --- a/tools/local-env/scripts/utils.js +++ b/tools/local-env/scripts/utils.js @@ -1,9 +1,28 @@ /* jshint node:true */ -const { existsSync } = require( 'node:fs' ); +const { constants, copyFileSync, existsSync } = require( 'node:fs' ); const local_env_utils = { + /** + * Creates the .env file from .env.example when one is not present. + * + * Docker Compose reads this file to resolve the image tags, so it must exist before any + * Compose command runs, not just before the containers are started. + */ + ensure_env_file: function() { + try { + copyFileSync( '.env.example', '.env', constants.COPYFILE_EXCL ); + } catch ( e ) { + // A .env that is already there is the common case and needs no warning. Any other + // failure means the scripts run without the settings from .env, which is worth + // reporting, but is never a reason to refuse to run a command such as `env:stop`. + if ( e.code !== 'EEXIST' ) { + console.warn( `Could not create a .env file from .env.example. ${ e.message }` ); + } + } + }, + /** * Determines which Docker compose files are required to properly configure the local environment given the * specified PHP version, database type, and database version. From 656fc295b9a535d91085efc237d4ba7e68c6a39a Mon Sep 17 00:00:00 2001 From: Adrian Moldovan <3854374+adimoldovan@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:49:28 +0300 Subject: [PATCH 4/4] Build/Test Tools: Address review feedback on the Docker environment retries. - Retry `env:restart` in the local Docker environment workflow. It ends in `env:start`, so it has the same registry and Packagist exposure as the steps that were already retried. - Correct the retry comments. They claimed a bad tag would not hold a runner, but the loop retries every failure regardless of its cause. - Resolve `.env` and `.env.example` against the repository root instead of the current directory, so running the scripts from a subdirectory no longer creates a stray `.env` there. --- .../workflows/reusable-end-to-end-tests.yml | 4 ++-- .../reusable-performance-test-v2.yml | 4 ++-- .../workflows/reusable-phpunit-tests-v1.yml | 4 ++-- .../workflows/reusable-phpunit-tests-v2.yml | 4 ++-- .../workflows/reusable-phpunit-tests-v3.yml | 4 ++-- ...sable-test-local-docker-environment-v1.yml | 22 +++++++++++++++---- tools/local-env/scripts/utils.js | 5 ++++- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 48e42da1b20ef..c5d16c7dd465d 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -106,8 +106,8 @@ jobs: run: npm run build # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 374a9abe841a0..7e262ccf37b06 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -151,8 +151,8 @@ jobs: run: npx playwright install --with-deps chromium # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do diff --git a/.github/workflows/reusable-phpunit-tests-v1.yml b/.github/workflows/reusable-phpunit-tests-v1.yml index 0f532ab3157fc..075fac1d631f4 100644 --- a/.github/workflows/reusable-phpunit-tests-v1.yml +++ b/.github/workflows/reusable-phpunit-tests-v1.yml @@ -129,8 +129,8 @@ jobs: key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml index d131151fa3383..c33fef4537f87 100644 --- a/.github/workflows/reusable-phpunit-tests-v2.yml +++ b/.github/workflows/reusable-phpunit-tests-v2.yml @@ -126,8 +126,8 @@ jobs: key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. # # This runs before the Composer step below, which is the first thing to pull the PHP image. - name: Pull Docker images (with retry) diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 097493962c316..020566b2e935d 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -206,8 +206,8 @@ jobs: docker -v # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml index 3db53d3c2fb94..2de72b03d9af8 100644 --- a/.github/workflows/reusable-test-local-docker-environment-v1.yml +++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml @@ -137,8 +137,8 @@ jobs: docker -v # Anonymous pulls share a Docker Hub quota across the whole runner IP pool, and transient - # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips - # without holding a runner for minutes when the failure is a bad tag, not the network. + # registry errors are routine. Three attempts with a 10s/20s backoff clear the blips. Every + # failure is retried, so a deterministic one such as a bad tag costs 30s before it reports. - name: Pull Docker images (with retry) run: | for attempt in 1 2 3; do @@ -187,8 +187,22 @@ jobs: - name: Install WordPress run: npm run env:install - - name: Restart Docker environment - run: npm run env:restart + # `env:restart` ends in `env:start`, so it has the same network exposure. + - name: Restart Docker environment (with retry) + run: | + for attempt in 1 2 3; do + if npm run env:restart; then + break + fi + + if [ "$attempt" -eq 3 ]; then + echo "npm run env:restart failed after $attempt attempts." + exit 1 + fi + + echo "npm run env:restart failed (attempt $attempt); retrying..." + sleep $(( attempt * 10 )) + done - name: Test a CLI command run: npm run env:cli option get siteurl diff --git a/tools/local-env/scripts/utils.js b/tools/local-env/scripts/utils.js index 708ca397d9ea3..86311a077ec6b 100644 --- a/tools/local-env/scripts/utils.js +++ b/tools/local-env/scripts/utils.js @@ -1,6 +1,9 @@ /* jshint node:true */ const { constants, copyFileSync, existsSync } = require( 'node:fs' ); +const { join } = require( 'node:path' ); + +const repo_root = join( __dirname, '..', '..', '..' ); const local_env_utils = { @@ -12,7 +15,7 @@ const local_env_utils = { */ ensure_env_file: function() { try { - copyFileSync( '.env.example', '.env', constants.COPYFILE_EXCL ); + copyFileSync( join( repo_root, '.env.example' ), join( repo_root, '.env' ), constants.COPYFILE_EXCL ); } catch ( e ) { // A .env that is already there is the common case and needs no warning. Any other // failure means the scripts run without the settings from .env, which is worth