Changeset 62021
- Timestamp:
- 03/13/2026 09:56:05 PM (2 months ago)
- Location:
- trunk
- Files:
-
- 4 edited
-
Gruntfile.js (modified) (1 diff)
-
package.json (modified) (2 diffs)
-
tools/gutenberg/download.js (modified) (3 diffs)
-
tools/gutenberg/utils.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/Gruntfile.js
r61978 r62021 1590 1590 grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { 1591 1591 const done = this.async(); 1592 const args = [ 'tools/gutenberg/download.js' ];1593 if ( grunt.option( 'force' ) ) {1594 args.push( '--force' );1595 }1596 1592 grunt.util.spawn( { 1597 1593 cmd: 'node', 1598 args ,1594 args: [ 'tools/gutenberg/download.js' ], 1599 1595 opts: { stdio: 'inherit' } 1600 1596 }, function( error ) { -
trunk/package.json
r61988 r62021 113 113 }, 114 114 "scripts": { 115 "postinstall": "npm run gutenberg: download",115 "postinstall": "npm run gutenberg:verify", 116 116 "build": "grunt build", 117 117 "build:dev": "grunt build --dev", … … 141 141 "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", 142 142 "gutenberg:copy": "node tools/gutenberg/copy.js", 143 "gutenberg:verify": "node tools/gutenberg/utils.js", 143 144 "gutenberg:download": "node tools/gutenberg/download.js", 144 145 "vendor:copy": "node tools/vendors/copy-vendors.js", -
trunk/tools/gutenberg/download.js
r61873 r62021 5 5 * 6 6 * This script downloads a pre-built Gutenberg tar.gz artifact from the GitHub 7 * Container Registry and extracts it into the ./gutenberg directory. 7 * Container Registry and extracts it into the ./gutenberg directory. Any 8 * existing gutenberg directory is removed before extraction. 8 9 * 9 10 * The artifact is identified by the "gutenberg.sha" value in the root … … 18 19 const { Writable } = require( 'stream' ); 19 20 const { pipeline } = require( 'stream/promises' ); 20 const path = require( 'path' );21 21 const zlib = require( 'zlib' ); 22 const { gutenbergDir, readGutenbergConfig , verifyGutenbergVersion} = require( './utils' );22 const { gutenbergDir, readGutenbergConfig } = require( './utils' ); 23 23 24 24 /** 25 25 * Main execution function. 26 *27 * @param {boolean} force - Whether to force a fresh download even if the gutenberg directory exists.28 26 */ 29 async function main( force) {27 async function main() { 30 28 console.log( '🔍 Checking Gutenberg configuration...' ); 31 29 … … 46 44 } 47 45 48 // Skip download if the gutenberg directory already exists and --force is not set. 49 let downloaded = false; 50 if ( ! force && fs.existsSync( gutenbergDir ) ) { 51 console.log( '\nℹ️ The `gutenberg` directory already exists. Use `npm run grunt gutenberg:download -- --force` to download a fresh copy.' ); 52 } else { 53 downloaded = true; 46 // Step 1: Get an anonymous GHCR token for pulling. 47 console.log( '\n🔑 Fetching GHCR token...' ); 48 let token; 49 try { 50 const response = await fetch( `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` ); 51 if ( ! response.ok ) { 52 throw new Error( `Failed to fetch token: ${ response.status } ${ response.statusText }` ); 53 } 54 const data = await response.json(); 55 token = data.token; 56 if ( ! token ) { 57 throw new Error( 'No token in response' ); 58 } 59 console.log( '✅ Token acquired' ); 60 } catch ( error ) { 61 console.error( '❌ Failed to fetch token:', error.message ); 62 process.exit( 1 ); 63 } 54 64 55 // Step 1: Get an anonymous GHCR token for pulling. 56 console.log( '\n🔑 Fetching GHCR token...' ); 57 let token; 58 try { 59 const response = await fetch( `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` ); 60 if ( ! response.ok ) { 61 throw new Error( `Failed to fetch token: ${ response.status } ${ response.statusText }` ); 62 } 63 const data = await response.json(); 64 token = data.token; 65 if ( ! token ) { 66 throw new Error( 'No token in response' ); 67 } 68 console.log( '✅ Token acquired' ); 69 } catch ( error ) { 70 console.error( '❌ Failed to fetch token:', error.message ); 71 process.exit( 1 ); 65 // Step 2: Get the manifest to find the blob digest. 66 console.log( `\n📋 Fetching manifest for ${ sha }...` ); 67 let digest; 68 try { 69 const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, { 70 headers: { 71 Authorization: `Bearer ${ token }`, 72 Accept: 'application/vnd.oci.image.manifest.v1+json', 73 }, 74 } ); 75 if ( ! response.ok ) { 76 throw new Error( `Failed to fetch manifest: ${ response.status } ${ response.statusText }` ); 77 } 78 const manifest = await response.json(); 79 digest = manifest?.layers?.[ 0 ]?.digest; 80 if ( ! digest ) { 81 throw new Error( 'No layer digest found in manifest' ); 82 } 83 console.log( `✅ Blob digest: ${ digest }` ); 84 } catch ( error ) { 85 console.error( '❌ Failed to fetch manifest:', error.message ); 86 process.exit( 1 ); 87 } 88 89 // Remove existing gutenberg directory so the extraction is clean. 90 if ( fs.existsSync( gutenbergDir ) ) { 91 console.log( '\n🗑️ Removing existing gutenberg directory...' ); 92 fs.rmSync( gutenbergDir, { recursive: true, force: true } ); 93 } 94 95 fs.mkdirSync( gutenbergDir, { recursive: true } ); 96 97 /* 98 * Step 3: Stream the blob directly through gunzip into tar, writing 99 * into ./gutenberg with no temporary file on disk. 100 */ 101 console.log( `\n📥 Downloading and extracting artifact...` ); 102 try { 103 const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { 104 headers: { 105 Authorization: `Bearer ${ token }`, 106 }, 107 } ); 108 if ( ! response.ok ) { 109 throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); 72 110 } 73 111 74 // Step 2: Get the manifest to find the blob digest. 75 console.log( `\n📋 Fetching manifest for ${ sha }...` ); 76 let digest; 77 try { 78 const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ sha }`, { 79 headers: { 80 Authorization: `Bearer ${ token }`, 81 Accept: 'application/vnd.oci.image.manifest.v1+json', 82 }, 112 /* 113 * Spawn tar to read from stdin and extract into gutenbergDir. 114 * `tar` is available on macOS, Linux, and Windows 10+. 115 */ 116 const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { 117 stdio: [ 'pipe', 'inherit', 'inherit' ], 118 } ); 119 120 const tarDone = new Promise( ( resolve, reject ) => { 121 tar.on( 'close', ( code ) => { 122 if ( code !== 0 ) { 123 reject( new Error( `tar exited with code ${ code }` ) ); 124 } else { 125 resolve(); 126 } 83 127 } ); 84 if ( ! response.ok ) { 85 throw new Error( `Failed to fetch manifest: ${ response.status } ${ response.statusText }` ); 86 } 87 const manifest = await response.json(); 88 digest = manifest?.layers?.[ 0 ]?.digest; 89 if ( ! digest ) { 90 throw new Error( 'No layer digest found in manifest' ); 91 } 92 console.log( `✅ Blob digest: ${ digest }` ); 93 } catch ( error ) { 94 console.error( '❌ Failed to fetch manifest:', error.message ); 95 process.exit( 1 ); 96 } 97 98 // Remove existing gutenberg directory so the extraction is clean. 99 if ( fs.existsSync( gutenbergDir ) ) { 100 console.log( '\n🗑️ Removing existing gutenberg directory...' ); 101 fs.rmSync( gutenbergDir, { recursive: true, force: true } ); 102 } 103 104 fs.mkdirSync( gutenbergDir, { recursive: true } ); 128 tar.on( 'error', reject ); 129 } ); 105 130 106 131 /* 107 * Step 3: Stream the blob directly through gunzip into tar, writing 108 * into ./gutenberg with no temporary file on disk. 132 * Pipe: fetch body → gunzip → tar stdin. 133 * Decompressing in Node keeps the pipeline error handling 134 * consistent and means tar only sees plain tar data on stdin. 109 135 */ 110 console.log( `\n📥 Downloading and extracting artifact...` ); 111 try { 112 const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, { 113 headers: { 114 Authorization: `Bearer ${ token }`, 115 }, 116 } ); 117 if ( ! response.ok ) { 118 throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); 119 } 136 await pipeline( 137 response.body, 138 zlib.createGunzip(), 139 Writable.toWeb( tar.stdin ), 140 ); 120 141 121 /* 122 * Spawn tar to read from stdin and extract into gutenbergDir. 123 * `tar` is available on macOS, Linux, and Windows 10+. 124 */ 125 const tar = spawn( 'tar', [ '-x', '-C', gutenbergDir ], { 126 stdio: [ 'pipe', 'inherit', 'inherit' ], 127 } ); 142 await tarDone; 128 143 129 const tarDone = new Promise( ( resolve, reject ) => { 130 tar.on( 'close', ( code ) => { 131 if ( code !== 0 ) { 132 reject( new Error( `tar exited with code ${ code }` ) ); 133 } else { 134 resolve(); 135 } 136 } ); 137 tar.on( 'error', reject ); 138 } ); 139 140 /* 141 * Pipe: fetch body → gunzip → tar stdin. 142 * Decompressing in Node keeps the pipeline error handling 143 * consistent and means tar only sees plain tar data on stdin. 144 */ 145 await pipeline( 146 response.body, 147 zlib.createGunzip(), 148 Writable.toWeb( tar.stdin ), 149 ); 150 151 await tarDone; 152 153 console.log( '✅ Download and extraction complete' ); 154 } catch ( error ) { 155 console.error( '❌ Download/extraction failed:', error.message ); 156 process.exit( 1 ); 157 } 144 console.log( '✅ Download and extraction complete' ); 145 } catch ( error ) { 146 console.error( '❌ Download/extraction failed:', error.message ); 147 process.exit( 1 ); 158 148 } 159 149 160 // Verify the downloaded version matches the expected SHA. 161 verifyGutenbergVersion(); 162 163 if ( downloaded ) { 164 console.log( '\n✅ Gutenberg download complete!' ); 165 } 150 console.log( '\n✅ Gutenberg download complete!' ); 166 151 } 167 152 168 153 // Run main function. 169 const force = process.argv.includes( '--force' ); 170 main( force ).catch( ( error ) => { 154 main().catch( ( error ) => { 171 155 console.error( '❌ Unexpected error:', error ); 172 156 process.exit( 1 ); -
trunk/tools/gutenberg/utils.js
r61873 r62021 5 5 * 6 6 * Shared helpers used by the Gutenberg download script. When run directly, 7 * verifies that the installed Gutenberg build matches the SHA in package.json. 7 * verifies that the installed Gutenberg build matches the SHA in package.json, 8 * and automatically downloads the correct version when needed. 8 9 * 9 10 * @package WordPress 10 11 */ 11 12 13 const { spawnSync } = require( 'child_process' ); 12 14 const fs = require( 'fs' ); 13 15 const path = require( 'path' ); … … 16 18 const rootDir = path.resolve( __dirname, '../..' ); 17 19 const gutenbergDir = path.join( rootDir, 'gutenberg' ); 20 const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); 18 21 19 22 /** … … 40 43 41 44 /** 45 * Trigger a fresh download of the Gutenberg artifact by spawning download.js. 46 * Exits the process if the download fails. 47 */ 48 function downloadGutenberg() { 49 const result = spawnSync( 'node', [ path.join( __dirname, 'download.js' ) ], { stdio: 'inherit' } ); 50 if ( result.status !== 0 ) { 51 process.exit( result.status ?? 1 ); 52 } 53 } 54 55 /** 42 56 * Verify that the installed Gutenberg version matches the expected SHA in 43 * package.json. Logs progress to the console and exits with a non-zero code 44 * on failure. 57 * package.json. Automatically downloads the correct version when the directory 58 * is missing, the hash file is absent, or the hash does not match. Logs 59 * progress to the console and exits with a non-zero code on failure. 45 60 */ 46 61 function verifyGutenbergVersion() { … … 55 70 } 56 71 57 const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); 72 // Check for conditions that require a fresh download. 73 if ( ! fs.existsSync( gutenbergDir ) ) { 74 console.log( 'ℹ️ Gutenberg directory not found. Downloading...' ); 75 downloadGutenberg(); 76 } else { 77 let installedHash = null; 78 try { 79 installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); 80 } catch ( error ) { 81 if ( error.code !== 'ENOENT' ) { 82 console.error( `❌ ${ error.message }` ); 83 process.exit( 1 ); 84 } 85 } 86 87 if ( installedHash === null ) { 88 console.log( 'ℹ️ Hash file not found. Downloading expected version...' ); 89 downloadGutenberg(); 90 } else if ( installedHash !== sha ) { 91 console.log( `ℹ️ Hash mismatch (found ${ installedHash }, expected ${ sha }). Downloading expected version...` ); 92 downloadGutenberg(); 93 } 94 } 95 96 // Final verification — confirms the download (if any) produced the correct version. 58 97 try { 59 98 const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); 60 99 if ( installedHash !== sha ) { 61 console.error( 62 `❌ SHA mismatch: expected ${ sha } but found ${ installedHash }. Run \`npm run grunt gutenberg:download -- --force\` to download the correct version.` 63 ); 100 console.error( `❌ SHA mismatch after download: expected ${ sha } but found ${ installedHash }.` ); 64 101 process.exit( 1 ); 65 102 } 66 103 } catch ( error ) { 67 104 if ( error.code === 'ENOENT' ) { 68 console.error( `❌ .gutenberg-hash not found. Run \`npm run grunt gutenberg:download\` to download Gutenberg.`);105 console.error( '❌ .gutenberg-hash not found after download. This is unexpected.' ); 69 106 } else { 70 107 console.error( `❌ ${ error.message }` );
Note: See TracChangeset
for help on using the changeset viewer.