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' );
1917const fs = require ( 'fs' ) ;
2018const path = require ( 'path' ) ;
2119
22- // Constants
23- const GUTENBERG_REPO = 'https://github.com/WordPress/gutenberg.git' ;
24-
2520// Paths
2621const rootDir = path . resolve ( __dirname , '../..' ) ;
2722const gutenbergDir = path . join ( rootDir , 'gutenberg' ) ;
2823const 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 */
3936function 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 = {} ) {
13772async 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