Changeset 62422
- Timestamp:
- 05/27/2026 06:18:14 PM (4 weeks ago)
- Location:
- trunk
- Files:
-
- 5 edited
-
package-lock.json (modified) (3 diffs)
-
package.json (modified) (1 diff)
-
tools/gutenberg/download.js (modified) (9 diffs)
-
tools/gutenberg/utils.js (modified) (8 diffs)
-
tsconfig.json (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/package-lock.json
r62411 r62422 48 48 "@types/htmlhint": "1.1.5", 49 49 "@types/jquery": "3.5.34", 50 "@types/node": "20.19.41", 50 51 "@types/underscore": "1.13.0", 51 52 "@wordpress/e2e-test-utils-playwright": "1.42.0", … … 5611 5612 }, 5612 5613 "node_modules/@types/node": { 5613 "version": "14.14.20", 5614 "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", 5615 "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", 5616 "dev": true 5614 "version": "20.19.41", 5615 "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", 5616 "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", 5617 "dev": true, 5618 "license": "MIT", 5619 "dependencies": { 5620 "undici-types": "~6.21.0" 5621 } 5617 5622 }, 5618 5623 "node_modules/@types/node-forge": { … … 32039 32044 } 32040 32045 }, 32046 "node_modules/undici-types": { 32047 "version": "6.21.0", 32048 "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 32049 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 32050 "dev": true, 32051 "license": "MIT" 32052 }, 32041 32053 "node_modules/unicode-canonical-property-names-ecmascript": { 32042 32054 "version": "2.0.1", -
trunk/package.json
r62411 r62422 36 36 "@types/htmlhint": "1.1.5", 37 37 "@types/jquery": "3.5.34", 38 "@types/node": "20.19.41", 38 39 "@types/underscore": "1.13.0", 39 40 "@wordpress/e2e-test-utils-playwright": "1.42.0", -
trunk/tools/gutenberg/download.js
r62021 r62422 9 9 * 10 10 * The artifact is identified by the "gutenberg.sha" value in the root 11 * package.json, which is used as the OCI image tag for the gutenberg-build 12 * package on GitHub Container Registry. 11 * package.json, which is used as the OCI tag for the gutenberg-wp-develop-build 12 * package on GitHub Container Registry. The value is normally a Git SHA, but 13 * may also be a mutable tag (e.g. "trunk", "pr-12345") in a pull request that 14 * wants to track the latest build of a stream. When the ref is a mutable tag, 15 * the script resolves it to the immutable SHA tag for the actual blob fetch 16 * and falls back to the mutable tag's manifest when the immutable tag is 17 * unavailable. 13 18 * 14 19 * @package WordPress … … 17 22 const { spawn } = require( 'child_process' ); 18 23 const fs = require( 'fs' ); 19 const { Writable } = require( 'stream' );24 const { Readable } = require( 'stream' ); 20 25 const { pipeline } = require( 'stream/promises' ); 21 26 const zlib = require( 'zlib' ); 22 const { gutenbergDir, readGutenbergConfig } = require( './utils' ); 27 const { 28 gutenbergDir, 29 readGutenbergConfig, 30 fetchGhcrToken, 31 fetchManifest, 32 } = require( './utils' ); 33 34 /** 35 * Resolve the manifest to use for downloading. 36 * 37 * For immutable refs (SHA values), the ref is used directly. 38 * 39 * For mutable refs, the mutable tag's manifest is fetched first and the 40 * `image.revision` annotation is read. The corresponding immutable SHA tag is 41 * then preferred. If the immutable SHA tag is unavailable, fall back to the 42 * manifest already fetched via the mutable tag. 43 * 44 * @param {{ ref: string, ghcrRepo: string, isMutable: boolean }} config 45 * @param {string} token 46 * @return {Promise<{ manifest: Record<string, any>, resolvedRef: string }>} 47 */ 48 async function resolveDownloadManifest( config, token ) { 49 const { ref, ghcrRepo, isMutable } = config; 50 51 const initialManifest = await fetchManifest( ref, ghcrRepo, token ); 52 53 if ( ! isMutable ) { 54 return { manifest: initialManifest, resolvedRef: ref }; 55 } 56 57 const revision = 58 initialManifest?.annotations?.[ 'org.opencontainers.image.revision' ]; 59 if ( ! revision ) { 60 console.log( 61 `ℹ️ No image.revision annotation on "${ ref }"; using mutable tag for download.` 62 ); 63 return { manifest: initialManifest, resolvedRef: ref }; 64 } 65 66 try { 67 const immutableManifest = await fetchManifest( revision, ghcrRepo, token ); 68 return { manifest: immutableManifest, resolvedRef: revision }; 69 } catch ( error ) { 70 if ( /** @type {{ status?: number }} */ ( error ).status === 404 ) { 71 console.log( 72 `ℹ️ Immutable SHA tag ${ revision } unavailable; falling back to mutable tag "${ ref }".` 73 ); 74 return { manifest: initialManifest, resolvedRef: ref }; 75 } 76 throw error; 77 } 78 } 23 79 24 80 /** … … 32 88 * 33 89 * Note: ghcr stands for GitHub Container Registry where wordpress-develop ready builds of the Gutenberg plugin 34 * are published on every repository push event.90 * are published by the Gutenberg build-plugin-zip workflow. 35 91 */ 36 let sha, ghcrRepo; 37 try { 38 ( { sha, ghcrRepo } = readGutenbergConfig() ); 39 console.log( ` SHA: ${ sha }` ); 40 console.log( ` GHCR repository: ${ ghcrRepo }` ); 41 } catch ( error ) { 42 console.error( '❌ Error reading package.json:', error.message ); 92 let config; 93 try { 94 config = readGutenbergConfig(); 95 console.log( 96 ` Ref: ${ config.ref }${ 97 config.isMutable ? ' (mutable tag)' : '' 98 }` 99 ); 100 console.log( ` GHCR repository: ${ config.ghcrRepo }` ); 101 } catch ( error ) { 102 console.error( '❌ Error reading package.json:', /** @type {Error} */ ( error ).message ); 43 103 process.exit( 1 ); 44 104 } … … 48 108 let token; 49 109 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 } 110 token = await fetchGhcrToken( config.ghcrRepo ); 59 111 console.log( '✅ Token acquired' ); 60 112 } catch ( error ) { 61 console.error( '❌ Failed to fetch token:', error.message ); 62 process.exit( 1 ); 63 } 64 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 } 113 console.error( '❌ Failed to fetch token:', /** @type {Error} */ ( error ).message ); 114 process.exit( 1 ); 115 } 116 117 // Step 2: Resolve the manifest to use for download. 118 console.log( `\n📋 Fetching manifest for ${ config.ref }...` ); 119 let manifest, resolvedRef; 120 try { 121 ( { manifest, resolvedRef } = await resolveDownloadManifest( 122 config, 123 token 124 ) ); 125 if ( resolvedRef !== config.ref ) { 126 console.log( ` Resolved to immutable SHA tag: ${ resolvedRef }` ); 127 } 128 } catch ( error ) { 129 console.error( '❌ Failed to fetch manifest:', /** @type {Error} */ ( error ).message ); 130 process.exit( 1 ); 131 } 132 133 const digest = manifest?.layers?.[ 0 ]?.digest; 134 if ( ! digest ) { 135 console.error( '❌ No layer digest found in manifest' ); 136 process.exit( 1 ); 137 } 138 console.log( `✅ Blob digest: ${ digest }` ); 88 139 89 140 // Remove existing gutenberg directory so the extraction is clean. … … 101 152 console.log( `\n📥 Downloading and extracting artifact...` ); 102 153 try { 103 const response = await fetch( `https://ghcr.io/v2/${ ghcrRepo }/blobs/${ digest }`, {154 const response = await fetch( `https://ghcr.io/v2/${ config.ghcrRepo }/blobs/${ digest }`, { 104 155 headers: { 105 156 Authorization: `Bearer ${ token }`, … … 108 159 if ( ! response.ok ) { 109 160 throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` ); 161 } 162 if ( ! response.body ) { 163 throw new Error( 'Blob response has no body' ); 110 164 } 111 165 … … 118 172 } ); 119 173 174 /** @type {Promise<void>} */ 120 175 const tarDone = new Promise( ( resolve, reject ) => { 121 176 tar.on( 'close', ( code ) => { … … 135 190 */ 136 191 await pipeline( 137 response.body, 192 Readable.fromWeb( 193 /** @type {import('stream/web').ReadableStream} */ ( response.body ) 194 ), 138 195 zlib.createGunzip(), 139 Writable.toWeb( tar.stdin ),196 tar.stdin, 140 197 ); 141 198 … … 144 201 console.log( '✅ Download and extraction complete' ); 145 202 } catch ( error ) { 146 console.error( '❌ Download/extraction failed:', error.message );203 console.error( '❌ Download/extraction failed:', /** @type {Error} */ ( error ).message ); 147 204 process.exit( 1 ); 148 205 } -
trunk/tools/gutenberg/utils.js
r62052 r62422 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,8 * and automatically downloads the correct version when needed.7 * verifies that the installed Gutenberg build matches the value in 8 * package.json and automatically downloads the correct version when needed. 9 9 * 10 10 * @package WordPress … … 20 20 const hashFilePath = path.join( gutenbergDir, '.gutenberg-hash' ); 21 21 22 // A 40-character lowercase hex string is treated as an immutable Git SHA tag. 23 // Anything else (e.g. "trunk", "release-19.5", "pr-12345") is treated as a 24 // mutable tag published by the Gutenberg build-plugin-zip workflow. 25 const SHA_PATTERN = /^[a-f0-9]{40}$/i; 26 27 const MANIFEST_ACCEPT = 'application/vnd.oci.image.manifest.v1+json'; 28 22 29 /** 23 30 * Read Gutenberg configuration from package.json. 24 31 * 25 * @return {{ sha: string, ghcrRepo: string }} The Gutenberg configuration. 32 * `gutenberg.sha` is always committed as a pinned SHA, but a contributor 33 * may temporarily set it to a mutable tag published by the Gutenberg repository 34 * (e.g. "trunk", "release-19.5", "pr-12345") to track the latest build of that 35 * stream or test changes before merging. 36 * 37 * @return {{ ref: string, ghcrRepo: string, isMutable: boolean }} The 38 * resolved configuration. `ref` is the OCI tag to look up; `isMutable` 39 * is true when the value is not a SHA-shaped string. 26 40 * @throws {Error} If the configuration is missing or invalid. 27 41 */ 28 42 function readGutenbergConfig() { 29 43 const packageJson = require( path.join( rootDir, 'package.json' ) ); 30 const sha= packageJson.gutenberg?.sha;44 const ref = packageJson.gutenberg?.sha; 31 45 const ghcrRepo = packageJson.gutenberg?.ghcrRepo; 32 46 33 if ( ! sha) {47 if ( ! ref ) { 34 48 throw new Error( 'Missing "gutenberg.sha" in package.json' ); 35 49 } … … 39 53 } 40 54 41 return { sha, ghcrRepo }; 55 const isMutable = ! SHA_PATTERN.test( ref ); 56 57 return { ref, ghcrRepo, isMutable }; 58 } 59 60 /** 61 * Fetch an anonymous pull token for the given GHCR repository. 62 * 63 * @param {string} ghcrRepo The "owner/repo/package" path on ghcr.io. 64 * @return {Promise<string>} The bearer token. 65 */ 66 async function fetchGhcrToken( ghcrRepo ) { 67 const response = await fetch( 68 `https://ghcr.io/token?scope=repository:${ ghcrRepo }:pull&service=ghcr.io` 69 ); 70 if ( ! response.ok ) { 71 throw new Error( 72 `Failed to fetch GHCR token: ${ response.status } ${ response.statusText }` 73 ); 74 } 75 const data = await response.json(); 76 if ( ! data.token ) { 77 throw new Error( 'No token in GHCR response' ); 78 } 79 return data.token; 80 } 81 82 /** 83 * Fetch a manifest from GHCR by tag. 84 * 85 * @param {string} ref The tag (SHA or mutable tag). 86 * @param {string} ghcrRepo The "owner/repo/package" path on ghcr.io. 87 * @param {string} token Bearer token from fetchGhcrToken. 88 * @return {Promise<Record<string, any>>} Parsed manifest JSON. 89 */ 90 async function fetchManifest( ref, ghcrRepo, token ) { 91 const response = await fetch( 92 `https://ghcr.io/v2/${ ghcrRepo }/manifests/${ ref }`, 93 { 94 headers: { 95 Authorization: `Bearer ${ token }`, 96 Accept: MANIFEST_ACCEPT, 97 }, 98 } 99 ); 100 if ( ! response.ok ) { 101 const error = /** @type {Error & { status?: number }} */ ( 102 new Error( 103 `Failed to fetch manifest for "${ ref }": ${ response.status } ${ response.statusText }` 104 ) 105 ); 106 error.status = response.status; 107 throw error; 108 } 109 return response.json(); 110 } 111 112 /** 113 * Resolve the expected source SHA for the configured ref. 114 * 115 * For immutable refs (SHA), the expected SHA is the ref itself and no network 116 * call is required. For mutable refs, the manifest's 117 * `org.opencontainers.image.revision` annotation is fetched and returned, 118 * which reflects the SHA value published to the mutable tag most recently. 119 * 120 * @param {{ ref: string, ghcrRepo: string, isMutable: boolean }} config 121 * @return {Promise<string>} The expected SHA. 122 */ 123 async function resolveExpectedSha( { ref, ghcrRepo, isMutable } ) { 124 if ( ! isMutable ) { 125 return ref; 126 } 127 128 const token = await fetchGhcrToken( ghcrRepo ); 129 const manifest = await fetchManifest( ref, ghcrRepo, token ); 130 const revision = 131 manifest?.annotations?.[ 'org.opencontainers.image.revision' ]; 132 if ( ! revision ) { 133 throw new Error( 134 `Manifest for "${ ref }" has no org.opencontainers.image.revision annotation` 135 ); 136 } 137 return revision; 42 138 } 43 139 … … 60 156 61 157 /** 62 * Verify that the installed Gutenberg version matches the expected SHA in 63 * package.json. Automatically downloads the correct version when the directory 64 * is missing, the hash file is absent, or the hash does not match. Logs 65 * progress to the console and exits with a non-zero code on failure. 66 */ 67 function verifyGutenbergVersion() { 158 * Verify that the installed Gutenberg version matches the expected SHA. 159 * 160 * For SHA refs, the expected SHA is the configured value. For mutable refs, 161 * the expected SHA is whatever the mutable tag currently points to in GHCR 162 * (read from the manifest's image.revision annotation). The installed 163 * `.gutenberg-hash` is compared against the expected SHA; on mismatch, a 164 * fresh download is triggered. 165 */ 166 async function verifyGutenbergVersion() { 68 167 console.log( '\n🔍 Verifying Gutenberg version...' ); 69 168 70 let sha;169 let config; 71 170 try { 72 ( { sha } = readGutenbergConfig());171 config = readGutenbergConfig(); 73 172 } catch ( error ) { 74 console.error( '❌ Error reading package.json:', error.message ); 75 process.exit( 1 ); 173 console.error( '❌ Error reading package.json:', /** @type {Error} */ ( error ).message ); 174 process.exit( 1 ); 175 } 176 177 const { ref, isMutable } = config; 178 console.log( 179 ` Ref: ${ ref }${ isMutable ? ' (mutable tag)' : '' }` 180 ); 181 182 let expectedSha; 183 try { 184 expectedSha = await resolveExpectedSha( config ); 185 } catch ( error ) { 186 console.error( '❌ Failed to resolve expected SHA:', /** @type {Error} */ ( error ).message ); 187 process.exit( 1 ); 188 } 189 190 if ( isMutable ) { 191 console.log( ` Latest build for "${ ref }": ${ expectedSha }` ); 76 192 } 77 193 … … 85 201 installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); 86 202 } catch ( error ) { 87 if ( error.code !== 'ENOENT' ) { 88 console.error( `❌ ${ error.message }` ); 203 const err = /** @type {NodeJS.ErrnoException} */ ( error ); 204 if ( err.code !== 'ENOENT' ) { 205 console.error( `❌ ${ err.message }` ); 89 206 process.exit( 1 ); 90 207 } … … 94 211 console.log( 'ℹ️ Hash file not found. Downloading expected version...' ); 95 212 downloadGutenberg(); 96 } else if ( installedHash !== sha ) {97 console.log( `ℹ️ Hash mismatch (found ${ installedHash }, expected ${ sha }). Downloading expected version...` );213 } else if ( installedHash !== expectedSha ) { 214 console.log( `ℹ️ Hash mismatch (found ${ installedHash }, expected ${ expectedSha }). Downloading expected version...` ); 98 215 downloadGutenberg(); 99 216 } … … 103 220 try { 104 221 const installedHash = fs.readFileSync( hashFilePath, 'utf8' ).trim(); 105 if ( installedHash !== sha ) {106 console.error( `❌ SHA mismatch after download: expected ${ sha } but found ${ installedHash }.` );222 if ( installedHash !== expectedSha ) { 223 console.error( `❌ SHA mismatch after download: expected ${ expectedSha } but found ${ installedHash }.` ); 107 224 process.exit( 1 ); 108 225 } 109 226 } catch ( error ) { 110 if ( error.code === 'ENOENT' ) { 227 const err = /** @type {NodeJS.ErrnoException} */ ( error ); 228 if ( err.code === 'ENOENT' ) { 111 229 console.error( '❌ .gutenberg-hash not found after download. This is unexpected.' ); 112 230 } else { 113 console.error( `❌ ${ err or.message }` );231 console.error( `❌ ${ err.message }` ); 114 232 } 115 233 process.exit( 1 ); … … 119 237 } 120 238 121 module.exports = { rootDir, gutenbergDir, readGutenbergConfig, verifyGutenbergVersion }; 239 module.exports = { 240 rootDir, 241 gutenbergDir, 242 readGutenbergConfig, 243 verifyGutenbergVersion, 244 fetchGhcrToken, 245 fetchManifest, 246 resolveExpectedSha, 247 }; 122 248 123 249 if ( require.main === module ) { 124 verifyGutenbergVersion(); 125 } 250 verifyGutenbergVersion().catch( ( error ) => { 251 console.error( '❌ Unexpected error:', error ); 252 process.exit( 1 ); 253 } ); 254 } -
trunk/tsconfig.json
r61800 r62422 20 20 "typeRoots": [ "./typings", "./node_modules/@types" ], 21 21 "types": [ 22 "node", 22 23 "wp-globals", 23 24 "codemirror/addon/lint/lint", … … 29 30 "src/js/_enqueues/wp/code-editor.js", 30 31 "src/js/_enqueues/lib/codemirror/javascript-lint.js", 31 "src/js/_enqueues/lib/codemirror/htmlhint-kses.js" 32 "src/js/_enqueues/lib/codemirror/htmlhint-kses.js", 33 "tools/gutenberg/download.js", 34 "tools/gutenberg/utils.js" 32 35 ] 33 36 }
Note: See TracChangeset
for help on using the changeset viewer.