Skip to content

Commit acbabdf

Browse files
authored
Merge branch 'trunk' into collaboration/presence-api
2 parents 68b4ecd + 7578976 commit acbabdf

5 files changed

Lines changed: 273 additions & 71 deletions

File tree

package-lock.json

Lines changed: 16 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@types/espree": "10.1.0",
3636
"@types/htmlhint": "1.1.5",
3737
"@types/jquery": "3.5.34",
38+
"@types/node": "20.19.41",
3839
"@types/underscore": "1.13.0",
3940
"@wordpress/e2e-test-utils-playwright": "1.42.0",
4041
"@wordpress/prettier-config": "4.42.0",

tools/gutenberg/download.js

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,74 @@
88
* existing gutenberg directory is removed before extraction.
99
*
1010
* 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.
1318
*
1419
* @package WordPress
1520
*/
1621

1722
const { spawn } = require( 'child_process' );
1823
const fs = require( 'fs' );
19-
const { Writable } = require( 'stream' );
24+
const { Readable } = require( 'stream' );
2025
const { pipeline } = require( 'stream/promises' );
2126
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+
}
2379

2480
/**
2581
* Main execution function.
@@ -31,61 +87,56 @@ async function main() {
3187
* Read Gutenberg configuration from package.json.
3288
*
3389
* 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.
3591
*/
36-
let sha, ghcrRepo;
92+
let config;
3793
try {
38-
( { sha, ghcrRepo } = readGutenbergConfig() );
39-
console.log( ` SHA: ${ sha }` );
40-
console.log( ` GHCR repository: ${ ghcrRepo }` );
94+
config = readGutenbergConfig();
95+
console.log(
96+
` Ref: ${ config.ref }${
97+
config.isMutable ? ' (mutable tag)' : ''
98+
}`
99+
);
100+
console.log( ` GHCR repository: ${ config.ghcrRepo }` );
41101
} catch ( error ) {
42-
console.error( '❌ Error reading package.json:', error.message );
102+
console.error( '❌ Error reading package.json:', /** @type {Error} */ ( error ).message );
43103
process.exit( 1 );
44104
}
45105

46106
// Step 1: Get an anonymous GHCR token for pulling.
47107
console.log( '\n🔑 Fetching GHCR token...' );
48108
let token;
49109
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 );
59111
console.log( '✅ Token acquired' );
60112
} catch ( error ) {
61-
console.error( '❌ Failed to fetch token:', error.message );
113+
console.error( '❌ Failed to fetch token:', /** @type {Error} */ ( error ).message );
62114
process.exit( 1 );
63115
}
64116

65-
// Step 2: Get the manifest to find the blob digest.
66-
console.log( `\n📋 Fetching manifest for ${ sha }...` );
67-
let digest;
117+
// Step 2: Resolve the manifest to use for download.
118+
console.log( `\n📋 Fetching manifest for ${ config.ref }...` );
119+
let manifest, resolvedRef;
68120
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' );
121+
( { manifest, resolvedRef } = await resolveDownloadManifest(
122+
config,
123+
token
124+
) );
125+
if ( resolvedRef !== config.ref ) {
126+
console.log( ` Resolved to immutable SHA tag: ${ resolvedRef }` );
82127
}
83-
console.log( `✅ Blob digest: ${ digest }` );
84128
} catch ( error ) {
85-
console.error( '❌ Failed to fetch manifest:', error.message );
129+
console.error( '❌ Failed to fetch manifest:', /** @type {Error} */ ( error ).message );
86130
process.exit( 1 );
87131
}
88132

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 }` );
139+
89140
// Remove existing gutenberg directory so the extraction is clean.
90141
if ( fs.existsSync( gutenbergDir ) ) {
91142
console.log( '\n🗑️ Removing existing gutenberg directory...' );
@@ -100,14 +151,17 @@ async function main() {
100151
*/
101152
console.log( `\n📥 Downloading and extracting artifact...` );
102153
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 }`, {
104155
headers: {
105156
Authorization: `Bearer ${ token }`,
106157
},
107158
} );
108159
if ( ! response.ok ) {
109160
throw new Error( `Failed to download blob: ${ response.status } ${ response.statusText }` );
110161
}
162+
if ( ! response.body ) {
163+
throw new Error( 'Blob response has no body' );
164+
}
111165

112166
/*
113167
* Spawn tar to read from stdin and extract into gutenbergDir.
@@ -117,6 +171,7 @@ async function main() {
117171
stdio: [ 'pipe', 'inherit', 'inherit' ],
118172
} );
119173

174+
/** @type {Promise<void>} */
120175
const tarDone = new Promise( ( resolve, reject ) => {
121176
tar.on( 'close', ( code ) => {
122177
if ( code !== 0 ) {
@@ -134,16 +189,18 @@ async function main() {
134189
* consistent and means tar only sees plain tar data on stdin.
135190
*/
136191
await pipeline(
137-
response.body,
192+
Readable.fromWeb(
193+
/** @type {import('stream/web').ReadableStream} */ ( response.body )
194+
),
138195
zlib.createGunzip(),
139-
Writable.toWeb( tar.stdin ),
196+
tar.stdin,
140197
);
141198

142199
await tarDone;
143200

144201
console.log( '✅ Download and extraction complete' );
145202
} catch ( error ) {
146-
console.error( '❌ Download/extraction failed:', error.message );
203+
console.error( '❌ Download/extraction failed:', /** @type {Error} */ ( error ).message );
147204
process.exit( 1 );
148205
}
149206

0 commit comments

Comments
 (0)