From d6a8fe00d0effeaca92330a2d6063fed40d9ff87 Mon Sep 17 00:00:00 2001
From: Webizito <31759403+Webizito@users.noreply.github.com>
Date: Fri, 3 Apr 2026 23:48:33 +0530
Subject: [PATCH 01/17] Site Health: Add missing dashicons class to error span
fixes #65014
---
src/wp-admin/includes/class-wp-site-health.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php
index 75e046ef8ffa7..f10f0b2980b51 100644
--- a/src/wp-admin/includes/class-wp-site-health.php
+++ b/src/wp-admin/includes/class-wp-site-health.php
@@ -1357,7 +1357,7 @@ public function get_test_dotorg_communication() {
$result['description'] .= sprintf(
'
%s
',
sprintf(
- '%s %s',
+ '%s %s',
/* translators: Hidden accessibility text. */
__( 'Error' ),
sprintf(
From e107c1911c1afb7e150383169858a335ab33ba29 Mon Sep 17 00:00:00 2001
From: Webizito <31759403+Webizito@users.noreply.github.com>
Date: Sat, 4 Apr 2026 13:43:34 +0530
Subject: [PATCH 02/17] Update src/wp-admin/includes/class-wp-site-health.php
Site Health: Move dashicons class before error class for consistency
Co-authored-by: Stephen A. Bernhardt
---
src/wp-admin/includes/class-wp-site-health.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php
index f10f0b2980b51..17ebd8a1c0693 100644
--- a/src/wp-admin/includes/class-wp-site-health.php
+++ b/src/wp-admin/includes/class-wp-site-health.php
@@ -1357,7 +1357,7 @@ public function get_test_dotorg_communication() {
$result['description'] .= sprintf(
'%s
',
sprintf(
- '%s %s',
+ '%s %s',
/* translators: Hidden accessibility text. */
__( 'Error' ),
sprintf(
From 9eaa5549f2d355c6ee2da2b1360c47f3cea48e04 Mon Sep 17 00:00:00 2001
From: Jon Surrell
Date: Thu, 18 Jun 2026 16:45:40 +0000
Subject: [PATCH 03/17] Charset: Limit _wp_scan_utf8() ASCII scan to remaining
code points.
The ASCII fast-path in `_wp_scan_utf8()` uses `strspn()` to skip past ASCII bytes. When a code point limit was provided without a byte limit, the scan would include the rest of the input even when there was a code point limit. Because ASCII characters are single-byte code points, the fast-path scan length can be bounded by the number of remaining code points. This improves performance when working with some large documents.
Developed in https://github.com/WordPress/wordpress-develop/pull/12214.
Follow-up to [60768].
Props jonsurrell, dmsnell, zieladam.
Fixes #65483. See #63863.
git-svn-id: https://develop.svn.wordpress.org/trunk@62523 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-includes/compat-utf8.php | 2 +-
.../tests/compat/wpUtf8CodePointSpan.php | 103 ++++++++++++++++++
2 files changed, 104 insertions(+), 1 deletion(-)
create mode 100644 tests/phpunit/tests/compat/wpUtf8CodePointSpan.php
diff --git a/src/wp-includes/compat-utf8.php b/src/wp-includes/compat-utf8.php
index e1cab36ea3244..5fa8cde158789 100644
--- a/src/wp-includes/compat-utf8.php
+++ b/src/wp-includes/compat-utf8.php
@@ -65,7 +65,7 @@ function _wp_scan_utf8( string $bytes, int &$at, int &$invalid_length, ?int $max
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" .
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f",
$i,
- $end - $i
+ min( $end - $i, $max_count - $count )
);
if ( $count + $ascii_byte_count >= $max_count ) {
diff --git a/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php b/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php
new file mode 100644
index 0000000000000..4bb5e0c272223
--- /dev/null
+++ b/tests/phpunit/tests/compat/wpUtf8CodePointSpan.php
@@ -0,0 +1,103 @@
+assertSame(
+ $expected_span,
+ _wp_utf8_codepoint_span( $text, $byte_offset, $max_code_points, $found_code_points ),
+ 'Should have found the expected byte span.'
+ );
+
+ $this->assertSame(
+ $expected_found,
+ $found_code_points,
+ 'Should have reported the expected number of code points.'
+ );
+ }
+
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public static function data_codepoint_spans() {
+ $long_ascii_run = str_repeat( 'a', 1024 );
+
+ return array(
+ 'zero code point budget' => array(
+ 'abcdef',
+ 0,
+ 0,
+ 0,
+ 0,
+ ),
+ 'long ASCII run at start' => array(
+ $long_ascii_run,
+ 0,
+ 5,
+ 5,
+ 5,
+ ),
+ 'long ASCII run from non-zero offset' => array(
+ "zz{$long_ascii_run}",
+ 2,
+ 5,
+ 5,
+ 5,
+ ),
+ 'multibyte character before the boundary' => array(
+ "ab\u{1F170}cd",
+ 0,
+ 2,
+ 2,
+ 2,
+ ),
+ 'multibyte character at the boundary' => array(
+ "ab\u{1F170}cd",
+ 0,
+ 3,
+ strlen( "ab\u{1F170}" ),
+ 3,
+ ),
+ 'invalid span after the boundary' => array(
+ "ab\xF0\x9Fzz",
+ 0,
+ 2,
+ 2,
+ 2,
+ ),
+ 'invalid span at the boundary' => array(
+ "ab\xF0\x9Fzz",
+ 0,
+ 3,
+ 4,
+ 3,
+ ),
+ );
+ }
+}
From ddb792b902719995bd804693ca25da43a2826d9c Mon Sep 17 00:00:00 2001
From: Dennis Snell
Date: Thu, 18 Jun 2026 17:49:04 +0000
Subject: [PATCH 04/17] Performance: avoid over-allocation in
wp_is_numeric_array()
When a trace of allocations revealed that `wp_is_numeric_array()`
accounted for a significant fraction of the allocations in a page
render, it was observed that the function eagerly allocates and copies
array keys and then filters them when all it wants to know is whether a
single key in the array meets a condition.
In this patch the `array_filter( array_keys() )` invocation is replaced
with early-aborting iteration to avoid the memory allocation and
copying.
This patch was prepared as part of WCEU 2026 Contributor Day.
Developed in: https://github.com/WordPress/wordpress-develop/pull/12100
Discussed in: https://core.trac.wordpress.org/ticket/65467
Follow-up to [34927].
Props dmsnell, westonruter, yusufmudagal.
Fixes #65467.
git-svn-id: https://develop.svn.wordpress.org/trunk@62524 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-includes/functions.php | 29 ++++++++++++++++---
.../tests/functions/wpIsNumericArray.php | 23 ++++++++++-----
2 files changed, 40 insertions(+), 12 deletions(-)
diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php
index d7d2ff3fed89a..355d9f8a1ec37 100644
--- a/src/wp-includes/functions.php
+++ b/src/wp-includes/functions.php
@@ -5290,20 +5290,41 @@ function _wp_to_kebab_case( $input_string ) {
/**
* Determines if the variable is a numeric-indexed array.
*
+ * Note! This answers a different question than {@see array_is_list()} and is
+ * more flexible to handle situations where some numeric array indices
+ * have been removed. A numeric-indexed array is only a “list” when the
+ * array keys form a contiguous range from zero to the highest key.
+ *
+ * Example:
+ *
+ * true === wp_is_numeric_array( array( 1, 2, 3, 4 ) );
+ * false === wp_is_numeric_array( array( 'name' => 'WordPress' ) );
+ *
+ * // All-numeric keys vs. list.
+ * $above_two = array_filter( array( 1, 2, 8, 9 ), fn ( $v ) => $v > 2 );
+ * $above_two === array( '2' => 8, '3' => 9 );
+ * true === wp_is_numeric_array( $above_two );
+ * false === array_is_list( $above_two );
+ *
* @since 4.4.0
*
* @param mixed $data Variable to check.
* @return bool Whether the variable is a list.
+ *
+ * @phpstan-assert-if-true array $data
*/
-function wp_is_numeric_array( $data ) {
+function wp_is_numeric_array( $data ): bool {
if ( ! is_array( $data ) ) {
return false;
}
- $keys = array_keys( $data );
- $string_keys = array_filter( $keys, 'is_string' );
+ foreach ( $data as $key => $value ) {
+ if ( is_string( $key ) ) {
+ return false;
+ }
+ }
- return count( $string_keys ) === 0;
+ return true;
}
/**
diff --git a/tests/phpunit/tests/functions/wpIsNumericArray.php b/tests/phpunit/tests/functions/wpIsNumericArray.php
index 4eeab0af81f2a..4bf7b0cc1695b 100644
--- a/tests/phpunit/tests/functions/wpIsNumericArray.php
+++ b/tests/phpunit/tests/functions/wpIsNumericArray.php
@@ -26,27 +26,34 @@ public function test_wp_is_numeric_array( $input, $expected ) {
*/
public function data_wp_is_numeric_array() {
return array(
- 'no index' => array(
+ 'no index' => array(
'test_array' => array( 'www', 'eee' ),
'expected' => true,
),
- 'text index' => array(
+ 'text index' => array(
'test_array' => array( 'www' => 'eee' ),
'expected' => false,
),
- 'numeric index' => array(
+ 'numeric index' => array(
'test_array' => array( 99 => 'eee' ),
'expected' => true,
),
- '- numeric index' => array(
+ 'filtered list (missing numeric keys)' => array(
+ 'test_array' => array_filter(
+ array( 1, 12, 13, 15, 16, 17, 20 ),
+ fn ( $v ) => 0 === $v % 2
+ ),
+ 'expected' => true,
+ ),
+ '- numeric index' => array(
'test_array' => array( -11 => 'eee' ),
'expected' => true,
),
- 'numeric string index' => array(
+ 'numeric string index' => array(
'test_array' => array( '11' => 'eee' ),
'expected' => true,
),
- 'nested number index' => array(
+ 'nested number index' => array(
'test_array' => array(
'next' => array(
11 => 'vvv',
@@ -54,7 +61,7 @@ public function data_wp_is_numeric_array() {
),
'expected' => false,
),
- 'nested string index' => array(
+ 'nested string index' => array(
'test_array' => array(
'11' => array(
'eee' => 'vvv',
@@ -62,7 +69,7 @@ public function data_wp_is_numeric_array() {
),
'expected' => true,
),
- 'not an array' => array(
+ 'not an array' => array(
'test_array' => null,
'expected' => false,
),
From 30a91a941d3823ae3ecdade3a64a0c7804cf2cb5 Mon Sep 17 00:00:00 2001
From: Jonathan Desrosiers
Date: Thu, 18 Jun 2026 21:08:09 +0000
Subject: [PATCH 05/17] Build/Test Tools: Ensure all built files are deleted as
expected.
Block editor-related files can currently become stale or are not always deleted from `src` through the relevant `grunt clean` commands reliably. In the past, this primarily caused issues locally when a CSS file was copied from the `@wordpress/block-library` npm package into `src` and later removed from the package entirely. The result was a failing `grunt verify:old-files` task until the `grunt clean` command was run with the `--dev` flag.
After [61438] this issue presented in new ways. Mainly, files would remain in the core.svn.wordpress.org build repository indefinitely unless explicitly deleted. [62051] brought the `grunt clean` tasks up to date, but there are still paths where files remain unexpectedly or have outdated contents after rebuilding. This can cause incomplete or inaccurate commits where built files subject to version control are not updated correctly, especially when changing the `gutenberg.sha` value in `package.json`.
This change improves the build script to ensure that all files sourced from the zip file with assets built by the Gutenberg repository are always fresh and up to date, and any files that are deleted from the built zip file are also deleted from version control appropriately (in both the `develop` and `core` repositories).
A handful of changes were required to accomplish this:
- All Gutenberg-sourced outputs are written to `src/` regardless of `--dev`. In production builds, `build:gutenberg` runs before `build:files`, and `copy:files` propagates the tree to `build/`.
- `gutenbergFiles` has been split into two different arrays: `gutenbergUnversionedFiles` and `gutenbergVersionedFiles`. The `src` argument for the `clean:gutenberg` task is dynamically populated at run time with a bare `grunt clean` cleaning only the unversioned subset (so version-controlled files are not unexpectedly deleted), and explicit `clean:gutenberg` (or any chain through `build:gutenberg`) cleans both, removing files deleted upstream from version control.
- `clean:gutenberg` no longer wipes non-Gutenberg sourced files from `wp-includes/js/`. All file/path lists have been updated to only match files the related tasks are directly responsible for managing.
- `tools/gutenberg/copy.js` has been added to `tsconfig.json` and brought under `tsc --build` strict-mode checking. The large `copyBlockAssets()` function was broken into one named function per asset type, each typed against the relevant `COPY_CONFIG` slice. The split is a code-clarity improvement, not a bug fix.
Props desrosj, westonruter, jorbin, adamsilverstein.
Fixes #65452.
git-svn-id: https://develop.svn.wordpress.org/trunk@62525 602fd350-edb4-49c9-b593-d223f7449a82
---
Gruntfile.js | 128 +++++++----
package.json | 2 +-
tools/gutenberg/copy.js | 458 +++++++++++++++++++++++----------------
tools/gutenberg/utils.js | 4 +-
tsconfig.json | 1 +
5 files changed, 371 insertions(+), 222 deletions(-)
diff --git a/Gruntfile.js b/Gruntfile.js
index 2ce79d03bddc6..dae8c3e972e4c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -41,18 +41,43 @@ module.exports = function(grunt) {
'wp-admin/css/colors/**/*.css',
],
- // Built js files, in /src or /build.
+ // Built JavaScript files that do not belong to a more specific group.
jsFiles = [
'wp-admin/js/',
- 'wp-includes/js/',
+ 'wp-includes/js/*',
+ /*
+ * This directory has shared responsibility and is managed through
+ * gutenbergUnversionedFiles, webpackFiles, and copy:vendor-js.
+ */
+ '!wp-includes/js/dist',
+ 'wp-includes/js/dist/vendor/*.js',
+ // Managed by the Gutenberg-related tasks.
+ '!wp-includes/js/dist/vendor/react-jsx-runtime*',
+ ],
+
+ // Files sourced from the Gutenberg repository built asset that are ignored by version control.
+ gutenbergUnversionedFiles = [
+ SOURCE_DIR + 'wp-includes/blocks/*/*.css',
+ SOURCE_DIR + 'wp-includes/css/dist',
+ SOURCE_DIR + 'wp-includes/js/dist/*.js',
+ SOURCE_DIR + 'wp-includes/js/dist/script-modules',
+ SOURCE_DIR + 'wp-includes/js/dist/vendor/react-jsx-runtime*',
],
- // All files copied from the Gutenberg repository excluded from version control.
- gutenbergFiles = [
- 'wp-includes/js/dist',
- 'wp-includes/css/dist',
- // Old location kept temporarily to ensure they are cleaned up.
- 'wp-includes/icons',
+ // Files sourced from the Gutenberg repository built asset that are managed through version control.
+ gutenbergVersionedFiles = [
+ // Block assets (block.json, top-level PHP, nested PHP helpers).
+ SOURCE_DIR + 'wp-includes/blocks/*',
+ '!' + SOURCE_DIR + 'wp-includes/blocks/index.php',
+ SOURCE_DIR + 'wp-includes/images/icon-library',
+ SOURCE_DIR + 'wp-includes/theme.json',
+ SOURCE_DIR + 'wp-includes/theme-i18n.json',
+ // Routes and pages.
+ SOURCE_DIR + 'wp-includes/build',
+ // PHP manifests generated by gutenberg:copy.
+ SOURCE_DIR + 'wp-includes/assets/icon-library-manifest.php',
+ SOURCE_DIR + 'wp-includes/assets/script-loader-packages.php',
+ SOURCE_DIR + 'wp-includes/assets/script-modules-packages.php',
],
// All files built by Webpack, in /src or /build.
@@ -241,10 +266,32 @@ module.exports = function(grunt) {
return setFilePath( WORKING_DIR, file );
} ),
- // Clean files built by the tools/gutenberg scripts.
- gutenberg: gutenbergFiles.map( function( file ) {
- return setFilePath( WORKING_DIR, file );
- }),
+ /*
+ * Clean files sourced from the downloaded zip file built by the Gutenberg repository.
+ *
+ * All files originating from the Gutenberg repository's built assets (both tracked and untracked by version
+ * control) are deleted when `clean:gutenberg` is explicitly called. This ensures that versioned files that
+ * have been deleted upstream are also removed from version control in this repository.
+ *
+ * When `clean:gutenberg` is not explicitly called and run through `grunt clean`, only ignored files are
+ * cleaned.
+ */
+ gutenberg: {
+ get src() {
+ const cli = grunt.cli.tasks;
+ // Preserve versioned files only when running bare `grunt clean`.
+ const isBareCleanSweep =
+ cli.includes( 'clean' ) &&
+ ! cli.includes( 'clean:gutenberg' );
+
+ if ( isBareCleanSweep ) {
+ return gutenbergUnversionedFiles;
+ } else {
+ return gutenbergUnversionedFiles.concat( gutenbergVersionedFiles );
+ }
+ },
+ },
+
dynamic: {
dot: true,
expand: true,
@@ -289,7 +336,6 @@ module.exports = function(grunt) {
expand: true,
cwd: SOURCE_DIR,
src: buildFiles.concat( [
- '!wp-includes/assets/**', // Assets is extracted into separate copy tasks.
'!js/**', // JavaScript is extracted into separate copy tasks.
'!.{svn,git}', // Exclude version control folders.
'!wp-includes/version.php', // Exclude version.php.
@@ -666,24 +712,18 @@ module.exports = function(grunt) {
'constants.php',
'pages/**/*.php',
],
- dest: WORKING_DIR + 'wp-includes/build/',
+ dest: SOURCE_DIR + 'wp-includes/build/',
} ],
},
/*
- * Only copy files relevant to the routes specified in the registry file.
- *
- * While the registry file does not contain any experimental routes, the `gutenberg/build/routes` directory
- * includes the files for all registered routes. Only the files related to the routes specified in the
- * registry should be included in the WordPress build.
- *
- * The `src` list is populated at task runtime by `routes:setup`, which reads the registry after
- * `gutenberg:download` has run. See the `routes:setup` task registration for implementation details.
+ * The list of route source files is populated from the contents of the registry.php file at task runtime by
+ * `routes:setup`.
*/
routes: {
expand: true,
cwd: 'gutenberg/build',
src: [],
- dest: WORKING_DIR + 'wp-includes/build/',
+ dest: SOURCE_DIR + 'wp-includes/build/',
},
'gutenberg-js': {
files: [ {
@@ -692,7 +732,7 @@ module.exports = function(grunt) {
src: [
'pages/**/*.js',
],
- dest: WORKING_DIR + 'wp-includes/build/',
+ dest: SOURCE_DIR + 'wp-includes/build/',
} ],
},
'gutenberg-modules': {
@@ -706,7 +746,7 @@ module.exports = function(grunt) {
// with no debugging value over the minified versions.
'!vips/!(*.min).js',
],
- dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/',
+ dest: SOURCE_DIR + 'wp-includes/js/dist/script-modules/',
} ],
},
'gutenberg-styles': {
@@ -719,7 +759,7 @@ module.exports = function(grunt) {
// Per-block CSS is copied to wp-includes/blocks/ by tools/gutenberg/copy.js.
'!block-library/*/**',
],
- dest: WORKING_DIR + 'wp-includes/css/dist/',
+ dest: SOURCE_DIR + 'wp-includes/css/dist/',
} ],
},
'gutenberg-theme-json': {
@@ -738,11 +778,11 @@ module.exports = function(grunt) {
files: [
{
src: 'gutenberg/lib/theme.json',
- dest: WORKING_DIR + 'wp-includes/theme.json',
+ dest: SOURCE_DIR + 'wp-includes/theme.json',
},
{
src: 'gutenberg/lib/theme-i18n.json',
- dest: WORKING_DIR + 'wp-includes/theme-i18n.json',
+ dest: SOURCE_DIR + 'wp-includes/theme-i18n.json',
},
],
},
@@ -750,8 +790,8 @@ module.exports = function(grunt) {
files: [ {
expand: true,
cwd: 'gutenberg/packages/icons/src/library',
- src: '*.svg',
- dest: WORKING_DIR + 'wp-includes/images/icon-library',
+ src: [ '*.svg' ],
+ dest: SOURCE_DIR + 'wp-includes/images/icon-library',
} ],
},
'icon-library-manifest': {
@@ -773,7 +813,7 @@ module.exports = function(grunt) {
},
files: [ {
src: 'gutenberg/packages/icons/src/manifest.php',
- dest: WORKING_DIR + 'wp-includes/assets/icon-library-manifest.php',
+ dest: SOURCE_DIR + 'wp-includes/assets/icon-library-manifest.php',
} ],
},
},
@@ -1667,7 +1707,7 @@ module.exports = function(grunt) {
*/
grunt.util.spawn( {
grunt: true,
- args: [ 'build:gutenberg', '--dev' ],
+ args: [ 'build:gutenberg' ],
opts: { stdio: 'inherit' }
}, function( buildError ) {
done( ! buildError );
@@ -1677,10 +1717,9 @@ module.exports = function(grunt) {
grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg JS packages and block assets to WordPress Core.', function() {
const done = this.async();
- const buildDir = grunt.option( 'dev' ) ? 'src' : 'build';
grunt.util.spawn( {
cmd: 'node',
- args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ],
+ args: [ 'tools/gutenberg/copy.js' ],
opts: { stdio: 'inherit' }
}, function( error ) {
done( ! error );
@@ -2164,10 +2203,23 @@ module.exports = function(grunt) {
} );
} );
- grunt.registerTask( 'build:gutenberg', [
- 'copy:gutenberg-php',
+ // Detects and copies stable routes.
+ grunt.registerTask( 'build:routes', [
'routes:setup',
'copy:routes',
+ ] );
+
+ /*
+ * Refresh the Gutenberg-sourced content in src/.
+ *
+ * clean:gutenberg must run first to ensure files removed upstream are purged.
+ *
+ * Because all of these tasks write to src/, the outcome is identical for build and build:dev.
+ */
+ grunt.registerTask( 'build:gutenberg', [
+ 'clean:gutenberg',
+ 'copy:gutenberg-php',
+ 'build:routes',
'copy:gutenberg-js',
'gutenberg:copy',
'copy:gutenberg-modules',
@@ -2181,21 +2233,21 @@ module.exports = function(grunt) {
if ( grunt.option( 'dev' ) ) {
grunt.task.run( [
'gutenberg:verify',
+ 'build:gutenberg',
'build:js',
'build:css',
'build:codemirror',
- 'build:gutenberg',
'build:certificates'
] );
} else {
grunt.task.run( [
'gutenberg:verify',
+ 'build:gutenberg',
'build:certificates',
'build:files',
'build:js',
'build:css',
'build:codemirror',
- 'build:gutenberg',
'replace:source-maps',
'verify:build'
] );
diff --git a/package.json b/package.json
index 430efdd2fba85..429e0469dd491 100644
--- a/package.json
+++ b/package.json
@@ -141,6 +141,6 @@
"typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan",
"gutenberg:copy": "node tools/gutenberg/copy.js",
"gutenberg:verify": "node tools/gutenberg/utils.js",
- "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg --dev"
+ "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg"
}
}
diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js
index 8589c9581bed1..3da78e4b14611 100644
--- a/tools/gutenberg/copy.js
+++ b/tools/gutenberg/copy.js
@@ -6,32 +6,59 @@
* This script copies and transforms Gutenberg's build output to WordPress Core.
* It handles path transformations from plugin structure to Core structure.
*
+ * Since a number of files sourced from the downloaded zip file are subject to
+ * version control, the `src/` directory is used as the destination for all
+ * outputs of this file (both versioned and unversioned).
+ *
+ * Grunt will copy the files appropriately when running `build` instead of
+ * `build:dev`, and the repository's configured ignore rules will manage what
+ * can be committed.
+ *
* @package WordPress
*/
const fs = require( 'fs' );
const path = require( 'path' );
-const json2php = require( 'json2php' );
+const json2php = /** @type {typeof import('json2php').default} */ (
+ /** @type {unknown} */ ( require( 'json2php' ) )
+);
const { fromString } = require( 'php-array-reader' );
-// Paths.
const rootDir = path.resolve( __dirname, '../..' );
const gutenbergDir = path.join( rootDir, 'gutenberg' );
const gutenbergBuildDir = path.join( gutenbergDir, 'build' );
+const wpIncludesDir = path.join( rootDir, 'src', 'wp-includes' );
+
+/**
+ * JS package copy configuration.
+ *
+ * @typedef ScriptsConfig
+ * @type {object}
+ * @property {string} source - Gutenberg-relative source directory (e.g. `'scripts'`).
+ * @property {string} destination - Subpath under `wp-includes/` where packages land (e.g. `'js/dist'`).
+ * @property {boolean} copyDirectories - Whether to copy whole directories (with optional renames) as-is.
+ * @property {Record} directoryRenames - Map of source directory name → destination directory name.
+ */
-/*
- * Determine build target from command line argument (--dev or --build-dir).
- * Default to 'src' for development.
+/**
+ * One block family entry — block library, widget blocks, etc.
+ *
+ * @typedef BlockConfigSource
+ * @type {object}
+ * @property {string} name - Human-readable label (e.g. `'block-library'`, `'widgets'`).
+ * @property {string} scripts - Gutenberg-relative path to the block scripts directory.
+ * @property {string} styles - Gutenberg-relative path to the block styles directory.
+ * @property {string} php - Gutenberg-relative path to the block PHP directory.
*/
-const args = process.argv.slice( 2 );
-const buildDirArg = args.find( ( arg ) => arg.startsWith( '--build-dir=' ) );
-const buildTarget = buildDirArg
- ? buildDirArg.split( '=' )[ 1 ]
- : args.includes( '--dev' )
- ? 'src'
- : 'build';
-const wpIncludesDir = path.join( rootDir, buildTarget, 'wp-includes' );
+/**
+ * Block copy configuration.
+ *
+ * @typedef BlockConfig
+ * @type {object}
+ * @property {string} destination - Subpath under `wp-includes/` where blocks land (e.g. `'blocks'`).
+ * @property {BlockConfigSource[]} sources - One entry per block family.
+ */
/**
* Copy configuration.
@@ -81,7 +108,7 @@ const COPY_CONFIG = {
* @throws Error when PHP source file unable to be read or parsed.
*
* @param {string} phpFilepath Absolute path of PHP file returning a single value.
- * @return {Object|Array} JavaScript representation of value from input file.
+ * @return {any} JavaScript representation of value from input file.
*/
function readReturnedValueFromPHPFile( phpFilepath ) {
const content = fs.readFileSync( phpFilepath, 'utf8' );
@@ -109,104 +136,244 @@ function isExperimentalBlock( blockJsonPath ) {
}
/**
- * Copy all assets for blocks from Gutenberg to Core.
- * Handles scripts, styles, PHP, and JSON for all block types in a unified way.
+ * Generate a list of stable blocks.
*
- * @param {Object} config - Block configuration from COPY_CONFIG.blocks
+ * Blocks marked as `"__experimental": true` in a `block.json` file are excluded.
+ *
+ * @param {string} scriptsSrc - Path to the Gutenberg scripts source (e.g. `scripts/block-library`).
+ * @return {string[]} Stable block directory names.
*/
-function copyBlockAssets( config ) {
- const blocksDest = path.join( wpIncludesDir, config.destination );
+function getStableBlocks( scriptsSrc ) {
+ if ( ! fs.existsSync( scriptsSrc ) ) {
+ return [];
+ }
+ return fs
+ .readdirSync( scriptsSrc, { withFileTypes: true } )
+ .filter( ( entry ) => entry.isDirectory() )
+ .map( ( entry ) => entry.name )
+ .filter( ( blockName ) => ! isExperimentalBlock(
+ path.join( scriptsSrc, blockName, 'block.json' )
+ ) );
+}
- for ( const source of config.sources ) {
- const scriptsSrc = path.join( gutenbergBuildDir, source.scripts );
- const stylesSrc = path.join( gutenbergBuildDir, source.styles );
- const phpSrc = path.join( gutenbergBuildDir, source.php );
+/**
+ * Copy JavaScript files.
+ *
+ * @param {ScriptsConfig} config - Scripts configuration from `COPY_CONFIG.scripts`.
+ */
+function copyScripts( config ) {
+ const scriptsSrc = path.join( gutenbergBuildDir, config.source );
+ const scriptsDest = path.join( wpIncludesDir, config.destination );
- if ( ! fs.existsSync( scriptsSrc ) ) {
- continue;
- }
+ if ( ! fs.existsSync( scriptsSrc ) ) {
+ return;
+ }
- // Get all block directories from the scripts source.
- const blockDirs = fs
- .readdirSync( scriptsSrc, { withFileTypes: true } )
- .filter( ( entry ) => entry.isDirectory() )
- .map( ( entry ) => entry.name );
-
- for ( const blockName of blockDirs ) {
- // Skip experimental blocks.
- const blockJsonPath = path.join(
- scriptsSrc,
- blockName,
- 'block.json'
- );
- if ( isExperimentalBlock( blockJsonPath ) ) {
- continue;
+ const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } );
+
+ for ( const entry of entries ) {
+ const src = path.join( scriptsSrc, entry.name );
+
+ if ( entry.isDirectory() ) {
+ // Check if this should be copied as a directory (like vendors/).
+ if (
+ config.copyDirectories &&
+ config.directoryRenames &&
+ config.directoryRenames[ entry.name ]
+ ) {
+ /*
+ * Copy special directories with rename (vendors/ → vendor/).
+ * Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules).
+ */
+ const destName = config.directoryRenames[ entry.name ];
+ const dest = path.join( scriptsDest, destName );
+
+ if ( entry.name === 'vendors' ) {
+ // Only copy react-jsx-runtime files, skip react and react-dom.
+ const vendorFiles = fs.readdirSync( src );
+ let copiedCount = 0;
+ fs.mkdirSync( dest, { recursive: true } );
+ for ( const file of vendorFiles ) {
+ if (
+ file.startsWith( 'react-jsx-runtime' ) &&
+ file.endsWith( '.js' )
+ ) {
+ const srcFile = path.join( src, file );
+ const destFile = path.join( dest, file );
+
+ fs.copyFileSync( srcFile, destFile );
+ copiedCount++;
+ }
+ }
+ console.log(
+ ` ✅ ${ entry.name }/ → ${ destName }/ (react-jsx-runtime only, ${ copiedCount } files)`
+ );
+ }
+ } else {
+ /*
+ * Flatten package structure: package-name/index.js → package-name.js.
+ * This matches Core's expected file structure.
+ */
+ const packageFiles = fs.readdirSync( src );
+
+ for ( const file of packageFiles ) {
+ if ( /^index\.(js|min\.js)$/.test( file ) ) {
+ const srcFile = path.join( src, file );
+ // Replace 'index.' with 'package-name.'.
+ const destFile = file.replace(
+ /^index\./,
+ `${ entry.name }.`
+ );
+ const destPath = path.join( scriptsDest, destFile );
+
+ fs.mkdirSync( path.dirname( destPath ), {
+ recursive: true,
+ } );
+
+ fs.copyFileSync( srcFile, destPath );
+ }
+ }
}
+ } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) {
+ // Copy root-level JS files.
+ const dest = path.join( scriptsDest, entry.name );
+ fs.mkdirSync( path.dirname( dest ), { recursive: true } );
+ fs.copyFileSync( src, dest );
+ }
+ }
+
+ console.log( ' ✅ JavaScript packages copied' );
+}
+/**
+ * Copy `block.json` files for every stable block.
+ *
+ * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`.
+ */
+function copyBlockJson( config ) {
+ const blocksDest = path.join( wpIncludesDir, config.destination );
+
+ for ( const source of config.sources ) {
+ const scriptsSrc = path.join( gutenbergBuildDir, source.scripts );
+ const blocks = getStableBlocks( scriptsSrc );
+
+ for ( const blockName of blocks ) {
+ const blockSrc = path.join( scriptsSrc, blockName );
const blockDest = path.join( blocksDest, blockName );
fs.mkdirSync( blockDest, { recursive: true } );
- // 1. Copy scripts/JSON (everything except PHP)
- const blockScriptsSrc = path.join( scriptsSrc, blockName );
- if ( fs.existsSync( blockScriptsSrc ) ) {
- fs.cpSync(
- blockScriptsSrc,
- blockDest,
- {
- recursive: true,
- // Skip PHP, copied from build in steps 3 & 4.
- filter: f => ! f.endsWith( '.php' ),
- }
+ const blockJsonSrc = path.join( blockSrc, 'block.json' );
+ if ( fs.existsSync( blockJsonSrc ) ) {
+ fs.copyFileSync(
+ blockJsonSrc,
+ path.join( blockDest, 'block.json' )
);
}
+ }
- // 2. Copy styles (if they exist in per-block directory)
- const blockStylesSrc = path.join( stylesSrc, blockName );
- if ( fs.existsSync( blockStylesSrc ) ) {
- const cssFiles = fs
- .readdirSync( blockStylesSrc )
- .filter( ( file ) => file.endsWith( '.css' ) );
- for ( const cssFile of cssFiles ) {
- fs.copyFileSync(
- path.join( blockStylesSrc, cssFile ),
- path.join( blockDest, cssFile )
- );
- }
- }
+ console.log(
+ ` ✅ ${ source.name } block.json copied (${ blocks.length } blocks)`
+ );
+ }
+}
- // 3. Copy PHP from build
- const blockPhpSrc = path.join( phpSrc, `${ blockName }.php` );
- const phpDest = path.join(
- wpIncludesDir,
- config.destination,
- `${ blockName }.php`
- );
- if ( fs.existsSync( blockPhpSrc ) ) {
- fs.copyFileSync( blockPhpSrc, phpDest );
+/**
+ * Copy block PHP files for every stable block.
+ *
+ * Handles both the top-level `.php` dynamic block files and any nested
+ * `*.php` helpers under `/` (e.g. `navigation-link/shared/render-submenu-icon.php`).
+ *
+ * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`.
+ */
+function copyBlockPhp( config ) {
+ const blocksDest = path.join( wpIncludesDir, config.destination );
+
+ for ( const source of config.sources ) {
+ const scriptsSrc = path.join( gutenbergBuildDir, source.scripts );
+ const phpSrc = path.join( gutenbergBuildDir, source.php );
+ const blocks = getStableBlocks( scriptsSrc );
+
+ for ( const blockName of blocks ) {
+ // Top-level .php (dynamic block file).
+ const topLevelPhpSrc = path.join( phpSrc, `${ blockName }.php` );
+ const topLevelPhpDest = path.join( blocksDest, `${ blockName }.php` );
+ if ( fs.existsSync( topLevelPhpSrc ) ) {
+ fs.mkdirSync( blocksDest, { recursive: true } );
+ fs.copyFileSync( topLevelPhpSrc, topLevelPhpDest );
}
- // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php)
+ // Nested PHP helpers under /, excluding the block's own index.php.
const blockPhpDir = path.join( phpSrc, blockName );
if ( fs.existsSync( blockPhpDir ) ) {
+ const blockDest = path.join( blocksDest, blockName );
const rootIndex = path.join( blockPhpDir, 'index.php' );
+
+ /**
+ * @param {string} src
+ * @return {boolean}
+ */
+ function hasPhpFiles( src ) {
+ const stat = fs.statSync( src );
+ if ( stat.isDirectory() ) {
+ return fs.readdirSync( src, { withFileTypes: true } ).some(
+ ( entry ) => hasPhpFiles( path.join( src, entry.name ) )
+ );
+ }
+ return src.endsWith( '.php' ) && src !== rootIndex;
+ }
+
fs.cpSync( blockPhpDir, blockDest, {
recursive: true,
- filter: function hasPhpFiles( src ) {
- const stat = fs.statSync( src );
- if ( stat.isDirectory() ) {
- return fs.readdirSync( src, { withFileTypes: true } ).some(
- ( entry ) => hasPhpFiles( path.join( src, entry.name ) )
- );
- }
- // Copy PHP files, but skip root index.php (handled by step 3).
- return src.endsWith( '.php' ) && src !== rootIndex;
- },
+ filter: hasPhpFiles,
} );
}
}
console.log(
- ` ✅ ${ source.name } blocks copied (${ blockDirs.length } blocks)`
+ ` ✅ ${ source.name } block PHP copied (${ blocks.length } blocks)`
+ );
+ }
+}
+
+/**
+ * Copy per-block CSS files for every stable block.
+ *
+ * @param {BlockConfig} config - Block configuration from `COPY_CONFIG.blocks`.
+ */
+function copyBlockStyles( config ) {
+ const blocksDest = path.join( wpIncludesDir, config.destination );
+
+ for ( const source of config.sources ) {
+ const scriptsSrc = path.join( gutenbergBuildDir, source.scripts );
+ const stylesSrc = path.join( gutenbergBuildDir, source.styles );
+ const blocks = getStableBlocks( scriptsSrc );
+
+ let stylesCopied = 0;
+ for ( const blockName of blocks ) {
+ const blockStylesSrc = path.join( stylesSrc, blockName );
+ if ( ! fs.existsSync( blockStylesSrc ) ) {
+ continue;
+ }
+
+ const blockDest = path.join( blocksDest, blockName );
+ fs.mkdirSync( blockDest, { recursive: true } );
+
+ const cssFiles = fs
+ .readdirSync( blockStylesSrc )
+ .filter( ( file ) => file.endsWith( '.css' ) );
+ for ( const cssFile of cssFiles ) {
+ fs.copyFileSync(
+ path.join( blockStylesSrc, cssFile ),
+ path.join( blockDest, cssFile )
+ );
+ }
+ if ( cssFiles.length > 0 ) {
+ stylesCopied++;
+ }
+ }
+
+ console.log(
+ ` ✅ ${ source.name } block CSS copied (${ stylesCopied } blocks)`
);
}
}
@@ -218,6 +385,7 @@ function copyBlockAssets( config ) {
*/
function generateScriptModulesPackages() {
const modulesDir = path.join( gutenbergBuildDir, 'modules' );
+ /** @type {Record} */
const assets = {};
/**
@@ -254,7 +422,7 @@ function generateScriptModulesPackages() {
} catch ( error ) {
console.error(
` ⚠️ Error reading ${ relativePath }:`,
- error.message
+ error instanceof Error ? error.message : String( error )
);
}
}
@@ -291,6 +459,7 @@ function generateScriptModulesPackages() {
*/
function generateScriptLoaderPackages() {
const scriptsDir = path.join( gutenbergBuildDir, 'scripts' );
+ /** @type {Record} */
const assets = {};
if ( ! fs.existsSync( scriptsDir ) ) {
@@ -326,7 +495,7 @@ function generateScriptLoaderPackages() {
} catch ( error ) {
console.error(
` ⚠️ Error reading ${ entry.name }/index.min.asset.php:`,
- error.message
+ error instanceof Error ? error.message : String( error )
);
}
}
@@ -354,9 +523,10 @@ function generateScriptLoaderPackages() {
}
/**
- * Generate require-dynamic-blocks.php and require-static-blocks.php.
- * Reads all block.json files from wp-includes/blocks and categorizes them.
- * Only includes blocks from block-library, not widgets.
+ * Generate `require-*-blocks.php` files.
+ *
+ * Reads all `block.json` files from the block-library (widgets are ignored) and
+ * creates `require-dynamic-blocks.php` and `require-static-blocks.php` files.
*/
function generateBlockRegistrationFiles() {
const blocksDir = path.join( wpIncludesDir, 'blocks' );
@@ -447,12 +617,15 @@ ${ staticBlocks.map( ( name ) => `\t'${ name }',` ).join( '\n' ) }
}
/**
- * Generate blocks-json.php from all block.json files.
- * Reads all block.json files and combines them into a single PHP array.
- * Uses json2php to maintain consistency with Core's formatting.
+ * Generate a `blocks-json.php` file.
+ *
+ * Reads all `block.json` files and combines them into a single PHP array.
+ *
+ * This must run after `copyBlockJson` has populated `wp-includes/blocks/`.
*/
function generateBlocksJson() {
const blocksDir = path.join( wpIncludesDir, 'blocks' );
+ /** @type {Record} */
const blocks = {};
if ( ! fs.existsSync( blocksDir ) ) {
@@ -478,7 +651,7 @@ function generateBlocksJson() {
} catch ( error ) {
console.error(
` ⚠️ Error reading ${ entry.name }/block.json:`,
- error.message
+ error instanceof Error ? error.message : String( error )
);
}
}
@@ -508,7 +681,7 @@ function generateBlocksJson() {
* Main execution function.
*/
async function main() {
- console.log( `📦 Copying Gutenberg build to ${ buildTarget }/...` );
+ console.log( '📦 Copying Gutenberg build to src/...' );
if ( ! fs.existsSync( gutenbergBuildDir ) ) {
console.error( '❌ Gutenberg build directory not found' );
@@ -518,95 +691,18 @@ async function main() {
// 1. Copy JavaScript packages.
console.log( '\n📦 Copying JavaScript packages...' );
- const scriptsConfig = COPY_CONFIG.scripts;
- const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source );
- const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination );
+ copyScripts( COPY_CONFIG.scripts );
- if ( fs.existsSync( scriptsSrc ) ) {
- const entries = fs.readdirSync( scriptsSrc, { withFileTypes: true } );
+ console.log( '\n📦 Copying block.json files...' );
+ copyBlockJson( COPY_CONFIG.blocks );
- for ( const entry of entries ) {
- const src = path.join( scriptsSrc, entry.name );
-
- if ( entry.isDirectory() ) {
- // Check if this should be copied as a directory (like vendors/).
- if (
- scriptsConfig.copyDirectories &&
- scriptsConfig.directoryRenames &&
- scriptsConfig.directoryRenames[ entry.name ]
- ) {
- /*
- * Copy special directories with rename (vendors/ → vendor/).
- * Only copy react-jsx-runtime from vendors (react and react-dom come from Core's node_modules).
- */
- const destName =
- scriptsConfig.directoryRenames[ entry.name ];
- const dest = path.join( scriptsDest, destName );
-
- if ( entry.name === 'vendors' ) {
- // Only copy react-jsx-runtime files, skip react and react-dom.
- const vendorFiles = fs.readdirSync( src );
- let copiedCount = 0;
- fs.mkdirSync( dest, { recursive: true } );
- for ( const file of vendorFiles ) {
- if (
- file.startsWith( 'react-jsx-runtime' ) &&
- file.endsWith( '.js' )
- ) {
- const srcFile = path.join( src, file );
- const destFile = path.join( dest, file );
-
- fs.copyFileSync( srcFile, destFile );
- copiedCount++;
- }
- }
- console.log(
- ` ✅ ${ entry.name }/ → ${ destName }/ (react-jsx-runtime only, ${ copiedCount } files)`
- );
- }
- } else {
- /*
- * Flatten package structure: package-name/index.js → package-name.js.
- * This matches Core's expected file structure.
- */
- const packageFiles = fs.readdirSync( src );
-
- for ( const file of packageFiles ) {
- if (
- /^index\.(js|min\.js)$/.test( file )
- ) {
- const srcFile = path.join( src, file );
- // Replace 'index.' with 'package-name.'.
- const destFile = file.replace(
- /^index\./,
- `${ entry.name }.`
- );
- const destPath = path.join( scriptsDest, destFile );
-
- fs.mkdirSync( path.dirname( destPath ), {
- recursive: true,
- } );
-
- fs.copyFileSync( srcFile, destPath );
- }
- }
- }
- } else if ( entry.isFile() && entry.name.endsWith( '.js' ) ) {
- // Copy root-level JS files.
- const dest = path.join( scriptsDest, entry.name );
- fs.mkdirSync( path.dirname( dest ), { recursive: true } );
- fs.copyFileSync( src, dest );
- }
- }
-
- console.log( ' ✅ JavaScript packages copied' );
- }
+ console.log( '\n📦 Copying block PHP files...' );
+ copyBlockPhp( COPY_CONFIG.blocks );
- // 2. Copy blocks (unified: scripts, styles, PHP, JSON).
- console.log( '\n📦 Copying blocks...' );
- copyBlockAssets( COPY_CONFIG.blocks );
+ console.log( '\n📦 Copying block CSS files...' );
+ copyBlockStyles( COPY_CONFIG.blocks );
- // 3. Generate script-modules-packages.php from individual asset files.
+ // 3. Generate script-modules-packages.php.
console.log( '\n📦 Generating script-modules-packages.php...' );
generateScriptModulesPackages();
diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js
index 43047b5ee5dd7..3ba95199578b4 100644
--- a/tools/gutenberg/utils.js
+++ b/tools/gutenberg/utils.js
@@ -139,7 +139,7 @@ async function resolveExpectedSha( { ref, ghcrRepo, isMutable } ) {
/**
* Trigger a fresh download of the Gutenberg artifact by spawning download.js,
- * then run `grunt build:gutenberg --dev` to copy the build to src/.
+ * then run `grunt build:gutenberg` to copy the build into src/.
* Exits the process if either step fails.
*/
function downloadGutenberg() {
@@ -148,7 +148,7 @@ function downloadGutenberg() {
process.exit( downloadResult.status ?? 1 );
}
- const buildResult = spawnSync( 'grunt', [ 'build:gutenberg', '--dev' ], { stdio: 'inherit', shell: true } );
+ const buildResult = spawnSync( 'grunt', [ 'build:gutenberg' ], { stdio: 'inherit', shell: true } );
if ( buildResult.status !== 0 ) {
process.exit( buildResult.status ?? 1 );
}
diff --git a/tsconfig.json b/tsconfig.json
index 87abe9fb7a42b..e9f36c374ac89 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -30,6 +30,7 @@
"src/js/_enqueues/wp/code-editor.js",
"src/js/_enqueues/lib/codemirror/javascript-lint.js",
"src/js/_enqueues/lib/codemirror/htmlhint-kses.js",
+ "tools/gutenberg/copy.js",
"tools/gutenberg/download.js",
"tools/gutenberg/utils.js"
]
From 49e7ffff2ea4ad08e62a3f9d8e431041042fc6cf Mon Sep 17 00:00:00 2001
From: Aaron Jorbin
Date: Thu, 18 Jun 2026 21:20:32 +0000
Subject: [PATCH 06/17] Editor: Allow publish meta box action row to wrap.
Instead of crowding the row, actions added by extenders should wrap to new lines. This change has been tested in both the classic editor plugin and hotfix plugin.
Follow-up to [61645].
Props abhishekfdd, masteradhoc, rlucian, sabernhardt, cogdesign, threadi, darshitrajyaguru97, desrosj, davidbaumwald, jorbin.
Fixes #65286.
git-svn-id: https://develop.svn.wordpress.org/trunk@62526 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-admin/css/common.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css
index f48b8048c76e5..53933f0ac28a2 100644
--- a/src/wp-admin/css/common.css
+++ b/src/wp-admin/css/common.css
@@ -934,6 +934,7 @@ a#remove-post-thumbnail:hover,
border-top: 1px solid #dcdcde;
background: #f6f7f7;
display: flex;
+ flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
From a7a761265df8ba476d9f6c90c347cfc6dda1f8ee Mon Sep 17 00:00:00 2001
From: Sergey Biryukov
Date: Thu, 18 Jun 2026 22:19:27 +0000
Subject: [PATCH 07/17] Docs: Correct variable reference in `wpdb::delete()`
DocBlock.
Follow-up to [47740].
Props nareshbheda, manishxdp.
Fixes #65470.
git-svn-id: https://develop.svn.wordpress.org/trunk@62527 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-includes/class-wpdb.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php
index e5300e6d75122..babd45a885f1c 100644
--- a/src/wp-includes/class-wpdb.php
+++ b/src/wp-includes/class-wpdb.php
@@ -2758,7 +2758,7 @@ public function update( $table, $data, $where, $format = null, $where_format = n
* @param string[]|string $where_format Optional. An array of formats to be mapped to each of the values in $where.
* If string, that format will be used for all of the items in $where.
* A format is one of '%d', '%f', '%s' (integer, float, string).
- * If omitted, all values in $data will be treated as strings unless otherwise
+ * If omitted, all values in $where will be treated as strings unless otherwise
* specified in wpdb::$field_types. Default null.
* @return int|false The number of rows deleted, or false on error.
*/
From 76c934f3079133c66530f30c6d0d5e557bf7e552 Mon Sep 17 00:00:00 2001
From: Weston Ruter
Date: Thu, 18 Jun 2026 23:23:35 +0000
Subject: [PATCH 08/17] Docs: Clarify return value semantics of `wpdb` query
methods.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This eliminates over 400 PHPStan errors from the core codebase.
* Clarify the inline documentation for the four `wpdb` query methods — `get_results()`, `get_row()`, `get_col()`, and `get_var()`.
* Add `@phpstan-return` conditional types that mirror each method's runtime dispatch on `$query` and `$output`.
* Add `@phpstan-param` tags narrowing `$output` to the documented constants.
* Document that `get_var()` returns `null` both on failure and when the matched cell value is an empty string, directing consumers to `$this->last_error` to distinguish the two cases.
* Tighten the `@return` in `get_results()` from `array|object|null` to `array|null`, since the method never returns a bare `stdClass`; the `object` was a copy/paste artifact from `get_row()`.
* Fix a deprecated use of `null` as an array offset (PHP 8.5) in the `OBJECT_K` branch when a row's first column is SQL `NULL`.
* Gather `get_col()` data as a true list.
* Suggest `ext-mysqli` in `composer.json`, which `wpdb` requires at runtime.
Developed in https://github.com/WordPress/wordpress-develop/pull/11855.
Props apermo, westonruter.
See #30257, #64898.
Fixes #65261.
git-svn-id: https://develop.svn.wordpress.org/trunk@62529 602fd350-edb4-49c9-b593-d223f7449a82
---
composer.json | 3 +-
src/wp-includes/class-wpdb.php | 94 +++++++++++++++++++++++++++++-----
2 files changed, 83 insertions(+), 14 deletions(-)
diff --git a/composer.json b/composer.json
index 5c016d37316c1..6500e7ccbf8af 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,8 @@
"php": ">=7.4"
},
"suggest": {
- "ext-dom": "*"
+ "ext-dom": "*",
+ "ext-mysqli": "*"
},
"require-dev": {
"composer/ca-bundle": "1.5.12",
diff --git a/src/wp-includes/class-wpdb.php b/src/wp-includes/class-wpdb.php
index babd45a885f1c..e9d7f986d5801 100644
--- a/src/wp-includes/class-wpdb.php
+++ b/src/wp-includes/class-wpdb.php
@@ -3019,12 +3019,16 @@ protected function process_field_lengths( $data, $table ) {
* the value in the column and row specified is returned. If $query is null,
* the value in the specified column and row from the previous SQL result is returned.
*
+ * Returns null both on failure and when the matched cell value is an empty
+ * string. To distinguish the two cases, check {@see self::$last_error}.
+ *
* @since 0.71
*
* @param string|null $query Optional. SQL query. Defaults to null, use the result from the previous query.
* @param int $x Optional. Column of value to return. Indexed from 0. Default 0.
* @param int $y Optional. Row of value to return. Indexed from 0. Default 0.
- * @return string|null Database query result (as string), or null on failure.
+ * @return string|null Database query result (as string), or null on failure or when the value is an empty string.
+ * @phpstan-return non-empty-string|null
*/
public function get_var( $query = null, $x = 0, $y = 0 ) {
$this->func_call = "\$db->get_var(\"$query\", $x, $y)";
@@ -3039,6 +3043,14 @@ public function get_var( $query = null, $x = 0, $y = 0 ) {
// Extract var out of cached results based on x,y vals.
if ( ! empty( $this->last_result[ $y ] ) ) {
+ /**
+ * Column values.
+ *
+ * These are returned from the database as strings, or null for SQL NULL, but get_object_vars() types the
+ * property values as mixed.
+ *
+ * @var list $values
+ */
$values = array_values( get_object_vars( $this->last_result[ $y ] ) );
}
@@ -3059,6 +3071,24 @@ public function get_var( $query = null, $x = 0, $y = 0 ) {
* respectively. Default OBJECT.
* @param int $y Optional. Row to return. Indexed from 0. Default 0.
* @return array|object|null Database query result in format specified by $output or null on failure.
+ * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output
+ * @phpstan-return (
+ * $query is non-falsy-string
+ * ? (
+ * $output is 'OBJECT'
+ * ? stdClass|null
+ * : (
+ * $output is 'ARRAY_A'
+ * ? array|null
+ * : (
+ * $output is 'ARRAY_N'
+ * ? list|null
+ * : null
+ * )
+ * )
+ * )
+ * : null
+ * )
*/
public function get_row( $query = null, $output = OBJECT, $y = 0 ) {
$this->func_call = "\$db->get_row(\"$query\",$output,$y)";
@@ -3104,6 +3134,7 @@ public function get_row( $query = null, $output = OBJECT, $y = 0 ) {
* @param string|null $query Optional. SQL query. Defaults to previous query.
* @param int $x Optional. Column to return. Indexed from 0. Default 0.
* @return array Database query result. Array indexed from 0 by SQL result row number.
+ * @phpstan-return list
*/
public function get_col( $query = null, $x = 0 ) {
if ( $query ) {
@@ -3118,7 +3149,7 @@ public function get_col( $query = null, $x = 0 ) {
// Extract the column values.
if ( $this->last_result ) {
for ( $i = 0, $j = count( $this->last_result ); $i < $j; $i++ ) {
- $new_array[ $i ] = $this->get_var( null, $x, $i );
+ $new_array[] = $this->get_var( null, $x, $i );
}
}
return $new_array;
@@ -3129,18 +3160,47 @@ public function get_col( $query = null, $x = 0 ) {
*
* Executes a SQL query and returns the entire SQL result.
*
+ * Returns an empty array when no rows match or when the database
+ * reports an error for the query. Returns null when $query is empty,
+ * when $output is not one of the recognized constants, or when the
+ * query cannot run because the connection is not ready.
+ *
* @since 0.71
*
- * @param string $query SQL query.
- * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants.
- * With one of the first three, return an array of rows indexed
- * from 0 by SQL result row number. Each row is an associative array
- * (column => value, ...), a numerically indexed array (0 => value, ...),
- * or an object ( ->column = value ), respectively. With OBJECT_K,
- * return an associative array of row objects keyed by the value
- * of each row's first column's value. Duplicate keys are discarded.
- * Default OBJECT.
- * @return array|object|null Database query results.
+ * @param string|null $query SQL query.
+ * @param string $output Optional. Any of ARRAY_A | ARRAY_N | OBJECT | OBJECT_K constants.
+ * With one of the first three, return an array of rows indexed
+ * from 0 by SQL result row number. Each row is an associative array
+ * (column => value, ...), a numerically indexed array (0 => value, ...),
+ * or an object ( ->column = value ), respectively. With OBJECT_K,
+ * return an associative array of row objects keyed by the value
+ * of each row's first column's value. Duplicate keys are discarded.
+ * Default OBJECT.
+ * @return array|null Database query results. Empty array when no rows match
+ * or on database error. Null when $query is empty, when
+ * $output is invalid, or when the connection is not ready.
+ * @phpstan-param 'OBJECT'|'OBJECT_K'|'ARRAY_A'|'ARRAY_N' $output
+ * @phpstan-return (
+ * $query is non-falsy-string
+ * ? (
+ * $output is 'OBJECT'
+ * ? list|null
+ * : (
+ * $output is 'OBJECT_K'
+ * ? array
+ * : (
+ * $output is 'ARRAY_A'
+ * ? list>
+ * : (
+ * $output is 'ARRAY_N'
+ * ? list>
+ * : null
+ * )
+ * )
+ * )
+ * )
+ * : null
+ * )
*/
public function get_results( $query = null, $output = OBJECT ) {
$this->func_call = "\$db->get_results(\"$query\", $output)";
@@ -3167,7 +3227,15 @@ public function get_results( $query = null, $output = OBJECT ) {
if ( $this->last_result ) {
foreach ( $this->last_result as $row ) {
$var_by_ref = get_object_vars( $row );
- $key = array_shift( $var_by_ref );
+ /**
+ * The first column's value is used as the key.
+ *
+ * A SQL NULL value surfaces as null here, so coerce it to an empty string to avoid the deprecated
+ * use of null as an array offset (PHP 8.5+).
+ *
+ * @var array-key $key
+ */
+ $key = array_shift( $var_by_ref ) ?? '';
if ( ! isset( $new_array[ $key ] ) ) {
$new_array[ $key ] = $row;
}
From eafa314e07665279fc5106a52370b138578de341 Mon Sep 17 00:00:00 2001
From: Aki Hamano
Date: Fri, 19 Jun 2026 06:41:13 +0000
Subject: [PATCH 09/17] KSES: Allow SVG presentation attributes in
safe_style_css.
Add SVG presentation attributes to the list of CSS properties allowed by `safecss_filter_attr()`, so inline SVG markup can be styled via the `style` attribute.
This ports Gutenberg PR #79172 to Core.
Props afercia, westonruter, wildworks.
Fixes #65457.
git-svn-id: https://develop.svn.wordpress.org/trunk@62530 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-includes/kses.php | 66 ++++++++++++++++++++++++++++++++++++
tests/phpunit/tests/kses.php | 38 +++++++++++++++++++++
2 files changed, 104 insertions(+)
diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php
index a45d1697ea40a..b6c24b77e2695 100644
--- a/src/wp-includes/kses.php
+++ b/src/wp-includes/kses.php
@@ -2579,6 +2579,7 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
* Filters the list of allowed CSS attributes.
*
* @since 2.8.1
+ * @since 7.1.0 Added support for SVG presentation attributes.
*
* @param string[] $attr Array of allowed CSS attributes.
*/
@@ -2737,6 +2738,71 @@ function safecss_filter_attr( $css, $deprecated = '' ) {
'aspect-ratio',
'container-type',
+ 'fill',
+ 'fill-opacity',
+ 'fill-rule',
+
+ 'stroke',
+ 'stroke-dasharray',
+ 'stroke-dashoffset',
+ 'stroke-linecap',
+ 'stroke-linejoin',
+ 'stroke-miterlimit',
+ 'stroke-opacity',
+ 'stroke-width',
+
+ 'color-interpolation',
+ 'color-interpolation-filters',
+ 'paint-order',
+ 'stop-color',
+ 'stop-opacity',
+ 'flood-color',
+ 'flood-opacity',
+ 'lighting-color',
+
+ 'marker',
+ 'marker-end',
+ 'marker-mid',
+ 'marker-start',
+
+ 'clip-path',
+ 'clip-rule',
+ 'mask',
+ 'mask-type',
+
+ 'cx',
+ 'cy',
+ 'r',
+ 'rx',
+ 'ry',
+ 'x',
+ 'y',
+ 'd',
+
+ 'alignment-baseline',
+ 'baseline-shift',
+ 'dominant-baseline',
+ 'glyph-orientation-horizontal',
+ 'glyph-orientation-vertical',
+ 'text-anchor',
+ 'unicode-bidi',
+ 'word-spacing',
+
+ 'font-size-adjust',
+ 'font-stretch',
+
+ 'color-rendering',
+ 'image-rendering',
+ 'shape-rendering',
+ 'text-rendering',
+ 'vector-effect',
+
+ 'transform',
+ 'transform-origin',
+
+ 'pointer-events',
+ 'visibility',
+
// Custom CSS properties.
'--*',
)
diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php
index db507a6b26550..871723b98361c 100644
--- a/tests/phpunit/tests/kses.php
+++ b/tests/phpunit/tests/kses.php
@@ -1000,6 +1000,7 @@ public function test_wp_kses_attr_no_attributes_allowed_with_false() {
* @ticket 58551
* @ticket 60132
* @ticket 64414
+ * @ticket 65457
*
* @dataProvider data_safecss_filter_attr
*
@@ -1473,6 +1474,43 @@ public function data_safecss_filter_attr() {
'css' => 'display: grid',
'expected' => 'display: grid',
),
+ // SVG presentation attributes introduced in 7.1.0.
+ array(
+ 'css' => 'fill: none',
+ 'expected' => 'fill: none',
+ ),
+ array(
+ 'css' => 'fill-rule: evenodd',
+ 'expected' => 'fill-rule: evenodd',
+ ),
+ array(
+ 'css' => 'stroke: red',
+ 'expected' => 'stroke: red',
+ ),
+ array(
+ 'css' => 'stroke-width: 2',
+ 'expected' => 'stroke-width: 2',
+ ),
+ array(
+ 'css' => 'stroke-linecap: round',
+ 'expected' => 'stroke-linecap: round',
+ ),
+ array(
+ 'css' => 'paint-order: stroke',
+ 'expected' => 'paint-order: stroke',
+ ),
+ array(
+ 'css' => 'vector-effect: non-scaling-stroke',
+ 'expected' => 'vector-effect: non-scaling-stroke',
+ ),
+ array(
+ 'css' => 'clip-rule: evenodd',
+ 'expected' => 'clip-rule: evenodd',
+ ),
+ array(
+ 'css' => 'text-anchor: middle',
+ 'expected' => 'text-anchor: middle',
+ ),
);
}
From 601ede9028e272227f55119ac58b93f247478fd1 Mon Sep 17 00:00:00 2001
From: Andrea Fercia
Date: Fri, 19 Jun 2026 11:02:30 +0000
Subject: [PATCH 10/17] KSES: Add command and commandfor to the list of allowed
attributes for buttons.
Developed in: https://github.com/WordPress/wordpress-develop/pull/11483
Props pratiknawkar94, joedolson, westonruter, afercia.
Fixes #64576.
git-svn-id: https://develop.svn.wordpress.org/trunk@62531 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-includes/kses.php | 2 ++
tests/phpunit/tests/kses.php | 11 +++++++++++
2 files changed, 13 insertions(+)
diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php
index b6c24b77e2695..0edb36d9c80bb 100644
--- a/src/wp-includes/kses.php
+++ b/src/wp-includes/kses.php
@@ -109,6 +109,8 @@
),
'br' => array(),
'button' => array(
+ 'command' => true,
+ 'commandfor' => true,
'disabled' => true,
'name' => true,
'type' => true,
diff --git a/tests/phpunit/tests/kses.php b/tests/phpunit/tests/kses.php
index 871723b98361c..9ed3a45b2d90e 100644
--- a/tests/phpunit/tests/kses.php
+++ b/tests/phpunit/tests/kses.php
@@ -1928,6 +1928,17 @@ public function test_wp_kses_main_tag_standard_attributes() {
$this->assertEqualHTML( $html, wp_kses_post( $html ) );
}
+ /**
+ * Test that Invoker Commands API attributes are preserved on buttons in post content.
+ *
+ * @ticket 64576
+ */
+ public function test_wp_kses_button_invoker_command_attributes() {
+ $html = 'Content
';
+
+ $this->assertEqualHTML( $html, wp_kses_post( $html ) );
+ }
+
/**
* Test that object tags are allowed under limited circumstances.
*
From 08be8caca1cb13dfd4a35ee384b8ea19c105db54 Mon Sep 17 00:00:00 2001
From: Sergey Biryukov
Date: Fri, 19 Jun 2026 12:45:59 +0000
Subject: [PATCH 11/17] Docs: Correct typo in a comment in
`WP_Upgrader::install_package()`.
Follow-up to [59039], [59291].
Props harishtewari, salmanshafiq8630, amitjoel85, SergeyBiryukov.
Fixes #65492.
git-svn-id: https://develop.svn.wordpress.org/trunk@62532 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-admin/includes/class-wp-upgrader.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php
index 695ce50bf0d7e..ba27113ff73de 100644
--- a/src/wp-admin/includes/class-wp-upgrader.php
+++ b/src/wp-admin/includes/class-wp-upgrader.php
@@ -528,7 +528,7 @@ public function install_package( $args = array() ) {
/*
* Give the upgrade an additional 300 seconds (5 minutes) to ensure the install
* doesn't prematurely timeout having used up the maximum script execution time
- * upacking and downloading in WP_Upgrader->run().
+ * downloading and unpacking in WP_Upgrader->run().
*/
if ( function_exists( 'set_time_limit' ) ) {
set_time_limit( 300 );
From c0678efdcbcfec6d213fa06058f847cf0ae1ccca Mon Sep 17 00:00:00 2001
From: Jonathan Desrosiers
Date: Fri, 19 Jun 2026 19:22:14 +0000
Subject: [PATCH 12/17] Build/Test Tools: Include unversioned and binary files
in change detection.
Every GitHub Actions workflow performing operations that may modify versioned files ends with a `git diff` check for uncommitted changes. The workflow run fails when changes exist to ensure requried changes are not missed in a given commit.
Because the `git diff` only detects unstaged changes to tracked files, newly created files that were not also added to version control can easily go undetected. `git diff` also does not include changes to binary files by default.
This commit makes the following changes to improve the related steps in workflow files:
- The command used for detecting is changed to `git status --porcelain`, which only reports as clean when there are no changes to tracked files (both staged or unstaged) and there are no untracked files.
- The related workflows have been updated to include `git add -A` before creating any patches to ensure that all new, modified, and deleted files are represented in the diff files created.
- The `--binary` flag has been added to all `git diff` commands creating patches to ensure those changes are also included.
Props desrosj.
See #64893.
git-svn-id: https://develop.svn.wordpress.org/trunk@62533 602fd350-edb4-49c9-b593-d223f7449a82
---
.../workflows/reusable-check-built-files.yml | 21 +++++++-----
.../reusable-coding-standards-javascript.yml | 11 +++++--
.../reusable-coding-standards-php.yml | 11 +++++--
.../workflows/reusable-end-to-end-tests.yml | 11 +++++--
.../workflows/reusable-javascript-tests.yml | 11 +++++--
.../reusable-javascript-type-checking-v1.yml | 11 +++++--
.../reusable-performance-test-v2.yml | 11 +++++--
.../workflows/reusable-php-compatibility.yml | 11 +++++--
.../reusable-phpstan-static-analysis-v1.yml | 11 +++++--
.../workflows/reusable-phpunit-tests-v2.yml | 11 +++++--
.../workflows/reusable-phpunit-tests-v3.yml | 11 +++++--
.../reusable-test-core-build-process.yml | 32 ++++++++++++++----
.../reusable-test-gutenberg-build-process.yml | 12 +++++--
...sable-test-local-docker-environment-v1.yml | 11 +++++--
.../workflows/test-and-zip-default-themes.yml | 33 ++++++++++++++-----
.gitignore | 5 +++
16 files changed, 166 insertions(+), 58 deletions(-)
diff --git a/.github/workflows/reusable-check-built-files.yml b/.github/workflows/reusable-check-built-files.yml
index 033b2a46ac3e9..5a673bf496ace 100644
--- a/.github/workflows/reusable-check-built-files.yml
+++ b/.github/workflows/reusable-check-built-files.yml
@@ -24,9 +24,10 @@ jobs:
# - Builds Emoji files.
# - Builds bundled Root Certificate files.
# - Builds WordPress.
- # - Checks for changes to versioned files.
- # - Displays the result of git diff for debugging purposes.
- # - Saves the diff to a patch file.
+ # - Checks for uncommitted changes.
+ # - Stages all uncommitted changes and adds any unversioned files.
+ # - Displays a diff of all staged changes.
+ # - Saves staged changes to a .diff file.
# - Uploads the patch file as an artifact.
update-built-files:
name: Check and update built files
@@ -78,22 +79,26 @@ jobs:
- name: Build WordPress
run: npm run build:dev
- - name: Check for changes to versioned files
+ - name: Check for uncommitted changes
id: built-file-check
run: |
- if git diff --quiet; then
+ if [ -z "$(git status --porcelain)" ]; then
echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT"
else
echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT"
fi
- - name: Display changes to versioned files
+ - name: Stage all changes for diff generation
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff
+ run: git add -A
+
+ - name: Display all uncommitted changes
+ if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
+ run: git diff --cached
- name: Save diff to a file
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff > ./changes.diff
+ run: git diff --cached --binary > ./changes.diff
# Uploads the diff file as an artifact.
- name: Upload diff file as artifact
diff --git a/.github/workflows/reusable-coding-standards-javascript.yml b/.github/workflows/reusable-coding-standards-javascript.yml
index eac5bbdc352f2..b15a5bacf6d46 100644
--- a/.github/workflows/reusable-coding-standards-javascript.yml
+++ b/.github/workflows/reusable-coding-standards-javascript.yml
@@ -24,7 +24,7 @@ jobs:
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Run the WordPress JSHint checks.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
jshint:
name: JavaScript checks
runs-on: ubuntu-24.04
@@ -57,5 +57,10 @@ jobs:
- name: Run JSHint
run: npm run grunt jshint
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-coding-standards-php.yml b/.github/workflows/reusable-coding-standards-php.yml
index fab8bffb31e11..980d20b75f4b6 100644
--- a/.github/workflows/reusable-coding-standards-php.yml
+++ b/.github/workflows/reusable-coding-standards-php.yml
@@ -37,7 +37,7 @@ jobs:
# - Generate a report for displaying issues as pull request annotations.
# - Runs PHPCS on the `tests` directory without (warnings included).
# - Generate a report for displaying `test` directory issues as pull request annotations.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
phpcs:
name: PHP checks
runs-on: ubuntu-24.04
@@ -105,5 +105,10 @@ jobs:
if: ${{ inputs.old-branch }}
run: phpcbf
- - name: Ensure version-controlled files are not modified during the tests
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml
index 87f90f1b53039..c39e03a6c0ab0 100644
--- a/.github/workflows/reusable-end-to-end-tests.yml
+++ b/.github/workflows/reusable-end-to-end-tests.yml
@@ -61,7 +61,7 @@ jobs:
# - Install additional languages.
# - Run the E2E tests.
# - Uploads screenshots and HTML snapshots as an artifact.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
e2e-tests:
name: SCRIPT_DEBUG ${{ inputs.LOCAL_SCRIPT_DEBUG && 'enabled' || 'disabled' }}
runs-on: ubuntu-24.04
@@ -153,5 +153,10 @@ jobs:
if-no-files-found: ignore
include-hidden-files: true
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-javascript-tests.yml b/.github/workflows/reusable-javascript-tests.yml
index 3988ec9d6b055..d260dd71c4c11 100644
--- a/.github/workflows/reusable-javascript-tests.yml
+++ b/.github/workflows/reusable-javascript-tests.yml
@@ -25,7 +25,7 @@ jobs:
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Run the WordPress QUnit tests.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
test-js:
name: Run QUnit tests
runs-on: ubuntu-24.04
@@ -67,5 +67,10 @@ jobs:
- name: Run QUnit tests
run: npm run grunt qunit:compiled
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-javascript-type-checking-v1.yml b/.github/workflows/reusable-javascript-type-checking-v1.yml
index 9dabd01e27fa0..d1f484c39c36c 100644
--- a/.github/workflows/reusable-javascript-type-checking-v1.yml
+++ b/.github/workflows/reusable-javascript-type-checking-v1.yml
@@ -23,7 +23,7 @@ jobs:
# - Configures caching for TypeScript build info.
# - Runs JavaScript type checking.
# - Saves the TypeScript build info.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
typecheck:
name: Run JavaScript type checking
runs-on: ubuntu-24.04
@@ -72,5 +72,10 @@ jobs:
*.tsbuildinfo
key: "ts-build-info-${{ github.run_id }}"
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml
index c0279c37fe64b..7bafb8fff4894 100644
--- a/.github/workflows/reusable-performance-test-v2.yml
+++ b/.github/workflows/reusable-performance-test-v2.yml
@@ -102,7 +102,7 @@ jobs:
# - Install MU plugin.
# - Run performance tests.
# - Archive artifacts.
- # - Ensure version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
performance:
name: Test ${{ inputs.subject == 'base' && inputs.BASE_TAG || inputs.subject }}
runs-on: ubuntu-24.04
@@ -272,5 +272,10 @@ jobs:
if-no-files-found: error
include-hidden-files: true
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-php-compatibility.yml b/.github/workflows/reusable-php-compatibility.yml
index 7756330282e6f..ed7cf3abbfae2 100644
--- a/.github/workflows/reusable-php-compatibility.yml
+++ b/.github/workflows/reusable-php-compatibility.yml
@@ -30,7 +30,7 @@ jobs:
# - Make Composer packages available globally.
# - Runs the PHP compatibility tests.
# - Generate a report for displaying issues as pull request annotations.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
php-compatibility:
name: Run compatibility checks
runs-on: ubuntu-24.04
@@ -86,5 +86,10 @@ jobs:
if: ${{ always() && steps.phpcs.outcome == 'failure' }}
run: cs2pr ./.cache/phpcs-compat-report.xml
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml
index c73c1e5e692fe..e2976df943817 100644
--- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml
+++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml
@@ -33,7 +33,7 @@ jobs:
# - Configures caching for PHPStan static analysis scans.
# - Runs PHPStan static analysis (with Pull Request annotations).
# - Saves the PHPStan result cache.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
phpstan:
name: Run PHP static analysis
runs-on: ubuntu-24.04
@@ -102,5 +102,10 @@ jobs:
path: .cache
key: "phpstan-result-cache-${{ github.run_id }}"
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml
index 5e078b6ef0c2e..21f71546bdb2d 100644
--- a/.github/workflows/reusable-phpunit-tests-v2.yml
+++ b/.github/workflows/reusable-phpunit-tests-v2.yml
@@ -84,7 +84,7 @@ jobs:
# - Logs debug information from inside the WordPress Docker container.
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
test-php:
name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single Site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }}
runs-on: ${{ inputs.os }}
@@ -208,5 +208,10 @@ jobs:
if: ${{ ! inputs.split_slow }}
run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml
index 64507323a617b..e08ef2d3c6824 100644
--- a/.github/workflows/reusable-phpunit-tests-v3.yml
+++ b/.github/workflows/reusable-phpunit-tests-v3.yml
@@ -113,7 +113,7 @@ jobs:
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
# - Upload the code coverage report to Codecov.io.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
# - Checks out the WordPress Test reporter repository.
# - Submit the test results to the WordPress.org host test results.
phpunit-tests:
@@ -268,8 +268,13 @@ jobs:
flags: ${{ inputs.multisite && 'multisite' || 'single' }},php
fail_ci_if_error: true
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
- name: Checkout the WordPress Test Reporter
if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }}
diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml
index 1566d1583a807..e931f43145050 100644
--- a/.github/workflows/reusable-test-core-build-process.yml
+++ b/.github/workflows/reusable-test-core-build-process.yml
@@ -49,15 +49,16 @@ jobs:
# Verifies that installing npm dependencies and building WordPress works as expected.
#
# Performs the following steps:
+ # - Prevent line ending conversions (Windows only).
# - Checks out the repository.
# - Sets up Node.js.
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Builds WordPress to run from the desired location (src or build).
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes after building.
# - Creates a ZIP of the built WordPress files (when building to the build directory).
# - Cleans up after building WordPress.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes after cleaning.
# - Uploads the ZIP as a GitHub Actions artifact (when building to the build directory).
# - Saves the pull request number to a text file.
# - Uploads the pull request number as an artifact.
@@ -69,6 +70,13 @@ jobs:
timeout-minutes: 20
steps:
+ # Windows can convert LF to CRLF on checkout, which can make built/generated files appear as modified.
+ - name: Prevent line ending conversions on Windows
+ if: ${{ contains( inputs.os, 'windows-' ) }}
+ run: |
+ git config --global core.autocrlf false
+ git config --global core.eol lf
+
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -119,8 +127,14 @@ jobs:
- name: Build WordPress to run from ${{ inputs.directory }}
run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }}
- - name: Ensure version-controlled files are not modified or deleted during building
- run: git diff --exit-code
+ - name: Check for uncommitted changes after building
+ shell: bash
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
- name: Create ZIP of built files
if: ${{ inputs.directory == 'build' && contains( inputs.os, 'ubuntu-' ) }}
@@ -129,8 +143,14 @@ jobs:
- name: Clean after building to run from ${{ inputs.directory }}
run: npm run grunt ${{ inputs.directory == 'src' && 'clean -- --dev' || 'clean' }}
- - name: Ensure version-controlled files are not modified or deleted during cleaning
- run: git diff --exit-code
+ - name: Check for uncommitted changes after cleaning
+ shell: bash
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
- name: Upload ZIP as a GitHub Actions artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
diff --git a/.github/workflows/reusable-test-gutenberg-build-process.yml b/.github/workflows/reusable-test-gutenberg-build-process.yml
index 4a780d08ee07f..ae5e4cd74d298 100644
--- a/.github/workflows/reusable-test-gutenberg-build-process.yml
+++ b/.github/workflows/reusable-test-gutenberg-build-process.yml
@@ -39,7 +39,7 @@ jobs:
# - Installs Core npm dependencies.
# - Builds WordPress to run from the relevant location (src or build).
# - Builds Gutenberg.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes after building.
build-process-tests:
name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }}
permissions:
@@ -96,5 +96,11 @@ jobs:
run: npm run build
working-directory: ${{ env.GUTENBERG_DIRECTORY }}
- - name: Ensure version-controlled files are not modified or deleted during building
- run: git diff --exit-code
+ - name: Check for uncommitted changes after building
+ shell: bash
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml
index 8f1a556afa2b4..50118d45045ee 100644
--- a/.github/workflows/reusable-test-local-docker-environment-v1.yml
+++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml
@@ -71,7 +71,7 @@ jobs:
# - Runs a WP CLI command.
# - Tests the logs command.
# - Tests the reset command.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for any uncommitted changes.
local-docker-environment-tests:
name: ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.memcached && ' with memcached' || '' }}${{ 'example.org' != inputs.tests-domain && format( ' {0}', inputs.tests-domain ) || '' }}
permissions:
@@ -166,5 +166,10 @@ jobs:
- name: Reset the Docker environment
run: npm run env:reset
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected:"
+ git status --porcelain
+ exit 1
+ fi
diff --git a/.github/workflows/test-and-zip-default-themes.yml b/.github/workflows/test-and-zip-default-themes.yml
index 1a44a8ff12e3a..604f531462a67 100644
--- a/.github/workflows/test-and-zip-default-themes.yml
+++ b/.github/workflows/test-and-zip-default-themes.yml
@@ -112,7 +112,12 @@ jobs:
# - Sets up Node.js.
# - Installs npm dependencies.
# - Runs the theme build script.
- # - Ensures version-controlled files are not modified or deleted.
+ # - Checks for uncommitted changes.
+ # - Stages all uncommitted changes and adds any unversioned files.
+ # - Displays a diff of all staged changes.
+ # - Saves staged changes to a .diff file.
+ # - Uploads the diff file as an artifact.
+ # - Fails the job when uncommitted changes are detected.
test-build-scripts:
name: Test ${{ matrix.theme }} build script
runs-on: ubuntu-24.04
@@ -156,23 +161,27 @@ jobs:
- name: Build theme
run: npm run build
- - name: Check for changes to versioned files
+ - name: Check for uncommitted changes
id: built-file-check
if: ${{ github.event_name == 'pull_request' }}
run: |
- if git diff --quiet; then
+ if [ -z "$(git status --porcelain)" ]; then
echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT"
else
echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT"
fi
- - name: Display changes to versioned files
+ - name: Stage all changes for diff generation
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff
+ run: git add -A
+
+ - name: Display all uncommitted changes
+ if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
+ run: git diff --cached
- name: Save diff to a file
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff > ./changes.diff
+ run: git diff --cached --binary > ./changes.diff
# Uploads the diff file as an artifact.
- name: Upload diff file as artifact
@@ -182,13 +191,21 @@ jobs:
name: pr-built-file-changes
path: src/wp-content/themes/${{ matrix.theme }}/changes.diff
- - name: Ensure version-controlled files are not modified or deleted
- run: git diff --exit-code
+ - name: Check for uncommitted changes after building
+ run: |
+ if [ -n "$(git status --porcelain)" ]; then
+ echo "Uncommitted changes detected after build:"
+ git status --porcelain
+ exit 1
+ fi
# Prepares bundled themes for release.
#
# Performs the following steps:
# - Checks out the repository.
+ # - Sets up Node.js.
+ # - Installs npm dependencies.
+ # - Runs the theme build script.
# - Uploads the theme files as a workflow artifact (files uploaded as an artifact are automatically zipped).
bundle-theme:
name: Create ${{ matrix.theme }} ZIP file
diff --git a/.gitignore b/.gitignore
index 15876fa47fee8..5a7f9b5aef66e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,6 +48,11 @@ wp-tests-config.php
/artifacts
/setup.log
/coverage
+/codecov
+codecov.*
+before.zip
+wordpress.zip
+wp-code-coverage-*.xml
# Files and folders that get created in wp-content
/src/wp-content/blogs.dir
From 4e2c2231f1c395104e9d8e5e088705fe471ce7a9 Mon Sep 17 00:00:00 2001
From: Jonathan Desrosiers
Date: Fri, 19 Jun 2026 20:07:18 +0000
Subject: [PATCH 13/17] Build/Test Tools: Account for old branches in file
checks.
Because old branches reference the reusable workflow files in `trunk` and their respective `.gitignore` files were not updated, some adjustments are needed to prevent failures in numbered branches.
Follow up to [62533].
See #64893.
git-svn-id: https://develop.svn.wordpress.org/trunk@62534 602fd350-edb4-49c9-b593-d223f7449a82
---
.github/workflows/reusable-phpunit-tests-v2.yml | 7 +------
.github/workflows/reusable-test-core-build-process.yml | 2 +-
2 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml
index 21f71546bdb2d..275166661a3d0 100644
--- a/.github/workflows/reusable-phpunit-tests-v2.yml
+++ b/.github/workflows/reusable-phpunit-tests-v2.yml
@@ -209,9 +209,4 @@ jobs:
run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__
- name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml
index e931f43145050..8792b586d7a9f 100644
--- a/.github/workflows/reusable-test-core-build-process.yml
+++ b/.github/workflows/reusable-test-core-build-process.yml
@@ -146,7 +146,7 @@ jobs:
- name: Check for uncommitted changes after cleaning
shell: bash
run: |
- if [ -n "$(git status --porcelain)" ]; then
+ if [ -z "$(git status --porcelain -- . ':!wordpress.zip')" ]; then
echo "Uncommitted changes detected:"
git status --porcelain
exit 1
From e9823d5fdd3ecd33d2485707b4d3764857c0edae Mon Sep 17 00:00:00 2001
From: Jonathan Desrosiers
Date: Fri, 19 Jun 2026 20:09:32 +0000
Subject: [PATCH 14/17] Build/Test Tools: Change file filters for testing old
branches.
This adds the `reusable-phpunit-tests-v3.yml` file to the list of changes that will run the `test-old-branches.yml` workflow.
There are several numbered branches that reference this reusable workflow, so any changes should be verified across all calling branches.
See #64893.
git-svn-id: https://develop.svn.wordpress.org/trunk@62535 602fd350-edb4-49c9-b593-d223f7449a82
---
.github/workflows/test-old-branches.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml
index 74f9c2d43d54c..ae651290d9cd8 100644
--- a/.github/workflows/test-old-branches.yml
+++ b/.github/workflows/test-old-branches.yml
@@ -7,7 +7,7 @@ on:
- trunk
paths:
- '.github/workflows/test-old-branches.yml'
- - '.github/workflows/reusable-phpunit-tests-v[1-2].yml'
+ - '.github/workflows/reusable-phpunit-tests-v[1-3].yml'
# Run twice a month on the 1st and 15th at 00:00 UTC.
schedule:
- cron: '0 0 1 * *'
From 7976257f558c85946116084a6ee64aed421c55ed Mon Sep 17 00:00:00 2001
From: Jonathan Desrosiers
Date: Fri, 19 Jun 2026 20:33:02 +0000
Subject: [PATCH 15/17] Build/Test Tools: Revert [62533], [62534].
More work is needed to ensure these changes do not introduce failures in old branches.
Reverts [62533], [62534].
See #64893.
git-svn-id: https://develop.svn.wordpress.org/trunk@62536 602fd350-edb4-49c9-b593-d223f7449a82
---
.../workflows/reusable-check-built-files.yml | 21 +++++-------
.../reusable-coding-standards-javascript.yml | 11 ++-----
.../reusable-coding-standards-php.yml | 11 ++-----
.../workflows/reusable-end-to-end-tests.yml | 11 ++-----
.../workflows/reusable-javascript-tests.yml | 11 ++-----
.../reusable-javascript-type-checking-v1.yml | 11 ++-----
.../reusable-performance-test-v2.yml | 11 ++-----
.../workflows/reusable-php-compatibility.yml | 11 ++-----
.../reusable-phpstan-static-analysis-v1.yml | 11 ++-----
.../workflows/reusable-phpunit-tests-v2.yml | 4 +--
.../workflows/reusable-phpunit-tests-v3.yml | 11 ++-----
.../reusable-test-core-build-process.yml | 32 ++++--------------
.../reusable-test-gutenberg-build-process.yml | 12 ++-----
...sable-test-local-docker-environment-v1.yml | 11 ++-----
.../workflows/test-and-zip-default-themes.yml | 33 +++++--------------
.gitignore | 5 ---
16 files changed, 57 insertions(+), 160 deletions(-)
diff --git a/.github/workflows/reusable-check-built-files.yml b/.github/workflows/reusable-check-built-files.yml
index 5a673bf496ace..033b2a46ac3e9 100644
--- a/.github/workflows/reusable-check-built-files.yml
+++ b/.github/workflows/reusable-check-built-files.yml
@@ -24,10 +24,9 @@ jobs:
# - Builds Emoji files.
# - Builds bundled Root Certificate files.
# - Builds WordPress.
- # - Checks for uncommitted changes.
- # - Stages all uncommitted changes and adds any unversioned files.
- # - Displays a diff of all staged changes.
- # - Saves staged changes to a .diff file.
+ # - Checks for changes to versioned files.
+ # - Displays the result of git diff for debugging purposes.
+ # - Saves the diff to a patch file.
# - Uploads the patch file as an artifact.
update-built-files:
name: Check and update built files
@@ -79,26 +78,22 @@ jobs:
- name: Build WordPress
run: npm run build:dev
- - name: Check for uncommitted changes
+ - name: Check for changes to versioned files
id: built-file-check
run: |
- if [ -z "$(git status --porcelain)" ]; then
+ if git diff --quiet; then
echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT"
else
echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT"
fi
- - name: Stage all changes for diff generation
+ - name: Display changes to versioned files
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git add -A
-
- - name: Display all uncommitted changes
- if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff --cached
+ run: git diff
- name: Save diff to a file
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff --cached --binary > ./changes.diff
+ run: git diff > ./changes.diff
# Uploads the diff file as an artifact.
- name: Upload diff file as artifact
diff --git a/.github/workflows/reusable-coding-standards-javascript.yml b/.github/workflows/reusable-coding-standards-javascript.yml
index b15a5bacf6d46..eac5bbdc352f2 100644
--- a/.github/workflows/reusable-coding-standards-javascript.yml
+++ b/.github/workflows/reusable-coding-standards-javascript.yml
@@ -24,7 +24,7 @@ jobs:
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Run the WordPress JSHint checks.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
jshint:
name: JavaScript checks
runs-on: ubuntu-24.04
@@ -57,10 +57,5 @@ jobs:
- name: Run JSHint
run: npm run grunt jshint
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-coding-standards-php.yml b/.github/workflows/reusable-coding-standards-php.yml
index 980d20b75f4b6..fab8bffb31e11 100644
--- a/.github/workflows/reusable-coding-standards-php.yml
+++ b/.github/workflows/reusable-coding-standards-php.yml
@@ -37,7 +37,7 @@ jobs:
# - Generate a report for displaying issues as pull request annotations.
# - Runs PHPCS on the `tests` directory without (warnings included).
# - Generate a report for displaying `test` directory issues as pull request annotations.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
phpcs:
name: PHP checks
runs-on: ubuntu-24.04
@@ -105,10 +105,5 @@ jobs:
if: ${{ inputs.old-branch }}
run: phpcbf
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified during the tests
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml
index c39e03a6c0ab0..87f90f1b53039 100644
--- a/.github/workflows/reusable-end-to-end-tests.yml
+++ b/.github/workflows/reusable-end-to-end-tests.yml
@@ -61,7 +61,7 @@ jobs:
# - Install additional languages.
# - Run the E2E tests.
# - Uploads screenshots and HTML snapshots as an artifact.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
e2e-tests:
name: SCRIPT_DEBUG ${{ inputs.LOCAL_SCRIPT_DEBUG && 'enabled' || 'disabled' }}
runs-on: ubuntu-24.04
@@ -153,10 +153,5 @@ jobs:
if-no-files-found: ignore
include-hidden-files: true
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-javascript-tests.yml b/.github/workflows/reusable-javascript-tests.yml
index d260dd71c4c11..3988ec9d6b055 100644
--- a/.github/workflows/reusable-javascript-tests.yml
+++ b/.github/workflows/reusable-javascript-tests.yml
@@ -25,7 +25,7 @@ jobs:
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Run the WordPress QUnit tests.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
test-js:
name: Run QUnit tests
runs-on: ubuntu-24.04
@@ -67,10 +67,5 @@ jobs:
- name: Run QUnit tests
run: npm run grunt qunit:compiled
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-javascript-type-checking-v1.yml b/.github/workflows/reusable-javascript-type-checking-v1.yml
index d1f484c39c36c..9dabd01e27fa0 100644
--- a/.github/workflows/reusable-javascript-type-checking-v1.yml
+++ b/.github/workflows/reusable-javascript-type-checking-v1.yml
@@ -23,7 +23,7 @@ jobs:
# - Configures caching for TypeScript build info.
# - Runs JavaScript type checking.
# - Saves the TypeScript build info.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
typecheck:
name: Run JavaScript type checking
runs-on: ubuntu-24.04
@@ -72,10 +72,5 @@ jobs:
*.tsbuildinfo
key: "ts-build-info-${{ github.run_id }}"
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml
index 7bafb8fff4894..c0279c37fe64b 100644
--- a/.github/workflows/reusable-performance-test-v2.yml
+++ b/.github/workflows/reusable-performance-test-v2.yml
@@ -102,7 +102,7 @@ jobs:
# - Install MU plugin.
# - Run performance tests.
# - Archive artifacts.
- # - Checks for any uncommitted changes.
+ # - Ensure version-controlled files are not modified or deleted.
performance:
name: Test ${{ inputs.subject == 'base' && inputs.BASE_TAG || inputs.subject }}
runs-on: ubuntu-24.04
@@ -272,10 +272,5 @@ jobs:
if-no-files-found: error
include-hidden-files: true
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-php-compatibility.yml b/.github/workflows/reusable-php-compatibility.yml
index ed7cf3abbfae2..7756330282e6f 100644
--- a/.github/workflows/reusable-php-compatibility.yml
+++ b/.github/workflows/reusable-php-compatibility.yml
@@ -30,7 +30,7 @@ jobs:
# - Make Composer packages available globally.
# - Runs the PHP compatibility tests.
# - Generate a report for displaying issues as pull request annotations.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
php-compatibility:
name: Run compatibility checks
runs-on: ubuntu-24.04
@@ -86,10 +86,5 @@ jobs:
if: ${{ always() && steps.phpcs.outcome == 'failure' }}
run: cs2pr ./.cache/phpcs-compat-report.xml
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml
index e2976df943817..c73c1e5e692fe 100644
--- a/.github/workflows/reusable-phpstan-static-analysis-v1.yml
+++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml
@@ -33,7 +33,7 @@ jobs:
# - Configures caching for PHPStan static analysis scans.
# - Runs PHPStan static analysis (with Pull Request annotations).
# - Saves the PHPStan result cache.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
phpstan:
name: Run PHP static analysis
runs-on: ubuntu-24.04
@@ -102,10 +102,5 @@ jobs:
path: .cache
key: "phpstan-result-cache-${{ github.run_id }}"
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml
index 275166661a3d0..5e078b6ef0c2e 100644
--- a/.github/workflows/reusable-phpunit-tests-v2.yml
+++ b/.github/workflows/reusable-phpunit-tests-v2.yml
@@ -84,7 +84,7 @@ jobs:
# - Logs debug information from inside the WordPress Docker container.
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
test-php:
name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single Site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }}
runs-on: ${{ inputs.os }}
@@ -208,5 +208,5 @@ jobs:
if: ${{ ! inputs.split_slow }}
run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__
- - name: Check for uncommitted changes
+ - name: Ensure version-controlled files are not modified or deleted
run: git diff --exit-code
diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml
index e08ef2d3c6824..64507323a617b 100644
--- a/.github/workflows/reusable-phpunit-tests-v3.yml
+++ b/.github/workflows/reusable-phpunit-tests-v3.yml
@@ -113,7 +113,7 @@ jobs:
# - Install WordPress within the Docker container.
# - Run the PHPUnit tests.
# - Upload the code coverage report to Codecov.io.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
# - Checks out the WordPress Test reporter repository.
# - Submit the test results to the WordPress.org host test results.
phpunit-tests:
@@ -268,13 +268,8 @@ jobs:
flags: ${{ inputs.multisite && 'multisite' || 'single' }},php
fail_ci_if_error: true
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
- name: Checkout the WordPress Test Reporter
if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }}
diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml
index 8792b586d7a9f..1566d1583a807 100644
--- a/.github/workflows/reusable-test-core-build-process.yml
+++ b/.github/workflows/reusable-test-core-build-process.yml
@@ -49,16 +49,15 @@ jobs:
# Verifies that installing npm dependencies and building WordPress works as expected.
#
# Performs the following steps:
- # - Prevent line ending conversions (Windows only).
# - Checks out the repository.
# - Sets up Node.js.
# - Logs debug information about the GitHub Action runner.
# - Installs npm dependencies.
# - Builds WordPress to run from the desired location (src or build).
- # - Checks for any uncommitted changes after building.
+ # - Ensures version-controlled files are not modified or deleted.
# - Creates a ZIP of the built WordPress files (when building to the build directory).
# - Cleans up after building WordPress.
- # - Checks for any uncommitted changes after cleaning.
+ # - Ensures version-controlled files are not modified or deleted.
# - Uploads the ZIP as a GitHub Actions artifact (when building to the build directory).
# - Saves the pull request number to a text file.
# - Uploads the pull request number as an artifact.
@@ -70,13 +69,6 @@ jobs:
timeout-minutes: 20
steps:
- # Windows can convert LF to CRLF on checkout, which can make built/generated files appear as modified.
- - name: Prevent line ending conversions on Windows
- if: ${{ contains( inputs.os, 'windows-' ) }}
- run: |
- git config --global core.autocrlf false
- git config --global core.eol lf
-
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -127,14 +119,8 @@ jobs:
- name: Build WordPress to run from ${{ inputs.directory }}
run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }}
- - name: Check for uncommitted changes after building
- shell: bash
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted during building
+ run: git diff --exit-code
- name: Create ZIP of built files
if: ${{ inputs.directory == 'build' && contains( inputs.os, 'ubuntu-' ) }}
@@ -143,14 +129,8 @@ jobs:
- name: Clean after building to run from ${{ inputs.directory }}
run: npm run grunt ${{ inputs.directory == 'src' && 'clean -- --dev' || 'clean' }}
- - name: Check for uncommitted changes after cleaning
- shell: bash
- run: |
- if [ -z "$(git status --porcelain -- . ':!wordpress.zip')" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted during cleaning
+ run: git diff --exit-code
- name: Upload ZIP as a GitHub Actions artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
diff --git a/.github/workflows/reusable-test-gutenberg-build-process.yml b/.github/workflows/reusable-test-gutenberg-build-process.yml
index ae5e4cd74d298..4a780d08ee07f 100644
--- a/.github/workflows/reusable-test-gutenberg-build-process.yml
+++ b/.github/workflows/reusable-test-gutenberg-build-process.yml
@@ -39,7 +39,7 @@ jobs:
# - Installs Core npm dependencies.
# - Builds WordPress to run from the relevant location (src or build).
# - Builds Gutenberg.
- # - Checks for any uncommitted changes after building.
+ # - Ensures version-controlled files are not modified or deleted.
build-process-tests:
name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }}
permissions:
@@ -96,11 +96,5 @@ jobs:
run: npm run build
working-directory: ${{ env.GUTENBERG_DIRECTORY }}
- - name: Check for uncommitted changes after building
- shell: bash
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted during building
+ run: git diff --exit-code
diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml
index 50118d45045ee..8f1a556afa2b4 100644
--- a/.github/workflows/reusable-test-local-docker-environment-v1.yml
+++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml
@@ -71,7 +71,7 @@ jobs:
# - Runs a WP CLI command.
# - Tests the logs command.
# - Tests the reset command.
- # - Checks for any uncommitted changes.
+ # - Ensures version-controlled files are not modified or deleted.
local-docker-environment-tests:
name: ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.memcached && ' with memcached' || '' }}${{ 'example.org' != inputs.tests-domain && format( ' {0}', inputs.tests-domain ) || '' }}
permissions:
@@ -166,10 +166,5 @@ jobs:
- name: Reset the Docker environment
run: npm run env:reset
- - name: Check for uncommitted changes
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
diff --git a/.github/workflows/test-and-zip-default-themes.yml b/.github/workflows/test-and-zip-default-themes.yml
index 604f531462a67..1a44a8ff12e3a 100644
--- a/.github/workflows/test-and-zip-default-themes.yml
+++ b/.github/workflows/test-and-zip-default-themes.yml
@@ -112,12 +112,7 @@ jobs:
# - Sets up Node.js.
# - Installs npm dependencies.
# - Runs the theme build script.
- # - Checks for uncommitted changes.
- # - Stages all uncommitted changes and adds any unversioned files.
- # - Displays a diff of all staged changes.
- # - Saves staged changes to a .diff file.
- # - Uploads the diff file as an artifact.
- # - Fails the job when uncommitted changes are detected.
+ # - Ensures version-controlled files are not modified or deleted.
test-build-scripts:
name: Test ${{ matrix.theme }} build script
runs-on: ubuntu-24.04
@@ -161,27 +156,23 @@ jobs:
- name: Build theme
run: npm run build
- - name: Check for uncommitted changes
+ - name: Check for changes to versioned files
id: built-file-check
if: ${{ github.event_name == 'pull_request' }}
run: |
- if [ -z "$(git status --porcelain)" ]; then
+ if git diff --quiet; then
echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT"
else
echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT"
fi
- - name: Stage all changes for diff generation
+ - name: Display changes to versioned files
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git add -A
-
- - name: Display all uncommitted changes
- if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff --cached
+ run: git diff
- name: Save diff to a file
if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }}
- run: git diff --cached --binary > ./changes.diff
+ run: git diff > ./changes.diff
# Uploads the diff file as an artifact.
- name: Upload diff file as artifact
@@ -191,21 +182,13 @@ jobs:
name: pr-built-file-changes
path: src/wp-content/themes/${{ matrix.theme }}/changes.diff
- - name: Check for uncommitted changes after building
- run: |
- if [ -n "$(git status --porcelain)" ]; then
- echo "Uncommitted changes detected after build:"
- git status --porcelain
- exit 1
- fi
+ - name: Ensure version-controlled files are not modified or deleted
+ run: git diff --exit-code
# Prepares bundled themes for release.
#
# Performs the following steps:
# - Checks out the repository.
- # - Sets up Node.js.
- # - Installs npm dependencies.
- # - Runs the theme build script.
# - Uploads the theme files as a workflow artifact (files uploaded as an artifact are automatically zipped).
bundle-theme:
name: Create ${{ matrix.theme }} ZIP file
diff --git a/.gitignore b/.gitignore
index 5a7f9b5aef66e..15876fa47fee8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,11 +48,6 @@ wp-tests-config.php
/artifacts
/setup.log
/coverage
-/codecov
-codecov.*
-before.zip
-wordpress.zip
-wp-code-coverage-*.xml
# Files and folders that get created in wp-content
/src/wp-content/blogs.dir
From eddc96c723e772c9b1757e4e5a882612cde66294 Mon Sep 17 00:00:00 2001
From: Sergey Biryukov
Date: Sat, 20 Jun 2026 23:33:35 +0000
Subject: [PATCH 16/17] Docs: Correct typo in a comment in
`wp_create_image_subsizes()`.
Follow-up to [59317].
Props khokansardar, nimeshatxecurify, sabernhardt, SergeyBiryukov.
Fixes #65468.
git-svn-id: https://develop.svn.wordpress.org/trunk@62537 602fd350-edb4-49c9-b593-d223f7449a82
---
src/wp-admin/includes/image.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-admin/includes/image.php b/src/wp-admin/includes/image.php
index 95084b1db0576..935c613d561e9 100644
--- a/src/wp-admin/includes/image.php
+++ b/src/wp-admin/includes/image.php
@@ -316,7 +316,7 @@ function wp_create_image_subsizes( $file, $attachment_id ) {
}
if ( $scale_down ) {
- // Resize the image. This will also convet it if needed.
+ // Resize the image. This will also convert it if needed.
$resized = $editor->resize( $threshold, $threshold );
} elseif ( $convert ) {
// The image will be converted (if possible) when saved.
From 2004d6367658496a6068f334aae7040273e90b60 Mon Sep 17 00:00:00 2001
From: Webizito <31759403+Webizito@users.noreply.github.com>
Date: Sun, 21 Jun 2026 13:35:07 +0530
Subject: [PATCH 17/17] Update src/wp-admin/includes/class-wp-site-health.php
Add aria-hidden attribute to Site Health error dashicon per review feedback.
Co-authored-by: Stephen A. Bernhardt
---
src/wp-admin/includes/class-wp-site-health.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php
index 17ebd8a1c0693..71d3953179218 100644
--- a/src/wp-admin/includes/class-wp-site-health.php
+++ b/src/wp-admin/includes/class-wp-site-health.php
@@ -1357,7 +1357,7 @@ public function get_test_dotorg_communication() {
$result['description'] .= sprintf(
'%s
',
sprintf(
- '%s %s',
+ '%s %s',
/* translators: Hidden accessibility text. */
__( 'Error' ),
sprintf(