From 5416851aded3b86a53edca8fc177bc663e2f7829 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 1 Jun 2026 14:44:27 +0530 Subject: [PATCH 1/5] Themes: Reset update notice when credentials modal is canceled --- src/js/_enqueues/wp/updates.js | 4 +++ tests/qunit/wp-admin/js/updates.js | 40 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/js/_enqueues/wp/updates.js b/src/js/_enqueues/wp/updates.js index ef4b47e66093e..ea0039fd4512f 100644 --- a/src/js/_enqueues/wp/updates.js +++ b/src/js/_enqueues/wp/updates.js @@ -2515,6 +2515,10 @@ } else if ( 'themes' === pagenow || 'themes-network' === pagenow ) { if ( 'update-theme' === job.action ) { $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.update-message' ); + + if ( 'themes' === pagenow ) { + $message = $message.add( $( '#update-theme' ).closest( '.notice' ) ); + } } else if ( 'delete-theme' === job.action && 'themes-network' === pagenow ) { $message = $( '[data-slug="' + job.data.slug + '"]' ).find( '.row-actions a.delete' ); } else if ( 'delete-theme' === job.action && 'themes' === pagenow ) { diff --git a/tests/qunit/wp-admin/js/updates.js b/tests/qunit/wp-admin/js/updates.js index 9d3948811abfd..6c10a8fea868f 100644 --- a/tests/qunit/wp-admin/js/updates.js +++ b/tests/qunit/wp-admin/js/updates.js @@ -158,6 +158,46 @@ jQuery( function( $ ) { assert.equal( jQuery.ajax.getCall( 0 ).args[0].data.slug, 'twentyeleven' ); } ); + QUnit.test( 'Canceling the credentials modal restores the theme details notice', function( assert ) { + var overlayNotice = $( + '
' + + '

Update Available

' + + '

There is a new version of Twenty Eleven available. update now.

' + + '
' + ).appendTo( '#qunit-fixture' ), + eventTarget = overlayNotice.find( '#update-theme' ), + rowNotice = $( + '
' + + '
' + + '

There is a new version of Twenty Eleven available. update now.

' + + '
' + + '
' + ).appendTo( '#qunit-fixture' ); + + $( '
' ) + .appendTo( '#qunit-fixture' ); + + wp.updates.shouldRequestFilesystemCredentials = true; + wp.updates.filesystemCredentials.available = false; + + wp.updates.maybeRequestFilesystemCredentials( $.Event( 'click', { + target: eventTarget[0] + } ) ); + + wp.updates.updateTheme( { slug: 'twentyeleven' } ); + + assert.strictEqual( wp.updates.queue.length, 1, 'Theme update waits for credentials.' ); + assert.true( overlayNotice.hasClass( 'updating-message' ), 'Overlay notice is marked as updating.' ); + assert.true( rowNotice.find( '.update-message' ).hasClass( 'updating-message' ), 'Theme row notice is marked as updating.' ); + + wp.updates.requestForCredentialsModalCancel(); + + assert.false( overlayNotice.hasClass( 'updating-message' ), 'Overlay notice resets after cancel.' ); + assert.false( rowNotice.find( '.update-message' ).hasClass( 'updating-message' ), 'Theme row notice resets after cancel.' ); + assert.notStrictEqual( overlayNotice.text().indexOf( 'Updating...' ), 0, 'Overlay notice no longer shows the updating text.' ); + assert.notStrictEqual( rowNotice.text().indexOf( 'Updating...' ), 0, 'Theme row notice no longer shows the updating text.' ); + } ); + QUnit.test( 'Installing a theme should call the API', function( assert ) { wp.updates.installTheme( { slug: 'twentyeleven' } ); assert.ok( jQuery.ajax.calledOnce ); From acc523d58342d07ab16f3bf8fc8a4d6f67204289 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 1 Jun 2026 15:19:09 +0530 Subject: [PATCH 2/5] Build/Test Tools: Skip DB check during local env config creation --- tools/local-env/scripts/install.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/local-env/scripts/install.js b/tools/local-env/scripts/install.js index 038ecc3a67d5e..cba7f1169e3ad 100644 --- a/tools/local-env/scripts/install.js +++ b/tools/local-env/scripts/install.js @@ -9,8 +9,8 @@ const local_env_utils = require( './utils' ); dotenvExpand.expand( dotenv.config() ); -// Create wp-config.php. -wp_cli( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --force --config-file="wp-config.php"` ); +// Create wp-config.php without waiting on the database to accept connections. +wp_cli( `config create --dbname=wordpress_develop --dbuser=root --dbpass=password --dbhost=mysql --skip-check --force --config-file="wp-config.php"` ); // Add the debug settings to wp-config.php. // Windows requires this to be done as an additional step, rather than using the --extra-php option in the previous step. From 0eada8835f1604937a144544422db07ea8420df9 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 1 Jun 2026 15:30:00 +0530 Subject: [PATCH 3/5] Build/Test Tools: Retry transient Gutenberg GHCR fetches --- tools/gutenberg/download.js | 13 ++++-- tools/gutenberg/utils.js | 88 +++++++++++++++++++++++++++++++++++-- 2 files changed, 93 insertions(+), 8 deletions(-) diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index fd76c6c7a7836..687552c66ccb5 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -27,6 +27,7 @@ const zlib = require( 'zlib' ); const { gutenbergDir, readGutenbergConfig, + fetchWithRetry, fetchGhcrToken, fetchManifest, } = require( './utils' ); @@ -151,11 +152,15 @@ async function main() { */ console.log( `\n📥 Downloading and extracting artifact...` ); try { - const response = await fetch( `https://ghcr.io/v2/${ config.ghcrRepo }/blobs/${ digest }`, { - headers: { - Authorization: `Bearer ${ token }`, + const response = await fetchWithRetry( + `https://ghcr.io/v2/${ config.ghcrRepo }/blobs/${ digest }`, + { + headers: { + Authorization: `Bearer ${ token }`, + }, }, - } ); + `blob ${ digest }` + ); if ( ! response.ok ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 43047b5ee5dd7..9e645b02952c5 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -25,6 +25,82 @@ const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); const SHA_PATTERN = /^[a-f0-9]{40}$/i; const MANIFEST_ACCEPT = 'application/vnd.oci.image.manifest.v1+json'; +const RETRYABLE_HTTP_STATUSES = new Set( [ 429, 500, 502, 503, 504 ] ); +const MAX_FETCH_RETRIES = 3; +const RETRY_DELAY_MS = 1000; + +/** + * Pause execution for a short duration between transient fetch retries. + * + * @param {number} delayMs Delay in milliseconds. + * @return {Promise} + */ +function sleep( delayMs ) { + return new Promise( ( resolve ) => { + setTimeout( resolve, delayMs ); + } ); +} + +/** + * Determine whether a fetch failure should be retried. + * + * @param {Response | undefined} response Fetch response when available. + * @param {unknown} error Fetch error when the request failed before a response. + * @return {boolean} Whether the failure appears transient. + */ +function shouldRetryFetch( response, error ) { + if ( response ) { + return RETRYABLE_HTTP_STATUSES.has( response.status ); + } + + return error instanceof TypeError; +} + +/** + * Fetch a URL with retries for transient GHCR and network failures. + * + * @param {string} url Request URL. + * @param {RequestInit} options Fetch options. + * @param {string} resourceLabel Human-readable resource name for logs. + * @return {Promise} The successful or final response. + */ +async function fetchWithRetry( url, options, resourceLabel ) { + let lastError; + + for ( let attempt = 1; attempt <= MAX_FETCH_RETRIES; attempt++ ) { + let response; + + try { + response = await fetch( url, options ); + } catch ( error ) { + lastError = error; + + if ( attempt === MAX_FETCH_RETRIES || ! shouldRetryFetch( undefined, error ) ) { + throw error; + } + + console.warn( + `Retrying ${ resourceLabel } after network failure (${ attempt }/${ MAX_FETCH_RETRIES })...` + ); + await sleep( RETRY_DELAY_MS * attempt ); + continue; + } + + if ( response.ok || ! shouldRetryFetch( response ) || attempt === MAX_FETCH_RETRIES ) { + return response; + } + + lastError = new Error( + `Failed to fetch ${ resourceLabel }: ${ response.status } ${ response.statusText }` + ); + console.warn( + `Retrying ${ resourceLabel } after ${ response.status } ${ response.statusText } (${ attempt }/${ MAX_FETCH_RETRIES })...` + ); + await sleep( RETRY_DELAY_MS * attempt ); + } + + throw lastError; +} /** * Read Gutenberg configuration from package.json. @@ -64,8 +140,10 @@ function readGutenbergConfig() { * @return {Promise} The bearer token. */ async function fetchGhcrToken( ghcrRepo ) { - const response = await fetch( - `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` + const response = await fetchWithRetry( + `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, + {}, + 'GHCR token' ); if ( ! response.ok ) { throw new Error( @@ -88,14 +166,15 @@ async function fetchGhcrToken( ghcrRepo ) { * @return {Promise>} Parsed manifest JSON. */ async function fetchManifest( ref, ghcrRepo, token ) { - const response = await fetch( + const response = await fetchWithRetry( `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ ref }`, { headers: { Authorization: `Bearer ${ token }`, Accept: MANIFEST_ACCEPT, }, - } + }, + `manifest for "${ ref }"` ); if ( ! response.ok ) { const error = /** @type {Error & { status?: number }} */ ( @@ -241,6 +320,7 @@ module.exports = { gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, + fetchWithRetry, fetchGhcrToken, fetchManifest, resolveExpectedSha, From 8e78403ed40fd2309e49dd2d979f3bdfdf2345a8 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 1 Jun 2026 19:18:10 +0530 Subject: [PATCH 4/5] Build/Test Tools: Fix Gutenberg retry helper typing --- tools/gutenberg/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 9e645b02952c5..763ed800ef53f 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -86,7 +86,7 @@ async function fetchWithRetry( url, options, resourceLabel ) { continue; } - if ( response.ok || ! shouldRetryFetch( response ) || attempt === MAX_FETCH_RETRIES ) { + if ( response.ok || ! shouldRetryFetch( response, undefined ) || attempt === MAX_FETCH_RETRIES ) { return response; } From 595ccb4430fa6b91f368dd4fa9235634824ce1f3 Mon Sep 17 00:00:00 2001 From: ArkaPrabhaChowdhury Date: Mon, 1 Jun 2026 20:19:22 +0530 Subject: [PATCH 5/5] Build/Test Tools: Retry transient Docker image pulls --- tools/local-env/scripts/start.js | 58 +++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/tools/local-env/scripts/start.js b/tools/local-env/scripts/start.js index 66559d4c10b85..caef49de2bf14 100644 --- a/tools/local-env/scripts/start.js +++ b/tools/local-env/scripts/start.js @@ -5,6 +5,12 @@ const dotenvExpand = require( 'dotenv-expand' ); const { execSync, spawnSync } = require( 'child_process' ); const local_env_utils = require( './utils' ); const { constants, copyFile } = require( 'node:fs' ); +const RETRYABLE_DOCKER_PULL_ERRORS = [ + 'context deadline exceeded', + 'Client.Timeout exceeded while awaiting headers', + 'request canceled while waiting for connection', +]; +const MAX_DOCKER_START_ATTEMPTS = 3; // Copy the default .env file when one is not present. copyFile( '.env.example', '.env', constants.COPYFILE_EXCL, () => { @@ -32,18 +38,46 @@ if ( process.env.LOCAL_PHP_MEMCACHED === 'true' ) { containers.push( 'memcached' ); } -spawnSync( - 'docker', - [ - 'compose', - ...composeFiles.map( ( composeFile ) => [ '-f', composeFile ] ).flat(), - 'up', - '--quiet-pull', - '-d', - ...containers, - ], - { stdio: 'inherit' } -); +let dockerUpResult; +for ( let attempt = 1; attempt <= MAX_DOCKER_START_ATTEMPTS; attempt++ ) { + dockerUpResult = spawnSync( + 'docker', + [ + 'compose', + ...composeFiles.map( ( composeFile ) => [ '-f', composeFile ] ).flat(), + 'up', + '--quiet-pull', + '-d', + ...containers, + ], + { encoding: 'utf8' } + ); + + if ( dockerUpResult.stdout ) { + process.stdout.write( dockerUpResult.stdout ); + } + + if ( dockerUpResult.stderr ) { + process.stderr.write( dockerUpResult.stderr ); + } + + if ( dockerUpResult.status === 0 ) { + break; + } + + const output = `${ dockerUpResult.stdout || '' }\n${ dockerUpResult.stderr || '' }`; + const isRetryable = RETRYABLE_DOCKER_PULL_ERRORS.some( ( errorText ) => + output.includes( errorText ) + ); + + if ( ! isRetryable || attempt === MAX_DOCKER_START_ATTEMPTS ) { + process.exit( dockerUpResult.status || 1 ); + } + + console.warn( + `Retrying Docker environment startup after transient registry failure (${ attempt }/${ MAX_DOCKER_START_ATTEMPTS })...` + ); +} // If Docker Toolbox is being used, we need to manually forward LOCAL_PORT to the Docker VM. if ( process.env.DOCKER_TOOLBOX_INSTALL_PATH ) {