From 29e570df37ecdaac8e80db75b357ec3c7a5fa186 Mon Sep 17 00:00:00 2001 From: Ramon Fincken Date: Sat, 7 Jun 2025 10:46:19 +0200 Subject: [PATCH 1/2] [TASK] first attempt to reduce NPM need bu using the Core nightly build as source of truth for CSS and JS files --- .env.default | 3 +++ patch_gruntfile.sh | 18 ++++++++++++++++++ prepare.php | 11 ++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 patch_gruntfile.sh diff --git a/.env.default b/.env.default index 84de84c..e9c0af7 100644 --- a/.env.default +++ b/.env.default @@ -24,6 +24,9 @@ export WPT_REPORT_API_KEY= # (Optionally) define an alternate reporting URL export WPT_REPORT_URL= +# Path to the WP Core nightly files +export WPT_CORE_NIGHTLY_DIR=/tmp/nightly + # Credentials for a database that can be written to and reset. # WARNING!!! This database will be destroyed between tests. Only use safe database credentials. # Please note that you must escape _or_ refrain from using # as special character in your credentials. diff --git a/patch_gruntfile.sh b/patch_gruntfile.sh new file mode 100644 index 0000000..0736af4 --- /dev/null +++ b/patch_gruntfile.sh @@ -0,0 +1,18 @@ +sourcefile=$1 +awk ' +BEGIN { inside = 0 } +/grunt\.registerTask\(.*'\''build:(js|css)'\''/ { + inside = 1 + match($0, /'\''build:(js|css)'\''/, m) + task = m[1] + print "grunt.registerTask('\''build:" task "'\'', function() {" + print " grunt.log.writeln('\''Main task build:" task " is not used'\'');" + print "});" + next +} +inside && /\);/ { + inside = 0 + next +} +!inside +' $sourcefile > Gruntfile.tmp && mv Gruntfile.tmp $sourcefile diff --git a/prepare.php b/prepare.php index 2948b70..0253fdd 100644 --- a/prepare.php +++ b/prepare.php @@ -126,7 +126,16 @@ 'cd ' . escapeshellarg( $WPT_PREPARE_DIR . '/tests/phpunit/data/plugins/' ) . '; unzip wordpress-importer.zip; rm wordpress-importer.zip', // Change directory to the preparation directory, install npm dependencies, and build the project. - 'cd ' . escapeshellarg( $WPT_PREPARE_DIR ) . '; npm install && npm run build' + /// 'cd ' . escapeshellarg( $WPT_PREPARE_DIR ) . '; npm install && npm run build', + + // Install NPM but skip the css and js build step + 'cd ' . escapeshellarg( $WPT_PREPARE_DIR ) . ' && npm install && bash patch_gruntfile.sh Gruntfile.js && npm run build --force', + + // Refresh nightly + 'mkdir -p ' . escapeshellarg( $WPT_CORE_NIGHTLY_DIR ) . ' && cd ' . escapeshellarg( $WPT_CORE_NIGHTLY_DIR ) . ' && wp-cli core download --force --version=nightly', + + // Now rsync the CSS and JS files from nightly + 'cd ' . escapeshellarg( $WPT_CORE_NIGHTLY_DIR ) . ' && rsync -zarv --include="*/" --include="*.js" --include="*.css" --exclude="*" --include="*.js" --include="*.css" . '.. escapeshellarg( $WPT_PREPARE_DIR ) . '/build', ) ); From 1c63c40a22b737495459db2042d306f0b1687cce Mon Sep 17 00:00:00 2001 From: Kamran Abdul Aziz Date: Thu, 8 Oct 2026 13:30:16 +0530 Subject: [PATCH 2/2] Install the built files from the WordPress/WordPress mirror (opt-in) The test suite runs against src/ of the wordpress-develop checkout, which needs the generated scripts, styles and block assets. Until now the only way to get them was npm install and npm run build:dev, so every host needs Node.js and npm for a PHP test run. With WPT_ASSETS_SOURCE=mirror, prepare.php reads the checked-out revision from the clone, finds the WordPress/WordPress commit built from that revision (every mirror commit names it), downloads that tree and copies the files the checkout ignores into src/. Tracked files stay as checked out. When no mirror commit exists for the revision yet, or the expected files are missing, the npm build runs as before. The default stays npm. Supersedes the approach first tried in this PR, which patched core's Gruntfile and copied the nightly build into build/. --- .env.default | 10 ++- README.md | 16 ++++ functions.php | 194 +++++++++++++++++++++++++++++++++++++++++++-- patch_gruntfile.sh | 18 ----- prepare.php | 47 +++++++++-- 5 files changed, 251 insertions(+), 34 deletions(-) delete mode 100644 patch_gruntfile.sh diff --git a/.env.default b/.env.default index 48f0fe7..7a242c5 100644 --- a/.env.default +++ b/.env.default @@ -24,9 +24,6 @@ export WPT_REPORT_API_KEY="" # (Optionally) define an alternate reporting URL export WPT_REPORT_URL="" -# Path to the WP Core nightly files -export WPT_CORE_NIGHTLY_DIR=/tmp/nightly - # Credentials for a database that can be written to and reset. # WARNING!!! This database will be destroyed between tests. Only use safe database credentials. # Please note that you must escape _or_ refrain from using # as special character in your credentials. @@ -72,6 +69,13 @@ export WPT_SSH_PRIVATE_KEY_BASE64="" # Any other truthy value will also enable debug mode. export WPT_DEBUG= +# (Optionally) choose where the built files for the tests come from. +# "npm" (default) runs `npm install` and `npm run build:dev` in the checkout. +# "mirror" downloads the files that WordPress/WordPress carries for the exact +# checked-out revision; npm is not run unless the mirror has no commit for that +# revision yet or the download fails, then the npm build runs as before. +export WPT_ASSETS_SOURCE="${WPT_ASSETS_SOURCE-npm}" + # Certificate validation # Use 1 to validate, and 0 to not validate export WPT_CERTIFICATE_VALIDATION=1 diff --git a/README.md b/README.md index 6e8b015..96fc1df 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ To use the Runner, the following is required to test WordPress version 6.6 or la - PHP Composer - Git, RSync, WGet, UnZip +With `WPT_ASSETS_SOURCE=mirror` (see [Configuring the Runner](#configuring-the-runner)) the built files are downloaded from the WordPress/WordPress mirror for the exact checked-out revision, and npm is not run when that succeeds. Keep NodeJS and npm installed anyway: the Runner falls back to the npm build when the mirror has no commit for the revision yet or the download fails. + A full list of compatible versions of PHP for each version of WordPress [can be found in the WordPress Core Handbook](https://make.wordpress.org/core/handbook/references/php-compatibility-and-wordpress-versions/). Test environment: @@ -244,6 +246,12 @@ export WPT_SSH_PRIVATE_KEY_BASE64="" # Use 'verbose' to increase verbosity; also runs npm with --loglevel=silly export WPT_DEBUG="" +# Where the built files for the tests come from +# "npm" (default) runs npm install and npm run build:dev in the checkout +# "mirror" downloads them from WordPress/WordPress for the checked-out revision +# and runs npm only as a fallback (revision not mirrored yet, download failed) +export WPT_ASSETS_SOURCE="npm" + # Certificate validation # Use 1 to validate, and 0 to not validate export WPT_CERTIFICATE_VALIDATION=1 @@ -401,6 +409,14 @@ Output logging. Use 'verbose' to increase verbosity. When set, the npm install a export WPT_DEBUG="" ``` +**Built files source** + +The test suite runs against `src/` of the wordpress-develop checkout, which needs the generated scripts, styles and block assets. By default they are built with `npm install` and `npm run build:dev`. With `mirror`, the Runner reads the checked-out revision from the clone, finds the commit of the [WordPress/WordPress](https://github.com/WordPress/WordPress) mirror that was built from that revision (every mirror commit names it, about a minute behind develop), downloads that tree (about 60 MB) and copies the files the checkout ignores into `src/`, so Node.js and npm are not used on that path. When no mirror commit exists for the revision yet, or the download does not hold the expected files, the npm build runs instead, so Node.js and npm still need to be installed. The lookup uses the public GitHub API without authentication. + +``` +export WPT_ASSETS_SOURCE="npm" +``` + **Certificate validation** TLS Certificate validation. Use `1` to validate, and `0` to not validate. diff --git a/functions.php b/functions.php index 66ec652..c93a642 100644 --- a/functions.php +++ b/functions.php @@ -70,11 +70,12 @@ function check_required_env( $check_db = true ) { * } */ function setup_runner_env_vars() { - $test_dir = trim( getenv( 'WPT_TEST_DIR' ) ); - $prepare_dir = trim( getenv( 'WPT_PREPARE_DIR' ) ); - $ssh_options = trim( getenv( 'WPT_SSH_OPTIONS' ) ); - $php_exec = trim( getenv( 'WPT_PHP_EXECUTABLE' ) ); - $rm_test_dir = trim( getenv( 'WPT_RM_TEST_DIR_CMD' ) ); + $test_dir = trim( getenv( 'WPT_TEST_DIR' ) ); + $prepare_dir = trim( getenv( 'WPT_PREPARE_DIR' ) ); + $ssh_options = trim( getenv( 'WPT_SSH_OPTIONS' ) ); + $php_exec = trim( getenv( 'WPT_PHP_EXECUTABLE' ) ); + $rm_test_dir = trim( getenv( 'WPT_RM_TEST_DIR_CMD' ) ); + $assets_source = strtolower( trim( getenv( 'WPT_ASSETS_SOURCE' ) ) ); $runner_configuration = array( 'WPT_TEST_DIR' => '' !== $test_dir ? $test_dir : '/tmp/wp-test-runner', @@ -90,6 +91,7 @@ function setup_runner_env_vars() { 'WPT_RM_TEST_DIR_CMD' => '' !== $rm_test_dir ? $rm_test_dir : 'rm -rf ' . escapeshellarg( $runner_configuration['WPT_TEST_DIR'] ), 'WPT_REPORT_API_KEY' => trim( getenv( 'WPT_REPORT_API_KEY' ) ), 'WPT_DEBUG' => (bool) getenv( 'WPT_DEBUG' ), + 'WPT_ASSETS_SOURCE' => '' !== $assets_source ? $assets_source : 'npm', ) ); } @@ -567,6 +569,188 @@ function wpt_runner_get_db_server_version( $db_host, $db_user, $db_password, $db // phpcs:enable WordPress.DB.RestrictedFunctions +/** + * Reads the Subversion revision of a wordpress-develop checkout from its last commit message. + * + * The git mirror of wordpress-develop ends every commit message with a line such as + * `git-svn-id: https://develop.svn.wordpress.org/trunk@64232 ...`, also in a shallow clone. + * + * @param string $checkout_dir Path to the wordpress-develop checkout. + * @return array|null Array with 'path' (trunk or branches/x.y) and 'revision' (int), or null when unknown. + */ +function wpt_runner_get_checkout_revision( $checkout_dir ) { + $output = array(); + $retval = 0; + exec( 'git -C ' . escapeshellarg( $checkout_dir ) . ' log -1 --format=%B 2>/dev/null', $output, $retval ); + + if ( 0 !== $retval ) { + return null; + } + + if ( ! preg_match( '#git-svn-id: https://develop\.svn\.wordpress\.org/(trunk|branches/[0-9.]+)@([0-9]+)#', implode( "\n", $output ), $matches ) ) { + return null; + } + + return array( + 'path' => $matches[1], + 'revision' => (int) $matches[2], + ); +} + +/** + * Finds the commit of the WordPress/WordPress built mirror that was built from a develop revision. + * + * Every mirror commit message carries a line such as `Built from https://develop.svn.wordpress.org/trunk@64232`. + * Trunk is mirrored on `master`, a branch such as `branches/7.1` on `7.1-branch`. The lookup reads the latest + * 100 commits of that mirror branch through the GitHub API, unauthenticated. + * + * @param string $svn_path Subversion path of the checkout: `trunk` or `branches/x.y`. + * @param int $revision Subversion revision of the checkout. + * @return string|null Commit SHA on the mirror, or null when no commit for that revision is among the latest 100. + */ +function wpt_runner_find_mirror_commit( $svn_path, $revision ) { + $ref = 'trunk' === $svn_path ? 'master' : substr( $svn_path, strlen( 'branches/' ) ) . '-branch'; + $url = 'https://api.github.com/repos/WordPress/WordPress/commits?sha=' . rawurlencode( $ref ) . '&per_page=100'; + + $process = curl_init( $url ); + curl_setopt_array( + $process, + array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => array( + 'Accept: application/vnd.github+json', + 'User-Agent: WordPress-phpunit-test-runner', + ), + ) + ); + $body = curl_exec( $process ); + $status = (int) curl_getinfo( $process, CURLINFO_HTTP_CODE ); + + if ( ! is_string( $body ) || 200 !== $status ) { + log_message( 'Built assets: the GitHub API answered ' . $status . ' for the mirror commit list.' ); + return null; + } + + $commits = json_decode( $body, true ); + + if ( ! is_array( $commits ) ) { + return null; + } + + $needle = '#' . preg_quote( 'Built from https://develop.svn.wordpress.org/' . $svn_path . '@' . $revision, '#' ) . '(?![0-9])#'; + + foreach ( $commits as $commit ) { + if ( ! isset( $commit['sha'], $commit['commit']['message'] ) ) { + continue; + } + + if ( preg_match( $needle, (string) $commit['commit']['message'] ) && preg_match( '/^[0-9a-f]{40}$/', (string) $commit['sha'] ) ) { + return (string) $commit['sha']; + } + } + + return null; +} + +/** + * Installs the built files of a wordpress-develop checkout from the WordPress/WordPress mirror. + * + * The mirror is built from every develop revision, so its tree for the checked-out revision holds the same + * generated files that `npm run build:dev` would write into `src/`: scripts, styles, the block assets, the + * jQuery copies, the minified bundled-theme styles and so on. Only files that the checkout ignores are copied + * (`git check-ignore`), so tracked files stay exactly as checked out. The caller runs the npm build when this + * returns false; files copied before a failure are simply overwritten by that build. + * + * @param string $checkout_dir Path to the wordpress-develop checkout. + * @param bool $verbose Whether to let wget and rsync print their progress. + * @return bool Whether the files were installed and the sentinel files are in place. + */ +function wpt_runner_install_built_assets_from_mirror( $checkout_dir, $verbose = false ) { + $revision = wpt_runner_get_checkout_revision( $checkout_dir ); + + if ( null === $revision ) { + log_message( 'Built assets: could not read the Subversion revision of the checkout.' ); + return false; + } + + log_message( 'Built assets: the checkout is ' . $revision['path'] . '@' . $revision['revision'] . ', looking for its built mirror commit.' ); + + $sha = wpt_runner_find_mirror_commit( $revision['path'], $revision['revision'] ); + + if ( null === $sha ) { + log_message( 'Built assets: no usable mirror commit for that revision (not built yet, or the lookup failed).' ); + return false; + } + + log_message( 'Built assets: mirror commit ' . $sha . '.' ); + + // Unique per process: concurrent runs may prepare the same revision. + $work_dir = rtrim( sys_get_temp_dir(), '/' ) . '/wpt-built-assets-' . substr( $sha, 0, 12 ) . '-' . getmypid() . '-' . bin2hex( random_bytes( 4 ) ); + $tarball = $work_dir . '/mirror.tar.gz'; + $tree = $work_dir . '/tree'; + $list = $work_dir . '/files.txt'; + $cleanup = 'rm -rf ' . escapeshellarg( $work_dir ); + + $steps = array( + 'mkdir -p ' . escapeshellarg( $tree ), + 'wget' . ( $verbose ? '' : ' -q' ) . ' -O ' . escapeshellarg( $tarball ) . ' ' . escapeshellarg( 'https://github.com/WordPress/WordPress/archive/' . $sha . '.tar.gz' ), + 'tar -xzf ' . escapeshellarg( $tarball ) . ' -C ' . escapeshellarg( $tree ) . ' --strip-components=1', + // Only the files the checkout ignores under src/ are build products; the rest is checked out already. + 'cd ' . escapeshellarg( $tree ) . ' && find wp-admin wp-includes wp-content/themes -type f | sed \'s|^|src/|\' | git -C ' . escapeshellarg( $checkout_dir ) . ' check-ignore --stdin | sed \'s|^src/||\' > ' . escapeshellarg( $list ), + ); + + foreach ( $steps as $step ) { + log_message( $step ); + passthru( $step, $retval ); + + if ( 0 !== $retval ) { + log_message( 'Built assets: the step above failed with exit code ' . $retval . '.' ); + passthru( $cleanup ); + return false; + } + } + + $files = is_file( $list ) ? array_filter( file( $list, FILE_IGNORE_NEW_LINES ) ) : array(); + + if ( array() === $files ) { + log_message( 'Built assets: the mirror tree holds no file that the checkout ignores.' ); + passthru( $cleanup ); + return false; + } + + $copy = 'rsync -a' . ( $verbose ? 'v' : '' ) . ' --files-from=' . escapeshellarg( $list ) . ' ' . escapeshellarg( $tree . '/' ) . ' ' . escapeshellarg( rtrim( $checkout_dir, '/' ) . '/src/' ); + log_message( $copy ); + passthru( $copy, $retval ); + + if ( 0 !== $retval ) { + log_message( 'Built assets: rsync failed with exit code ' . $retval . '.' ); + passthru( $cleanup ); + return false; + } + + passthru( $cleanup ); + + // A few files every build writes; their absence means the mirror tree is not what the tests need. + $sentinels = array( + 'wp-includes/js/jquery/jquery.js', + 'wp-includes/js/dist/blocks.js', + 'wp-includes/css/dist/block-library/style.css', + 'wp-admin/js/common.js', + ); + + foreach ( $sentinels as $sentinel ) { + if ( ! is_file( rtrim( $checkout_dir, '/' ) . '/src/' . $sentinel ) ) { + log_message( 'Built assets: ' . $sentinel . ' is missing after the copy.' ); + return false; + } + } + + log_message( 'Built assets: ' . count( $files ) . ' files installed into src/ from mirror commit ' . substr( $sha, 0, 12 ) . '.' ); + + return true; +} + /** * Collects details about the testing environment. * diff --git a/patch_gruntfile.sh b/patch_gruntfile.sh deleted file mode 100644 index 0736af4..0000000 --- a/patch_gruntfile.sh +++ /dev/null @@ -1,18 +0,0 @@ -sourcefile=$1 -awk ' -BEGIN { inside = 0 } -/grunt\.registerTask\(.*'\''build:(js|css)'\''/ { - inside = 1 - match($0, /'\''build:(js|css)'\''/, m) - task = m[1] - print "grunt.registerTask('\''build:" task "'\'', function() {" - print " grunt.log.writeln('\''Main task build:" task " is not used'\'');" - print "});" - next -} -inside && /\);/ { - inside = 0 - next -} -!inside -' $sourcefile > Gruntfile.tmp && mv Gruntfile.tmp $sourcefile diff --git a/prepare.php b/prepare.php index e7ada9d..82f8ea6 100644 --- a/prepare.php +++ b/prepare.php @@ -83,13 +83,18 @@ */ $npm_verbosity = $runner_vars['WPT_DEBUG'] ? ' --loglevel=silly' : ''; +if ( ! in_array( $runner_vars['WPT_ASSETS_SOURCE'], array( 'npm', 'mirror' ), true ) ) { + error_message( 'WPT_ASSETS_SOURCE must be "npm" or "mirror", "' . $runner_vars['WPT_ASSETS_SOURCE'] . '" given.' ); +} + /* * Checkout and prepare wordpress-develop for testing. * * The following actions are performed: * - Creates a directory to prepare wordpress-develop. * - Clones the WordPress/wordpress-develop repository from GitHub. - * - Install npm dependencies and run the development build script. + * - Puts the built files in place: from the WordPress/WordPress mirror when + * WPT_ASSETS_SOURCE is "mirror", otherwise with the npm development build. */ // Prepare an array of shell commands to set up the testing environment. perform_operations( @@ -102,16 +107,42 @@ // The '--depth=1' flag creates a shallow clone with a history truncated to the last commit. 'git clone --depth=1 https://github.com/WordPress/wordpress-develop.git ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ), - /* - * Change directory to the preparation directory, install npm dependencies, and build the project. - * The development build writes the built files into `src/`, which is the directory the test suite - * runs against. It is the same build that WordPress core's own PHPUnit workflow runs. - */ - 'cd ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . '; npm install' . $npm_verbosity . ' && npm run build:dev' . $npm_verbosity, - ) ); +/* + * The test suite runs against `src/`, which needs the generated scripts, styles and block assets. + * The WordPress/WordPress mirror is built from every develop revision, so with WPT_ASSETS_SOURCE=mirror + * those files are downloaded for the exact checked-out revision instead of being built with npm. + * When no mirror commit exists for the revision yet, or the download does not hold the expected + * files, the npm build below runs as it always did. + */ +$built_assets_installed = false; + +if ( 'mirror' === $runner_vars['WPT_ASSETS_SOURCE'] ) { + log_message( 'Installing the built files from the WordPress/WordPress mirror (WPT_ASSETS_SOURCE=mirror).' ); + $built_assets_installed = wpt_runner_install_built_assets_from_mirror( $runner_vars['WPT_PREPARE_DIR'], $runner_vars['WPT_DEBUG'] ); + + if ( ! $built_assets_installed ) { + log_message( 'Falling back to the npm build.' ); + } +} + +if ( ! $built_assets_installed ) { + perform_operations( + array( + + /* + * Change directory to the preparation directory, install npm dependencies, and build the project. + * The development build writes the built files into `src/`, which is the directory the test suite + * runs against. It is the same build that WordPress core's own PHPUnit workflow runs. + */ + 'cd ' . escapeshellarg( $runner_vars['WPT_PREPARE_DIR'] ) . '; npm install' . $npm_verbosity . ' && npm run build:dev' . $npm_verbosity, + + ) + ); +} + // Log a message indicating the start of the variable replacement process for configuration. log_message( 'Replacing variables in wp-tests-config.php' );