Skip to content

Commit 7578976

Browse files
committed
Build/Test Tools: Support testing unmerged changes from Gutenberg.
The `gutenberg.sha` property in the `package.json` file expects a full-length SHA value to be specified in order to download the built assets from the Gutenberg repository and include the files through the build script. After WordPress/gutenberg#78211, assets are now published for `pull_request` events in addition to `push` so long as the `HEAD` branch of the pull request exists in the repository and not in a fork. This makes the built assets for pull requests available for testing within `wordpress-develop` before they are merged through a `pr-###` tag on the GHCR package. To test a given pull request, just set the `gutenberg.sha` value to the corresponding `pr-###` tag. In addition to pull request-specific tags, each `wp/X.Y`, `release/X.Y`, and `trunk` now have a tag that can be used to test the latest changes. Each time `build` or `build:dev` is called the script will attempt to confirm the latest version of the assets are present locally, pulling down the latest changes when an update is available. All of the tags described in this changeset are mutable, so they should never be committed. Only immutable SHA values should be used for `gutenberg.sha` in commits to this repository. This commit also adds the related JavaScript files to the TypeScript configuration file. Props westonruter, jorbin, manhar. Fixes #65224. git-svn-id: https://develop.svn.wordpress.org/trunk@62422 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 0561e77 commit 7578976

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)