Skip to content

Commit c604524

Browse files
committed
Build/Test Tools: Improve Gutenberg artifact fetching.
This improves how the built Gutenberg asset is retrieved from the GitHub Container Registry to avoid situations where the download fails when the directory already exists. - The related `postinstall` command has changed from `gutenberg:download` to `gutenberg:verify`. - The `--force` option has been removed. `gutenberg:download` will now download a fresh copy every time it's run. - The `gutenberg:verify` script is now the preferred entry point for managing the files within the `gutenberg` directory. It will only trigger a downoad if the hashes do not match, or the folder is missing entirely. Follow up to [61438], [61873], [61874]. Props bernhard-reiter. See #64393. git-svn-id: https://develop.svn.wordpress.org/trunk@62021 602fd350-edb4-49c9-b593-d223f7449a82
1 parent b00c4ac commit c604524

4 files changed

Lines changed: 145 additions & 127 deletions

File tree

Gruntfile.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,13 +1589,9 @@ module.exports = function(grunt) {
15891589

15901590
grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() {
15911591
const done = this.async();
1592-
const args = [ 'tools/gutenberg/download.js' ];
1593-
if ( grunt.option( 'force' ) ) {
1594-
args.push( '--force' );
1595-
}
15961592
grunt.util.spawn( {
15971593
cmd: 'node',
1598-
args,
1594+
args: [ 'tools/gutenberg/download.js' ],
15991595
opts: { stdio: 'inherit' }
16001596
}, function( error ) {
16011597
done( ! error );

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@
112112
"wicg-inert": "3.1.3"
113113
},
114114
"scripts": {
115-
"postinstall": "npm run gutenberg:download",
115+
"postinstall": "npm run gutenberg:verify",
116116
"build": "grunt build",
117117
"build:dev": "grunt build --dev",
118118
"build:gutenberg": "grunt build:gutenberg",
@@ -140,6 +140,7 @@
140140
"test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js",
141141
"typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan",
142142
"gutenberg:copy": "node tools/gutenberg/copy.js",
143+
"gutenberg:verify": "node tools/gutenberg/utils.js",
143144
"gutenberg:download": "node tools/gutenberg/download.js",
144145
"vendor:copy": "node tools/vendors/copy-vendors.js",
145146
"sync-gutenberg-packages": "grunt sync-gutenberg-packages",

tools/gutenberg/download.js

Lines changed: 97 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* Download Gutenberg Repository Script.
55
*
66
* 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.
89
*
910
* The artifact is identified by the "gutenberg.sha" value in the root
1011
* package.json, which is used as the OCI image tag for the gutenberg-build
@@ -17,16 +18,13 @@ const { spawn } = require( 'child_process' );
1718
const fs = require( 'fs' );
1819
const { Writable } = require( 'stream' );
1920
const { pipeline } = require( 'stream/promises' );
20-
const path = require( 'path' );
2121
const zlib = require( 'zlib' );
22-
const { gutenbergDir, readGutenbergConfig, verifyGutenbergVersion } = require( './utils' );
22+
const { gutenbergDir, readGutenbergConfig } = require( './utils' );
2323

2424
/**
2525
* Main execution function.
26-
*
27-
* @param {boolean} force - Whether to force a fresh download even if the gutenberg directory exists.
2826
*/
29-
async function main( force ) {
27+
async function main() {
3028
console.log( '🔍 Checking Gutenberg configuration...' );
3129

3230
/*
@@ -45,129 +43,115 @@ async function main( force ) {
4543
process.exit( 1 );
4644
}
4745

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;
54-
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 );
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 }` );
7253
}
73-
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-
},
83-
} );
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 );
54+
const data = await response.json();
55+
token = data.token;
56+
if ( ! token ) {
57+
throw new Error( 'No token in response' );
9658
}
59+
console.log( '✅ Token acquired' );
60+
} catch ( error ) {
61+
console.error( '❌ Failed to fetch token:', error.message );
62+
process.exit( 1 );
63+
}
9764

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 } );
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 }` );
10277
}
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+
}
10388

104-
fs.mkdirSync( gutenbergDir, { recursive: true } );
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 }` );
110+
}
105111

106112
/*
107-
* Step 3: Stream the blob directly through gunzip into tar, writing
108-
* into ./gutenberg with no temporary file on disk.
113+
* Spawn tar to read from stdin and extract into gutenbergDir.
114+
* `tar` is available on macOS, Linux, and Windows 10+.
109115
*/
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-
}
120-
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-
} );
128-
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 );
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+
}
138127
} );
128+
tar.on( 'error', reject );
129+
} );
139130

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-
}
158-
}
131+
/*
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.
135+
*/
136+
await pipeline(
137+
response.body,
138+
zlib.createGunzip(),
139+
Writable.toWeb( tar.stdin ),
140+
);
159141

160-
// Verify the downloaded version matches the expected SHA.
161-
verifyGutenbergVersion();
142+
await tarDone;
162143

163-
if ( downloaded ) {
164-
console.log( '\n✅ Gutenberg download complete!' );
144+
console.log( '✅ Download and extraction complete' );
145+
} catch ( error ) {
146+
console.error( '❌ Download/extraction failed:', error.message );
147+
process.exit( 1 );
165148
}
149+
150+
console.log( '\n✅ Gutenberg download complete!' );
166151
}
167152

168153
// Run main function.
169-
const force = process.argv.includes( '--force' );
170-
main( force ).catch( ( error ) => {
154+
main().catch( ( error ) => {
171155
console.error( '❌ Unexpected error:', error );
172156
process.exit( 1 );
173157
} );

tools/gutenberg/utils.js

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@
44
* Gutenberg build utilities.
55
*
66
* 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.
89
*
910
* @package WordPress
1011
*/
1112

13+
const { spawnSync } = require( 'child_process' );
1214
const fs = require( 'fs' );
1315
const path = require( 'path' );
1416

1517
// Paths.
1618
const rootDir = path.resolve( __dirname, '../..' );
1719
const gutenbergDir = path.join( rootDir, 'gutenberg' );
20+
const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' );
1821

1922
/**
2023
* Read Gutenberg configuration from package.json.
@@ -38,10 +41,22 @@ function readGutenbergConfig() {
3841
return { sha, ghcrRepo };
3942
}
4043

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+
4155
/**
4256
* 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.
4560
*/
4661
function verifyGutenbergVersion() {
4762
console.log( '\n🔍 Verifying Gutenberg version...' );
@@ -54,18 +69,40 @@ function verifyGutenbergVersion() {
5469
process.exit( 1 );
5570
}
5671

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.
5897
try {
5998
const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim();
6099
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 }.` );
64101
process.exit( 1 );
65102
}
66103
} catch ( error ) {
67104
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.' );
69106
} else {
70107
console.error( `❌ ${ error.message }` );
71108
}

0 commit comments

Comments
 (0)