From 01f10471258d50a7946c204780e0a0f8084ab2e0 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:29:41 +0100 Subject: [PATCH 01/11] Build/Test Tools: Time out Playwright browser installs. In the performance tests workflow, `playwright install --with-deps` hung until the 6 hour job limit in runs 40570600406, 40571039922 and 40575912886, leaving the job unfinished until GitHub cancelled it. Across the recent trunk runs, the step took under a minute in most jobs and at most about 10 minutes during the same slowdown. A 15 minute timeout fails a hung install quickly without failing slow ones. --- .github/workflows/reusable-performance-test-v2.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index a4a9021510c1c..6361356e2fa34 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -159,6 +159,9 @@ jobs: run: npm ci - name: Install Playwright browsers + # A hung install would otherwise run until the 6 hour job limit. The cap + # leaves room for slow downloads. + timeout-minutes: 15 run: npm exec --no -- playwright install --with-deps chromium - name: Start Docker environment From 70abfd907e053a2ae5f1ccca5c8b8b7bee678568 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:31:30 +0100 Subject: [PATCH 02/11] Build/Test Tools: Retry the mock data download. The performance tests workflow downloads the theme unit test data with a single `curl -O`. In run 40571772421 the connection was reset at 98% and the job failed with `curl: (56) Recv failure`. Retry the download with the flags the Set up WP-CLI action uses, and fail on HTTP errors instead of importing an error page. Use `--no-progress-meter` instead of `--silent --show-error`, so retries show in the log. curl has no transfer timeout, `wp import` fetches each attachment with a 300 second timeout, and the performance job has no job timeout, so a stalled download could run for hours. Cap the step at 5 minutes; it took at most 29 seconds in the last 120 trunk runs. --- .github/workflows/reusable-performance-test-v2.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 6361356e2fa34..f3a52af88acee 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -243,8 +243,11 @@ jobs: run: npm run env:cli -- plugin install wordpress-importer --activate --path="/var/www/${LOCAL_DIR}" - name: Import mock data + # Neither curl nor the import's attachment downloads time out quickly, so a + # stalled download could otherwise run until the 6 hour job limit. + timeout-minutes: 5 run: | - curl -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml + curl --fail --no-progress-meter --location --retry 5 --retry-all-errors -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" rm themeunittestdata.wordpress.xml From 30f2157380bf1e5b700179f8ccb692f34429d99c Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:32:03 +0100 Subject: [PATCH 03/11] Build/Test Tools: Retry the WordPress Importer download. `wp plugin install wordpress-importer` queries the WordPress.org API and downloads the plugin once. In performance test runs 39060627317 and 39485836492 the request failed and the job stopped with "The 'wordpress-importer' plugin could not be found." Download the plugin zip on the runner with the same retrying curl command as the mock data, install it from the mounted workspace, and remove it, as the mock data step does with its file. Use the `latest-stable` URL: it redirects to the current release's versioned zip, the one WP-CLI installed. The unversioned zip is the plugin's SVN trunk, which WordPress.org lists as "Development Version". WP-CLI's download times out after 300 seconds. curl has no transfer timeout and the performance job has no job timeout, so a stalled download would now run for 6 hours. Cap the step at 5 minutes; it took at most 5 seconds in the last 120 trunk runs. --- .github/workflows/reusable-performance-test-v2.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index f3a52af88acee..325ca6b7194f5 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -240,7 +240,14 @@ jobs: npm run env:cli -- theme enable twentytwentyfive --network --path="/var/www/${LOCAL_DIR}" - name: Install WordPress Importer plugin - run: npm run env:cli -- plugin install wordpress-importer --activate --path="/var/www/${LOCAL_DIR}" + # Downloaded on the runner so failed downloads are retried: WP-CLI tries once. + # curl has no transfer timeout, so a stalled download would otherwise run until + # the 6 hour job limit. + timeout-minutes: 5 + run: | + curl --fail --no-progress-meter --location --retry 5 --retry-all-errors --output wordpress-importer.zip https://downloads.wordpress.org/plugin/wordpress-importer.latest-stable.zip + npm run env:cli -- plugin install wordpress-importer.zip --activate --path="/var/www/${LOCAL_DIR}" + rm wordpress-importer.zip - name: Import mock data # Neither curl nor the import's attachment downloads time out quickly, so a From 19873366971be70317238a9ccd06dac614fae2e1 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:32:31 +0100 Subject: [PATCH 04/11] Build/Test Tools: Retry the baseline WordPress download. `wp core download` fetches the baseline release from WordPress.org once. In performance test run 39174433151 it failed with "cURL error 28: Resolving timed out". Try it up to three times, waiting 10 and then 20 seconds as the local environment's `docker compose` retries do, and warning on each failed attempt. Replacing it with a retrying curl was considered, but `wp core download` also verifies the release's md5 hash, which a plain extract would not reproduce. --- .../workflows/reusable-performance-test-v2.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 325ca6b7194f5..0d121d35d0695 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -205,9 +205,22 @@ jobs: - name: Set the environment to the baseline version if: ${{ inputs.subject == 'base' }} + # WP-CLI requests the release from WordPress.org once, so a network error fails the job. + # It extracts only after a complete download, and `--force` lets a retry overwrite files. + env: + WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 run: | VERSION="${BASE_TAG%.0}" - wp core download --version="$VERSION" --force --path="${LOCAL_DIR}" + for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do + if wp core download --version="$VERSION" --force --path="${LOCAL_DIR}"; then + exit 0 + fi + echo "::warning::wp core download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." + if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then + sleep $(( attempt * 10 )) + fi + done + exit 1 - name: Install object cache drop-in if: ${{ inputs.memcached }} From 7a1529ed47909662cff64c6d3ca91281659f5e19 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:33:03 +0100 Subject: [PATCH 05/11] Build/Test Tools: Retry the npm install in Set up npm. The Set up npm action installs the npm version required by `devEngines` with a single `npm install --global`. In performance test runs 38800425109 and 39405765547 the registry reset the connection (`ECONNRESET`, "Invalid response body"), which npm does not retry, and the job failed. Try the install up to three times, waiting 10 and then 20 seconds and warning on each failed attempt, then fail the step. Every trunk workflow that installs npm dependencies uses this action. --- .github/actions/setup-npm/action.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/actions/setup-npm/action.yml b/.github/actions/setup-npm/action.yml index fba5bd0257bcc..fe433ce7606d5 100644 --- a/.github/actions/setup-npm/action.yml +++ b/.github/actions/setup-npm/action.yml @@ -18,11 +18,22 @@ runs: id: npm env: WORKING_DIRECTORY: ${{ inputs.working-directory }} + NPM_INSTALL_MAX_ATTEMPTS: 3 run: | NPM_VERSION="$(jq -r '.devEngines.packageManager.version // empty' package.json)" if [ -n "$NPM_VERSION" ]; then # Contributors need the floor; CI stays on that major's newest release. - npm install --global "npm@${NPM_VERSION/>=/^}" + # The registry can reset the connection mid-download (ECONNRESET), so retry. + for (( attempt = 1; attempt <= NPM_INSTALL_MAX_ATTEMPTS; attempt++ )); do + if npm install --global "npm@${NPM_VERSION/>=/^}"; then + break + fi + echo "::warning::npm install failed on attempt $attempt of $NPM_INSTALL_MAX_ATTEMPTS." + if (( attempt == NPM_INSTALL_MAX_ATTEMPTS )); then + exit 1 + fi + sleep $(( attempt * 10 )) + done echo "Required npm version: $NPM_VERSION" else echo "No devEngines.packageManager.version, keeping the bundled npm." From 96c26977bd857a9821aa8660df8fec884176894d Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 10:33:25 +0100 Subject: [PATCH 06/11] Build/Test Tools: Skip performance artifacts if tests never ran. The Archive artifacts step runs on `always()` and errors when the `artifacts` directory is missing. When a setup step fails, the tests never run, so every such job also reported "No files were found with the provided path: artifacts", a second error that hides the real one in the job summary. Run the step only when the performance tests step did not skip. A test run that fails or is cancelled still uploads its results, and a run that produced no results still errors. --- .github/workflows/reusable-performance-test-v2.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 0d121d35d0695..8b9c8474dfc18 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -297,13 +297,15 @@ jobs: cp ./tests/performance/wp-content/mu-plugins/server-timing.php "./${LOCAL_DIR}/wp-content/mu-plugins/server-timing.php" - name: Run performance tests + id: performance-tests run: npm run test:performance env: TEST_RESULTS_PREFIX: ${{ inputs.subject != 'current' && inputs.subject || '' }} - name: Archive artifacts uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - if: always() + # Without test results, a failed setup step would report a second failure here. + if: ${{ always() && steps.performance-tests.outcome != 'skipped' }} with: name: performance-${{ inputs.multisite && 'multisite' || 'single' }}-${{ inputs.memcached && 'memcached' || 'default' }}-${{ inputs.subject }} path: artifacts From 9b1b3321fe2fd58dec98870e6a83517d01ee2465 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 12:00:50 +0100 Subject: [PATCH 07/11] Build/Test Tools: Retry the Gutenberg download in E2E tests. `wp plugin install gutenberg` downloads the plugin from WordPress.org once, so a network error fails the E2E job. The performance workflow failed the same way installing the WordPress Importer (runs 39060627317, 39485836492). Download the zip on the runner with curl retries, as the performance workflow now does for the importer, then install it from the workspace, which the CLI container mounts at /var/www. Release branches from 6.4 call this workflow from trunk with a pinned `gutenberg-version`, which maps to the versioned zip URL. Unpinned branches use the `latest-stable` URL, which redirects to the current release's zip, as WP-CLI installed; the unversioned zip is SVN trunk. The job's 20 minute timeout bounds a stalled download. Installing from a zip doesn't log the plugin version, and trunk, 7.0 and 7.1 install the latest Gutenberg, so print the installed version. --- .github/workflows/reusable-end-to-end-tests.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 40db644c5a2d6..25c88a8f40652 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -138,10 +138,15 @@ jobs: - name: Install Gutenberg if: ${{ inputs.install-gutenberg }} + # Downloaded on the runner so failed downloads are retried: WP-CLI tries once. run: | - npm run env:cli -- plugin install gutenberg \ - ${{ inputs.gutenberg-version && '--version="${GUTENBERG_VERSION}"' || '' }} \ - --path="/var/www/${LOCAL_DIR}" + curl --fail --no-progress-meter --location --retry 5 --retry-all-errors \ + --output gutenberg.zip \ + "https://downloads.wordpress.org/plugin/gutenberg.${GUTENBERG_VERSION:-latest-stable}.zip" + npm run env:cli -- plugin install gutenberg.zip --path="/var/www/${LOCAL_DIR}" + rm gutenberg.zip + # Installing from a zip doesn't log the version; unpinned branches get the latest. + npm run env:cli -- plugin get gutenberg --field=version --path="/var/www/${LOCAL_DIR}" env: GUTENBERG_VERSION: ${{ inputs.gutenberg-version }} From a4ef5569d22a5bbcf60b62cca836d8018ea2f511 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 29 Oct 2026 12:00:50 +0100 Subject: [PATCH 08/11] Build/Test Tools: Retry WordPress downloads in install tests. The install and upgrade testing workflows download the WordPress version they start from once, with `wp core download`. Any network error fails the job, as it did for the performance baseline in run 39174433151. Try the download up to three times, waiting 10 and then 20 seconds, as the performance workflow now does for its baseline. WP-CLI downloads to a temporary file and extracts only after the download succeeds, so a retry starts from an empty directory. Both workflows run large matrices, so even rare failures repeat across jobs. The upgrade testing `wp core update` steps also download from WordPress.org but are left as they are: they are the upgrade under test, and retrying a partly applied upgrade could hide a real failure. --- .github/workflows/install-testing.yml | 18 ++++++++++++++---- .github/workflows/reusable-upgrade-testing.yml | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml index a1a634b6bac39..f6a36875b524b 100644 --- a/.github/workflows/install-testing.yml +++ b/.github/workflows/install-testing.yml @@ -143,12 +143,22 @@ jobs: # WP-CLI 2.x extracts tarballs with PharData, which truncates paths longer than 100 characters. # Zip archives are not affected, so download the zip for every version. - name: Download WordPress + # WP-CLI requests WordPress from WordPress.org once, so a network error fails the job. + # It extracts only after a complete download, so a retry starts from an empty directory. run: | - case "${WP_VERSION}" in - nightly|trunk) wp core download --version=nightly ;; - *) wp core download "https://wordpress.org/wordpress-${WP_VERSION}.zip" ;; - esac + for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do + case "${WP_VERSION}" in + nightly|trunk) wp core download --version=nightly && exit 0 ;; + *) wp core download "https://wordpress.org/wordpress-${WP_VERSION}.zip" && exit 0 ;; + esac + echo "::warning::WordPress download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." + if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then + sleep $(( attempt * 10 )) + fi + done + exit 1 env: + WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 WP_VERSION: ${{ inputs.wp-version || 'nightly' }} - name: Create wp-config.php file diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml index 8b05d38ea3d69..c9ae0043cc22f 100644 --- a/.github/workflows/reusable-upgrade-testing.yml +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -95,8 +95,21 @@ jobs: uses: $/.github/actions/setup-wp-cli - name: Download WordPress ${{ inputs.wp }} - run: wp core download --version="${WP_VERSION}" + # WP-CLI requests the release from WordPress.org once, so a network error fails the job. + # It extracts only after a complete download, so a retry starts from an empty directory. + run: | + for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do + if wp core download --version="${WP_VERSION}"; then + exit 0 + fi + echo "::warning::wp core download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." + if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then + sleep $(( attempt * 10 )) + fi + done + exit 1 env: + WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 WP_VERSION: ${{ inputs.wp }} - name: Create wp-config.php file From a099d52f6096accd78b8f8bac0019289c484a0f5 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 3 Nov 2026 15:29:49 +0100 Subject: [PATCH 09/11] Build/Test Tools: Move retries to shared actions The curl downloads and `wp core download` retry loops were repeated inline across the performance, E2E, installation and upgrade workflows. Move them into two composite actions, as suggested in review: - `download-with-retry` runs curl with `--retry 5 --retry-all-errors` for the Gutenberg, WordPress Importer and mock data downloads. - `run-with-retry` runs a command up to three times, waiting 10 and then 20 seconds, for the WordPress downloads. Commands read values from the calling step's env, not expressions, as the command runs as code. Release branches call these workflows from trunk, so the actions are referenced with `$/`, as `setup-wp-cli` is, and added to the callers' path filters so changes to them run the workflows that use them. The `setup-npm` retry stays inline: it already lives in a composite action, and moving it would change the directory npm runs from, which its devEngines check depends on. --- .../actions/download-with-retry/action.yml | 22 +++++++++++ .github/actions/run-with-retry/action.yml | 28 ++++++++++++++ .github/workflows/end-to-end-tests.yml | 1 + .github/workflows/install-testing.yml | 18 ++++----- .github/workflows/performance.yml | 2 + .../workflows/reusable-end-to-end-tests.yml | 15 +++++--- .../reusable-performance-test-v2.yml | 38 +++++++++---------- .../workflows/reusable-upgrade-testing.yml | 15 ++------ .github/workflows/upgrade-develop-testing.yml | 2 + .github/workflows/upgrade-testing.yml | 2 + 10 files changed, 95 insertions(+), 48 deletions(-) create mode 100644 .github/actions/download-with-retry/action.yml create mode 100644 .github/actions/run-with-retry/action.yml diff --git a/.github/actions/download-with-retry/action.yml b/.github/actions/download-with-retry/action.yml new file mode 100644 index 0000000000000..456420c6e3d9b --- /dev/null +++ b/.github/actions/download-with-retry/action.yml @@ -0,0 +1,22 @@ +name: 'Download with retry' +description: 'Downloads a file with curl, retrying on any error.' + +inputs: + url: + description: 'The URL to download.' + required: true + output: + description: 'The path to save the file to, relative to the workspace.' + required: true + +# curl has no transfer timeout: set `timeout-minutes` on the calling step, or a stalled +# download runs until the 6 hour job limit. +runs: + using: 'composite' + steps: + - name: Download the file + shell: bash + run: curl --fail --no-progress-meter --location --retry 5 --retry-all-errors --output "$OUTPUT" "$URL" + env: + URL: ${{ inputs.url }} + OUTPUT: ${{ inputs.output }} diff --git a/.github/actions/run-with-retry/action.yml b/.github/actions/run-with-retry/action.yml new file mode 100644 index 0000000000000..b7d6ce692d765 --- /dev/null +++ b/.github/actions/run-with-retry/action.yml @@ -0,0 +1,28 @@ +name: 'Run with retry' +description: 'Runs a Bash command, retrying it when it fails. For downloads curl can retry itself, use download-with-retry.' + +inputs: + command: + description: 'The Bash command to run. It runs as code: read values from the `env` of the calling step, not from expressions.' + required: true + +runs: + using: 'composite' + steps: + - name: Run the command + shell: bash + # Waits 10 s, then 20 s, between attempts, like `tools/local-env/scripts/utils.js`. + run: | + for (( attempt = 1; attempt <= MAX_ATTEMPTS; attempt++ )); do + if bash -eo pipefail -c "$COMMAND"; then + exit 0 + fi + echo "::warning::Command failed on attempt $attempt of $MAX_ATTEMPTS." + if (( attempt < MAX_ATTEMPTS )); then + sleep $(( attempt * 10 )) + fi + done + exit 1 + env: + COMMAND: ${{ inputs.command }} + MAX_ATTEMPTS: 3 diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 9e03641b978fd..bd35810aad280 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -44,6 +44,7 @@ on: - 'tools/vendors/**' - 'tools/webpack/**' - '.github/actions/setup-npm/**' + - '.github/actions/download-with-retry/**' - '.github/workflows/end-to-end-tests.yml' - '.github/workflows/reusable-end-to-end-tests*.yml' workflow_dispatch: diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml index f6a36875b524b..38a805be491da 100644 --- a/.github/workflows/install-testing.yml +++ b/.github/workflows/install-testing.yml @@ -13,6 +13,7 @@ on: # Always test the workflow after it's updated. paths: - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/actions/smoke-check/**' - '.github/workflows/install-testing.yml' - '.version-support-*.json' @@ -21,6 +22,7 @@ on: # Always test the workflow when changes are suggested. paths: - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/actions/smoke-check/**' - '.version-support-*.json' - '.github/workflows/install-testing.yml' @@ -145,20 +147,14 @@ jobs: - name: Download WordPress # WP-CLI requests WordPress from WordPress.org once, so a network error fails the job. # It extracts only after a complete download, so a retry starts from an empty directory. - run: | - for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do + uses: $/.github/actions/run-with-retry + with: + command: | case "${WP_VERSION}" in - nightly|trunk) wp core download --version=nightly && exit 0 ;; - *) wp core download "https://wordpress.org/wordpress-${WP_VERSION}.zip" && exit 0 ;; + nightly|trunk) wp core download --version=nightly ;; + *) wp core download "https://wordpress.org/wordpress-${WP_VERSION}.zip" ;; esac - echo "::warning::WordPress download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." - if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then - sleep $(( attempt * 10 )) - fi - done - exit 1 env: - WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 WP_VERSION: ${{ inputs.wp-version || 'nightly' }} - name: Create wp-config.php file diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index cc555cf9ef0e7..8a73f538eb021 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -44,7 +44,9 @@ on: - 'tools/vendors/**' - 'tools/webpack/**' - '.github/actions/setup-npm/**' + - '.github/actions/download-with-retry/**' - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/workflows/performance.yml' - '.github/workflows/reusable-performance-*.yml' workflow_dispatch: diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml index 25c88a8f40652..0a8e15f66cccc 100644 --- a/.github/workflows/reusable-end-to-end-tests.yml +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -136,19 +136,22 @@ jobs: LOCAL_SCRIPT_DEBUG: ${{ inputs.LOCAL_SCRIPT_DEBUG }} run: npm run env:install - - name: Install Gutenberg + - name: Download Gutenberg if: ${{ inputs.install-gutenberg }} # Downloaded on the runner so failed downloads are retried: WP-CLI tries once. + # Release branches run this workflow from trunk: `$/` resolves to this workflow's commit, not the caller's. + uses: $/.github/actions/download-with-retry + with: + url: https://downloads.wordpress.org/plugin/gutenberg.${{ inputs.gutenberg-version || 'latest-stable' }}.zip + output: gutenberg.zip + + - name: Install Gutenberg + if: ${{ inputs.install-gutenberg }} run: | - curl --fail --no-progress-meter --location --retry 5 --retry-all-errors \ - --output gutenberg.zip \ - "https://downloads.wordpress.org/plugin/gutenberg.${GUTENBERG_VERSION:-latest-stable}.zip" npm run env:cli -- plugin install gutenberg.zip --path="/var/www/${LOCAL_DIR}" rm gutenberg.zip # Installing from a zip doesn't log the version; unpinned branches get the latest. npm run env:cli -- plugin get gutenberg --field=version --path="/var/www/${LOCAL_DIR}" - env: - GUTENBERG_VERSION: ${{ inputs.gutenberg-version }} - name: Install additional languages run: | diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 8b9c8474dfc18..9c7b3c2e44598 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -207,20 +207,9 @@ jobs: if: ${{ inputs.subject == 'base' }} # WP-CLI requests the release from WordPress.org once, so a network error fails the job. # It extracts only after a complete download, and `--force` lets a retry overwrite files. - env: - WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 - run: | - VERSION="${BASE_TAG%.0}" - for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do - if wp core download --version="$VERSION" --force --path="${LOCAL_DIR}"; then - exit 0 - fi - echo "::warning::wp core download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." - if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then - sleep $(( attempt * 10 )) - fi - done - exit 1 + uses: $/.github/actions/run-with-retry + with: + command: wp core download --version="${BASE_TAG%.0}" --force --path="${LOCAL_DIR}" - name: Install object cache drop-in if: ${{ inputs.memcached }} @@ -252,22 +241,33 @@ jobs: npm run env:cli -- theme enable twentytwentyfour --network --path="/var/www/${LOCAL_DIR}" npm run env:cli -- theme enable twentytwentyfive --network --path="/var/www/${LOCAL_DIR}" - - name: Install WordPress Importer plugin + - name: Download WordPress Importer plugin # Downloaded on the runner so failed downloads are retried: WP-CLI tries once. # curl has no transfer timeout, so a stalled download would otherwise run until # the 6 hour job limit. timeout-minutes: 5 + uses: $/.github/actions/download-with-retry + with: + url: https://downloads.wordpress.org/plugin/wordpress-importer.latest-stable.zip + output: wordpress-importer.zip + + - name: Install WordPress Importer plugin run: | - curl --fail --no-progress-meter --location --retry 5 --retry-all-errors --output wordpress-importer.zip https://downloads.wordpress.org/plugin/wordpress-importer.latest-stable.zip npm run env:cli -- plugin install wordpress-importer.zip --activate --path="/var/www/${LOCAL_DIR}" rm wordpress-importer.zip + - name: Download mock data + timeout-minutes: 5 + uses: $/.github/actions/download-with-retry + with: + url: https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml + output: themeunittestdata.wordpress.xml + - name: Import mock data - # Neither curl nor the import's attachment downloads time out quickly, so a - # stalled download could otherwise run until the 6 hour job limit. + # The import's attachment downloads don't time out quickly, so a stalled + # download could otherwise run until the 6 hour job limit. timeout-minutes: 5 run: | - curl --fail --no-progress-meter --location --retry 5 --retry-all-errors -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" rm themeunittestdata.wordpress.xml diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml index c9ae0043cc22f..5bfadd0213f8f 100644 --- a/.github/workflows/reusable-upgrade-testing.yml +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -97,19 +97,10 @@ jobs: - name: Download WordPress ${{ inputs.wp }} # WP-CLI requests the release from WordPress.org once, so a network error fails the job. # It extracts only after a complete download, so a retry starts from an empty directory. - run: | - for (( attempt = 1; attempt <= WP_CORE_DOWNLOAD_MAX_ATTEMPTS; attempt++ )); do - if wp core download --version="${WP_VERSION}"; then - exit 0 - fi - echo "::warning::wp core download failed on attempt $attempt of $WP_CORE_DOWNLOAD_MAX_ATTEMPTS." - if (( attempt < WP_CORE_DOWNLOAD_MAX_ATTEMPTS )); then - sleep $(( attempt * 10 )) - fi - done - exit 1 + uses: $/.github/actions/run-with-retry + with: + command: wp core download --version="${WP_VERSION}" env: - WP_CORE_DOWNLOAD_MAX_ATTEMPTS: 3 WP_VERSION: ${{ inputs.wp }} - name: Create wp-config.php file diff --git a/.github/workflows/upgrade-develop-testing.yml b/.github/workflows/upgrade-develop-testing.yml index e097bac30c24f..555bdbdb47dac 100644 --- a/.github/workflows/upgrade-develop-testing.yml +++ b/.github/workflows/upgrade-develop-testing.yml @@ -15,6 +15,7 @@ on: - '*.php' # PHP files in the root, like wp-config-sample.php. # Confirm any changes to relevant workflow files. - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/actions/smoke-check/**' - '.github/actions/setup-npm/**' - '.github/workflows/upgrade-develop-testing.yml' @@ -31,6 +32,7 @@ on: # Confirm any changes to relevant workflow files. - '.github/actions/setup-npm/**' - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/workflows/upgrade-develop-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' schedule: diff --git a/.github/workflows/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml index 9eadc0683f8d9..b7f6775e24e32 100644 --- a/.github/workflows/upgrade-testing.yml +++ b/.github/workflows/upgrade-testing.yml @@ -10,6 +10,7 @@ on: # Always test the workflow after it's updated. paths: - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/actions/smoke-check/**' - '.github/workflows/upgrade-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' @@ -20,6 +21,7 @@ on: # Always test the workflow when changes are suggested. paths: - '.github/actions/setup-wp-cli/**' + - '.github/actions/run-with-retry/**' - '.github/workflows/upgrade-testing.yml' - '.github/workflows/reusable-upgrade-testing.yml' workflow_dispatch: From 9d53427beacbdc7bd6f40d0aa9a16887e871f9f6 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 3 Nov 2026 15:56:54 +0100 Subject: [PATCH 10/11] Build/Test Tools: Raise the mock data import cap The WordPress Importer waits up to 300 s per attachment download and skips attachments that fail, so a 5 minute cap turned one stalled attachment into a failed job. 15 minutes still bounds a hung import. --- .github/workflows/reusable-performance-test-v2.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 9c7b3c2e44598..3477796f16877 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -264,9 +264,9 @@ jobs: output: themeunittestdata.wordpress.xml - name: Import mock data - # The import's attachment downloads don't time out quickly, so a stalled - # download could otherwise run until the 6 hour job limit. - timeout-minutes: 5 + # The importer waits up to 300 s per attachment download and skips the ones that + # fail: the cap leaves room for a stalled attachment, not a hung import. + timeout-minutes: 15 run: | npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" rm themeunittestdata.wordpress.xml From 931f1617dcd0be6d50d3466a47316b441c14cc95 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 3 Nov 2026 15:57:24 +0100 Subject: [PATCH 11/11] Build/Test Tools: Retry stalled downloads download-with-retry left stalls to `timeout-minutes` on the calling step, which ends the step without retrying, and the E2E Gutenberg download had no step cap at all. Have curl fail an attempt that takes over 30 seconds to connect or stays under 1 KB/s for 30 seconds: curl retries timeouts, so stalls are now retried like other errors, for every caller. Drop the per-step caps on the performance downloads, and start no retry after 10 minutes so a long Retry-After can't stretch the waits past that. --- .github/actions/download-with-retry/action.yml | 7 ++++--- .github/workflows/reusable-performance-test-v2.yml | 4 ---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/actions/download-with-retry/action.yml b/.github/actions/download-with-retry/action.yml index 456420c6e3d9b..b2697efdb431f 100644 --- a/.github/actions/download-with-retry/action.yml +++ b/.github/actions/download-with-retry/action.yml @@ -9,14 +9,15 @@ inputs: description: 'The path to save the file to, relative to the workspace.' required: true -# curl has no transfer timeout: set `timeout-minutes` on the calling step, or a stalled -# download runs until the 6 hour job limit. runs: using: 'composite' steps: - name: Download the file shell: bash - run: curl --fail --no-progress-meter --location --retry 5 --retry-all-errors --output "$OUTPUT" "$URL" + # A stalled attempt (connecting for over 30 s, or under 1 KB/s for 30 s) fails and is + # retried: curl has no transfer timeout, and `timeout-minutes` would end the step unretried. + # No retry starts after 10 minutes, so a long `Retry-After` can't stretch the waits. + run: curl --fail --no-progress-meter --location --retry 5 --retry-all-errors --retry-max-time 600 --connect-timeout 30 --speed-limit 1024 --speed-time 30 --output "$OUTPUT" "$URL" env: URL: ${{ inputs.url }} OUTPUT: ${{ inputs.output }} diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml index 3477796f16877..5d3b8ee26ecc4 100644 --- a/.github/workflows/reusable-performance-test-v2.yml +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -243,9 +243,6 @@ jobs: - name: Download WordPress Importer plugin # Downloaded on the runner so failed downloads are retried: WP-CLI tries once. - # curl has no transfer timeout, so a stalled download would otherwise run until - # the 6 hour job limit. - timeout-minutes: 5 uses: $/.github/actions/download-with-retry with: url: https://downloads.wordpress.org/plugin/wordpress-importer.latest-stable.zip @@ -257,7 +254,6 @@ jobs: rm wordpress-importer.zip - name: Download mock data - timeout-minutes: 5 uses: $/.github/actions/download-with-retry with: url: https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml