Skip to content

Commit 093b35d

Browse files
committed
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`.
1 parent 8b11ad1 commit 093b35d

1 file changed

Lines changed: 80 additions & 138 deletions

File tree

tools/gutenberg/checkout-gutenberg.js

Lines changed: 80 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
/**
44
* Checkout Gutenberg Repository Script
55
*
6-
* This script checks out the Gutenberg repository at a specific commit/branch/tag
7-
* as specified in the root package.json's "gutenberg" configuration.
6+
* This script downloads a pre-built Gutenberg zip artifact from the GitHub
7+
* Container Registry and extracts it into the ./gutenberg directory.
88
*
9-
* It handles:
10-
* - Initial clone if directory doesn't exist
11-
* - Updating existing checkout to correct ref
12-
* - Installing dependencies with npm ci
13-
* - Idempotent operation (safe to run multiple times)
9+
* The artifact is identified by the "gutenberg.ref" SHA in the root
10+
* package.json, which is used as the OCI image tag for the gutenberg-build
11+
* package on GHCR.
1412
*
1513
* @package WordPress
1614
*/
@@ -19,109 +17,46 @@ const { spawn } = require( 'child_process' );
1917
const fs = require( 'fs' );
2018
const path = require( 'path' );
2119

22-
// Constants
23-
const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git';
24-
2520
// Paths
2621
const rootDir = path.resolve( __dirname, '../..' );
2722
const gutenbergDir = path.join( rootDir, 'gutenberg' );
2823
const packageJsonPath = path.join( rootDir, 'package.json' );
2924

25+
// GHCR configuration
26+
const GHCR_REPO = 'desrosj/gutenberg/gutenberg-build';
27+
3028
/**
31-
* Execute a command and return a promise.
32-
* Captures output and only displays it on failure for cleaner logs.
29+
* Execute a command, streaming stdio directly so progress is visible.
3330
*
3431
* @param {string} command - Command to execute.
3532
* @param {string[]} args - Command arguments.
3633
* @param {Object} options - Spawn options.
37-
* @return {Promise} Promise that resolves when command completes.
34+
* @return {Promise<string>} Promise that resolves with stdout when command completes successfully.
3835
*/
3936
function exec( command, args, options = {} ) {
4037
return new Promise( ( resolve, reject ) => {
4138
let stdout = '';
42-
let stderr = '';
4339

4440
const child = spawn( command, args, {
4541
cwd: options.cwd || rootDir,
46-
stdio: [ 'ignore', 'pipe', 'pipe' ],
47-
shell: process.platform === 'win32', // Use shell on Windows to find .cmd files
42+
stdio: options.captureOutput ? [ 'ignore', 'pipe', 'inherit' ] : 'inherit',
43+
shell: process.platform === 'win32',
4844
...options,
4945
} );
5046

51-
// Capture output
52-
if ( child.stdout ) {
47+
if ( options.captureOutput && child.stdout ) {
5348
child.stdout.on( 'data', ( data ) => {
5449
stdout += data.toString();
5550
} );
5651
}
5752

58-
if ( child.stderr ) {
59-
child.stderr.on( 'data', ( data ) => {
60-
stderr += data.toString();
61-
} );
62-
}
63-
6453
child.on( 'close', ( code ) => {
6554
if ( code !== 0 ) {
66-
// Show output only on failure
67-
if ( stdout ) {
68-
console.error( '\nCommand output:' );
69-
console.error( stdout );
70-
}
71-
if ( stderr ) {
72-
console.error( '\nCommand errors:' );
73-
console.error( stderr );
74-
}
7555
reject(
7656
new Error(
77-
`${ command } ${ args.join(
78-
' '
79-
) } failed with code ${ code }`
57+
`${ command } ${ args.join( ' ' ) } failed with code ${ code }`
8058
)
8159
);
82-
} else {
83-
resolve();
84-
}
85-
} );
86-
87-
child.on( 'error', reject );
88-
} );
89-
}
90-
91-
/**
92-
* Execute a command and capture its output.
93-
*
94-
* @param {string} command - Command to execute.
95-
* @param {string[]} args - Command arguments.
96-
* @param {Object} options - Spawn options.
97-
* @return {Promise<string>} Promise that resolves with command output.
98-
*/
99-
function execOutput( command, args, options = {} ) {
100-
return new Promise( ( resolve, reject ) => {
101-
const child = spawn( command, args, {
102-
cwd: options.cwd || rootDir,
103-
shell: process.platform === 'win32', // Use shell on Windows to find .cmd files
104-
...options,
105-
} );
106-
107-
let stdout = '';
108-
let stderr = '';
109-
110-
if ( child.stdout ) {
111-
child.stdout.on( 'data', ( data ) => {
112-
stdout += data.toString();
113-
} );
114-
}
115-
116-
if ( child.stderr ) {
117-
child.stderr.on( 'data', ( data ) => {
118-
stderr += data.toString();
119-
} );
120-
}
121-
122-
child.on( 'close', ( code ) => {
123-
if ( code !== 0 ) {
124-
reject( new Error( `${ command } failed: ${ stderr }` ) );
12560
} else {
12661
resolve( stdout.trim() );
12762
}
@@ -137,7 +72,7 @@ function execOutput( command, args, options = {} ) {
13772
async function main() {
13873
console.log( '🔍 Checking Gutenberg configuration...' );
13974

140-
// Read Gutenberg ref from package.json
75+
// Read Gutenberg ref from package.json.
14176
let ref;
14277
try {
14378
const packageJson = JSON.parse(
@@ -149,87 +84,94 @@ async function main() {
14984
throw new Error( 'Missing "gutenberg.ref" in package.json' );
15085
}
15186

152-
console.log( ` Repository: ${ GUTENBERG_REPO }` );
15387
console.log( ` Reference: ${ ref }` );
15488
} catch ( error ) {
15589
console.error( '❌ Error reading package.json:', error.message );
15690
process.exit( 1 );
15791
}
15892

159-
// Check if Gutenberg directory exists
160-
const gutenbergExists = fs.existsSync( gutenbergDir );
161-
162-
if ( ! gutenbergExists ) {
163-
console.log( '\n📥 Cloning Gutenberg repository (shallow clone)...' );
164-
try {
165-
// Generic shallow clone approach that works for both branches and commit hashes
166-
// 1. Clone with no checkout and shallow depth
167-
await exec( 'git', [
168-
'clone',
169-
'--depth',
170-
'1',
171-
'--no-checkout',
172-
GUTENBERG_REPO,
173-
'gutenberg',
174-
] );
175-
176-
// 2. Fetch the specific ref with depth 1 (works for branches, tags, and commits)
177-
await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], {
178-
cwd: gutenbergDir,
179-
} );
93+
const zipName = `gutenberg-${ ref }.zip`;
94+
const zipPath = path.join( rootDir, zipName );
18095

181-
// 3. Checkout FETCH_HEAD
182-
await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], {
183-
cwd: gutenbergDir,
184-
} );
185-
186-
console.log( '✅ Cloned successfully' );
187-
} catch ( error ) {
188-
console.error( '❌ Clone failed:', error.message );
189-
process.exit( 1 );
96+
// Step 1: Get an anonymous GHCR token for pulling.
97+
console.log( '\n🔑 Fetching GHCR token...' );
98+
let token;
99+
try {
100+
const tokenJson = await exec( 'curl', [
101+
'--silent',
102+
'--fail',
103+
`https://ghcr.io/token?scope=repository:${ GHCR_REPO }:pull&service=ghcr.io`,
104+
], { captureOutput: true } );
105+
token = JSON.parse( tokenJson ).token;
106+
if ( ! token ) {
107+
throw new Error( 'No token in response' );
190108
}
191-
} else {
192-
console.log( '\n✅ Gutenberg directory already exists' );
109+
console.log( '✅ Token acquired' );
110+
} catch ( error ) {
111+
console.error( '❌ Failed to fetch token:', error.message );
112+
process.exit( 1 );
193113
}
194114

195-
// Fetch and checkout target ref
196-
console.log( `\n📡 Fetching and checking out: ${ ref }` );
115+
// Step 2: Get the manifest to find the blob digest.
116+
console.log( `\n📋 Fetching manifest for ${ ref }...` );
117+
let digest;
197118
try {
198-
// Fetch the specific ref (works for branches, tags, and commit hashes)
199-
await exec( 'git', [ 'fetch', '--depth', '1', 'origin', ref ], {
200-
cwd: gutenbergDir,
201-
} );
202-
203-
// Checkout what was just fetched
204-
await exec( 'git', [ 'checkout', 'FETCH_HEAD' ], {
205-
cwd: gutenbergDir,
206-
} );
207-
208-
console.log( '✅ Checked out successfully' );
119+
const manifestJson = await exec( 'curl', [
120+
'--silent',
121+
'--fail',
122+
'--header', `Authorization: Bearer ${ token }`,
123+
'--header', 'Accept: application/vnd.oci.image.manifest.v1+json',
124+
`https://ghcr.io/v2/${ GHCR_REPO }/manifests/${ ref }`,
125+
], { captureOutput: true } );
126+
const manifest = JSON.parse( manifestJson );
127+
digest = manifest?.layers?.[ 0 ]?.digest;
128+
if ( ! digest ) {
129+
throw new Error( 'No layer digest found in manifest' );
130+
}
131+
console.log( `✅ Blob digest: ${ digest }` );
209132
} catch ( error ) {
210-
console.error( '❌ Fetch/checkout failed:', error.message );
133+
console.error( '❌ Failed to fetch manifest:', error.message );
211134
process.exit( 1 );
212135
}
213136

214-
// Install dependencies
215-
console.log( '\n📦 Installing dependencies...' );
216-
const nodeModulesExists = fs.existsSync(
217-
path.join( gutenbergDir, 'node_modules' )
218-
);
137+
// Step 3: Download the blob (the zip file).
138+
console.log( `\n📥 Downloading ${ zipName }...` );
139+
try {
140+
await exec( 'curl', [
141+
'--fail',
142+
'--location',
143+
'--header', `Authorization: Bearer ${ token }`,
144+
'--output', zipPath,
145+
`https://ghcr.io/v2/${ GHCR_REPO }/blobs/${ digest }`,
146+
] );
147+
console.log( '✅ Download complete' );
148+
} catch ( error ) {
149+
console.error( '❌ Download failed:', error.message );
150+
process.exit( 1 );
151+
}
219152

220-
if ( ! nodeModulesExists ) {
221-
console.log( ' (This may take a few minutes on first run)' );
153+
// Remove existing gutenberg directory so the unzip is clean.
154+
if ( fs.existsSync( gutenbergDir ) ) {
155+
console.log( '\n🗑️ Removing existing gutenberg directory...' );
156+
fs.rmSync( gutenbergDir, { recursive: true, force: true } );
222157
}
223158

159+
fs.mkdirSync( gutenbergDir, { recursive: true } );
160+
161+
// Extract the zip into ./gutenberg.
162+
console.log( `\n📦 Extracting ${ zipName } into ./gutenberg...` );
224163
try {
225-
await exec( 'npm', [ 'ci' ], { cwd: gutenbergDir } );
226-
console.log( '✅ Dependencies installed' );
164+
await exec( 'unzip', [ '-q', zipPath, '-d', gutenbergDir ] );
165+
console.log( '✅ Extraction complete' );
227166
} catch ( error ) {
228-
console.error( '❌ npm ci failed:', error.message );
167+
console.error( '❌ Extraction failed:', error.message );
229168
process.exit( 1 );
230169
}
231170

232-
console.log( '\n✅ Gutenberg checkout complete!' );
171+
// Clean up the zip file.
172+
fs.rmSync( zipPath );
173+
174+
console.log( '\n✅ Gutenberg download complete!' );
233175
}
234176

235177
// Run main function

0 commit comments

Comments
 (0)