From 2cc49349aad0d596d8a70c3f636607284a06a359 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:06:01 -0500 Subject: [PATCH 01/53] Remove the Gutenberg build script. With this new approach, a pre-built zip will be downloaded instead. --- Gruntfile.js | 11 -- package.json | 1 - tools/gutenberg/build-gutenberg.js | 192 ----------------------------- 3 files changed, 204 deletions(-) delete mode 100644 tools/gutenberg/build-gutenberg.js diff --git a/Gruntfile.js b/Gruntfile.js index 81da2faae228f..96b7eca8dfe39 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1479,17 +1479,6 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-build', 'Builds the Gutenberg repository.', function() { - const done = this.async(); - grunt.util.spawn( { - cmd: 'node', - args: [ 'tools/gutenberg/build-gutenberg.js' ], - opts: { stdio: 'inherit' } - }, function( error ) { - done( ! error ); - } ); - } ); - grunt.registerTask( 'gutenberg-copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; diff --git a/package.json b/package.json index 745ec98ab6b58..9c310779ecbea 100644 --- a/package.json +++ b/package.json @@ -132,7 +132,6 @@ "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", - "gutenberg:build": "node tools/gutenberg/build-gutenberg.js", "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js", "vendor:copy": "node tools/vendors/copy-vendors.js", diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js deleted file mode 100644 index 01cf4489de1fa..0000000000000 --- a/tools/gutenberg/build-gutenberg.js +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env node - -/** - * Build Gutenberg Script - * - * This script builds the Gutenberg repository using its build command - * as specified in the root package.json's "gutenberg" configuration. - * - * @package WordPress - */ - -const { spawn } = require( 'child_process' ); -const fs = require( 'fs' ); -const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const gutenbergDir = path.join( rootDir, 'gutenberg' ); - -/** - * Execute a command and return a promise. - * Captures output and only displays it on failure for cleaner logs. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves when command completes. - */ -function exec( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - let stdout = ''; - let stderr = ''; - - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: [ 'ignore', 'pipe', 'pipe' ], - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files - ...options, - } ); - - // Capture output - if ( child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - // Show output only on failure - if ( stdout ) { - console.error( '\nCommand output:' ); - console.error( stdout ); - } - if ( stderr ) { - console.error( '\nCommand errors:' ); - console.error( stderr ); - } - reject( - new Error( - `${ command } ${ args.join( - ' ' - ) } failed with code ${ code }` - ) - ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Main execution function. - */ -async function main() { - console.log( 'šŸ” Checking Gutenberg setup...' ); - - // Verify Gutenberg directory exists - if ( ! fs.existsSync( gutenbergDir ) ) { - console.error( 'āŒ Gutenberg directory not found at:', gutenbergDir ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); - process.exit( 1 ); - } - - // Verify node_modules exists - const nodeModulesPath = path.join( gutenbergDir, 'node_modules' ); - if ( ! fs.existsSync( nodeModulesPath ) ) { - console.error( 'āŒ Gutenberg dependencies not installed' ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); - process.exit( 1 ); - } - - console.log( 'āœ… Gutenberg directory found' ); - - // Modify Gutenberg's package.json for Core build - console.log( '\nāš™ļø Configuring build for WordPress Core...' ); - const gutenbergPackageJsonPath = path.join( gutenbergDir, 'package.json' ); - - try { - const content = fs.readFileSync( gutenbergPackageJsonPath, 'utf8' ); - const gutenbergPackageJson = JSON.parse( content ); - - // Set Core environment variables - gutenbergPackageJson.config = gutenbergPackageJson.config || {}; - gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; - gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; - - // Set wpPlugin.name for Core naming convention - gutenbergPackageJson.wpPlugin = gutenbergPackageJson.wpPlugin || {}; - gutenbergPackageJson.wpPlugin.name = 'wp'; - - fs.writeFileSync( - gutenbergPackageJsonPath, - JSON.stringify( gutenbergPackageJson, null, '\t' ) + '\n' - ); - - console.log( ' āœ… IS_GUTENBERG_PLUGIN = false' ); - console.log( ' āœ… IS_WORDPRESS_CORE = true' ); - console.log( ' āœ… wpPlugin.name = wp' ); - } catch ( error ) { - console.error( - 'āŒ Error modifying Gutenberg package.json:', - error.message - ); - process.exit( 1 ); - } - - // Build Gutenberg - console.log( '\nšŸ”Ø Building Gutenberg for WordPress Core...' ); - console.log( ' (This may take a few minutes)' ); - - const startTime = Date.now(); - - try { - // Invoke the build script directly with node instead of going through - // `npm run build --` to avoid shell argument mangling of the base-url - // value (which contains spaces, parentheses, and single quotes). - // The PATH is extended with node_modules/.bin so that bin commands - // like `wp-build` are found, matching what npm would normally provide. - const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); - await exec( 'node', [ - 'bin/build.mjs', - '--skip-types', - "--base-url=includes_url( 'build/' )", - ], { - cwd: gutenbergDir, - env: { - ...process.env, - PATH: binPath + path.delimiter + process.env.PATH, - }, - } ); - - const duration = Math.round( ( Date.now() - startTime ) / 1000 ); - console.log( `āœ… Build completed in ${ duration }s` ); - } catch ( error ) { - console.error( 'āŒ Build failed:', error.message ); - throw error; - } finally { - // Restore Gutenberg's package.json regardless of success or failure - await restorePackageJson(); - } -} - -/** - * Restore Gutenberg's package.json to its original state. - */ -async function restorePackageJson() { - console.log( '\nšŸ”„ Restoring Gutenberg package.json...' ); - try { - await exec( 'git', [ 'checkout', '--', 'package.json' ], { - cwd: gutenbergDir, - } ); - console.log( 'āœ… package.json restored' ); - } catch ( error ) { - console.warn( 'āš ļø Could not restore package.json:', error.message ); - } -} - -// Run main function -main().catch( ( error ) => { - console.error( 'āŒ Unexpected error:', error ); - process.exit( 1 ); -} ); From 956f1e2357ed07588f8c050b058843bfa0e0c9cb Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:12:31 -0500 Subject: [PATCH 02/53] Remove Sync Gutenberg script This is no longer necessary because a Git repository is no longer used. --- Gruntfile.js | 11 --- tools/gutenberg/sync-gutenberg.js | 149 ------------------------------ 2 files changed, 160 deletions(-) delete mode 100644 tools/gutenberg/sync-gutenberg.js diff --git a/Gruntfile.js b/Gruntfile.js index 96b7eca8dfe39..17081192ee9ab 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1491,17 +1491,6 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-sync', 'Syncs Gutenberg checkout and build if ref has changed.', function() { - const done = this.async(); - grunt.util.spawn( { - cmd: 'node', - args: [ 'tools/gutenberg/sync-gutenberg.js' ], - opts: { stdio: 'inherit' } - }, function( error ) { - done( ! error ); - } ); - } ); - grunt.registerTask( 'copy-vendor-scripts', 'Copies vendor scripts from node_modules to wp-includes/js/dist/vendor/.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js deleted file mode 100644 index 814188d920cfa..0000000000000 --- a/tools/gutenberg/sync-gutenberg.js +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env node - -/** - * Sync Gutenberg Script - * - * This script ensures Gutenberg is checked out and built for the correct ref. - * It follows the same pattern as install-changed: - * - Stores the built ref in .gutenberg-hash - * - Compares current package.json ref with stored hash - * - Only runs checkout + build when they differ - * - * @package WordPress - */ - -const { spawn } = require( 'child_process' ); -const fs = require( 'fs' ); -const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const gutenbergDir = path.join( rootDir, 'gutenberg' ); -const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -const packageJsonPath = path.join( rootDir, 'package.json' ); -const hashFilePath = path.join( rootDir, '.gutenberg-hash' ); - -/** - * Execute a command and return a promise. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves when command completes. - */ -function exec( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: 'inherit', - shell: process.platform === 'win32', - ...options, - } ); - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( - new Error( - `${ command } ${ args.join( ' ' ) } failed with code ${ code }` - ) - ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Read the expected Gutenberg ref from package.json. - * - * @return {string} The Gutenberg ref. - */ -function getExpectedRef() { - const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); - const ref = packageJson.gutenberg?.ref; - - if ( ! ref ) { - throw new Error( 'Missing "gutenberg.ref" in package.json' ); - } - - return ref; -} - -/** - * Read the stored hash from .gutenberg-hash file. - * - * @return {string|null} The stored ref, or null if file doesn't exist. - */ -function getStoredHash() { - try { - return fs.readFileSync( hashFilePath, 'utf8' ).trim(); - } catch ( error ) { - return null; - } -} - -/** - * Write the ref to .gutenberg-hash file. - * - * @param {string} ref - The ref to store. - */ -function writeHash( ref ) { - fs.writeFileSync( hashFilePath, ref + '\n' ); -} - -/** - * Check if Gutenberg build exists. - * - * @return {boolean} True if build directory exists. - */ -function hasBuild() { - return fs.existsSync( gutenbergBuildDir ); -} - -/** - * Main execution function. - */ -async function main() { - console.log( 'šŸ” Checking Gutenberg sync status...' ); - - const expectedRef = getExpectedRef(); - const storedHash = getStoredHash(); - - console.log( ` Expected ref: ${ expectedRef }` ); - console.log( ` Stored hash: ${ storedHash || '(none)' }` ); - - // Check if we need to rebuild - if ( storedHash === expectedRef && hasBuild() ) { - console.log( 'āœ… Gutenberg is already synced and built' ); - return; - } - - if ( storedHash !== expectedRef ) { - console.log( '\nšŸ“¦ Gutenberg ref has changed, rebuilding...' ); - } else { - console.log( '\nšŸ“¦ Gutenberg build not found, building...' ); - } - - // Run checkout - console.log( '\nšŸ”„ Running gutenberg:checkout...' ); - await exec( 'node', [ 'tools/gutenberg/checkout-gutenberg.js' ] ); - - // Run build - console.log( '\nšŸ”„ Running gutenberg:build...' ); - await exec( 'node', [ 'tools/gutenberg/build-gutenberg.js' ] ); - - // Write the hash after successful build - writeHash( expectedRef ); - console.log( `\nāœ… Updated .gutenberg-hash to ${ expectedRef }` ); - - console.log( '\nāœ… Gutenberg sync complete!' ); -} - -// Run main function -main().catch( ( error ) => { - console.error( 'āŒ Sync failed:', error.message ); - process.exit( 1 ); -} ); From 26f36c64e5663fb9a5da59de212ef812adaf209d Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:33:31 -0500 Subject: [PATCH 03/53] Remove the need to clone the Gutenberg repository. This switches to downloading a prebuilt zip file uploaded to the GitHub Container Registry for a given commit instead. This eliminates the need to run any Gutenberg build scripts within `wordpress-develop`. --- tools/gutenberg/checkout-gutenberg.js | 218 ++++++++++---------------- 1 file changed, 80 insertions(+), 138 deletions(-) diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/checkout-gutenberg.js index 42e35a1967b78..93b5a1dbd7f89 100644 --- a/tools/gutenberg/checkout-gutenberg.js +++ b/tools/gutenberg/checkout-gutenberg.js @@ -3,14 +3,12 @@ /** * Checkout Gutenberg Repository Script * - * This script checks out the Gutenberg repository at a specific commit/branch/tag - * as specified in the root package.json's "gutenberg" configuration. + * This script downloads a pre-built Gutenberg zip artifact from the GitHub + * Container Registry and extracts it into the ./gutenberg directory. * - * It handles: - * - Initial clone if directory doesn't exist - * - Updating existing checkout to correct ref - * - Installing dependencies with npm ci - * - Idempotent operation (safe to run multiple times) + * The artifact is identified by the "gutenberg.ref" SHA in the root + * package.json, which is used as the OCI image tag for the gutenberg-build + * package on GHCR. * * @package WordPress */ @@ -19,109 +17,46 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -// Constants -const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git'; - // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const packageJsonPath = path.join( rootDir, 'package.json' ); +// GHCR configuration +const GHCR_REPO = 'desrosj/gutenberg/gutenberg-build'; + /** - * Execute a command and return a promise. - * Captures output and only displays it on failure for cleaner logs. + * Execute a command, streaming stdio directly so progress is visible. * * @param {string} command - Command to execute. * @param {string[]} args - Command arguments. * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves when command completes. + * @return {Promise} Promise that resolves with stdout when command completes successfully. */ function exec( command, args, options = {} ) { return new Promise( ( resolve, reject ) => { let stdout = ''; - let stderr = ''; const child = spawn( command, args, { cwd: options.cwd || rootDir, - stdio: [ 'ignore', 'pipe', 'pipe' ], - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files + stdio: options.captureOutput ? [ 'ignore', 'pipe', 'inherit' ] : 'inherit', + shell: process.platform === 'win32', ...options, } ); - // Capture output - if ( child.stdout ) { + if ( options.captureOutput && child.stdout ) { child.stdout.on( 'data', ( data ) => { stdout += data.toString(); } ); } - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - child.on( 'close', ( code ) => { if ( code !== 0 ) { - // Show output only on failure - if ( stdout ) { - console.error( '\nCommand output:' ); - console.error( stdout ); - } - if ( stderr ) { - console.error( '\nCommand errors:' ); - console.error( stderr ); - } reject( new Error( - `${ command } ${ args.join( - ' ' - ) } failed with code ${ code }` + `${ command } ${ args.join( ' ' ) } failed with code ${ code }` ) ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Execute a command and capture its output. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves with command output. - */ -function execOutput( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files - ...options, - } ); - - let stdout = ''; - let stderr = ''; - - if ( child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( new Error( `${ command } failed: ${ stderr }` ) ); } else { resolve( stdout.trim() ); } @@ -137,7 +72,7 @@ function execOutput( command, args, options = {} ) { async function main() { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg ref from package.json + // Read Gutenberg ref from package.json. let ref; try { const packageJson = JSON.parse( @@ -149,87 +84,94 @@ async function main() { throw new Error( 'Missing "gutenberg.ref" in package.json' ); } - console.log( ` Repository: ${ GUTENBERG_REPO }` ); console.log( ` Reference: ${ ref }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); } - // Check if Gutenberg directory exists - const gutenbergExists = fs.existsSync( gutenbergDir ); - - if ( ! gutenbergExists ) { - console.log( '\nšŸ“„ Cloning Gutenberg repository (shallow clone)...' ); - try { - // Generic shallow clone approach that works for both branches and commit hashes - // 1. Clone with no checkout and shallow depth - await exec( 'git', [ - 'clone', - '--depth', - '1', - '--no-checkout', - GUTENBERG_REPO, - 'gutenberg', - ] ); - - // 2. Fetch the specific ref with depth 1 (works for branches, tags, and commits) - await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { - cwd: gutenbergDir, - } ); + const zipName = `gutenberg-${ ref }.zip`; + const zipPath = path.join( rootDir, zipName ); - // 3. Checkout FETCH_HEAD - await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { - cwd: gutenbergDir, - } ); - - console.log( 'āœ… Cloned successfully' ); - } catch ( error ) { - console.error( 'āŒ Clone failed:', error.message ); - process.exit( 1 ); + // Step 1: Get an anonymous GHCR token for pulling. + console.log( '\nšŸ”‘ Fetching GHCR token...' ); + let token; + try { + const tokenJson = await exec( 'curl', [ + '--silent', + '--fail', + `https://ghcr.io/token?scope=repository:${ GHCR_REPO }:pull&service=ghcr.io`, + ], { captureOutput: true } ); + token = JSON.parse( tokenJson ).token; + if ( ! token ) { + throw new Error( 'No token in response' ); } - } else { - console.log( '\nāœ… Gutenberg directory already exists' ); + console.log( 'āœ… Token acquired' ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch token:', error.message ); + process.exit( 1 ); } - // Fetch and checkout target ref - console.log( `\nšŸ“” Fetching and checking out: ${ ref }` ); + // Step 2: Get the manifest to find the blob digest. + console.log( `\nšŸ“‹ Fetching manifest for ${ ref }...` ); + let digest; try { - // Fetch the specific ref (works for branches, tags, and commit hashes) - await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { - cwd: gutenbergDir, - } ); - - // Checkout what was just fetched - await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { - cwd: gutenbergDir, - } ); - - console.log( 'āœ… Checked out successfully' ); + const manifestJson = await exec( 'curl', [ + '--silent', + '--fail', + '--header', `Authorization: Bearer ${ token }`, + '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', + `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ ref }`, + ], { captureOutput: true } ); + const manifest = JSON.parse( manifestJson ); + digest = manifest?.layers?.[ 0 ]?.digest; + if ( ! digest ) { + throw new Error( 'No layer digest found in manifest' ); + } + console.log( `āœ… Blob digest: ${ digest }` ); } catch ( error ) { - console.error( 'āŒ Fetch/checkout failed:', error.message ); + console.error( 'āŒ Failed to fetch manifest:', error.message ); process.exit( 1 ); } - // Install dependencies - console.log( '\nšŸ“¦ Installing dependencies...' ); - const nodeModulesExists = fs.existsSync( - path.join( gutenbergDir, 'node_modules' ) - ); + // Step 3: Download the blob (the zip file). + console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + try { + await exec( 'curl', [ + '--fail', + '--location', + '--header', `Authorization: Bearer ${ token }`, + '--output', zipPath, + `https://ghcr.io/v2/${ GHCR_REPO }/blobs/${ digest }`, + ] ); + console.log( 'āœ… Download complete' ); + } catch ( error ) { + console.error( 'āŒ Download failed:', error.message ); + process.exit( 1 ); + } - if ( ! nodeModulesExists ) { - console.log( ' (This may take a few minutes on first run)' ); + // Remove existing gutenberg directory so the unzip is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); } + fs.mkdirSync( gutenbergDir, { recursive: true } ); + + // Extract the zip into ./gutenberg. + console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); try { - await exec( 'npm', [ 'ci' ], { cwd: gutenbergDir } ); - console.log( 'āœ… Dependencies installed' ); + await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); + console.log( 'āœ… Extraction complete' ); } catch ( error ) { - console.error( 'āŒ npm ci failed:', error.message ); + console.error( 'āŒ Extraction failed:', error.message ); process.exit( 1 ); } - console.log( '\nāœ… Gutenberg checkout complete!' ); + // Clean up the zip file. + fs.rmSync( zipPath ); + + console.log( '\nāœ… Gutenberg download complete!' ); } // Run main function From a696ca78cc4f4de776eb8b0f09256f9a7afc89b9 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:50:00 -0500 Subject: [PATCH 04/53] Rename `checkout` related scripts to `download`. --- Gruntfile.js | 8 ++++---- package.json | 9 ++++----- .../{checkout-gutenberg.js => download-gutenberg.js} | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) rename tools/gutenberg/{checkout-gutenberg.js => download-gutenberg.js} (98%) diff --git a/Gruntfile.js b/Gruntfile.js index 17081192ee9ab..418b39c0a574c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1468,11 +1468,11 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. - grunt.registerTask( 'gutenberg-checkout', 'Checks out the Gutenberg repository.', function() { + grunt.registerTask( 'gutenberg-download', 'Downloads the Gutenberg build artifact.', function() { const done = this.async(); grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/checkout-gutenberg.js' ], + args: [ 'tools/gutenberg/download-gutenberg.js' ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); @@ -1932,7 +1932,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-sync', + 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'build:certificates' @@ -1944,7 +1944,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-sync', + 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'replace:source-maps', diff --git a/package.json b/package.json index 9c310779ecbea..bd4cea56a9fde 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "23b566c72e9c4a36219ef5d6e62890f05551f6cb" + "ref": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" }, "engines": { "node": ">=20.10.0", @@ -106,7 +106,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:sync && npm run gutenberg:copy -- --dev", + "postinstall": "npm run gutenberg:download && npm run gutenberg:copy -- --dev", "build": "grunt build", "build:dev": "grunt build --dev", "dev": "grunt watch --dev", @@ -131,11 +131,10 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", - "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", + "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", - "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js", "vendor:copy": "node tools/vendors/copy-vendors.js", "sync-gutenberg-packages": "grunt sync-gutenberg-packages", "postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build" } -} \ No newline at end of file +} diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/download-gutenberg.js similarity index 98% rename from tools/gutenberg/checkout-gutenberg.js rename to tools/gutenberg/download-gutenberg.js index 93b5a1dbd7f89..49a4fd974b5e8 100644 --- a/tools/gutenberg/checkout-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -6,7 +6,7 @@ * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * - * The artifact is identified by the "gutenberg.ref" SHA in the root + * The artifact is identified by the "gutenberg.ref" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build * package on GHCR. * From b66772e01d0b246f11641ef2fd7bd6534624847c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 14:51:53 -0500 Subject: [PATCH 05/53] Rename `ref` to `sha`. A REF is typically a human-readable path to a branch where as a SHA is a hash representing an individual commit. Since the latter is what's being used here, this aims to avoid confusion. --- package.json | 2 +- tools/gutenberg/download-gutenberg.js | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index bd4cea56a9fde..7a6d92121ecfe 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "ref": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" + "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 49a4fd974b5e8..a9ed00e7644ac 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -6,7 +6,7 @@ * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * - * The artifact is identified by the "gutenberg.ref" value in the root + * The artifact is identified by the "gutenberg.sha" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build * package on GHCR. * @@ -72,25 +72,25 @@ function exec( command, args, options = {} ) { async function main() { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg ref from package.json. - let ref; + // Read Gutenberg SHA from package.json. + let sha; try { const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); - ref = packageJson.gutenberg?.ref; + sha = packageJson.gutenberg?.sha; - if ( ! ref ) { - throw new Error( 'Missing "gutenberg.ref" in package.json' ); + if ( ! sha ) { + throw new Error( 'Missing "gutenberg.sha" in package.json' ); } - console.log( ` Reference: ${ ref }` ); + console.log( ` SHA: ${ sha }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); } - const zipName = `gutenberg-${ ref }.zip`; + const zipName = `gutenberg-${ sha }.zip`; const zipPath = path.join( rootDir, zipName ); // Step 1: Get an anonymous GHCR token for pulling. @@ -113,7 +113,7 @@ async function main() { } // Step 2: Get the manifest to find the blob digest. - console.log( `\nšŸ“‹ Fetching manifest for ${ ref }...` ); + console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); let digest; try { const manifestJson = await exec( 'curl', [ @@ -121,7 +121,7 @@ async function main() { '--fail', '--header', `Authorization: Bearer ${ token }`, '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ ref }`, + `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ sha }`, ], { captureOutput: true } ); const manifest = JSON.parse( manifestJson ); digest = manifest?.layers?.[ 0 ]?.digest; From 1b63326371d73c3bf87ead5af924aa82a573d002 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:02:51 -0500 Subject: [PATCH 06/53] Move the container registry reference out of code. --- Gruntfile.js | 8 ++++++-- package.json | 3 ++- tools/gutenberg/download-gutenberg.js | 25 +++++++++++++++---------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 418b39c0a574c..428a8046d532c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1468,11 +1468,15 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. - grunt.registerTask( 'gutenberg-download', 'Downloads the Gutenberg build artifact.', function() { + grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); + const args = [ 'tools/gutenberg/download-gutenberg.js' ]; + if ( grunt.option( 'force' ) ) { + args.push( '--force' ); + } grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/download-gutenberg.js' ], + args, opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); diff --git a/package.json b/package.json index 7a6d92121ecfe..06298bcb2bbbc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49" + "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49", + "ghcrRepo": "Gdesrosj/gutenberg/gutenberg-build" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index a9ed00e7644ac..3a0d158884d54 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -8,7 +8,7 @@ * * The artifact is identified by the "gutenberg.sha" value in the root * package.json, which is used as the OCI image tag for the gutenberg-build - * package on GHCR. + * package on GitHub Container Registry. * * @package WordPress */ @@ -22,9 +22,6 @@ const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const packageJsonPath = path.join( rootDir, 'package.json' ); -// GHCR configuration -const GHCR_REPO = 'desrosj/gutenberg/gutenberg-build'; - /** * Execute a command, streaming stdio directly so progress is visible. * @@ -68,23 +65,31 @@ function exec( command, args, options = {} ) { /** * Main execution function. + * + * @param {boolean} force - Whether to force a fresh download even if the gutenberg directory exists. */ -async function main() { +async function main( force ) { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg SHA from package.json. - let sha; + // Read Gutenberg configuration from package.json. + let sha, ghcrRepo; try { const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); sha = packageJson.gutenberg?.sha; + ghcrRepo = packageJson.gutenberg?.ghcrRepo; if ( ! sha ) { throw new Error( 'Missing "gutenberg.sha" in package.json' ); } + if ( ! ghcrRepo ) { + throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); + } + console.log( ` SHA: ${ sha }` ); + console.log( ` GHCR repository: ${ ghcrRepo }` ); } catch ( error ) { console.error( 'āŒ Error reading package.json:', error.message ); process.exit( 1 ); @@ -100,7 +105,7 @@ async function main() { const tokenJson = await exec( 'curl', [ '--silent', '--fail', - `https://ghcr.io/token?scope=repository:${ GHCR_REPO }:pull&service=ghcr.io`, + `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, ], { captureOutput: true } ); token = JSON.parse( tokenJson ).token; if ( ! token ) { @@ -121,7 +126,7 @@ async function main() { '--fail', '--header', `Authorization: Bearer ${ token }`, '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ sha }`, + `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, ], { captureOutput: true } ); const manifest = JSON.parse( manifestJson ); digest = manifest?.layers?.[ 0 ]?.digest; @@ -142,7 +147,7 @@ async function main() { '--location', '--header', `Authorization: Bearer ${ token }`, '--output', zipPath, - `https://ghcr.io/v2/${ GHCR_REPO }/blobs/${ digest }`, + `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, ] ); console.log( 'āœ… Download complete' ); } catch ( error ) { From aed9824b251c21b485122ae88fc02a4e6e9b85f3 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:06:39 -0500 Subject: [PATCH 07/53] Don't redownload without `--force` --- tools/gutenberg/download-gutenberg.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 3a0d158884d54..eb38701e6fdb7 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -95,6 +95,12 @@ async function main( force ) { process.exit( 1 ); } + // Skip download if the gutenberg directory already exists and --force is not set. + if ( ! force && fs.existsSync( gutenbergDir ) ) { + console.log( '\nāœ… The gutenberg directory already exists. Use --force to re-download.' ); + return; + } + const zipName = `gutenberg-${ sha }.zip`; const zipPath = path.join( rootDir, zipName ); @@ -180,7 +186,8 @@ async function main( force ) { } // Run main function -main().catch( ( error ) => { +const force = process.argv.includes( '--force' ); +main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); process.exit( 1 ); } ); From f120d19ca3673c3130bf668f201f995a55cbe780 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:14:53 -0500 Subject: [PATCH 08/53] Add verification of the SHA value in hash file. --- package.json | 2 +- tools/gutenberg/download-gutenberg.js | 167 +++++++++++++++----------- 2 files changed, 96 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 06298bcb2bbbc..bf2ae0b53de08 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "gutenberg": { "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49", - "ghcrRepo": "Gdesrosj/gutenberg/gutenberg-build" + "ghcrRepo": "desrosj/gutenberg/gutenberg-build" }, "engines": { "node": ">=20.10.0", diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index eb38701e6fdb7..f3ac989132256 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -96,93 +96,116 @@ async function main( force ) { } // Skip download if the gutenberg directory already exists and --force is not set. + let downloaded = false; if ( ! force && fs.existsSync( gutenbergDir ) ) { - console.log( '\nāœ… The gutenberg directory already exists. Use --force to re-download.' ); - return; - } + console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg-download -- --force` to download a fresh copy.' ); + } else { + downloaded = true; + const zipName = `gutenberg-${ sha }.zip`; + const zipPath = path.join( rootDir, zipName ); + + // Step 1: Get an anonymous GHCR token for pulling. + console.log( '\nšŸ”‘ Fetching GHCR token...' ); + let token; + try { + const tokenJson = await exec( 'curl', [ + '--silent', + '--fail', + `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, + ], { captureOutput: true } ); + token = JSON.parse( tokenJson ).token; + if ( ! token ) { + throw new Error( 'No token in response' ); + } + console.log( 'āœ… Token acquired' ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch token:', error.message ); + process.exit( 1 ); + } - const zipName = `gutenberg-${ sha }.zip`; - const zipPath = path.join( rootDir, zipName ); + // Step 2: Get the manifest to find the blob digest. + console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); + let digest; + try { + const manifestJson = await exec( 'curl', [ + '--silent', + '--fail', + '--header', `Authorization: Bearer ${ token }`, + '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', + `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, + ], { captureOutput: true } ); + const manifest = JSON.parse( manifestJson ); + digest = manifest?.layers?.[ 0 ]?.digest; + if ( ! digest ) { + throw new Error( 'No layer digest found in manifest' ); + } + console.log( `āœ… Blob digest: ${ digest }` ); + } catch ( error ) { + console.error( 'āŒ Failed to fetch manifest:', error.message ); + process.exit( 1 ); + } - // Step 1: Get an anonymous GHCR token for pulling. - console.log( '\nšŸ”‘ Fetching GHCR token...' ); - let token; - try { - const tokenJson = await exec( 'curl', [ - '--silent', - '--fail', - `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, - ], { captureOutput: true } ); - token = JSON.parse( tokenJson ).token; - if ( ! token ) { - throw new Error( 'No token in response' ); + // Step 3: Download the blob (the zip file). + console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + try { + await exec( 'curl', [ + '--fail', + '--location', + '--header', `Authorization: Bearer ${ token }`, + '--output', zipPath, + `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, + ] ); + console.log( 'āœ… Download complete' ); + } catch ( error ) { + console.error( 'āŒ Download failed:', error.message ); + process.exit( 1 ); } - console.log( 'āœ… Token acquired' ); - } catch ( error ) { - console.error( 'āŒ Failed to fetch token:', error.message ); - process.exit( 1 ); - } - // Step 2: Get the manifest to find the blob digest. - console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); - let digest; - try { - const manifestJson = await exec( 'curl', [ - '--silent', - '--fail', - '--header', `Authorization: Bearer ${ token }`, - '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, - ], { captureOutput: true } ); - const manifest = JSON.parse( manifestJson ); - digest = manifest?.layers?.[ 0 ]?.digest; - if ( ! digest ) { - throw new Error( 'No layer digest found in manifest' ); + // Remove existing gutenberg directory so the unzip is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); } - console.log( `āœ… Blob digest: ${ digest }` ); - } catch ( error ) { - console.error( 'āŒ Failed to fetch manifest:', error.message ); - process.exit( 1 ); - } - // Step 3: Download the blob (the zip file). - console.log( `\nšŸ“„ Downloading ${ zipName }...` ); - try { - await exec( 'curl', [ - '--fail', - '--location', - '--header', `Authorization: Bearer ${ token }`, - '--output', zipPath, - `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, - ] ); - console.log( 'āœ… Download complete' ); - } catch ( error ) { - console.error( 'āŒ Download failed:', error.message ); - process.exit( 1 ); - } + fs.mkdirSync( gutenbergDir, { recursive: true } ); - // Remove existing gutenberg directory so the unzip is clean. - if ( fs.existsSync( gutenbergDir ) ) { - console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); - fs.rmSync( gutenbergDir, { recursive: true, force: true } ); - } + // Extract the zip into ./gutenberg. + console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); + try { + await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); + console.log( 'āœ… Extraction complete' ); + } catch ( error ) { + console.error( 'āŒ Extraction failed:', error.message ); + process.exit( 1 ); + } - fs.mkdirSync( gutenbergDir, { recursive: true } ); + // Clean up the zip file. + fs.rmSync( zipPath ); + } - // Extract the zip into ./gutenberg. - console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); + // Verify the downloaded version matches the expected SHA. + console.log( '\nšŸ” Verifying Gutenberg version...' ); + const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); try { - await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); - console.log( 'āœ… Extraction complete' ); + const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); + if ( installedHash !== sha ) { + throw new Error( + `SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + ); + } + console.log( 'āœ… Version verified' ); } catch ( error ) { - console.error( 'āŒ Extraction failed:', error.message ); + if ( error.code === 'ENOENT' ) { + console.error( `āŒ ${ hashFilePath } not found. The downloaded artifact may be malformed.` ); + } else { + console.error( `āŒ ${ error.message }` ); + } process.exit( 1 ); } - // Clean up the zip file. - fs.rmSync( zipPath ); - - console.log( '\nāœ… Gutenberg download complete!' ); + if ( downloaded ) { + console.log( '\nāœ… Gutenberg download complete!' ); + } } // Run main function From 2e3f5069eadc42d31fcd5ac3410638d2bcffb1ff Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:24:50 -0500 Subject: [PATCH 09/53] Use the most recent hash from test packages. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf2ae0b53de08..8a87704a7714e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "e0c5fc81de25a4f837c063ca2c2db32d74698a49", + "sha": "b296f85d49a0e9e47753a64c31c355a56104e852", "ghcrRepo": "desrosj/gutenberg/gutenberg-build" }, "engines": { From 6dba1aad4ad038e00b24baf02af4930563046c1b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:33:22 -0500 Subject: [PATCH 10/53] Switch to using the GHCR package from `gutenberg`. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8a87704a7714e..9928ca5a9dbd1 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "b296f85d49a0e9e47753a64c31c355a56104e852", - "ghcrRepo": "desrosj/gutenberg/gutenberg-build" + "sha": "a0005e02051fd5b65a407d8b6d0fabfbe579b265", + "ghcrRepo": "WordPress/gutenberg/gutenberg-build" }, "engines": { "node": ">=20.10.0", From de7eb2d1c8599a0a70e69e1771c002b113d59faa Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:11:28 -0500 Subject: [PATCH 11/53] Update hash value. The Gutenberg repository has been updated to include the icon assets in the plugin build. See github.com/WordPress/gutenberg/pull/75866. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9928ca5a9dbd1..32d44a08bbf0d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "a0005e02051fd5b65a407d8b6d0fabfbe579b265", + "sha": "03b95683ab264e18908bdc4b789bf104d69cb2d3", "ghcrRepo": "WordPress/gutenberg/gutenberg-build" }, "engines": { From d21381f5394000974362e690e3476b01a64404fb Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:24:13 -0500 Subject: [PATCH 12/53] Apply suggestions from code review. - Add missing hard stop at the end of inline comment. - Make use if `fetch()` instead of `exec( 'curl' )`. Co-authored-by: Weston Ruter --- tools/gutenberg/download-gutenberg.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index f3ac989132256..356a568693f8a 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -108,12 +108,12 @@ async function main( force ) { console.log( '\nšŸ”‘ Fetching GHCR token...' ); let token; try { - const tokenJson = await exec( 'curl', [ - '--silent', - '--fail', - `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io`, - ], { captureOutput: true } ); - token = JSON.parse( tokenJson ).token; + const response = await fetch( `https://ghcr.io/token?scope=repository:${ghcrRepo}:pull&service=ghcr.io` ); + if ( ! response.ok ) { + throw new Error( `Failed to fetch token: ${response.status} ${response.statusText}` ); + } + const data = await response.json(); + token = data.token; if ( ! token ) { throw new Error( 'No token in response' ); } @@ -208,7 +208,7 @@ async function main( force ) { } } -// Run main function +// Run main function. const force = process.argv.includes( '--force' ); main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); From 6b06b35bc321ec59e59477be0d8ebec5e128e1e8 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:52:34 -0500 Subject: [PATCH 13/53] Replace remaining instances of `exec( 'curl' )`. --- tools/gutenberg/download-gutenberg.js | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 356a568693f8a..559e880678ae3 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -127,14 +127,16 @@ async function main( force ) { console.log( `\nšŸ“‹ Fetching manifest for ${ sha }...` ); let digest; try { - const manifestJson = await exec( 'curl', [ - '--silent', - '--fail', - '--header', `Authorization: Bearer ${ token }`, - '--header', 'Accept: application/vnd.oci.image.manifest.v1+json', - `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, - ], { captureOutput: true } ); - const manifest = JSON.parse( manifestJson ); + const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, { + headers: { + Authorization: `Bearer ${ token }`, + Accept: 'application/vnd.oci.image.manifest.v1+json', + }, + } ); + if ( ! response.ok ) { + throw new Error( `Failed to fetch manifest: ${ response.status } ${ response.statusText }` ); + } + const manifest = await response.json(); digest = manifest?.layers?.[ 0 ]?.digest; if ( ! digest ) { throw new Error( 'No layer digest found in manifest' ); @@ -148,13 +150,16 @@ async function main( force ) { // Step 3: Download the blob (the zip file). console.log( `\nšŸ“„ Downloading ${ zipName }...` ); try { - await exec( 'curl', [ - '--fail', - '--location', - '--header', `Authorization: Bearer ${ token }`, - '--output', zipPath, - `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, - ] ); + const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { + headers: { + Authorization: `Bearer ${ token }`, + }, + } ); + if ( ! response.ok ) { + throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); + } + const buffer = await response.arrayBuffer(); + fs.writeFileSync( zipPath, Buffer.from( buffer ) ); console.log( 'āœ… Download complete' ); } catch ( error ) { console.error( 'āŒ Download failed:', error.message ); From 1589ee0b78e9a8365d8509848b09a92698a177b1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:49:07 -0500 Subject: [PATCH 14/53] Correctly include block PHP files. --- tools/gutenberg/copy-gutenberg-build.js | 27 +++++++++---------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 0d1c454ca8085..d654a937eac2c 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -18,7 +18,6 @@ const glob = require( 'glob' ); const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -const gutenbergPackagesDir = path.join( gutenbergDir, 'packages' ); // Determine build target from command line argument (--dev or --build-dir) // Default to 'src' for development @@ -80,14 +79,12 @@ const COPY_CONFIG = { name: 'block-library', scripts: 'scripts/block-library', styles: 'styles/block-library', - php: 'block-library/src', }, { // Widget blocks name: 'widgets', scripts: 'scripts/widgets/blocks', styles: 'styles/widgets', - php: 'widgets/src/blocks', }, ], }, @@ -207,7 +204,6 @@ function copyBlockAssets( config ) { for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); const stylesSrc = path.join( gutenbergBuildDir, source.styles ); - const phpSrc = path.join( gutenbergPackagesDir, source.php ); if ( ! fs.existsSync( scriptsSrc ) ) { continue; @@ -241,7 +237,7 @@ function copyBlockAssets( config ) { blockDest, { recursive: true, - // Skip PHP, copied from packages + // Skip PHP, copied from build in steps 3 & 4 filter: f => ! f.endsWith( '.php' ), } ); @@ -261,33 +257,28 @@ function copyBlockAssets( config ) { } } - // 3. Copy PHP from packages - const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' ); + // 3. Copy PHP from build (flat sibling file: .php) + const blockPhpSrc = path.join( scriptsSrc, `${ blockName }.php` ); if ( fs.existsSync( blockPhpSrc ) ) { const phpDest = path.join( wpIncludesDir, config.destination, `${ blockName }.php` ); - const content = fs.readFileSync( blockPhpSrc, 'utf8' ); - fs.writeFileSync( phpDest, content ); + fs.copyFileSync( blockPhpSrc, phpDest ); } - // 4. Copy PHP subdirectories from packages (e.g., shared/helpers.php) - const blockPhpDir = path.join( phpSrc, blockName ); + // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) + const blockPhpDir = path.join( scriptsSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { - const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { recursive: true, - filter: function hasPhpFiles( src ) { + filter: ( src ) => { const stat = fs.statSync( src ); if ( stat.isDirectory() ) { - return fs.readdirSync( src, { withFileTypes: true } ).some( - ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) - ); + return true; } - // Copy PHP files, but skip root index.php (handled by step 3) - return src.endsWith( '.php' ) && src !== rootIndex; + return src.endsWith( '.php' ); }, } ); } From 8ffc312929e79723f7a84de3ac0050936a397412 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:50:50 -0500 Subject: [PATCH 15/53] Update upstream package details. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 32d44a08bbf0d..645a3a4bb6528 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "03b95683ab264e18908bdc4b789bf104d69cb2d3", - "ghcrRepo": "WordPress/gutenberg/gutenberg-build" + "sha": "e74e92462c854641599cd2267cd7c3e462581b78", + "ghcrRepo": "WordPress/gutenberg/wordpress-develop-build" }, "engines": { "node": ">=20.10.0", From 1df2afbadbad61bc45734c279238d4594b5b92db Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:21:28 -0500 Subject: [PATCH 16/53] Change package name. --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 645a3a4bb6528..625ae69da837e 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "e74e92462c854641599cd2267cd7c3e462581b78", - "ghcrRepo": "WordPress/gutenberg/wordpress-develop-build" + "sha": "4e046f70c45d9ebece3da71d262a5bcbc9c18e190", + "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { "node": ">=20.10.0", From cf2501316d8c36b518c4a6df8b7b039b61a11fff Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:44:36 -0500 Subject: [PATCH 17/53] Update pinned hash. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 625ae69da837e..4b60afd911917 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "4e046f70c45d9ebece3da71d262a5bcbc9c18e190", + "sha": "a5b737234f685709485cc61941acd32f3f7feb61", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From adad173f20ed8e3489d3641583b721036adfc4a1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:05:53 -0500 Subject: [PATCH 18/53] Update commit hash. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b60afd911917..9f444367de662 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "a5b737234f685709485cc61941acd32f3f7feb61", + "sha": "ee87528714ebbb9b673b8ced17d5d84c4209eca0", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From 9849e91fefaf8041589cc7225e01e339d7182e26 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:18:16 -0500 Subject: [PATCH 19/53] Revert changes no longer necessary. --- tools/gutenberg/copy-gutenberg-build.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index d654a937eac2c..595f784d30ded 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -79,12 +79,14 @@ const COPY_CONFIG = { name: 'block-library', scripts: 'scripts/block-library', styles: 'styles/block-library', + php: 'scripts/block-library', }, { // Widget blocks name: 'widgets', scripts: 'scripts/widgets/blocks', styles: 'styles/widgets', + php: 'scripts/widgets/blocks', }, ], }, @@ -204,6 +206,7 @@ function copyBlockAssets( config ) { for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); const stylesSrc = path.join( gutenbergBuildDir, source.styles ); + const phpSrc = source.php; if ( ! fs.existsSync( scriptsSrc ) ) { continue; @@ -257,8 +260,8 @@ function copyBlockAssets( config ) { } } - // 3. Copy PHP from build (flat sibling file: .php) - const blockPhpSrc = path.join( scriptsSrc, `${ blockName }.php` ); + // 3. Copy PHP from build + const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' ); if ( fs.existsSync( blockPhpSrc ) ) { const phpDest = path.join( wpIncludesDir, @@ -269,16 +272,19 @@ function copyBlockAssets( config ) { } // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) - const blockPhpDir = path.join( scriptsSrc, blockName ); + const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { fs.cpSync( blockPhpDir, blockDest, { recursive: true, - filter: ( src ) => { + filter: function hasPhpFiles( src ) => { const stat = fs.statSync( src ); if ( stat.isDirectory() ) { - return true; + return fs.readdirSync( src, { withFileTypes: true } ).some( + ( entry ) => hasPhpFiles( path.join( src, entry.name ) ) + ); } - return src.endsWith( '.php' ); + // Copy PHP files, but skip root index.php (handled by step 3) + return src.endsWith( '.php' ) && src !== rootIndex; }, } ); } From 13946e511e0a08e7bbcdbacb0140dfcfc77993b7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:21:34 -0500 Subject: [PATCH 20/53] A few more reverts. --- tools/gutenberg/copy-gutenberg-build.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 595f784d30ded..78a896625b20c 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -274,9 +274,10 @@ function copyBlockAssets( config ) { // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { + const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { recursive: true, - filter: function hasPhpFiles( src ) => { + filter: function hasPhpFiles( src ) { const stat = fs.statSync( src ); if ( stat.isDirectory() ) { return fs.readdirSync( src, { withFileTypes: true } ).some( From 59e3cb2fdf416b6580dee48a62b593a50a3c5b5b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:37:11 -0500 Subject: [PATCH 21/53] Built block files are different. --- tools/gutenberg/copy-gutenberg-build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 78a896625b20c..695d4c6f571b9 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -261,7 +261,7 @@ function copyBlockAssets( config ) { } // 3. Copy PHP from build - const blockPhpSrc = path.join( phpSrc, blockName, 'index.php' ); + const blockPhpSrc = path.join( phpSrc, `${ blockName }.php` ); if ( fs.existsSync( blockPhpSrc ) ) { const phpDest = path.join( wpIncludesDir, From 218d38dd56b90cb83812fe9c00b429fdc2087285 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:07:46 -0500 Subject: [PATCH 22/53] Correctly point to the nested block directories. --- tools/gutenberg/copy-gutenberg-build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 695d4c6f571b9..1e7858b6bdb29 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -272,7 +272,7 @@ function copyBlockAssets( config ) { } // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) - const blockPhpDir = path.join( phpSrc, blockName ); + const blockPhpDir = path.join( gutenbergBuildDir, phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { From 15cc9bcbd569824fe56a4d405f17d6e1be8bd2a8 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:25:36 -0500 Subject: [PATCH 23/53] Correct the destination path for blocks. --- tools/gutenberg/copy-gutenberg-build.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 1e7858b6bdb29..5e464ccec467a 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -266,6 +266,7 @@ function copyBlockAssets( config ) { const phpDest = path.join( wpIncludesDir, config.destination, + blockName, `${ blockName }.php` ); fs.copyFileSync( blockPhpSrc, phpDest ); From 0702c4089dab0e730996ac87cc3c468d601887ef Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Fri, 27 Feb 2026 20:15:47 -0500 Subject: [PATCH 24/53] Bug fixes. --- Gruntfile.js | 1 - tools/gutenberg/copy-gutenberg-build.js | 15 +++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 428a8046d532c..805114763c81b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -53,7 +53,6 @@ module.exports = function(grunt) { webpackFiles = [ 'wp-includes/assets/*', 'wp-includes/css/dist', - 'wp-includes/blocks/**/*.css', '!wp-includes/assets/script-loader-packages.min.php', '!wp-includes/assets/script-modules-packages.min.php', ], diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 5e464ccec467a..4a0f7f506024c 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -206,7 +206,7 @@ function copyBlockAssets( config ) { for ( const source of config.sources ) { const scriptsSrc = path.join( gutenbergBuildDir, source.scripts ); const stylesSrc = path.join( gutenbergBuildDir, source.styles ); - const phpSrc = source.php; + const phpSrc = path.join( gutenbergBuildDir, source.php ); if ( ! fs.existsSync( scriptsSrc ) ) { continue; @@ -262,18 +262,17 @@ function copyBlockAssets( config ) { // 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 ) ) { - const phpDest = path.join( - wpIncludesDir, - config.destination, - blockName, - `${ blockName }.php` - ); fs.copyFileSync( blockPhpSrc, phpDest ); } // 4. Copy PHP subdirectories from build (e.g., navigation-link/shared/*.php) - const blockPhpDir = path.join( gutenbergBuildDir, phpSrc, blockName ); + const blockPhpDir = path.join( phpSrc, blockName ); if ( fs.existsSync( blockPhpDir ) ) { const rootIndex = path.join( blockPhpDir, 'index.php' ); fs.cpSync( blockPhpDir, blockDest, { From 346e4df692484465680c1610cf9f91e3d4ffa0ca Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:10:43 -0500 Subject: [PATCH 25/53] Update Gutenberg hash. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9f444367de662..47ca24068a3cd 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "ee87528714ebbb9b673b8ced17d5d84c4209eca0", + "sha": "cd4cf58db37e9f774f321df14138dfef5d7e475a", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { From fdd1bc31cd1205bbf31f8caec352b5c3ad05e03e Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Sat, 28 Feb 2026 00:49:04 -0500 Subject: [PATCH 26/53] Address some review feedback. --- tools/gutenberg/download-gutenberg.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 559e880678ae3..d594e730dddd0 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -15,15 +15,17 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); +const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); -const packageJsonPath = path.join( rootDir, 'package.json' ); /** - * Execute a command, streaming stdio directly so progress is visible. + * Execute a command. By default, stdio is inherited so progress is visible in + * the terminal. When `options.captureOutput` is true, stdout is piped and the + * promise resolves with the captured stdout once the process exits. * * @param {string} command - Command to execute. * @param {string[]} args - Command arguments. @@ -74,9 +76,7 @@ async function main( force ) { // Read Gutenberg configuration from package.json. let sha, ghcrRepo; try { - const packageJson = JSON.parse( - fs.readFileSync( packageJsonPath, 'utf8' ) - ); + const packageJson = require( path.join( rootDir, 'package.json' ) ); sha = packageJson.gutenberg?.sha; ghcrRepo = packageJson.gutenberg?.ghcrRepo; @@ -158,8 +158,7 @@ async function main( force ) { if ( ! response.ok ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - const buffer = await response.arrayBuffer(); - fs.writeFileSync( zipPath, Buffer.from( buffer ) ); + await pipeline( response.body, fs.createWriteStream( zipPath ) ); console.log( 'āœ… Download complete' ); } catch ( error ) { console.error( 'āŒ Download failed:', error.message ); From dd6dfb45c0415c93e55b0655091a28ad1595513c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:11:45 -0500 Subject: [PATCH 27/53] Extract the verification logic out of download. --- Gruntfile.js | 15 ++++- tools/gutenberg/download-gutenberg.js | 46 ++++----------- tools/gutenberg/gutenberg-utils.js | 82 +++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 38 deletions(-) create mode 100644 tools/gutenberg/gutenberg-utils.js diff --git a/Gruntfile.js b/Gruntfile.js index 805114763c81b..eac925bca676b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1467,6 +1467,17 @@ module.exports = function(grunt) { } ); // Gutenberg integration tasks. + grunt.registerTask( 'gutenberg:verify', 'Verifies the installed Gutenberg version matches the expected SHA.', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/gutenberg-utils.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); const args = [ 'tools/gutenberg/download-gutenberg.js' ]; @@ -1932,22 +1943,22 @@ module.exports = function(grunt) { grunt.registerTask( 'build', function() { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ + 'gutenberg:verify', 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'build:certificates' ] ); } else { grunt.task.run( [ + 'gutenberg:verify', 'build:certificates', 'build:files', 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-download', 'gutenberg-copy', 'copy-vendor-scripts', 'replace:source-maps', diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index d594e730dddd0..360c39fbd237e 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -1,7 +1,7 @@ #!/usr/bin/env node /** - * Checkout Gutenberg Repository Script + * Download Gutenberg Repository Script. * * This script downloads a pre-built Gutenberg zip artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. @@ -17,10 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const gutenbergDir = path.join( rootDir, 'gutenberg' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './gutenberg-utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in @@ -73,21 +70,15 @@ function exec( command, args, options = {} ) { async function main( force ) { console.log( 'šŸ” Checking Gutenberg configuration...' ); - // Read Gutenberg configuration from package.json. + /* + * Read Gutenberg configuration from package.json. + * + * Note: ghcr stands for GitHub Container Registry where wordpress-develop ready builds of the Gutenberg plugin + * are published on every repository push event. + */ let sha, ghcrRepo; try { - const packageJson = require( path.join( rootDir, 'package.json' ) ); - sha = packageJson.gutenberg?.sha; - ghcrRepo = packageJson.gutenberg?.ghcrRepo; - - if ( ! sha ) { - throw new Error( 'Missing "gutenberg.sha" in package.json' ); - } - - if ( ! ghcrRepo ) { - throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); - } - + ( { sha, ghcrRepo } = readGutenbergConfig() ); console.log( ` SHA: ${ sha }` ); console.log( ` GHCR repository: ${ ghcrRepo }` ); } catch ( error ) { @@ -188,24 +179,7 @@ async function main( force ) { } // Verify the downloaded version matches the expected SHA. - console.log( '\nšŸ” Verifying Gutenberg version...' ); - const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); - try { - const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); - if ( installedHash !== sha ) { - throw new Error( - `SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` - ); - } - console.log( 'āœ… Version verified' ); - } catch ( error ) { - if ( error.code === 'ENOENT' ) { - console.error( `āŒ ${ hashFilePath } not found. The downloaded artifact may be malformed.` ); - } else { - console.error( `āŒ ${ error.message }` ); - } - process.exit( 1 ); - } + verifyGutenbergVersion(); if ( downloaded ) { console.log( '\nāœ… Gutenberg download complete!' ); diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/gutenberg-utils.js new file mode 100644 index 0000000000000..7040eb02e2dc3 --- /dev/null +++ b/tools/gutenberg/gutenberg-utils.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node + +/** + * Gutenberg build utilities. + * + * Shared helpers used by the Gutenberg download script. When run directly, + * verifies that the installed Gutenberg build matches the SHA in package.json. + * + * @package WordPress + */ + +const fs = require( 'fs' ); +const path = require( 'path' ); + +// Paths +const rootDir = path.resolve( __dirname, '../..' ); +const gutenbergDir = path.join( rootDir, 'gutenberg' ); + +/** + * Read Gutenberg configuration from package.json. + * + * @return {{ sha: string, ghcrRepo: string }} The Gutenberg configuration. + * @throws {Error} If the configuration is missing or invalid. + */ +function readGutenbergConfig() { + const packageJson = require( path.join( rootDir, 'package.json' ) ); + const sha = packageJson.gutenberg?.sha; + const ghcrRepo = packageJson.gutenberg?.ghcrRepo; + + if ( ! sha ) { + throw new Error( 'Missing "gutenberg.sha" in package.json' ); + } + + if ( ! ghcrRepo ) { + throw new Error( 'Missing "gutenberg.ghcrRepo" in package.json' ); + } + + return { sha, ghcrRepo }; +} + +/** + * Verify that the installed Gutenberg version matches the expected SHA in + * package.json. Logs progress to the console and exits with a non-zero code + * on failure. + */ +function verifyGutenbergVersion() { + console.log( '\nšŸ” Verifying Gutenberg version...' ); + + let sha; + try { + ( { sha } = readGutenbergConfig() ); + } catch ( error ) { + console.error( 'āŒ Error reading package.json:', error.message ); + process.exit( 1 ); + } + + const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); + try { + const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); + if ( installedHash !== sha ) { + console.error( + `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + ); + process.exit( 1 ); + } + } catch ( error ) { + if ( error.code === 'ENOENT' ) { + console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg-download\` to download Gutenberg.` ); + } else { + console.error( `āŒ ${ error.message }` ); + } + process.exit( 1 ); + } + + console.log( 'āœ… Version verified' ); +} + +module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion }; + +if ( require.main === module ) { + verifyGutenbergVersion(); +} From 9a9a604f368df43c5f3764140cd85232b3f3bcd9 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:36:21 -0500 Subject: [PATCH 28/53] Detect local changes to `gutenberg` directory This adds utility functions that calculate a hash when the plugin assets are downloaded and compares that hash to the actual directory files when building as it may cause unexpected results. --- tools/gutenberg/gutenberg-utils.js | 71 +++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/gutenberg-utils.js index 7040eb02e2dc3..95a607c9202fa 100644 --- a/tools/gutenberg/gutenberg-utils.js +++ b/tools/gutenberg/gutenberg-utils.js @@ -9,12 +9,14 @@ * @package WordPress */ +const crypto = require( 'crypto' ); const fs = require( 'fs' ); const path = require( 'path' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); +const gutenbergDirHashFile = path.join( rootDir, '.gutenberg-dir-hash' ); /** * Read Gutenberg configuration from package.json. @@ -75,8 +77,75 @@ function verifyGutenbergVersion() { console.log( 'āœ… Version verified' ); } -module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion }; +/** + * Calculate a hash of the Gutenberg directory and all its contents. + * + * This stores the hash of the Gutenberg directory in a `.gutenberg-hash` file + * to track when changes have been made to those files locally. + * + * Files are processed in sorted order so the result is deterministic. The hash + * incorporates each file's relative path and its contents. + */ +function hashGutenbergDir() { + const hash = crypto.createHash( 'sha256' ); + + /** + * Recursively collect all file paths under a directory, sorted. + * + * @param {string} dir - Directory to walk. + * @return {string[]} Sorted list of absolute file paths. + */ + function collectFiles( dir ) { + const files = []; + for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ).sort( ( a, b ) => a.name.localeCompare( b.name ) ) ) { + const fullPath = path.join( dir, entry.name ); + if ( entry.isDirectory() ) { + files.push( ...collectFiles( fullPath ) ); + } else { + files.push( fullPath ); + } + } + return files; + } + + for ( const filePath of collectFiles( gutenbergDir ) ) { + // Hash the relative path so the result is location-independent. + hash.update( path.relative( gutenbergDir, filePath ) ); + hash.update( fs.readFileSync( filePath ) ); + } + + const digest = hash.digest( 'hex' ); + fs.writeFileSync( gutenbergDirHashFile, digest ); + return digest; +} + +/** + * Checks for changes to the local gutenberg directory. + * + * This detects changes to the local gutenberg directory that have occurred since + * the directory was created, which may cause unexpected results. + */ +function checkGutenbergDirHash() { + if ( ! fs.existsSync( gutenbergDirHashFile ) ) { + console.warn( 'āš ļø .gutenberg-dir-hash not found. Files in the gutenberg directory may have changed since downloading.' ); + return; + } + + const storedHash = fs.readFileSync( gutenbergDirHashFile, 'utf8' ).trim(); + const currentHash = hashGutenbergDir(); + + if ( currentHash !== storedHash ) { + console.warn( 'āš ļø The gutenberg directory has changed since the last copy. The build scripts may produce unexpected results.' ); + return; + } + + console.log( 'āœ… The contents of the gutenberg directory have not been modified.' ); +} + +module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, hashGutenbergDir, checkGutenbergDirHash }; if ( require.main === module ) { verifyGutenbergVersion(); + + checkGutenbergDirHash(); } From cfbad947d9aee3e4e6a0952affb81d9d60c36535 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:36:53 -0500 Subject: [PATCH 29/53] Change hyphenated grunt tasks to use colons. --- Gruntfile.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index eac925bca676b..4a9bdbca46fdd 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1478,7 +1478,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-download', 'Downloads the built Gutenberg artifact.', function() { + grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); const args = [ 'tools/gutenberg/download-gutenberg.js' ]; if ( grunt.option( 'force' ) ) { @@ -1493,7 +1493,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg-copy', 'Copies Gutenberg build output to WordPress Core.', function() { + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; grunt.util.spawn( { @@ -1947,7 +1947,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-copy', + 'gutenberg:copy', 'copy-vendor-scripts', 'build:certificates' ] ); @@ -1959,7 +1959,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', - 'gutenberg-copy', + 'gutenberg:copy', 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' From 161f90cd16b0d4b68f69a693e997b24502a58918 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:45:46 -0500 Subject: [PATCH 30/53] Update inline comment within `base.neon` --- tests/phpstan/base.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index e3cc0beec92f0..68c548b44ab68 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -105,7 +105,7 @@ parameters: - ../../src/wp-includes/deprecated.php - ../../src/wp-includes/ms-deprecated.php - ../../src/wp-includes/pluggable-deprecated.php - # These files are sourced by wordpress/gutenberg in `tools/release/sync-stable-blocks.js`. + # These files are autogenerated by tools/gutenberg/copy-gutenberg-build.js. - ../../src/wp-includes/blocks # Third-party libraries. - ../../src/wp-admin/includes/class-ftp-pure.php From 88906312548f1fe533b351bb2960e73082ba6cef Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:40:50 -0500 Subject: [PATCH 31/53] Test approach to support git and zip --- .env.example | 16 ++ .gitignore | 1 + Gruntfile.js | 28 +++ package.json | 4 +- tools/gutenberg/build-gutenberg.js | 219 +++++++++++++++++++++ tools/gutenberg/checkout-gutenberg.js | 244 ++++++++++++++++++++++++ tools/gutenberg/copy-gutenberg-build.js | 3 +- tools/gutenberg/download-gutenberg.js | 8 +- tools/gutenberg/gutenberg-utils.js | 90 ++++++++- tools/gutenberg/sync-gutenberg.js | 60 ++++++ 10 files changed, 663 insertions(+), 10 deletions(-) create mode 100644 tools/gutenberg/build-gutenberg.js create mode 100644 tools/gutenberg/checkout-gutenberg.js create mode 100644 tools/gutenberg/sync-gutenberg.js diff --git a/.env.example b/.env.example index 76a4744165505..0b1f306d585c6 100644 --- a/.env.example +++ b/.env.example @@ -72,3 +72,19 @@ WP_BASE_URL=http://localhost:${LOCAL_PORT} # This silences the tips output by the dotenv package. ## DOTENV_CONFIG_QUIET=true + +## +# Local Gutenberg repository mode. +# +# When set to true, the build system will clone the Gutenberg git repository at +# the commit SHA specified in package.json instead of downloading a pre-built +# zip artifact. The repository will be built from source during `npm install` +# and before each `grunt build`. +# +# This is intended for contributors who need to work across both wordpress-develop +# and the Gutenberg repository simultaneously. +# +# Requirements: git, and a full Node.js environment capable of building Gutenberg. +# Note: The initial build can take several minutes. +## +# GUTENBERG_LOCAL_REPO=true diff --git a/.gitignore b/.gitignore index 1cd9da16b3ed0..1305caef7c0ed 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ wp-tests-config.php /src/wp-includes/theme.json /packagehash.txt /.gutenberg-hash +/.gutenberg-dir-hash /artifacts /setup.log /coverage diff --git a/Gruntfile.js b/Gruntfile.js index 4a9bdbca46fdd..1690a269efc41 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1493,6 +1493,32 @@ module.exports = function(grunt) { } ); } ); + grunt.registerTask( 'gutenberg:checkout', 'Clones the Gutenberg git repository at the SHA specified in package.json (local repository mode only).', function() { + const done = this.async(); + const args = [ 'tools/gutenberg/checkout-gutenberg.js' ]; + if ( grunt.option( 'force' ) ) { + args.push( '--force' ); + } + grunt.util.spawn( { + cmd: 'node', + args, + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + + grunt.registerTask( 'gutenberg:build', 'Builds Gutenberg from source for WordPress Core (local repository mode only).', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/build-gutenberg.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; @@ -1947,6 +1973,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', + 'gutenberg:build', 'gutenberg:copy', 'copy-vendor-scripts', 'build:certificates' @@ -1959,6 +1986,7 @@ module.exports = function(grunt) { 'build:js', 'build:css', 'build:codemirror', + 'gutenberg:build', 'gutenberg:copy', 'copy-vendor-scripts', 'replace:source-maps', diff --git a/package.json b/package.json index 47ca24068a3cd..e14c2b197926b 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "npm run gutenberg:download && npm run gutenberg:copy -- --dev", + "postinstall": "node tools/gutenberg/sync-gutenberg.js", "build": "grunt build", "build:dev": "grunt build --dev", "dev": "grunt watch --dev", @@ -133,6 +133,8 @@ "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", + "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", + "gutenberg:build": "node tools/gutenberg/build-gutenberg.js", "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", "vendor:copy": "node tools/vendors/copy-vendors.js", "sync-gutenberg-packages": "grunt sync-gutenberg-packages", diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js new file mode 100644 index 0000000000000..a05ae311b0ee4 --- /dev/null +++ b/tools/gutenberg/build-gutenberg.js @@ -0,0 +1,219 @@ +#!/usr/bin/env node + +/** + * Build Gutenberg Script + * + * This script builds the Gutenberg repository using its build command + * as specified in the root package.json's "gutenberg" configuration. + * + * @package WordPress + */ + +const { spawn, spawnSync } = require( 'child_process' ); +const fs = require( 'fs' ); +const path = require( 'path' ); +const { gutenbergDir, isGutenbergRepoClone } = require( './gutenberg-utils' ); + +// Paths +const rootDir = path.resolve( __dirname, '../..' ); +const buildShaFile = path.join( gutenbergDir, '.gutenberg-build-sha' ); + +/** + * Execute a command and return a promise. + * Captures output and only displays it on failure for cleaner logs. + * + * @param {string} command - Command to execute. + * @param {string[]} args - Command arguments. + * @param {Object} options - Spawn options. + * @return {Promise} Promise that resolves when command completes. + */ +function exec( command, args, options = {} ) { + return new Promise( ( resolve, reject ) => { + let stdout = ''; + let stderr = ''; + + const child = spawn( command, args, { + cwd: options.cwd || rootDir, + stdio: [ 'ignore', 'pipe', 'pipe' ], + shell: process.platform === 'win32', // Use shell on Windows to find .cmd files + ...options, + } ); + + // Capture output + if ( child.stdout ) { + child.stdout.on( 'data', ( data ) => { + stdout += data.toString(); + } ); + } + + if ( child.stderr ) { + child.stderr.on( 'data', ( data ) => { + stderr += data.toString(); + } ); + } + + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + // Show output only on failure + if ( stdout ) { + console.error( '\nCommand output:' ); + console.error( stdout ); + } + if ( stderr ) { + console.error( '\nCommand errors:' ); + console.error( stderr ); + } + reject( + new Error( + `${ command } ${ args.join( + ' ' + ) } failed with code ${ code }` + ) + ); + } else { + resolve(); + } + } ); + + child.on( 'error', reject ); + } ); +} + +/** + * Main execution function. + */ +async function main() { + if ( ! isGutenbergRepoClone() ) { + console.log( 'ā„¹ļø Skipping Gutenberg build: gutenberg/ is not a git clone.' ); + console.log( ' This step only runs in local repository mode (GUTENBERG_LOCAL_REPO=true).' ); + return; + } + + // Skip the build if HEAD has not changed since the last successful build. + const headResult = spawnSync( 'git', [ 'rev-parse', 'HEAD' ], { + cwd: gutenbergDir, + encoding: 'utf8', + } ); + const currentSha = headResult.status === 0 ? headResult.stdout.trim() : null; + + if ( currentSha && fs.existsSync( buildShaFile ) ) { + const builtSha = fs.readFileSync( buildShaFile, 'utf8' ).trim(); + if ( builtSha === currentSha ) { + console.log( `ā„¹ļø Gutenberg build is already up to date (${ currentSha.slice( 0, 12 ) }). Skipping.` ); + return; + } + } + + console.log( 'šŸ” Checking Gutenberg setup...' ); + + // Verify Gutenberg directory exists + if ( ! fs.existsSync( gutenbergDir ) ) { + console.error( 'āŒ Gutenberg directory not found at:', gutenbergDir ); + console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); + process.exit( 1 ); + } + + // Verify node_modules exists + const nodeModulesPath = path.join( gutenbergDir, 'node_modules' ); + if ( ! fs.existsSync( nodeModulesPath ) ) { + console.error( 'āŒ Gutenberg dependencies not installed' ); + console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); + process.exit( 1 ); + } + + console.log( 'āœ… Gutenberg directory found' ); + + // Modify Gutenberg's package.json for Core build + console.log( '\nāš™ļø Configuring build for WordPress Core...' ); + const gutenbergPackageJsonPath = path.join( gutenbergDir, 'package.json' ); + + try { + const content = fs.readFileSync( gutenbergPackageJsonPath, 'utf8' ); + const gutenbergPackageJson = JSON.parse( content ); + + // Set Core environment variables + gutenbergPackageJson.config = gutenbergPackageJson.config || {}; + gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; + gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; + + // Set wpPlugin.name for Core naming convention + gutenbergPackageJson.wpPlugin = gutenbergPackageJson.wpPlugin || {}; + gutenbergPackageJson.wpPlugin.name = 'wp'; + + fs.writeFileSync( + gutenbergPackageJsonPath, + JSON.stringify( gutenbergPackageJson, null, '\t' ) + '\n' + ); + + console.log( ' āœ… IS_GUTENBERG_PLUGIN = false' ); + console.log( ' āœ… IS_WORDPRESS_CORE = true' ); + console.log( ' āœ… wpPlugin.name = wp' ); + } catch ( error ) { + console.error( + 'āŒ Error modifying Gutenberg package.json:', + error.message + ); + process.exit( 1 ); + } + + // Build Gutenberg + console.log( '\nšŸ”Ø Building Gutenberg for WordPress Core...' ); + console.log( ' (This may take a few minutes)' ); + + const startTime = Date.now(); + + try { + // Invoke the build script directly with node instead of going through + // `npm run build --` to avoid shell argument mangling of the base-url + // value (which contains spaces, parentheses, and single quotes). + // The PATH is extended with node_modules/.bin so that bin commands + // like `wp-build` are found, matching what npm would normally provide. + const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); + await exec( 'node', [ + 'bin/build.mjs', + '--skip-types', + "--base-url=includes_url( 'build/' )", + ], { + cwd: gutenbergDir, + env: { + ...process.env, + PATH: binPath + path.delimiter + process.env.PATH, + }, + } ); + + const duration = Math.round( ( Date.now() - startTime ) / 1000 ); + console.log( `āœ… Build completed in ${ duration }s` ); + + // Record the built SHA so subsequent runs can skip if HEAD hasn't changed. + if ( currentSha ) { + fs.writeFileSync( buildShaFile, currentSha ); + } + } catch ( error ) { + console.error( 'āŒ Build failed:', error.message ); + throw error; + } finally { + // Restore Gutenberg's package.json regardless of success or failure + await restorePackageJson(); + } +} + +/** + * Restore Gutenberg's package.json to its original state. + */ +async function restorePackageJson() { + console.log( '\nšŸ”„ Restoring Gutenberg package.json...' ); + try { + await exec( 'git', [ 'checkout', '--', 'package.json' ], { + cwd: gutenbergDir, + } ); + console.log( 'āœ… package.json restored' ); + } catch ( error ) { + console.warn( 'āš ļø Could not restore package.json:', error.message ); + } +} + +// Run main function +main().catch( ( error ) => { + console.error( 'āŒ Unexpected error:', error ); + process.exit( 1 ); +} ); diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/checkout-gutenberg.js new file mode 100644 index 0000000000000..699aacc4014f2 --- /dev/null +++ b/tools/gutenberg/checkout-gutenberg.js @@ -0,0 +1,244 @@ +#!/usr/bin/env node + +/** + * Checkout Gutenberg Repository Script + * + * This script checks out the Gutenberg repository at a specific commit/branch/tag + * as specified in the root package.json's "gutenberg" configuration. + * + * It handles: + * - Initial clone if directory doesn't exist + * - Updating existing checkout to correct ref + * - Installing dependencies with npm ci + * - Idempotent operation (safe to run multiple times) + * + * @package WordPress + */ + +const { spawn } = require( 'child_process' ); +const fs = require( 'fs' ); +const path = require( 'path' ); +const { readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone } = require( './gutenberg-utils' ); + +// Constants +const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git'; + +// Paths +const rootDir = path.resolve( __dirname, '../..' ); +const gutenbergDir = path.join( rootDir, 'gutenberg' ); + +/** + * Execute a command and return a promise. + * Captures output and only displays it on failure for cleaner logs. + * + * @param {string} command - Command to execute. + * @param {string[]} args - Command arguments. + * @param {Object} options - Spawn options. + * @return {Promise} Promise that resolves when command completes. + */ +function exec( command, args, options = {} ) { + return new Promise( ( resolve, reject ) => { + let stdout = ''; + let stderr = ''; + + const child = spawn( command, args, { + cwd: options.cwd || rootDir, + stdio: [ 'ignore', 'pipe', 'pipe' ], + shell: process.platform === 'win32', // Use shell on Windows to find .cmd files + ...options, + } ); + + // Capture output + if ( child.stdout ) { + child.stdout.on( 'data', ( data ) => { + stdout += data.toString(); + } ); + } + + if ( child.stderr ) { + child.stderr.on( 'data', ( data ) => { + stderr += data.toString(); + } ); + } + + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + // Show output only on failure + if ( stdout ) { + console.error( '\nCommand output:' ); + console.error( stdout ); + } + if ( stderr ) { + console.error( '\nCommand errors:' ); + console.error( stderr ); + } + reject( + new Error( + `${ command } ${ args.join( + ' ' + ) } failed with code ${ code }` + ) + ); + } else { + resolve(); + } + } ); + + child.on( 'error', reject ); + } ); +} + +/** + * Execute a command and capture its output. + * + * @param {string} command - Command to execute. + * @param {string[]} args - Command arguments. + * @param {Object} options - Spawn options. + * @return {Promise} Promise that resolves with command output. + */ +function execOutput( command, args, options = {} ) { + return new Promise( ( resolve, reject ) => { + const child = spawn( command, args, { + cwd: options.cwd || rootDir, + shell: process.platform === 'win32', // Use shell on Windows to find .cmd files + ...options, + } ); + + let stdout = ''; + let stderr = ''; + + if ( child.stdout ) { + child.stdout.on( 'data', ( data ) => { + stdout += data.toString(); + } ); + } + + if ( child.stderr ) { + child.stderr.on( 'data', ( data ) => { + stderr += data.toString(); + } ); + } + + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( new Error( `${ command } failed: ${ stderr }` ) ); + } else { + resolve( stdout.trim() ); + } + } ); + + child.on( 'error', reject ); + } ); +} + +/** + * Main execution function. + */ +async function main( force ) { + if ( ! isLocalRepoMode() ) { + console.log( 'ā„¹ļø Skipping Gutenberg checkout: GUTENBERG_LOCAL_REPO is not set to true.' ); + console.log( ' Add GUTENBERG_LOCAL_REPO=true to your .env file to enable local repository mode.' ); + return; + } + + if ( ! force && isGutenbergRepoClone() ) { + console.log( 'ā„¹ļø The `gutenberg` directory is already a git clone.' ); + console.log( ' Use `npm run grunt gutenberg:checkout -- --force` to remove and re-clone.' ); + return; + } + + console.log( 'šŸ” Checking Gutenberg configuration...' ); + + // Read Gutenberg SHA from package.json + let ref; + try { + ( { sha: ref } = readGutenbergConfig() ); + console.log( ` Repository: ${ GUTENBERG_REPO }` ); + console.log( ` SHA: ${ ref }` ); + } catch ( error ) { + console.error( 'āŒ Error reading package.json:', error.message ); + process.exit( 1 ); + } + + // Check if Gutenberg directory exists + const gutenbergExists = fs.existsSync( gutenbergDir ); + + if ( ! gutenbergExists ) { + console.log( '\nšŸ“„ Cloning Gutenberg repository (shallow clone)...' ); + try { + // Generic shallow clone approach that works for both branches and commit hashes + // 1. Clone with no checkout and shallow depth + await exec( 'git', [ + 'clone', + '--depth', + '1', + '--no-checkout', + GUTENBERG_REPO, + 'gutenberg', + ] ); + + // 2. Fetch the specific ref with depth 1 (works for branches, tags, and commits) + await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { + cwd: gutenbergDir, + } ); + + // 3. Checkout FETCH_HEAD + await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { + cwd: gutenbergDir, + } ); + + console.log( 'āœ… Cloned successfully' ); + } catch ( error ) { + console.error( 'āŒ Clone failed:', error.message ); + process.exit( 1 ); + } + } else { + console.log( '\nāœ… Gutenberg directory already exists' ); + } + + // Fetch and checkout target ref + console.log( `\nšŸ“” Fetching and checking out: ${ ref }` ); + try { + // Fetch the specific ref (works for branches, tags, and commit hashes) + await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { + cwd: gutenbergDir, + } ); + + // Checkout what was just fetched + await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { + cwd: gutenbergDir, + } ); + + console.log( 'āœ… Checked out successfully' ); + } catch ( error ) { + console.error( 'āŒ Fetch/checkout failed:', error.message ); + process.exit( 1 ); + } + + // Install dependencies + console.log( '\nšŸ“¦ Installing dependencies...' ); + const nodeModulesExists = fs.existsSync( + path.join( gutenbergDir, 'node_modules' ) + ); + + if ( ! nodeModulesExists ) { + console.log( ' (This may take a few minutes on first run)' ); + } + + try { + await exec( 'npm', [ 'ci' ], { cwd: gutenbergDir } ); + console.log( 'āœ… Dependencies installed' ); + } catch ( error ) { + console.error( 'āŒ npm ci failed:', error.message ); + process.exit( 1 ); + } + + console.log( '\nāœ… Gutenberg checkout complete!' ); +} + +// Run main function +const force = process.argv.includes( '--force' ); +main( force ).catch( ( error ) => { + console.error( 'āŒ Unexpected error:', error ); + process.exit( 1 ); +} ); diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index 4a0f7f506024c..ad7c24b6de314 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -867,7 +867,8 @@ async function main() { // Verify Gutenberg build exists if ( ! fs.existsSync( gutenbergBuildDir ) ) { console.error( 'āŒ Gutenberg build directory not found' ); - console.error( ' Run: node tools/gutenberg/build-gutenberg.js' ); + console.error( ' In local repository mode: run `npm run grunt gutenberg:build`' ); + console.error( ' In default mode: run `npm run grunt gutenberg:download`' ); process.exit( 1 ); } diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download-gutenberg.js index 360c39fbd237e..c157b370a2d25 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download-gutenberg.js @@ -17,7 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); -const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './gutenberg-utils' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, isLocalRepoMode } = require( './gutenberg-utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in @@ -68,6 +68,12 @@ function exec( command, args, options = {} ) { * @param {boolean} force - Whether to force a fresh download even if the gutenberg directory exists. */ async function main( force ) { + if ( isLocalRepoMode() ) { + console.log( 'ā„¹ļø Skipping Gutenberg download: GUTENBERG_LOCAL_REPO=true is set.' ); + console.log( ' Use `npm run grunt gutenberg:checkout` to set up the local repository.' ); + return; + } + console.log( 'šŸ” Checking Gutenberg configuration...' ); /* diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/gutenberg-utils.js index 95a607c9202fa..435329bb5b9e5 100644 --- a/tools/gutenberg/gutenberg-utils.js +++ b/tools/gutenberg/gutenberg-utils.js @@ -3,8 +3,9 @@ /** * Gutenberg build utilities. * - * Shared helpers used by the Gutenberg download script. When run directly, - * verifies that the installed Gutenberg build matches the SHA in package.json. + * Shared helpers used by the Gutenberg download and checkout scripts. When run + * directly, verifies that the installed Gutenberg build matches the SHA in + * package.json. * * @package WordPress */ @@ -12,12 +13,19 @@ const crypto = require( 'crypto' ); const fs = require( 'fs' ); const path = require( 'path' ); +const { spawnSync } = require( 'child_process' ); +const dotenv = require( 'dotenv' ); +const dotenvExpand = require( 'dotenv-expand' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergDirHashFile = path.join( rootDir, '.gutenberg-dir-hash' ); +// Load .env so GUTENBERG_LOCAL_REPO and other vars are available regardless +// of how this module is invoked (grunt task, direct node call, postinstall). +dotenvExpand.expand( dotenv.config( { path: path.join( rootDir, '.env' ) } ) ); + /** * Read Gutenberg configuration from package.json. * @@ -40,10 +48,37 @@ function readGutenbergConfig() { return { sha, ghcrRepo }; } +/** + * Whether the contributor has opted into local Gutenberg repository mode. + * + * Returns true only when GUTENBERG_LOCAL_REPO=true is present in the .env file + * or the current process environment. + * + * @return {boolean} Whether local repo mode is active. + */ +function isLocalRepoMode() { + return process.env.GUTENBERG_LOCAL_REPO === 'true'; +} + +/** + * Whether the gutenberg/ directory is a git repository clone. + * + * This is true when gutenberg/.git exists, which indicates a contributor has + * run `gutenberg:checkout` rather than `gutenberg:download`. + * + * @return {boolean} Whether the gutenberg directory is a git clone. + */ +function isGutenbergRepoClone() { + return fs.existsSync( path.join( gutenbergDir, '.git' ) ); +} + /** * Verify that the installed Gutenberg version matches the expected SHA in * package.json. Logs progress to the console and exits with a non-zero code * on failure. + * + * In git clone mode, a SHA mismatch is a warning only — the developer may + * intentionally be on a different commit. In download mode it is a hard error. */ function verifyGutenbergVersion() { console.log( '\nšŸ” Verifying Gutenberg version...' ); @@ -56,18 +91,39 @@ function verifyGutenbergVersion() { process.exit( 1 ); } + if ( isGutenbergRepoClone() ) { + const result = spawnSync( 'git', [ 'rev-parse', 'HEAD' ], { + cwd: gutenbergDir, + encoding: 'utf8', + } ); + if ( result.status !== 0 ) { + console.error( 'āŒ Could not determine the current Gutenberg git commit.' ); + process.exit( 1 ); + } + const currentSha = result.stdout.trim(); + if ( currentSha !== sha ) { + console.warn( + `āš ļø Gutenberg HEAD (${ currentSha.slice( 0, 12 ) }) does not match the expected SHA (${ sha.slice( 0, 12 ) }).` + + ` Run \`npm run grunt gutenberg:checkout -- --force\` to reset to the expected SHA.` + ); + } else { + console.log( 'āœ… Version verified (git clone at expected SHA)' ); + } + return; + } + const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); try { const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); if ( installedHash !== sha ) { console.error( - `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg-download -- --force\` to download the correct version.` + `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg:download -- --force\` to download the correct version.` ); process.exit( 1 ); } } catch ( error ) { if ( error.code === 'ENOENT' ) { - console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg-download\` to download Gutenberg.` ); + console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg:download\` to download Gutenberg.` ); } else { console.error( `āŒ ${ error.message }` ); } @@ -122,10 +178,30 @@ function hashGutenbergDir() { /** * Checks for changes to the local gutenberg directory. * - * This detects changes to the local gutenberg directory that have occurred since - * the directory was created, which may cause unexpected results. + * In git clone mode, checks for uncommitted working tree changes via + * `git status`. In download mode, compares the directory file hash against the + * stored value in .gutenberg-dir-hash. + * + * Either way, only a warning is issued — the build is not aborted. */ function checkGutenbergDirHash() { + if ( isGutenbergRepoClone() ) { + const result = spawnSync( 'git', [ 'status', '--porcelain' ], { + cwd: gutenbergDir, + encoding: 'utf8', + } ); + if ( result.status !== 0 ) { + console.warn( 'āš ļø Could not check the gutenberg working tree status.' ); + return; + } + if ( result.stdout.trim() ) { + console.warn( 'āš ļø The gutenberg directory has uncommitted local changes. The build scripts may produce unexpected results.' ); + } else { + console.log( 'āœ… The gutenberg working tree has no uncommitted changes.' ); + } + return; + } + if ( ! fs.existsSync( gutenbergDirHashFile ) ) { console.warn( 'āš ļø .gutenberg-dir-hash not found. Files in the gutenberg directory may have changed since downloading.' ); return; @@ -142,7 +218,7 @@ function checkGutenbergDirHash() { console.log( 'āœ… The contents of the gutenberg directory have not been modified.' ); } -module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, hashGutenbergDir, checkGutenbergDirHash }; +module.exports = { rootDir, gutenbergDir, readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone, verifyGutenbergVersion, hashGutenbergDir, checkGutenbergDirHash }; if ( require.main === module ) { verifyGutenbergVersion(); diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js new file mode 100644 index 0000000000000..e6b8b2d04f2f9 --- /dev/null +++ b/tools/gutenberg/sync-gutenberg.js @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +/** + * Gutenberg Setup Dispatcher + * + * This is the postinstall entry point for Gutenberg setup. It detects whether + * the contributor is in local repository mode (GUTENBERG_LOCAL_REPO=true in + * their .env file) and runs the appropriate setup flow: + * + * - Default mode: download pre-built zip → copy to Core + * - Local repository mode: checkout git repo → build from source → copy to Core + * + * @package WordPress + */ + +const { spawn } = require( 'child_process' ); +const path = require( 'path' ); +const { isLocalRepoMode } = require( './gutenberg-utils' ); + +const rootDir = path.resolve( __dirname, '../..' ); + +/** + * Spawn a Node.js script and inherit stdio so output is visible in the terminal. + * + * @param {string} scriptPath - Path to the script, relative to the repo root. + * @param {string[]} args - Arguments to pass to the script. + * @return {Promise} Promise that resolves when the script exits successfully. + */ +function runScript( scriptPath, args = [] ) { + return new Promise( ( resolve, reject ) => { + const child = spawn( process.execPath, [ scriptPath, ...args ], { + cwd: rootDir, + stdio: 'inherit', + } ); + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( new Error( `${ scriptPath } exited with code ${ code }` ) ); + } else { + resolve(); + } + } ); + child.on( 'error', reject ); + } ); +} + +async function main() { + if ( isLocalRepoMode() ) { + console.log( 'ā„¹ļø Local Gutenberg repository mode active (GUTENBERG_LOCAL_REPO=true).\n' ); + await runScript( 'tools/gutenberg/checkout-gutenberg.js' ); + await runScript( 'tools/gutenberg/build-gutenberg.js' ); + } else { + await runScript( 'tools/gutenberg/download-gutenberg.js' ); + } + await runScript( 'tools/gutenberg/copy-gutenberg-build.js', [ '--dev' ] ); +} + +main().catch( ( error ) => { + console.error( 'āŒ Gutenberg setup failed:', error.message ); + process.exit( 1 ); +} ); From 26133cd0b2b3af047c858143c6668dd62bbd333e Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:55:05 -0500 Subject: [PATCH 32/53] Test building from Gutenberg in GHA. --- .../workflows/reusable-test-core-build-process.yml | 7 +++++++ .github/workflows/test-build-processes.yml | 3 +++ Gruntfile.js | 6 +++++- package.json | 7 ++++--- tools/gutenberg/build-gutenberg.js | 11 +++++++---- 5 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml index 4bec59e285c57..4c40bfd7e320b 100644 --- a/.github/workflows/reusable-test-core-build-process.yml +++ b/.github/workflows/reusable-test-core-build-process.yml @@ -36,6 +36,11 @@ on: required: false type: 'boolean' default: false + build-direct-from-gutenberg: + description: 'Whether to build directly from the Gutenberg repository.' + required: false + type: 'boolean' + default: false env: PUPPETEER_SKIP_DOWNLOAD: ${{ true }} @@ -67,6 +72,8 @@ jobs: contents: read runs-on: ${{ inputs.os }} timeout-minutes: 20 + env: + GUTENBERG_LOCAL_REPO: ${{ inputs.build-direct-from-gutenberg }} steps: - name: Checkout repository diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index 150c36ef0893c..befb81b006de6 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -59,18 +59,21 @@ jobs: os: [ 'ubuntu-24.04' ] directory: [ 'src', 'build' ] test-certificates: [ true ] + build-from-gutenberg: [ true, false ] include: # Only prepare artifacts for Playground once. - os: 'ubuntu-24.04' directory: 'build' save-build: true prepare-playground: ${{ github.event_name == 'pull_request' && true || '' }} + with: os: ${{ matrix.os }} directory: ${{ matrix.directory }} test-certificates: ${{ matrix.test-certificates && true || false }} save-build: ${{ matrix.save-build && matrix.save-build || false }} prepare-playground: ${{ matrix.prepare-playground && matrix.prepare-playground || false }} + build-direct-from-gutenberg: ${{ matrix.build-from-gutenberg && matrix.build-from-gutenberg || false }} # Tests the WordPress Core build process on additional operating systems. # diff --git a/Gruntfile.js b/Gruntfile.js index 1690a269efc41..c95b2e1ce7e30 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1510,9 +1510,13 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:build', 'Builds Gutenberg from source for WordPress Core (local repository mode only).', function() { const done = this.async(); + const args = [ 'tools/gutenberg/build-gutenberg.js' ]; + if ( grunt.option( 'force' ) ) { + args.push( '--force' ); + } grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/build-gutenberg.js' ], + args, opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); diff --git a/package.json b/package.json index e14c2b197926b..11885d0cb3e9b 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "wicg-inert": "3.1.3" }, "scripts": { - "postinstall": "node tools/gutenberg/sync-gutenberg.js", + "postinstall": "npm run gutenberg:sync", "build": "grunt build", "build:dev": "grunt build --dev", "dev": "grunt watch --dev", @@ -132,10 +132,11 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", - "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", - "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", "gutenberg:build": "node tools/gutenberg/build-gutenberg.js", + "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", + "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", + "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js", "vendor:copy": "node tools/vendors/copy-vendors.js", "sync-gutenberg-packages": "grunt sync-gutenberg-packages", "postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build" diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js index a05ae311b0ee4..2ce9de2af2cc3 100644 --- a/tools/gutenberg/build-gutenberg.js +++ b/tools/gutenberg/build-gutenberg.js @@ -82,24 +82,26 @@ function exec( command, args, options = {} ) { /** * Main execution function. */ -async function main() { +async function main( force ) { if ( ! isGutenbergRepoClone() ) { console.log( 'ā„¹ļø Skipping Gutenberg build: gutenberg/ is not a git clone.' ); console.log( ' This step only runs in local repository mode (GUTENBERG_LOCAL_REPO=true).' ); return; } - // Skip the build if HEAD has not changed since the last successful build. + // Skip the build if HEAD has not changed since the last successful build, + // unless --force is passed. const headResult = spawnSync( 'git', [ 'rev-parse', 'HEAD' ], { cwd: gutenbergDir, encoding: 'utf8', } ); const currentSha = headResult.status === 0 ? headResult.stdout.trim() : null; - if ( currentSha && fs.existsSync( buildShaFile ) ) { + if ( ! force && currentSha && fs.existsSync( buildShaFile ) ) { const builtSha = fs.readFileSync( buildShaFile, 'utf8' ).trim(); if ( builtSha === currentSha ) { console.log( `ā„¹ļø Gutenberg build is already up to date (${ currentSha.slice( 0, 12 ) }). Skipping.` ); + console.log( ' Use `npm run grunt gutenberg:build -- --force` to rebuild.' ); return; } } @@ -133,7 +135,6 @@ async function main() { // Set Core environment variables gutenbergPackageJson.config = gutenbergPackageJson.config || {}; - gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; // Set wpPlugin.name for Core naming convention @@ -175,6 +176,7 @@ async function main() { "--base-url=includes_url( 'build/' )", ], { cwd: gutenbergDir, + stdio: 'inherit', env: { ...process.env, PATH: binPath + path.delimiter + process.env.PATH, @@ -214,6 +216,7 @@ async function restorePackageJson() { // Run main function main().catch( ( error ) => { +main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); process.exit( 1 ); } ); From f9dc6886b170b3982f81fac51dc95818164fc03a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:55:35 -0500 Subject: [PATCH 33/53] Allow forcing of build. --- tools/gutenberg/build-gutenberg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js index 2ce9de2af2cc3..a44035d69d8f2 100644 --- a/tools/gutenberg/build-gutenberg.js +++ b/tools/gutenberg/build-gutenberg.js @@ -215,7 +215,7 @@ async function restorePackageJson() { } // Run main function -main().catch( ( error ) => { +const force = process.argv.includes( '--force' ); main( force ).catch( ( error ) => { console.error( 'āŒ Unexpected error:', error ); process.exit( 1 ); From 267ac373d31ecc89342faea42edad0e16cb8c256 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:05:57 -0500 Subject: [PATCH 34/53] Only upload artifact once. --- .github/workflows/test-build-processes.yml | 2 ++ tools/gutenberg/build-gutenberg.js | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index befb81b006de6..e861f22fe0627 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -60,12 +60,14 @@ jobs: directory: [ 'src', 'build' ] test-certificates: [ true ] build-from-gutenberg: [ true, false ] + include: # Only prepare artifacts for Playground once. - os: 'ubuntu-24.04' directory: 'build' save-build: true prepare-playground: ${{ github.event_name == 'pull_request' && true || '' }} + build-from-gutenberg: false with: os: ${{ matrix.os }} diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js index a44035d69d8f2..d8239ce8e656e 100644 --- a/tools/gutenberg/build-gutenberg.js +++ b/tools/gutenberg/build-gutenberg.js @@ -135,6 +135,7 @@ async function main( force ) { // Set Core environment variables gutenbergPackageJson.config = gutenbergPackageJson.config || {}; + gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; // Set wpPlugin.name for Core naming convention From e52a38f68b4ed65e190a31cec5790b234fe1f89c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:27:14 -0500 Subject: [PATCH 35/53] Avoid manipulating package.json directly. --- tools/gutenberg/build-gutenberg.js | 65 +++++------------------------- tools/gutenberg/sync-gutenberg.js | 1 + 2 files changed, 10 insertions(+), 56 deletions(-) diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build-gutenberg.js index d8239ce8e656e..422490b93ecdb 100644 --- a/tools/gutenberg/build-gutenberg.js +++ b/tools/gutenberg/build-gutenberg.js @@ -125,52 +125,19 @@ async function main( force ) { console.log( 'āœ… Gutenberg directory found' ); - // Modify Gutenberg's package.json for Core build - console.log( '\nāš™ļø Configuring build for WordPress Core...' ); - const gutenbergPackageJsonPath = path.join( gutenbergDir, 'package.json' ); - - try { - const content = fs.readFileSync( gutenbergPackageJsonPath, 'utf8' ); - const gutenbergPackageJson = JSON.parse( content ); - - // Set Core environment variables - gutenbergPackageJson.config = gutenbergPackageJson.config || {}; - gutenbergPackageJson.config.IS_GUTENBERG_PLUGIN = false; - gutenbergPackageJson.config.IS_WORDPRESS_CORE = true; - - // Set wpPlugin.name for Core naming convention - gutenbergPackageJson.wpPlugin = gutenbergPackageJson.wpPlugin || {}; - gutenbergPackageJson.wpPlugin.name = 'wp'; - - fs.writeFileSync( - gutenbergPackageJsonPath, - JSON.stringify( gutenbergPackageJson, null, '\t' ) + '\n' - ); - - console.log( ' āœ… IS_GUTENBERG_PLUGIN = false' ); - console.log( ' āœ… IS_WORDPRESS_CORE = true' ); - console.log( ' āœ… wpPlugin.name = wp' ); - } catch ( error ) { - console.error( - 'āŒ Error modifying Gutenberg package.json:', - error.message - ); - process.exit( 1 ); - } - // Build Gutenberg console.log( '\nšŸ”Ø Building Gutenberg for WordPress Core...' ); console.log( ' (This may take a few minutes)' ); const startTime = Date.now(); + // Invoke the build script directly with node instead of going through + // `npm run build --` to avoid shell argument mangling of the base-url + // value (which contains spaces, parentheses, and single quotes). + // The PATH is extended with node_modules/.bin so that bin commands + // like `wp-build` are found, matching what npm would normally provide. + const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); try { - // Invoke the build script directly with node instead of going through - // `npm run build --` to avoid shell argument mangling of the base-url - // value (which contains spaces, parentheses, and single quotes). - // The PATH is extended with node_modules/.bin so that bin commands - // like `wp-build` are found, matching what npm would normally provide. - const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); await exec( 'node', [ 'bin/build.mjs', '--skip-types', @@ -181,6 +148,9 @@ async function main( force ) { env: { ...process.env, PATH: binPath + path.delimiter + process.env.PATH, + IS_GUTENBERG_PLUGIN: 'false', + IS_WORDPRESS_CORE: 'true', + WP_PLUGIN_NAME: 'wp', }, } ); @@ -194,26 +164,9 @@ async function main( force ) { } catch ( error ) { console.error( 'āŒ Build failed:', error.message ); throw error; - } finally { - // Restore Gutenberg's package.json regardless of success or failure - await restorePackageJson(); } } -/** - * Restore Gutenberg's package.json to its original state. - */ -async function restorePackageJson() { - console.log( '\nšŸ”„ Restoring Gutenberg package.json...' ); - try { - await exec( 'git', [ 'checkout', '--', 'package.json' ], { - cwd: gutenbergDir, - } ); - console.log( 'āœ… package.json restored' ); - } catch ( error ) { - console.warn( 'āš ļø Could not restore package.json:', error.message ); - } -} // Run main function const force = process.argv.includes( '--force' ); diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js index e6b8b2d04f2f9..7536706bd2f5a 100644 --- a/tools/gutenberg/sync-gutenberg.js +++ b/tools/gutenberg/sync-gutenberg.js @@ -51,6 +51,7 @@ async function main() { } else { await runScript( 'tools/gutenberg/download-gutenberg.js' ); } + await runScript( 'tools/gutenberg/copy-gutenberg-build.js', [ '--dev' ] ); } From 04b93b9335c944c0e0dfb1373ca163614c513f70 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:30:02 -0500 Subject: [PATCH 36/53] Always copy after build. --- Gruntfile.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index c95b2e1ce7e30..ccddd6e73b82c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1508,7 +1508,7 @@ module.exports = function(grunt) { } ); } ); - grunt.registerTask( 'gutenberg:build', 'Builds Gutenberg from source for WordPress Core (local repository mode only).', function() { + grunt.registerTask( 'gutenberg:build:run', 'Builds Gutenberg from source for WordPress Core (local repository mode only).', function() { const done = this.async(); const args = [ 'tools/gutenberg/build-gutenberg.js' ]; if ( grunt.option( 'force' ) ) { @@ -1523,6 +1523,8 @@ module.exports = function(grunt) { } ); } ); + grunt.registerTask( 'gutenberg:build', 'Builds Gutenberg from source and copies the result to WordPress Core.', [ 'gutenberg:build:run', 'gutenberg:copy' ] ); + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; @@ -1978,7 +1980,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'gutenberg:build', - 'gutenberg:copy', 'copy-vendor-scripts', 'build:certificates' ] ); @@ -1991,7 +1992,6 @@ module.exports = function(grunt) { 'build:css', 'build:codemirror', 'gutenberg:build', - 'gutenberg:copy', 'copy-vendor-scripts', 'replace:source-maps', 'verify:build' From 6ca82f691dc5c919e0271b1996e5ec8868ea2d0b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:34:39 -0500 Subject: [PATCH 37/53] Remove `gutenberg` from `tools/gutenberg` names. --- Gruntfile.js | 10 +++++----- package.json | 10 +++++----- tests/phpstan/base.neon | 2 +- tools/gutenberg/{build-gutenberg.js => build.js} | 6 +++--- tools/gutenberg/{checkout-gutenberg.js => checkout.js} | 2 +- tools/gutenberg/{copy-gutenberg-build.js => copy.js} | 4 ++-- tools/gutenberg/{download-gutenberg.js => download.js} | 2 +- tools/gutenberg/{sync-gutenberg.js => sync.js} | 10 +++++----- tools/gutenberg/{gutenberg-utils.js => utils.js} | 0 webpack.config.js | 2 +- 10 files changed, 24 insertions(+), 24 deletions(-) rename tools/gutenberg/{build-gutenberg.js => build.js} (95%) rename tools/gutenberg/{checkout-gutenberg.js => checkout.js} (99%) rename tools/gutenberg/{copy-gutenberg-build.js => copy.js} (99%) rename tools/gutenberg/{download-gutenberg.js => download.js} (99%) rename tools/gutenberg/{sync-gutenberg.js => sync.js} (83%) rename tools/gutenberg/{gutenberg-utils.js => utils.js} (100%) diff --git a/Gruntfile.js b/Gruntfile.js index ccddd6e73b82c..7bddd60c6879c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1471,7 +1471,7 @@ module.exports = function(grunt) { const done = this.async(); grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/gutenberg-utils.js' ], + args: [ 'tools/gutenberg/utils.js' ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); @@ -1480,7 +1480,7 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { const done = this.async(); - const args = [ 'tools/gutenberg/download-gutenberg.js' ]; + const args = [ 'tools/gutenberg/download.js' ]; if ( grunt.option( 'force' ) ) { args.push( '--force' ); } @@ -1495,7 +1495,7 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:checkout', 'Clones the Gutenberg git repository at the SHA specified in package.json (local repository mode only).', function() { const done = this.async(); - const args = [ 'tools/gutenberg/checkout-gutenberg.js' ]; + const args = [ 'tools/gutenberg/checkout.js' ]; if ( grunt.option( 'force' ) ) { args.push( '--force' ); } @@ -1510,7 +1510,7 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:build:run', 'Builds Gutenberg from source for WordPress Core (local repository mode only).', function() { const done = this.async(); - const args = [ 'tools/gutenberg/build-gutenberg.js' ]; + const args = [ 'tools/gutenberg/build.js' ]; if ( grunt.option( 'force' ) ) { args.push( '--force' ); } @@ -1530,7 +1530,7 @@ module.exports = function(grunt) { const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; grunt.util.spawn( { cmd: 'node', - args: [ 'tools/gutenberg/copy-gutenberg-build.js', `--build-dir=${ buildDir }` ], + args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ], opts: { stdio: 'inherit' } }, function( error ) { done( ! error ); diff --git a/package.json b/package.json index 11885d0cb3e9b..2417afc92dda9 100644 --- a/package.json +++ b/package.json @@ -132,11 +132,11 @@ "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", - "gutenberg:build": "node tools/gutenberg/build-gutenberg.js", - "gutenberg:checkout": "node tools/gutenberg/checkout-gutenberg.js", - "gutenberg:copy": "node tools/gutenberg/copy-gutenberg-build.js", - "gutenberg:download": "node tools/gutenberg/download-gutenberg.js", - "gutenberg:sync": "node tools/gutenberg/sync-gutenberg.js", + "gutenberg:build": "node tools/gutenberg/build.js", + "gutenberg:checkout": "node tools/gutenberg/checkout.js", + "gutenberg:copy": "node tools/gutenberg/copy.js", + "gutenberg:download": "node tools/gutenberg/download.js", + "gutenberg:sync": "node tools/gutenberg/sync.js", "vendor:copy": "node tools/vendors/copy-vendors.js", "sync-gutenberg-packages": "grunt sync-gutenberg-packages", "postsync-gutenberg-packages": "grunt wp-packages:sync-stable-blocks && grunt build --dev && grunt build" diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 68c548b44ab68..4fe5b62afec1f 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -105,7 +105,7 @@ parameters: - ../../src/wp-includes/deprecated.php - ../../src/wp-includes/ms-deprecated.php - ../../src/wp-includes/pluggable-deprecated.php - # These files are autogenerated by tools/gutenberg/copy-gutenberg-build.js. + # These files are autogenerated by tools/gutenberg/copy.js. - ../../src/wp-includes/blocks # Third-party libraries. - ../../src/wp-admin/includes/class-ftp-pure.php diff --git a/tools/gutenberg/build-gutenberg.js b/tools/gutenberg/build.js similarity index 95% rename from tools/gutenberg/build-gutenberg.js rename to tools/gutenberg/build.js index 422490b93ecdb..77159d26493db 100644 --- a/tools/gutenberg/build-gutenberg.js +++ b/tools/gutenberg/build.js @@ -12,7 +12,7 @@ const { spawn, spawnSync } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -const { gutenbergDir, isGutenbergRepoClone } = require( './gutenberg-utils' ); +const { gutenbergDir, isGutenbergRepoClone } = require( './utils' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); @@ -111,7 +111,7 @@ async function main( force ) { // Verify Gutenberg directory exists if ( ! fs.existsSync( gutenbergDir ) ) { console.error( 'āŒ Gutenberg directory not found at:', gutenbergDir ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); + console.error( ' Run: node tools/gutenberg/checkout.js' ); process.exit( 1 ); } @@ -119,7 +119,7 @@ async function main( force ) { const nodeModulesPath = path.join( gutenbergDir, 'node_modules' ); if ( ! fs.existsSync( nodeModulesPath ) ) { console.error( 'āŒ Gutenberg dependencies not installed' ); - console.error( ' Run: node tools/gutenberg/checkout-gutenberg.js' ); + console.error( ' Run: node tools/gutenberg/checkout.js' ); process.exit( 1 ); } diff --git a/tools/gutenberg/checkout-gutenberg.js b/tools/gutenberg/checkout.js similarity index 99% rename from tools/gutenberg/checkout-gutenberg.js rename to tools/gutenberg/checkout.js index 699aacc4014f2..2d0f5298c2013 100644 --- a/tools/gutenberg/checkout-gutenberg.js +++ b/tools/gutenberg/checkout.js @@ -18,7 +18,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -const { readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone } = require( './gutenberg-utils' ); +const { readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone } = require( './utils' ); // Constants const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git'; diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy.js similarity index 99% rename from tools/gutenberg/copy-gutenberg-build.js rename to tools/gutenberg/copy.js index ad7c24b6de314..730ea99d69cca 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy.js @@ -555,7 +555,7 @@ function generateBlockRegistrationFiles() { // Generate require-dynamic-blocks.php const dynamicContent = ` `\t'${ name }',` ).join( '\n' ) } diff --git a/tools/gutenberg/download-gutenberg.js b/tools/gutenberg/download.js similarity index 99% rename from tools/gutenberg/download-gutenberg.js rename to tools/gutenberg/download.js index c157b370a2d25..1cc1a66156b07 100644 --- a/tools/gutenberg/download-gutenberg.js +++ b/tools/gutenberg/download.js @@ -17,7 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); -const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, isLocalRepoMode } = require( './gutenberg-utils' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, isLocalRepoMode } = require( './utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync.js similarity index 83% rename from tools/gutenberg/sync-gutenberg.js rename to tools/gutenberg/sync.js index 7536706bd2f5a..8923eee95a60b 100644 --- a/tools/gutenberg/sync-gutenberg.js +++ b/tools/gutenberg/sync.js @@ -15,7 +15,7 @@ const { spawn } = require( 'child_process' ); const path = require( 'path' ); -const { isLocalRepoMode } = require( './gutenberg-utils' ); +const { isLocalRepoMode } = require( './utils' ); const rootDir = path.resolve( __dirname, '../..' ); @@ -46,13 +46,13 @@ function runScript( scriptPath, args = [] ) { async function main() { if ( isLocalRepoMode() ) { console.log( 'ā„¹ļø Local Gutenberg repository mode active (GUTENBERG_LOCAL_REPO=true).\n' ); - await runScript( 'tools/gutenberg/checkout-gutenberg.js' ); - await runScript( 'tools/gutenberg/build-gutenberg.js' ); + await runScript( 'tools/gutenberg/checkout.js' ); + await runScript( 'tools/gutenberg/build.js' ); } else { - await runScript( 'tools/gutenberg/download-gutenberg.js' ); + await runScript( 'tools/gutenberg/download.js' ); } - await runScript( 'tools/gutenberg/copy-gutenberg-build.js', [ '--dev' ] ); + await runScript( 'tools/gutenberg/copy.js', [ '--dev' ] ); } main().catch( ( error ) => { diff --git a/tools/gutenberg/gutenberg-utils.js b/tools/gutenberg/utils.js similarity index 100% rename from tools/gutenberg/gutenberg-utils.js rename to tools/gutenberg/utils.js diff --git a/webpack.config.js b/webpack.config.js index 29ebbd696b875..2fbda4cf10165 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,7 +14,7 @@ module.exports = function ( // Only building Core-specific media files and development scripts. // Blocks, packages, script modules, and vendors are now sourced from - // the Gutenberg build (see tools/gutenberg/copy-gutenberg-build.js). + // the Gutenberg build (see tools/gutenberg/copy.js). // Note: developmentConfig returns an array of configs, so we spread it. const config = [ mediaConfig( env ), From ad5fc07e7a3f2e43254845bdff15b6c530446977 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:40:25 -0500 Subject: [PATCH 38/53] Run PHPUnit tests once using Gutenberg repo. --- .github/workflows/phpunit-tests.yml | 10 ++++++++++ .github/workflows/reusable-phpunit-tests-v3.yml | 10 +++++++++- .github/workflows/reusable-test-core-build-process.yml | 6 +++--- .github/workflows/test-build-processes.yml | 2 +- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index de2de9091677c..55c59d947ccc3 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -78,6 +78,7 @@ jobs: tests-domain: [ 'example.org' ] multisite: [ false, true ] memcached: [ false ] + build-from-gutenberg: [ false ] include: # Include jobs that test with memcached. @@ -118,6 +119,14 @@ jobs: multisite: false memcached: false report: true + # Build directly using the Gutenberg repository. + - os: 'ubuntu-24.04' + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org' + multisite: false + memcached: false + build-from-gutenberg: true with: os: ${{ matrix.os }} php: ${{ matrix.php }} @@ -129,6 +138,7 @@ jobs: tests-domain: ${{ matrix.tests-domain }} report: ${{ matrix.report || false }} + # # Creates a PHPUnit test job for each PHP/MariaDB combination. # diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 45198c20f5e52..24ba78ed31b99 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -72,6 +72,12 @@ on: required: false type: boolean default: false + build-from-gutenberg-repo: + description: 'Whether to build directly from the Gutenberg repository.' + required: false + type: 'boolean' + default: false + secrets: CODECOV_TOKEN: description: 'The Codecov token required for uploading reports.' @@ -118,11 +124,13 @@ jobs: # - Checks out the WordPress Test reporter repository. # - Submit the test results to the WordPress.org host test results. phpunit-tests: - name: ${{ ( inputs.phpunit-test-groups || inputs.coverage-report ) && format( 'PHP {0} with ', inputs.php ) || '' }} ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }}${{ inputs.db-innovation && ' (innovation release)' || '' }}${{ inputs.memcached && ' with memcached' || '' }}${{ inputs.report && ' (test reporting enabled)' || '' }} ${{ 'example.org' != inputs.tests-domain && inputs.tests-domain || '' }} + name: ${{ ( inputs.phpunit-test-groups || inputs.coverage-report ) && format( 'PHP {0} with ', inputs.php ) || '' }} ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }}${{ inputs.db-innovation && ' (innovation release)' || '' }}${{ inputs.memcached && ' with memcached' || '' }}${{ inputs.report && ' (test reporting enabled)' || '' }} ${{ 'example.org' != inputs.tests-domain && inputs.tests-domain || '' }}${{ inputs.build-from-gutenberg-repo && ' built from Gutenberg repo' || '' }} runs-on: ${{ inputs.os }} timeout-minutes: ${{ inputs.coverage-report && 120 || inputs.php == '8.4' && 30 || 20 }} permissions: contents: read + env: + GUTENBERG_LOCAL_REPO: ${{ inputs.build-from-gutenberg-repo }} steps: - name: Configure environment variables diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml index 4c40bfd7e320b..f19888d3fac65 100644 --- a/.github/workflows/reusable-test-core-build-process.yml +++ b/.github/workflows/reusable-test-core-build-process.yml @@ -36,7 +36,7 @@ on: required: false type: 'boolean' default: false - build-direct-from-gutenberg: + build-from-gutenberg-repo: description: 'Whether to build directly from the Gutenberg repository.' required: false type: 'boolean' @@ -67,13 +67,13 @@ jobs: # - Saves the pull request number to a text file. # - Uploads the pull request number as an artifact. build-process-tests: - name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }} + name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }}${{ inputs.build-from-gutenberg-repo && ' built from Gutenberg repo' || '' }} permissions: contents: read runs-on: ${{ inputs.os }} timeout-minutes: 20 env: - GUTENBERG_LOCAL_REPO: ${{ inputs.build-direct-from-gutenberg }} + GUTENBERG_LOCAL_REPO: ${{ inputs.build-from-gutenberg-repo }} steps: - name: Checkout repository diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml index e861f22fe0627..0a7ccf56e28a8 100644 --- a/.github/workflows/test-build-processes.yml +++ b/.github/workflows/test-build-processes.yml @@ -75,7 +75,7 @@ jobs: test-certificates: ${{ matrix.test-certificates && true || false }} save-build: ${{ matrix.save-build && matrix.save-build || false }} prepare-playground: ${{ matrix.prepare-playground && matrix.prepare-playground || false }} - build-direct-from-gutenberg: ${{ matrix.build-from-gutenberg && matrix.build-from-gutenberg || false }} + build-from-gutenberg-repo: ${{ matrix.build-from-gutenberg && matrix.build-from-gutenberg || false }} # Tests the WordPress Core build process on additional operating systems. # From cf071c891080df2e59e669ec4c2062e105291739 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:41:32 -0500 Subject: [PATCH 39/53] Add `php `version. --- .github/workflows/phpunit-tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 55c59d947ccc3..800537db03012 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -121,6 +121,7 @@ jobs: report: true # Build directly using the Gutenberg repository. - os: 'ubuntu-24.04' + php: '8.4' db-type: 'mysql' db-version: '8.4' tests-domain: 'example.org' From bc1f3bc0676732cd114329e3ff664577a52c3fb8 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:43:30 -0500 Subject: [PATCH 40/53] Pass input correctly. --- .github/workflows/phpunit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 800537db03012..9d4d9f8bbb795 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -138,7 +138,7 @@ jobs: phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} tests-domain: ${{ matrix.tests-domain }} report: ${{ matrix.report || false }} - + build-from-gutenberg-repo: ${{ matrix.build-from-gutenberg }} # # Creates a PHPUnit test job for each PHP/MariaDB combination. From 758df2c4f28f85442d490948890b8ad7924e0cdd Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:57:31 -0500 Subject: [PATCH 41/53] Re-aadd old file to do a proper copy. --- tools/gutenberg/sync-gutenberg.js | 149 ++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tools/gutenberg/sync-gutenberg.js diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js new file mode 100644 index 0000000000000..814188d920cfa --- /dev/null +++ b/tools/gutenberg/sync-gutenberg.js @@ -0,0 +1,149 @@ +#!/usr/bin/env node + +/** + * Sync Gutenberg Script + * + * This script ensures Gutenberg is checked out and built for the correct ref. + * It follows the same pattern as install-changed: + * - Stores the built ref in .gutenberg-hash + * - Compares current package.json ref with stored hash + * - Only runs checkout + build when they differ + * + * @package WordPress + */ + +const { spawn } = require( 'child_process' ); +const fs = require( 'fs' ); +const path = require( 'path' ); + +// Paths +const rootDir = path.resolve( __dirname, '../..' ); +const gutenbergDir = path.join( rootDir, 'gutenberg' ); +const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); +const packageJsonPath = path.join( rootDir, 'package.json' ); +const hashFilePath = path.join( rootDir, '.gutenberg-hash' ); + +/** + * Execute a command and return a promise. + * + * @param {string} command - Command to execute. + * @param {string[]} args - Command arguments. + * @param {Object} options - Spawn options. + * @return {Promise} Promise that resolves when command completes. + */ +function exec( command, args, options = {} ) { + return new Promise( ( resolve, reject ) => { + const child = spawn( command, args, { + cwd: options.cwd || rootDir, + stdio: 'inherit', + shell: process.platform === 'win32', + ...options, + } ); + + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( + new Error( + `${ command } ${ args.join( ' ' ) } failed with code ${ code }` + ) + ); + } else { + resolve(); + } + } ); + + child.on( 'error', reject ); + } ); +} + +/** + * Read the expected Gutenberg ref from package.json. + * + * @return {string} The Gutenberg ref. + */ +function getExpectedRef() { + const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); + const ref = packageJson.gutenberg?.ref; + + if ( ! ref ) { + throw new Error( 'Missing "gutenberg.ref" in package.json' ); + } + + return ref; +} + +/** + * Read the stored hash from .gutenberg-hash file. + * + * @return {string|null} The stored ref, or null if file doesn't exist. + */ +function getStoredHash() { + try { + return fs.readFileSync( hashFilePath, 'utf8' ).trim(); + } catch ( error ) { + return null; + } +} + +/** + * Write the ref to .gutenberg-hash file. + * + * @param {string} ref - The ref to store. + */ +function writeHash( ref ) { + fs.writeFileSync( hashFilePath, ref + '\n' ); +} + +/** + * Check if Gutenberg build exists. + * + * @return {boolean} True if build directory exists. + */ +function hasBuild() { + return fs.existsSync( gutenbergBuildDir ); +} + +/** + * Main execution function. + */ +async function main() { + console.log( 'šŸ” Checking Gutenberg sync status...' ); + + const expectedRef = getExpectedRef(); + const storedHash = getStoredHash(); + + console.log( ` Expected ref: ${ expectedRef }` ); + console.log( ` Stored hash: ${ storedHash || '(none)' }` ); + + // Check if we need to rebuild + if ( storedHash === expectedRef && hasBuild() ) { + console.log( 'āœ… Gutenberg is already synced and built' ); + return; + } + + if ( storedHash !== expectedRef ) { + console.log( '\nšŸ“¦ Gutenberg ref has changed, rebuilding...' ); + } else { + console.log( '\nšŸ“¦ Gutenberg build not found, building...' ); + } + + // Run checkout + console.log( '\nšŸ”„ Running gutenberg:checkout...' ); + await exec( 'node', [ 'tools/gutenberg/checkout-gutenberg.js' ] ); + + // Run build + console.log( '\nšŸ”„ Running gutenberg:build...' ); + await exec( 'node', [ 'tools/gutenberg/build-gutenberg.js' ] ); + + // Write the hash after successful build + writeHash( expectedRef ); + console.log( `\nāœ… Updated .gutenberg-hash to ${ expectedRef }` ); + + console.log( '\nāœ… Gutenberg sync complete!' ); +} + +// Run main function +main().catch( ( error ) => { + console.error( 'āŒ Sync failed:', error.message ); + process.exit( 1 ); +} ); From f29c40f79df674bd2c7437a059b57ded40f284bc Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:57:50 -0500 Subject: [PATCH 42/53] Delete new file to do proper copy. --- tools/gutenberg/sync.js | 61 ----------------------------------------- 1 file changed, 61 deletions(-) delete mode 100644 tools/gutenberg/sync.js diff --git a/tools/gutenberg/sync.js b/tools/gutenberg/sync.js deleted file mode 100644 index 8923eee95a60b..0000000000000 --- a/tools/gutenberg/sync.js +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env node - -/** - * Gutenberg Setup Dispatcher - * - * This is the postinstall entry point for Gutenberg setup. It detects whether - * the contributor is in local repository mode (GUTENBERG_LOCAL_REPO=true in - * their .env file) and runs the appropriate setup flow: - * - * - Default mode: download pre-built zip → copy to Core - * - Local repository mode: checkout git repo → build from source → copy to Core - * - * @package WordPress - */ - -const { spawn } = require( 'child_process' ); -const path = require( 'path' ); -const { isLocalRepoMode } = require( './utils' ); - -const rootDir = path.resolve( __dirname, '../..' ); - -/** - * Spawn a Node.js script and inherit stdio so output is visible in the terminal. - * - * @param {string} scriptPath - Path to the script, relative to the repo root. - * @param {string[]} args - Arguments to pass to the script. - * @return {Promise} Promise that resolves when the script exits successfully. - */ -function runScript( scriptPath, args = [] ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( process.execPath, [ scriptPath, ...args ], { - cwd: rootDir, - stdio: 'inherit', - } ); - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( new Error( `${ scriptPath } exited with code ${ code }` ) ); - } else { - resolve(); - } - } ); - child.on( 'error', reject ); - } ); -} - -async function main() { - if ( isLocalRepoMode() ) { - console.log( 'ā„¹ļø Local Gutenberg repository mode active (GUTENBERG_LOCAL_REPO=true).\n' ); - await runScript( 'tools/gutenberg/checkout.js' ); - await runScript( 'tools/gutenberg/build.js' ); - } else { - await runScript( 'tools/gutenberg/download.js' ); - } - - await runScript( 'tools/gutenberg/copy.js', [ '--dev' ] ); -} - -main().catch( ( error ) => { - console.error( 'āŒ Gutenberg setup failed:', error.message ); - process.exit( 1 ); -} ); From 42c0b64216e53dada49836fa7eb00975cd187b1e Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:58:15 -0500 Subject: [PATCH 43/53] Perform a proper rename. --- tools/gutenberg/sync-gutenberg.js | 149 ------------------------------ tools/gutenberg/sync.js | 61 ++++++++++++ 2 files changed, 61 insertions(+), 149 deletions(-) delete mode 100644 tools/gutenberg/sync-gutenberg.js create mode 100644 tools/gutenberg/sync.js diff --git a/tools/gutenberg/sync-gutenberg.js b/tools/gutenberg/sync-gutenberg.js deleted file mode 100644 index 814188d920cfa..0000000000000 --- a/tools/gutenberg/sync-gutenberg.js +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env node - -/** - * Sync Gutenberg Script - * - * This script ensures Gutenberg is checked out and built for the correct ref. - * It follows the same pattern as install-changed: - * - Stores the built ref in .gutenberg-hash - * - Compares current package.json ref with stored hash - * - Only runs checkout + build when they differ - * - * @package WordPress - */ - -const { spawn } = require( 'child_process' ); -const fs = require( 'fs' ); -const path = require( 'path' ); - -// Paths -const rootDir = path.resolve( __dirname, '../..' ); -const gutenbergDir = path.join( rootDir, 'gutenberg' ); -const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -const packageJsonPath = path.join( rootDir, 'package.json' ); -const hashFilePath = path.join( rootDir, '.gutenberg-hash' ); - -/** - * Execute a command and return a promise. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves when command completes. - */ -function exec( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: 'inherit', - shell: process.platform === 'win32', - ...options, - } ); - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( - new Error( - `${ command } ${ args.join( ' ' ) } failed with code ${ code }` - ) - ); - } else { - resolve(); - } - } ); - - child.on( 'error', reject ); - } ); -} - -/** - * Read the expected Gutenberg ref from package.json. - * - * @return {string} The Gutenberg ref. - */ -function getExpectedRef() { - const packageJson = JSON.parse( fs.readFileSync( packageJsonPath, 'utf8' ) ); - const ref = packageJson.gutenberg?.ref; - - if ( ! ref ) { - throw new Error( 'Missing "gutenberg.ref" in package.json' ); - } - - return ref; -} - -/** - * Read the stored hash from .gutenberg-hash file. - * - * @return {string|null} The stored ref, or null if file doesn't exist. - */ -function getStoredHash() { - try { - return fs.readFileSync( hashFilePath, 'utf8' ).trim(); - } catch ( error ) { - return null; - } -} - -/** - * Write the ref to .gutenberg-hash file. - * - * @param {string} ref - The ref to store. - */ -function writeHash( ref ) { - fs.writeFileSync( hashFilePath, ref + '\n' ); -} - -/** - * Check if Gutenberg build exists. - * - * @return {boolean} True if build directory exists. - */ -function hasBuild() { - return fs.existsSync( gutenbergBuildDir ); -} - -/** - * Main execution function. - */ -async function main() { - console.log( 'šŸ” Checking Gutenberg sync status...' ); - - const expectedRef = getExpectedRef(); - const storedHash = getStoredHash(); - - console.log( ` Expected ref: ${ expectedRef }` ); - console.log( ` Stored hash: ${ storedHash || '(none)' }` ); - - // Check if we need to rebuild - if ( storedHash === expectedRef && hasBuild() ) { - console.log( 'āœ… Gutenberg is already synced and built' ); - return; - } - - if ( storedHash !== expectedRef ) { - console.log( '\nšŸ“¦ Gutenberg ref has changed, rebuilding...' ); - } else { - console.log( '\nšŸ“¦ Gutenberg build not found, building...' ); - } - - // Run checkout - console.log( '\nšŸ”„ Running gutenberg:checkout...' ); - await exec( 'node', [ 'tools/gutenberg/checkout-gutenberg.js' ] ); - - // Run build - console.log( '\nšŸ”„ Running gutenberg:build...' ); - await exec( 'node', [ 'tools/gutenberg/build-gutenberg.js' ] ); - - // Write the hash after successful build - writeHash( expectedRef ); - console.log( `\nāœ… Updated .gutenberg-hash to ${ expectedRef }` ); - - console.log( '\nāœ… Gutenberg sync complete!' ); -} - -// Run main function -main().catch( ( error ) => { - console.error( 'āŒ Sync failed:', error.message ); - process.exit( 1 ); -} ); diff --git a/tools/gutenberg/sync.js b/tools/gutenberg/sync.js new file mode 100644 index 0000000000000..8923eee95a60b --- /dev/null +++ b/tools/gutenberg/sync.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node + +/** + * Gutenberg Setup Dispatcher + * + * This is the postinstall entry point for Gutenberg setup. It detects whether + * the contributor is in local repository mode (GUTENBERG_LOCAL_REPO=true in + * their .env file) and runs the appropriate setup flow: + * + * - Default mode: download pre-built zip → copy to Core + * - Local repository mode: checkout git repo → build from source → copy to Core + * + * @package WordPress + */ + +const { spawn } = require( 'child_process' ); +const path = require( 'path' ); +const { isLocalRepoMode } = require( './utils' ); + +const rootDir = path.resolve( __dirname, '../..' ); + +/** + * Spawn a Node.js script and inherit stdio so output is visible in the terminal. + * + * @param {string} scriptPath - Path to the script, relative to the repo root. + * @param {string[]} args - Arguments to pass to the script. + * @return {Promise} Promise that resolves when the script exits successfully. + */ +function runScript( scriptPath, args = [] ) { + return new Promise( ( resolve, reject ) => { + const child = spawn( process.execPath, [ scriptPath, ...args ], { + cwd: rootDir, + stdio: 'inherit', + } ); + child.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( new Error( `${ scriptPath } exited with code ${ code }` ) ); + } else { + resolve(); + } + } ); + child.on( 'error', reject ); + } ); +} + +async function main() { + if ( isLocalRepoMode() ) { + console.log( 'ā„¹ļø Local Gutenberg repository mode active (GUTENBERG_LOCAL_REPO=true).\n' ); + await runScript( 'tools/gutenberg/checkout.js' ); + await runScript( 'tools/gutenberg/build.js' ); + } else { + await runScript( 'tools/gutenberg/download.js' ); + } + + await runScript( 'tools/gutenberg/copy.js', [ '--dev' ] ); +} + +main().catch( ( error ) => { + console.error( 'āŒ Gutenberg setup failed:', error.message ); + process.exit( 1 ); +} ); From 4bc399f575418f939cff85e3777c8409ff015dd1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:03:00 -0500 Subject: [PATCH 44/53] Ensure a value is always present so matrix parses. --- .github/workflows/phpunit-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index 9d4d9f8bbb795..12c58a0ac2afb 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -138,7 +138,7 @@ jobs: phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} tests-domain: ${{ matrix.tests-domain }} report: ${{ matrix.report || false }} - build-from-gutenberg-repo: ${{ matrix.build-from-gutenberg }} + build-from-gutenberg-repo: ${{ matrix.build-from-gutenberg || false }} # # Creates a PHPUnit test job for each PHP/MariaDB combination. From e1011e9d0d5108161471377b70ab820363b9ffdf Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 14:03:50 -0500 Subject: [PATCH 45/53] Improve documentation in `.env.example`. --- .env.example | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/.env.example b/.env.example index 0b1f306d585c6..2c98c296eee45 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,21 @@ # - '{version}': any major.minor PHP version from 5.2 onwards. ## +## +# Local Gutenberg repository mode. +# +# When set to true, the build system will clone the Gutenberg git repository at +# the commit SHA specified in package.json instead of downloading a pre-built +# zip artifact. The repository will be built from source during `npm install` +# and before each `grunt build`. +# +# This is intended for contributors who need to work across both wordpress-develop +# and the Gutenberg repository simultaneously. +# +# Note: The initial build can take several minutes. +## +GUTENBERG_LOCAL_REPO=false + # The site will be available at http://localhost:LOCAL_PORT LOCAL_PORT=8889 @@ -72,19 +87,3 @@ WP_BASE_URL=http://localhost:${LOCAL_PORT} # This silences the tips output by the dotenv package. ## DOTENV_CONFIG_QUIET=true - -## -# Local Gutenberg repository mode. -# -# When set to true, the build system will clone the Gutenberg git repository at -# the commit SHA specified in package.json instead of downloading a pre-built -# zip artifact. The repository will be built from source during `npm install` -# and before each `grunt build`. -# -# This is intended for contributors who need to work across both wordpress-develop -# and the Gutenberg repository simultaneously. -# -# Requirements: git, and a full Node.js environment capable of building Gutenberg. -# Note: The initial build can take several minutes. -## -# GUTENBERG_LOCAL_REPO=true From 3532a54923791e38959f60f957b32f4ca2423070 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:16:35 -0500 Subject: [PATCH 46/53] Improvements to change tracking. --- .gitignore | 2 +- tools/gutenberg/build.js | 38 ++---- tools/gutenberg/checkout.js | 30 +++- tools/gutenberg/copy.js | 10 ++ tools/gutenberg/download.js | 9 +- tools/gutenberg/utils.js | 263 +++++++++++++++++++++++++++--------- 6 files changed, 261 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index 1305caef7c0ed..926f6ffb9dcd0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,8 +42,8 @@ wp-tests-config.php /src/wp-includes/build /src/wp-includes/theme.json /packagehash.txt -/.gutenberg-hash /.gutenberg-dir-hash +/.gutenberg-repo-hash /artifacts /setup.log /coverage diff --git a/tools/gutenberg/build.js b/tools/gutenberg/build.js index 77159d26493db..9f870ac992219 100644 --- a/tools/gutenberg/build.js +++ b/tools/gutenberg/build.js @@ -9,17 +9,17 @@ * @package WordPress */ -const { spawn, spawnSync } = require( 'child_process' ); +const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -const { gutenbergDir, isGutenbergRepoClone } = require( './utils' ); +const { gutenbergDir, isGutenbergRepoClone, dirHashChanged, writeDirHash } = require( './utils' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); -const buildShaFile = path.join( gutenbergDir, '.gutenberg-build-sha' ); /** * Execute a command and return a promise. + * * Captures output and only displays it on failure for cleaner logs. * * @param {string} command - Command to execute. @@ -84,26 +84,19 @@ function exec( command, args, options = {} ) { */ async function main( force ) { if ( ! isGutenbergRepoClone() ) { - console.log( 'ā„¹ļø Skipping Gutenberg build: gutenberg/ is not a git clone.' ); + console.log( 'ā„¹ļø Skipping Gutenberg build: gutenberg/ is not a git clone.' ); console.log( ' This step only runs in local repository mode (GUTENBERG_LOCAL_REPO=true).' ); return; } - // Skip the build if HEAD has not changed since the last successful build, - // unless --force is passed. - const headResult = spawnSync( 'git', [ 'rev-parse', 'HEAD' ], { - cwd: gutenbergDir, - encoding: 'utf8', - } ); - const currentSha = headResult.status === 0 ? headResult.stdout.trim() : null; - - if ( ! force && currentSha && fs.existsSync( buildShaFile ) ) { - const builtSha = fs.readFileSync( buildShaFile, 'utf8' ).trim(); - if ( builtSha === currentSha ) { - console.log( `ā„¹ļø Gutenberg build is already up to date (${ currentSha.slice( 0, 12 ) }). Skipping.` ); - console.log( ' Use `npm run grunt gutenberg:build -- --force` to rebuild.' ); - return; - } + // Skip the build if the source is unchanged since the last build. + // .gutenberg-dir-hash records a hash of gutenberg/ (excluding build/, + // node_modules/, .git/) written after each successful build. If the source + // is identical, there is nothing new to compile. + if ( ! force && ! dirHashChanged() ) { + console.log( 'ā„¹ļø The gutenberg directory files have not changed. Skipping build.' ); + console.log( ' Use `npm run grunt gutenberg:build -- --force` to rebuild anyway.' ); + return; } console.log( 'šŸ” Checking Gutenberg setup...' ); @@ -157,10 +150,9 @@ async function main( force ) { const duration = Math.round( ( Date.now() - startTime ) / 1000 ); console.log( `āœ… Build completed in ${ duration }s` ); - // Record the built SHA so subsequent runs can skip if HEAD hasn't changed. - if ( currentSha ) { - fs.writeFileSync( buildShaFile, currentSha ); - } + // Update .gutenberg-dir-hash so the next run can skip if the source + // is unchanged. + writeDirHash(); } catch ( error ) { console.error( 'āŒ Build failed:', error.message ); throw error; diff --git a/tools/gutenberg/checkout.js b/tools/gutenberg/checkout.js index 2d0f5298c2013..ca2f9feb3e081 100644 --- a/tools/gutenberg/checkout.js +++ b/tools/gutenberg/checkout.js @@ -18,7 +18,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const path = require( 'path' ); -const { readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone } = require( './utils' ); +const { readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone, writeRepoHash } = require( './utils' ); // Constants const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git'; @@ -204,10 +204,17 @@ async function main( force ) { cwd: gutenbergDir, } ); - // Checkout what was just fetched - await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { - cwd: gutenbergDir, - } ); + if ( force ) { + // Hard-reset the index and working tree to FETCH_HEAD, discarding + // any local modifications. This is the reliable path for --force. + await exec( 'git', [ 'reset', '--hard', 'FETCH_HEAD' ], { + cwd: gutenbergDir, + } ); + } else { + await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], { + cwd: gutenbergDir, + } ); + } console.log( 'āœ… Checked out successfully' ); } catch ( error ) { @@ -233,6 +240,19 @@ async function main( force ) { process.exit( 1 ); } + // Record the checked-out SHA so build.js and copy.js can detect if HEAD + // has been moved manually since this checkout was performed. + writeRepoHash( ref ); + + // Invalidate the stale directory hash. The source tree has just changed + // (new checkout), so the old hash is no longer valid. Removing the file + // ensures checkDirHash() and the build skip check stay silent until + // build.js writes a fresh hash after the first successful build. + const dirHashFile = path.join( rootDir, '.gutenberg-dir-hash' ); + if ( fs.existsSync( dirHashFile ) ) { + fs.rmSync( dirHashFile ); + } + console.log( '\nāœ… Gutenberg checkout complete!' ); } diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 730ea99d69cca..fd14e3536510f 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -13,6 +13,7 @@ const fs = require( 'fs' ); const path = require( 'path' ); const json2php = require( 'json2php' ); const glob = require( 'glob' ); +const { checkDirHash, checkRepoHash, isGutenbergRepoClone } = require( './utils' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); @@ -874,6 +875,14 @@ async function main() { console.log( 'āœ… Gutenberg build found' ); + // In local repo mode, warn if HEAD has moved since the last checkout. + if ( isGutenbergRepoClone() ) { + checkRepoHash(); + } + + // Warn if the build directory has changed since the last copy. + checkDirHash(); + // 1. Copy PHP infrastructure console.log( '\nšŸ“¦ Copying PHP infrastructure...' ); const phpConfig = COPY_CONFIG.phpInfrastructure; @@ -1129,6 +1138,7 @@ async function main() { console.log( ` Script modules: ${ modulesDest }` ); console.log( ` Styles: ${ stylesDest }` ); console.log( ` Blocks: ${ blocksDest }` ); + } // Run main function diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index 1cc1a66156b07..c66c95eba78e9 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -17,7 +17,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); const path = require( 'path' ); -const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, isLocalRepoMode } = require( './utils' ); +const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, writeDirHash, isLocalRepoMode } = require( './utils' ); /** * Execute a command. By default, stdio is inherited so progress is visible in @@ -184,6 +184,13 @@ async function main( force ) { fs.rmSync( zipPath ); } + if ( downloaded ) { + // Record a baseline hash of the freshly extracted directory before + // verifying, so that verifyGutenbergVersion()'s checkDirHash() call + // compares against the fresh state and does not produce a stale warning. + writeDirHash(); + } + // Verify the downloaded version matches the expected SHA. verifyGutenbergVersion(); diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 435329bb5b9e5..60865f8dbb18e 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -20,7 +20,29 @@ const dotenvExpand = require( 'dotenv-expand' ); // Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); + +/* + * Hash file paths (all at the repo root so they are not committed and are + * covered by the top-level .gitignore entries). + * + * .gutenberg-dir-hash — SHA-256 of gutenberg/ excluding build/, node_modules/, + * and .git/. Written after a successful build (clone mode) + * or download (download mode). Checked at verify time and + * before building to detect source changes. + * + * .gutenberg-repo-hash — The git commit SHA that was checked out into + * gutenberg/ by checkout.js. + * Written after checkout, checked before build and copy + * to warn when HEAD has moved away from the expected SHA. + */ const gutenbergDirHashFile = path.join( rootDir, '.gutenberg-dir-hash' ); +const gutenbergRepoHashFile = path.join( rootDir, '.gutenberg-repo-hash' ); + +/** + * Directories inside gutenberg/ to exclude when hashing. + * These are build artefacts or dependency caches, not source files. + */ +const DIR_HASH_EXCLUDE = new Set( [ 'build', 'node_modules', '.git' ] ); // Load .env so GUTENBERG_LOCAL_REPO and other vars are available regardless // of how this module is invoked (grunt task, direct node call, postinstall). @@ -78,7 +100,8 @@ function isGutenbergRepoClone() { * on failure. * * In git clone mode, a SHA mismatch is a warning only — the developer may - * intentionally be on a different commit. In download mode it is a hard error. + * intentionally be on a different commit. In download mode it is a hard error + * because the zip is supposed to exactly match. */ function verifyGutenbergVersion() { console.log( '\nšŸ” Verifying Gutenberg version...' ); @@ -109,21 +132,30 @@ function verifyGutenbergVersion() { } else { console.log( 'āœ… Version verified (git clone at expected SHA)' ); } + + // Warn if HEAD has drifted away from the SHA recorded at checkout time. + checkRepoHash(); + + // Warn if the source has changed since the last build. + checkDirHash(); return; } + // Download mode: the zip contains a .gutenberg-hash file with the commit SHA + // that produced the artifact. const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); try { const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); if ( installedHash !== sha ) { console.error( - `āŒ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg:download -- --force\` to download the correct version.` + `āŒ SHA mismatch: expected ${ sha } but the downloaded artifact was built from ${ installedHash }.` + + ` Run \`npm run grunt gutenberg:download -- --force\` to download the correct version.` ); process.exit( 1 ); } } catch ( error ) { if ( error.code === 'ENOENT' ) { - console.error( `āŒ .gutenberg-hash not found. Run \`npm run grunt gutenberg:download\` to download Gutenberg.` ); + console.error( `āŒ .gutenberg-hash not found inside gutenberg/. Run \`npm run grunt gutenberg:download\` to download Gutenberg.` ); } else { console.error( `āŒ ${ error.message }` ); } @@ -131,97 +163,206 @@ function verifyGutenbergVersion() { } console.log( 'āœ… Version verified' ); + + // Warn if gutenberg/ has changed since it was downloaded. + checkDirHash(); } +// --------------------------------------------------------------------------- +// Directory hash helpers +// --------------------------------------------------------------------------- + /** - * Calculate a hash of the Gutenberg directory and all its contents. + * Recursively collect all regular files under a directory, sorted. * - * This stores the hash of the Gutenberg directory in a `.gutenberg-hash` file - * to track when changes have been made to those files locally. + * Symlinks are skipped because they can point to directories and would throw + * EISDIR when read as regular files. * - * Files are processed in sorted order so the result is deterministic. The hash - * incorporates each file's relative path and its contents. + * @param {string} dir - Directory to walk. + * @param {Set} excludeDirs - Directory names to skip entirely. + * @return {string[]} Sorted list of absolute file paths. */ -function hashGutenbergDir() { - const hash = crypto.createHash( 'sha256' ); - - /** - * Recursively collect all file paths under a directory, sorted. - * - * @param {string} dir - Directory to walk. - * @return {string[]} Sorted list of absolute file paths. - */ - function collectFiles( dir ) { - const files = []; - for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ).sort( ( a, b ) => a.name.localeCompare( b.name ) ) ) { - const fullPath = path.join( dir, entry.name ); - if ( entry.isDirectory() ) { - files.push( ...collectFiles( fullPath ) ); - } else { - files.push( fullPath ); - } +function collectFiles( dir, excludeDirs = new Set() ) { + const files = []; + for ( const entry of fs.readdirSync( dir, { withFileTypes: true } ).sort( ( a, b ) => a.name.localeCompare( b.name ) ) ) { + if ( entry.isDirectory() && excludeDirs.has( entry.name ) ) { + continue; + } + const fullPath = path.join( dir, entry.name ); + if ( entry.isDirectory() ) { + files.push( ...collectFiles( fullPath, excludeDirs ) ); + } else if ( entry.isFile() ) { + files.push( fullPath ); } - return files; } + return files; +} - for ( const filePath of collectFiles( gutenbergDir ) ) { - // Hash the relative path so the result is location-independent. - hash.update( path.relative( gutenbergDir, filePath ) ); +/** + * Compute a deterministic SHA-256 hash of a directory's contents. + * + * Files are processed in sorted order so the result is stable across runs. + * Each file contributes its path relative to `hashRoot` and its contents to + * the hash so that renames and edits both produce a different digest. + * + * @param {string} hashRoot - Directory to hash. + * @param {Set} excludeDirs - Directory names to skip. + * @return {string} Hex digest. + */ +function computeDirHash( hashRoot, excludeDirs = new Set() ) { + const hash = crypto.createHash( 'sha256' ); + for ( const filePath of collectFiles( hashRoot, excludeDirs ) ) { + hash.update( path.relative( hashRoot, filePath ) ); hash.update( fs.readFileSync( filePath ) ); } + return hash.digest( 'hex' ); +} - const digest = hash.digest( 'hex' ); - fs.writeFileSync( gutenbergDirHashFile, digest ); - return digest; +/** + * Compute the hash of gutenberg/ excluding build/, node_modules/, and .git/. + * + * @return {string} Hex digest. + */ +function computeGutenbergHash() { + return computeDirHash( gutenbergDir, DIR_HASH_EXCLUDE ); +} + +/** + * Write the current gutenberg/ directory hash to .gutenberg-dir-hash. + * + * Call this after a successful build (clone mode) or download (download mode) + * so subsequent runs can detect when the source has changed. + */ +function writeDirHash() { + if ( ! fs.existsSync( gutenbergDir ) ) { + return; + } + fs.writeFileSync( gutenbergDirHashFile, computeGutenbergHash() ); } /** - * Checks for changes to the local gutenberg directory. + * Check whether gutenberg/ has changed since the last time writeDirHash() ran. * - * In git clone mode, checks for uncommitted working tree changes via - * `git status`. In download mode, compares the directory file hash against the - * stored value in .gutenberg-dir-hash. + * Returns true when .gutenberg-dir-hash does not exist (no baseline recorded + * yet) or when the stored hash differs from the current directory contents. * - * Either way, only a warning is issued — the build is not aborted. + * @return {boolean} True if the directory has changed. */ -function checkGutenbergDirHash() { - if ( isGutenbergRepoClone() ) { - const result = spawnSync( 'git', [ 'status', '--porcelain' ], { - cwd: gutenbergDir, - encoding: 'utf8', - } ); - if ( result.status !== 0 ) { - console.warn( 'āš ļø Could not check the gutenberg working tree status.' ); - return; +function dirHashChanged() { + if ( ! fs.existsSync( gutenbergDirHashFile ) ) { + return true; + } + const storedHash = fs.readFileSync( gutenbergDirHashFile, 'utf8' ).trim(); + return computeGutenbergHash() !== storedHash; +} + +/** + * Warn if gutenberg/ has changed since the last build or download. + * + * Reads .gutenberg-dir-hash and compares it to the current hash. Only a + * warning is issued — the calling script is not aborted. + */ +function checkDirHash() { + if ( ! fs.existsSync( gutenbergDir ) ) { + return; + } + + if ( ! fs.existsSync( gutenbergDirHashFile ) ) { + // No stored hash yet — nothing to compare against. + return; + } + + if ( dirHashChanged() ) { + if ( isGutenbergRepoClone() ) { + console.warn( + 'āš ļø The gutenberg/ source has changed since the last build.' + + ' The Gutenberg build process will be run before building Core.' + ); + } else { + console.warn( + 'āš ļø The gutenberg/ directory has changed since it was downloaded.' + + ' Run `npm run grunt gutenberg:download -- --force` to restore the original files,' + + ' or `npm run grunt gutenberg:copy` if you intentionally modified the build output.' + ); } - if ( result.stdout.trim() ) { - console.warn( 'āš ļø The gutenberg directory has uncommitted local changes. The build scripts may produce unexpected results.' ); + } else { + if ( isGutenbergRepoClone() ) { + console.log( 'āœ… No changes to the local Gutenberg checkout.' ); } else { - console.log( 'āœ… The gutenberg working tree has no uncommitted changes.' ); + console.log( 'āœ… Gutenberg directory contents match the downloaded version.' ); } - return; } +} - if ( ! fs.existsSync( gutenbergDirHashFile ) ) { - console.warn( 'āš ļø .gutenberg-dir-hash not found. Files in the gutenberg directory may have changed since downloading.' ); +// --------------------------------------------------------------------------- +// Repository hash helpers (local repo clone mode) +// --------------------------------------------------------------------------- + +/** + * Write a git commit SHA to .gutenberg-repo-hash. + * + * Call this immediately after checkout so subsequent commands can verify that + * HEAD has not drifted away from the expected commit. + * + * @param {string} sha - The full git commit SHA that was checked out. + */ +function writeRepoHash( sha ) { + fs.writeFileSync( gutenbergRepoHashFile, sha ); +} + +/** + * Warn if the current HEAD of gutenberg/ differs from the SHA recorded in + * .gutenberg-repo-hash. + * + * A difference means the contributor has manually moved HEAD (e.g. via + * `git checkout` or `git pull`) since the last `gutenberg:checkout` run. + * Only a warning is issued — the calling script is not aborted. + */ +function checkRepoHash() { + if ( ! fs.existsSync( gutenbergRepoHashFile ) ) { + // No stored hash — checkout.js has not been run yet, nothing to compare. return; } - const storedHash = fs.readFileSync( gutenbergDirHashFile, 'utf8' ).trim(); - const currentHash = hashGutenbergDir(); + const storedSha = fs.readFileSync( gutenbergRepoHashFile, 'utf8' ).trim(); + + const result = spawnSync( 'git', [ 'rev-parse', 'HEAD' ], { + cwd: gutenbergDir, + encoding: 'utf8', + } ); - if ( currentHash !== storedHash ) { - console.warn( 'āš ļø The gutenberg directory has changed since the last copy. The build scripts may produce unexpected results.' ); + if ( result.status !== 0 ) { + // Can't read HEAD — not a git repo or git not available. return; } - console.log( 'āœ… The contents of the gutenberg directory have not been modified.' ); + const currentSha = result.stdout.trim(); + if ( currentSha !== storedSha ) { + console.warn( + `āš ļø Gutenberg HEAD (${ currentSha.slice( 0, 12 ) }) does not match the last checked-out SHA (${ storedSha.slice( 0, 12 ) }).` + + ` HEAD has moved since the last \`gutenberg:checkout\` run.` + + ` Run \`npm run grunt gutenberg:build\` to rebuild from the current HEAD,` + + ` or \`npm run grunt gutenberg:checkout -- --force\` to reset to the expected commit.` + ); + } } -module.exports = { rootDir, gutenbergDir, readGutenbergConfig, isLocalRepoMode, isGutenbergRepoClone, verifyGutenbergVersion, hashGutenbergDir, checkGutenbergDirHash }; +module.exports = { + rootDir, + gutenbergDir, + readGutenbergConfig, + isLocalRepoMode, + isGutenbergRepoClone, + verifyGutenbergVersion, + computeDirHash, + computeGutenbergHash, + writeDirHash, + dirHashChanged, + checkDirHash, + writeRepoHash, + checkRepoHash, +}; if ( require.main === module ) { verifyGutenbergVersion(); - - checkGutenbergDirHash(); } From 47e133df7710f9e19d9d809fa9788dac44d4933c Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:40:42 -0500 Subject: [PATCH 47/53] Remove unused function. --- tools/gutenberg/checkout.js | 43 ------------------------------------- 1 file changed, 43 deletions(-) diff --git a/tools/gutenberg/checkout.js b/tools/gutenberg/checkout.js index ca2f9feb3e081..362971b15c64f 100644 --- a/tools/gutenberg/checkout.js +++ b/tools/gutenberg/checkout.js @@ -88,49 +88,6 @@ function exec( command, args, options = {} ) { } ); } -/** - * Execute a command and capture its output. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves with command output. - */ -function execOutput( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - shell: process.platform === 'win32', // Use shell on Windows to find .cmd files - ...options, - } ); - - let stdout = ''; - let stderr = ''; - - if ( child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - if ( child.stderr ) { - child.stderr.on( 'data', ( data ) => { - stderr += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( new Error( `${ command } failed: ${ stderr }` ) ); - } else { - resolve( stdout.trim() ); - } - } ); - - child.on( 'error', reject ); - } ); -} - /** * Main execution function. */ From 74b18c71cc979075196cb56509c59ac78e134276 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:51:48 -0500 Subject: [PATCH 48/53] Fix some JShint warnings. --- tools/gutenberg/copy.js | 8 ++++++-- tools/gutenberg/download.js | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index fd14e3536510f..3f5cb7835a951 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -753,8 +753,12 @@ function parsePHPArray( phpArrayContent ) { } else { currentPart += char; if ( ! inString ) { - if ( char === '(' ) depth++; - if ( char === ')' ) depth--; + if ( char === '(' ) { + depth++; + } + if ( char === ')' ) { + depth--; + } } } } diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index c66c95eba78e9..f911c691aa49c 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -16,7 +16,7 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); const { pipeline } = require( 'stream/promises' ); -const path = require( 'path' ); +const zlib = require( 'zlib' ); const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, writeDirHash, isLocalRepoMode } = require( './utils' ); /** From b93a4e54b18c601a46cb92084e1959300137de4b Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:52:01 -0500 Subject: [PATCH 49/53] Add `gutenberg:sync` task to Grunt. --- Gruntfile.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Gruntfile.js b/Gruntfile.js index 7bddd60c6879c..dccfeb1637f34 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1525,6 +1525,17 @@ module.exports = function(grunt) { grunt.registerTask( 'gutenberg:build', 'Builds Gutenberg from source and copies the result to WordPress Core.', [ 'gutenberg:build:run', 'gutenberg:copy' ] ); + grunt.registerTask( 'gutenberg:sync', 'Downloads or checks out Gutenberg at the expected SHA, builds if needed, and copies to WordPress Core.', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/sync.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg build output to WordPress Core.', function() { const done = this.async(); const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; From e5a526282de7d3ff197e8877d49309feaf27b036 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 19:37:20 -0500 Subject: [PATCH 50/53] Convert to using a `.tar.gz` file. This has better native support in Node. --- package.json | 2 +- tools/gutenberg/download.js | 116 ++++++++++++++---------------------- 2 files changed, 45 insertions(+), 73 deletions(-) diff --git a/package.json b/package.json index 2417afc92dda9..f4eec6bb3480a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "https://develop.svn.wordpress.org/trunk" }, "gutenberg": { - "sha": "cd4cf58db37e9f774f321df14138dfef5d7e475a", + "sha": "1ed7da329cc9ffb27cbe5d373ea15db309a135b7", "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" }, "engines": { diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index f911c691aa49c..f51fd818119ff 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -3,7 +3,7 @@ /** * Download Gutenberg Repository Script. * - * This script downloads a pre-built Gutenberg zip artifact from the GitHub + * This script downloads a pre-built Gutenberg tar.gz artifact from the GitHub * Container Registry and extracts it into the ./gutenberg directory. * * The artifact is identified by the "gutenberg.sha" value in the root @@ -15,53 +15,11 @@ const { spawn } = require( 'child_process' ); const fs = require( 'fs' ); +const { Writable } = require( 'stream' ); const { pipeline } = require( 'stream/promises' ); const zlib = require( 'zlib' ); const { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion, writeDirHash, isLocalRepoMode } = require( './utils' ); -/** - * Execute a command. By default, stdio is inherited so progress is visible in - * the terminal. When `options.captureOutput` is true, stdout is piped and the - * promise resolves with the captured stdout once the process exits. - * - * @param {string} command - Command to execute. - * @param {string[]} args - Command arguments. - * @param {Object} options - Spawn options. - * @return {Promise} Promise that resolves with stdout when command completes successfully. - */ -function exec( command, args, options = {} ) { - return new Promise( ( resolve, reject ) => { - let stdout = ''; - - const child = spawn( command, args, { - cwd: options.cwd || rootDir, - stdio: options.captureOutput ? [ 'ignore', 'pipe', 'inherit' ] : 'inherit', - shell: process.platform === 'win32', - ...options, - } ); - - if ( options.captureOutput && child.stdout ) { - child.stdout.on( 'data', ( data ) => { - stdout += data.toString(); - } ); - } - - child.on( 'close', ( code ) => { - if ( code !== 0 ) { - reject( - new Error( - `${ command } ${ args.join( ' ' ) } failed with code ${ code }` - ) - ); - } else { - resolve( stdout.trim() ); - } - } ); - - child.on( 'error', reject ); - } ); -} - /** * Main execution function. * @@ -95,19 +53,17 @@ async function main( force ) { // Skip download if the gutenberg directory already exists and --force is not set. let downloaded = false; if ( ! force && fs.existsSync( gutenbergDir ) ) { - console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg-download -- --force` to download a fresh copy.' ); + console.log( '\nā„¹ļø The `gutenberg` directory already exists. Use `npm run grunt gutenberg:download -- --force` to download a fresh copy.' ); } else { downloaded = true; - const zipName = `gutenberg-${ sha }.zip`; - const zipPath = path.join( rootDir, zipName ); // Step 1: Get an anonymous GHCR token for pulling. console.log( '\nšŸ”‘ Fetching GHCR token...' ); let token; try { - const response = await fetch( `https://ghcr.io/token?scope=repository:${ghcrRepo}:pull&service=ghcr.io` ); + const response = await fetch( `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` ); if ( ! response.ok ) { - throw new Error( `Failed to fetch token: ${response.status} ${response.statusText}` ); + throw new Error( `Failed to fetch token: ${ response.status } ${ response.statusText }` ); } const data = await response.json(); token = data.token; @@ -144,8 +100,17 @@ async function main( force ) { process.exit( 1 ); } - // Step 3: Download the blob (the zip file). - console.log( `\nšŸ“„ Downloading ${ zipName }...` ); + // Remove existing gutenberg directory so the extraction is clean. + if ( fs.existsSync( gutenbergDir ) ) { + console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); + fs.rmSync( gutenbergDir, { recursive: true, force: true } ); + } + + fs.mkdirSync( gutenbergDir, { recursive: true } ); + + // Step 3: Stream the blob directly through gunzip into tar, writing + // into ./gutenberg with no temporary file on disk. + console.log( `\nšŸ“„ Downloading and extracting artifact...` ); try { const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { headers: { @@ -155,33 +120,40 @@ async function main( force ) { if ( ! response.ok ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - await pipeline( response.body, fs.createWriteStream( zipPath ) ); - console.log( 'āœ… Download complete' ); - } catch ( error ) { - console.error( 'āŒ Download failed:', error.message ); - process.exit( 1 ); - } - // Remove existing gutenberg directory so the unzip is clean. - if ( fs.existsSync( gutenbergDir ) ) { - console.log( '\nšŸ—‘ļø Removing existing gutenberg directory...' ); - fs.rmSync( gutenbergDir, { recursive: true, force: true } ); - } + // Spawn tar to read from stdin and extract into gutenbergDir. + // `tar` is available on macOS, Linux, and Windows 10+. + const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { + stdio: [ 'pipe', 'inherit', 'inherit' ], + } ); - fs.mkdirSync( gutenbergDir, { recursive: true } ); + const tarDone = new Promise( ( resolve, reject ) => { + tar.on( 'close', ( code ) => { + if ( code !== 0 ) { + reject( new Error( `tar exited with code ${ code }` ) ); + } else { + resolve(); + } + } ); + tar.on( 'error', reject ); + } ); - // Extract the zip into ./gutenberg. - console.log( `\nšŸ“¦ Extracting ${ zipName } into ./gutenberg...` ); - try { - await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] ); - console.log( 'āœ… Extraction complete' ); + // Pipe: fetch body → gunzip → tar stdin. + // Decompressing in Node keeps the pipeline error handling + // consistent and means tar only sees plain tar data on stdin. + await pipeline( + response.body, + zlib.createGunzip(), + Writable.toWeb( tar.stdin ), + ); + + await tarDone; + + console.log( 'āœ… Download and extraction complete' ); } catch ( error ) { - console.error( 'āŒ Extraction failed:', error.message ); + console.error( 'āŒ Download/extraction failed:', error.message ); process.exit( 1 ); } - - // Clean up the zip file. - fs.rmSync( zipPath ); } if ( downloaded ) { From 0da3f5ea1c612121ed257b950d476818724efe5d Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:02:39 -0500 Subject: [PATCH 51/53] Addressing code review feedback. --- tools/gutenberg/checkout.js | 5 ++--- tools/gutenberg/utils.js | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/tools/gutenberg/checkout.js b/tools/gutenberg/checkout.js index 362971b15c64f..f1b0fe31ec784 100644 --- a/tools/gutenberg/checkout.js +++ b/tools/gutenberg/checkout.js @@ -156,14 +156,13 @@ async function main( force ) { // Fetch and checkout target ref console.log( `\nšŸ“” Fetching and checking out: ${ ref }` ); try { - // Fetch the specific ref (works for branches, tags, and commit hashes) + // Fetch the specific ref (works for branches, tags, and commit hashes). await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], { cwd: gutenbergDir, } ); if ( force ) { - // Hard-reset the index and working tree to FETCH_HEAD, discarding - // any local modifications. This is the reliable path for --force. + // Hard-reset the index and working tree to FETCH_HEAD, discarding any local modifications. await exec( 'git', [ 'reset', '--hard', 'FETCH_HEAD' ], { cwd: gutenbergDir, } ); diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index 60865f8dbb18e..b14d5ff356028 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -16,8 +16,6 @@ const path = require( 'path' ); const { spawnSync } = require( 'child_process' ); const dotenv = require( 'dotenv' ); const dotenvExpand = require( 'dotenv-expand' ); - -// Paths const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); @@ -42,7 +40,7 @@ const gutenbergRepoHashFile = path.join( rootDir, '.gutenberg-repo-hash' ); * Directories inside gutenberg/ to exclude when hashing. * These are build artefacts or dependency caches, not source files. */ -const DIR_HASH_EXCLUDE = new Set( [ 'build', 'node_modules', '.git' ] ); +const DIR_HASH_EXCLUDE = new Set( [ '.git', 'node_modules', 'phpunit', 'platform-docs', 'tests', 'vendor' ] ); // Load .env so GUTENBERG_LOCAL_REPO and other vars are available regardless // of how this module is invoked (grunt task, direct node call, postinstall). From 22a24fae3842cd87519cc630619301f4b148f667 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:24:37 -0500 Subject: [PATCH 52/53] Prevent race conditions. --- tools/gutenberg/checkout.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/gutenberg/checkout.js b/tools/gutenberg/checkout.js index f1b0fe31ec784..91818c961c683 100644 --- a/tools/gutenberg/checkout.js +++ b/tools/gutenberg/checkout.js @@ -204,10 +204,7 @@ async function main( force ) { // (new checkout), so the old hash is no longer valid. Removing the file // ensures checkDirHash() and the build skip check stay silent until // build.js writes a fresh hash after the first successful build. - const dirHashFile = path.join( rootDir, '.gutenberg-dir-hash' ); - if ( fs.existsSync( dirHashFile ) ) { - fs.rmSync( dirHashFile ); - } + fs.rmSync( path.join( rootDir, '.gutenberg-dir-hash' ), { force: true } ); console.log( '\nāœ… Gutenberg checkout complete!' ); } From b7a909fb9ad97092be62afeaf8e4d34b4c771bf1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Mon, 2 Mar 2026 20:32:38 -0500 Subject: [PATCH 53/53] Use correct multi-line inline comment format. --- tools/gutenberg/build.js | 28 +++++++++++++-------- tools/gutenberg/checkout.js | 22 ++++++++++------ tools/gutenberg/copy.js | 50 ++++++++++++++++++++++++------------- tools/gutenberg/download.js | 28 +++++++++++++-------- tools/gutenberg/utils.js | 12 ++++++--- 5 files changed, 89 insertions(+), 51 deletions(-) diff --git a/tools/gutenberg/build.js b/tools/gutenberg/build.js index 9f870ac992219..1f84a02a8b0e3 100644 --- a/tools/gutenberg/build.js +++ b/tools/gutenberg/build.js @@ -89,10 +89,12 @@ async function main( force ) { return; } - // Skip the build if the source is unchanged since the last build. - // .gutenberg-dir-hash records a hash of gutenberg/ (excluding build/, - // node_modules/, .git/) written after each successful build. If the source - // is identical, there is nothing new to compile. + /* + * Skip the build if the source is unchanged since the last build. + * .gutenberg-dir-hash records a hash of gutenberg/ (excluding build/, + * node_modules/, .git/) written after each successful build. If the source + * is identical, there is nothing new to compile. + */ if ( ! force && ! dirHashChanged() ) { console.log( 'ā„¹ļø The gutenberg directory files have not changed. Skipping build.' ); console.log( ' Use `npm run grunt gutenberg:build -- --force` to rebuild anyway.' ); @@ -124,11 +126,13 @@ async function main( force ) { const startTime = Date.now(); - // Invoke the build script directly with node instead of going through - // `npm run build --` to avoid shell argument mangling of the base-url - // value (which contains spaces, parentheses, and single quotes). - // The PATH is extended with node_modules/.bin so that bin commands - // like `wp-build` are found, matching what npm would normally provide. + /* + * Invoke the build script directly with node instead of going through + * `npm run build --` to avoid shell argument mangling of the base-url + * value (which contains spaces, parentheses, and single quotes). + * The PATH is extended with node_modules/.bin so that bin commands + * like `wp-build` are found, matching what npm would normally provide. + */ const binPath = path.join( gutenbergDir, 'node_modules', '.bin' ); try { await exec( 'node', [ @@ -150,8 +154,10 @@ async function main( force ) { const duration = Math.round( ( Date.now() - startTime ) / 1000 ); console.log( `āœ… Build completed in ${ duration }s` ); - // Update .gutenberg-dir-hash so the next run can skip if the source - // is unchanged. + /* + * Update .gutenberg-dir-hash so the next run can skip if the source + * is unchanged. + */ writeDirHash(); } catch ( error ) { console.error( 'āŒ Build failed:', error.message ); diff --git a/tools/gutenberg/checkout.js b/tools/gutenberg/checkout.js index 91818c961c683..399377c909dca 100644 --- a/tools/gutenberg/checkout.js +++ b/tools/gutenberg/checkout.js @@ -123,8 +123,10 @@ async function main( force ) { if ( ! gutenbergExists ) { console.log( '\nšŸ“„ Cloning Gutenberg repository (shallow clone)...' ); try { - // Generic shallow clone approach that works for both branches and commit hashes - // 1. Clone with no checkout and shallow depth + /* + * Generic shallow clone approach that works for both branches and commit hashes. + * 1. Clone with no checkout and shallow depth. + */ await exec( 'git', [ 'clone', '--depth', @@ -196,14 +198,18 @@ async function main( force ) { process.exit( 1 ); } - // Record the checked-out SHA so build.js and copy.js can detect if HEAD - // has been moved manually since this checkout was performed. + /* + * Record the checked-out SHA so build.js and copy.js can detect if HEAD + * has been moved manually since this checkout was performed. + */ writeRepoHash( ref ); - // Invalidate the stale directory hash. The source tree has just changed - // (new checkout), so the old hash is no longer valid. Removing the file - // ensures checkDirHash() and the build skip check stay silent until - // build.js writes a fresh hash after the first successful build. + /* + * Invalidate the stale directory hash. The source tree has just changed + * (new checkout), so the old hash is no longer valid. Removing the file + * ensures checkDirHash() and the build skip check stay silent until + * build.js writes a fresh hash after the first successful build. + */ fs.rmSync( path.join( rootDir, '.gutenberg-dir-hash' ), { force: true } ); console.log( '\nāœ… Gutenberg checkout complete!' ); diff --git a/tools/gutenberg/copy.js b/tools/gutenberg/copy.js index 3f5cb7835a951..7589735ec7c61 100644 --- a/tools/gutenberg/copy.js +++ b/tools/gutenberg/copy.js @@ -20,8 +20,10 @@ const rootDir = path.resolve( __dirname, '../..' ); const gutenbergDir = path.join( rootDir, 'gutenberg' ); const gutenbergBuildDir = path.join( gutenbergDir, 'build' ); -// Determine build target from command line argument (--dev or --build-dir) -// Default to 'src' for development +/* + * Determine build target from command line argument (--dev or --build-dir). + * Default to 'src' for development. + */ const args = process.argv.slice( 2 ); const buildDirArg = args.find( ( arg ) => arg.startsWith( '--build-dir=' ) ); const buildTarget = buildDirArg @@ -70,8 +72,10 @@ const COPY_CONFIG = { copyAll: true, }, - // Blocks (to wp-includes/blocks/) - // Unified configuration for all block types + /* + * Blocks (to wp-includes/blocks/). + * Unified configuration for all block types. + */ blocks: { destination: 'blocks', sources: [ @@ -833,10 +837,12 @@ function parsePHPArray( phpArrayContent ) { function transformPHPContent( content ) { let transformed = content; - // Fix boot module asset file path for Core's different directory structure - // FROM: __DIR__ . '/../../modules/boot/index.min.asset.php' - // TO: ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php' - // This is needed because Core copies modules to a different location than the plugin structure + /* + * Fix boot module asset file path for Core's different directory structure. + * FROM: __DIR__ . '/../../modules/boot/index.min.asset.php' + * TO: ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php' + * This is needed because Core copies modules to a different location than the plugin structure. + */ transformed = transformed.replace( /__DIR__\s*\.\s*['"]\/\.\.\/\.\.\/modules\/boot\/index\.min\.asset\.php['"]/g, "ABSPATH . WPINC . '/js/dist/script-modules/boot/index.min.asset.php'" @@ -852,9 +858,11 @@ function transformPHPContent( content ) { * @return {string} Transformed content. */ function transformManifestPHP( content ) { - // Remove 'gutenberg' text domain from _x() calls - // FROM: _x( '...', 'icon label', 'gutenberg' ) - // TO: _x( '...', 'icon label' ) + /* + * Remove 'gutenberg' text domain from _x() calls. + * FROM: _x( '...', 'icon label', 'gutenberg' ) + * TO: _x( '...', 'icon label' ) + */ const transformedContent = content.replace( /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g, '_x( $1, $2 )' @@ -928,9 +936,11 @@ async function main() { const scriptsSrc = path.join( gutenbergBuildDir, scriptsConfig.source ); const scriptsDest = path.join( wpIncludesDir, scriptsConfig.destination ); - // Transform function to remove source map comments from all JS files. - // Only match actual source map comments at the start of a line (possibly - // with whitespace), not occurrences inside string literals. + /* + * Transform function to remove source map comments from all JS files. + * Only match actual source map comments at the start of a line (possibly + * with whitespace), not occurrences inside string literals. + */ const removeSourceMaps = ( content ) => { return content.replace( /^\s*\/\/# sourceMappingURL=.*$/gm, '' ).trimEnd(); }; @@ -948,8 +958,10 @@ async function main() { 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) + /* + * 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 ); @@ -987,8 +999,10 @@ async function main() { ); } } else { - // Flatten package structure: package-name/index.js → package-name.js - // This matches Core's expected file structure + /* + * 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 ) { diff --git a/tools/gutenberg/download.js b/tools/gutenberg/download.js index f51fd818119ff..366cfa3cdc300 100644 --- a/tools/gutenberg/download.js +++ b/tools/gutenberg/download.js @@ -108,8 +108,10 @@ async function main( force ) { fs.mkdirSync( gutenbergDir, { recursive: true } ); - // Step 3: Stream the blob directly through gunzip into tar, writing - // into ./gutenberg with no temporary file on disk. + /* + * Step 3: Stream the blob directly through gunzip into tar, writing + * into ./gutenberg with no temporary file on disk. + */ console.log( `\nšŸ“„ Downloading and extracting artifact...` ); try { const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { @@ -121,8 +123,10 @@ async function main( force ) { throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); } - // Spawn tar to read from stdin and extract into gutenbergDir. - // `tar` is available on macOS, Linux, and Windows 10+. + /* + * Spawn tar to read from stdin and extract into gutenbergDir. + * `tar` is available on macOS, Linux, and Windows 10+. + */ const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { stdio: [ 'pipe', 'inherit', 'inherit' ], } ); @@ -138,9 +142,11 @@ async function main( force ) { tar.on( 'error', reject ); } ); - // Pipe: fetch body → gunzip → tar stdin. - // Decompressing in Node keeps the pipeline error handling - // consistent and means tar only sees plain tar data on stdin. + /* + * Pipe: fetch body → gunzip → tar stdin. + * Decompressing in Node keeps the pipeline error handling + * consistent and means tar only sees plain tar data on stdin. + */ await pipeline( response.body, zlib.createGunzip(), @@ -157,9 +163,11 @@ async function main( force ) { } if ( downloaded ) { - // Record a baseline hash of the freshly extracted directory before - // verifying, so that verifyGutenbergVersion()'s checkDirHash() call - // compares against the fresh state and does not produce a stale warning. + /* + * Record a baseline hash of the freshly extracted directory before + * verifying, so that verifyGutenbergVersion()'s checkDirHash() call + * compares against the fresh state and does not produce a stale warning. + */ writeDirHash(); } diff --git a/tools/gutenberg/utils.js b/tools/gutenberg/utils.js index b14d5ff356028..8ccd62cadb781 100644 --- a/tools/gutenberg/utils.js +++ b/tools/gutenberg/utils.js @@ -42,8 +42,10 @@ const gutenbergRepoHashFile = path.join( rootDir, '.gutenberg-repo-hash' ); */ const DIR_HASH_EXCLUDE = new Set( [ '.git', 'node_modules', 'phpunit', 'platform-docs', 'tests', 'vendor' ] ); -// Load .env so GUTENBERG_LOCAL_REPO and other vars are available regardless -// of how this module is invoked (grunt task, direct node call, postinstall). +/* + * Load .env so GUTENBERG_LOCAL_REPO and other vars are available regardless + * of how this module is invoked (grunt task, direct node call, postinstall). + */ dotenvExpand.expand( dotenv.config( { path: path.join( rootDir, '.env' ) } ) ); /** @@ -139,8 +141,10 @@ function verifyGutenbergVersion() { return; } - // Download mode: the zip contains a .gutenberg-hash file with the commit SHA - // that produced the artifact. + /* + * Download mode: the zip contains a .gutenberg-hash file with the commit SHA + * that produced the artifact. + */ const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); try { const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim();