From a623dd41b10537dd2b3d1e3545f43170cac761a7 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 5 Oct 2026 15:57:21 +0530 Subject: [PATCH 1/3] Release Tooling: Read the npm v12 JSON output shapes npm v12 wraps `view --json` in an array and keys `pack --json` by package name. The publish preflight read the former as an object, so every retry saw an undefined version and failed before pushing tags; the vitest consumer test read the latter as an array and threw. Both now accept either shape, and the preflight test covers both. Reported in https://github.com/WordPress/gutenberg/pull/82732#discussion_r4181764977. --- tools/release/commands/packages.js | 7 ++- tools/release/commands/test/packages.js | 56 +++++++++++++--------- tools/validation/test-vitest-consumers.mjs | 6 ++- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/tools/release/commands/packages.js b/tools/release/commands/packages.js index 62b9856f1e65ad..b9c975b9047909 100644 --- a/tools/release/commands/packages.js +++ b/tools/release/commands/packages.js @@ -723,7 +723,7 @@ function isNpmPackageVersionMissing( error ) { } /** - * Parses npm JSON command output. + * Parses npm JSON command output for a single package. * * @param {string} output Command stdout. * @param {string} description Output description for error messages. @@ -731,13 +731,16 @@ function isNpmPackageVersionMissing( error ) { * @return {*} Parsed JSON output. */ function parseNpmJsonOutput( output, description ) { + let parsed; try { - return JSON.parse( output ); + parsed = JSON.parse( output ); } catch { throw new Error( `Unable to parse npm registry ${ description }: ${ output }` ); } + /* npm v12 always wraps `view --json` in an array; older versions return the object. */ + return Array.isArray( parsed ) ? parsed[ 0 ] : parsed; } /** diff --git a/tools/release/commands/test/packages.js b/tools/release/commands/test/packages.js index 65533802e29d4b..14572ca98b701f 100644 --- a/tools/release/commands/test/packages.js +++ b/tools/release/commands/test/packages.js @@ -549,30 +549,40 @@ describe( 'runNpmPublishPreflight', () => { expect( console ).toHaveLogged(); } ); - it( 'accepts a published version from the prepared commit with the expected dist-tag', async () => { - const commandFn = vi - .fn() - .mockResolvedValueOnce( WHOAMI ) - .mockResolvedValueOnce( { - stdout: '{"version":"4.50.0","gitHead":"publish-sha","dist-tags":{"latest":"4.50.0"}}', - } ); + it.each( [ + [ + 'an object, as npm v11 returns', + '{"version":"4.50.0","gitHead":"publish-sha","dist-tags":{"latest":"4.50.0"}}', + ], + [ + 'an array, as npm v12 returns', + '[{"version":"4.50.0","gitHead":"publish-sha","dist-tags":{"latest":"4.50.0"}}]', + ], + ] )( + 'accepts a published version from the prepared commit with the expected dist-tag when npm view returns %s', + async ( _description, stdout ) => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { stdout } ); - await expect( - runNpmPublishPreflight( - { - distTag: 'latest', - gitWorkingDirectoryPath: '/repo', - publishCommit: 'publish-sha', - releasePackages: [ - { name: '@wordpress/a11y', version: '4.50.0' }, - ], - }, - { commandFn } - ) - ).resolves.toEqual( [ '@wordpress/a11y' ] ); - expect( commandFn ).toHaveBeenCalledTimes( 2 ); - expect( console ).toHaveLogged(); - } ); + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).resolves.toEqual( [ '@wordpress/a11y' ] ); + expect( commandFn ).toHaveBeenCalledTimes( 2 ); + expect( console ).toHaveLogged(); + } + ); it( 'fails when a published version came from another commit', async () => { const commandFn = vi diff --git a/tools/validation/test-vitest-consumers.mjs b/tools/validation/test-vitest-consumers.mjs index e3c06faa5f3d68..dcf4c7984d85bb 100644 --- a/tools/validation/test-vitest-consumers.mjs +++ b/tools/validation/test-vitest-consumers.mjs @@ -118,7 +118,11 @@ function pack( name ) { directory ) ); - return `file:${ path.join( directory, output[ 0 ].filename ) }`; + /* npm v12 keys `pack --json` output by package name; older versions return an array. */ + const [ packed ] = Array.isArray( output ) + ? output + : Object.values( output ); + return `file:${ path.join( directory, packed.filename ) }`; } const scripts = values.scripts ?? pack( 'scripts' ); const eslintPlugin = values[ 'eslint-plugin' ] ?? pack( 'eslint-plugin' ); From 0b65d6676607d555dea025d9984784edf99525de Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 5 Oct 2026 17:44:12 +0530 Subject: [PATCH 2/3] Release Tooling: Validate the npm view and pack result count Unwrap the npm view array in the preflight, where the call is made, instead of in the shared JSON parser, and fail when it does not hold exactly one version. Share one npm pack reader between the two validation scripts, parsing stdout only and requiring one result. --- tools/release/commands/packages.js | 23 +++++++----- tools/release/commands/test/packages.js | 34 ++++++++++++++++++ tools/validation/read-npm-pack-result.mjs | 28 +++++++++++++++ tools/validation/read-npm-pack-result.test.js | 31 ++++++++++++++++ tools/validation/test-vitest-consumers.mjs | 36 +++++++++---------- .../validation/validate-package-contents.mjs | 7 ++-- 6 files changed, 128 insertions(+), 31 deletions(-) create mode 100644 tools/validation/read-npm-pack-result.mjs create mode 100644 tools/validation/read-npm-pack-result.test.js diff --git a/tools/release/commands/packages.js b/tools/release/commands/packages.js index b9c975b9047909..52d03302bdb2d1 100644 --- a/tools/release/commands/packages.js +++ b/tools/release/commands/packages.js @@ -723,7 +723,7 @@ function isNpmPackageVersionMissing( error ) { } /** - * Parses npm JSON command output for a single package. + * Parses npm JSON command output. * * @param {string} output Command stdout. * @param {string} description Output description for error messages. @@ -731,16 +731,13 @@ function isNpmPackageVersionMissing( error ) { * @return {*} Parsed JSON output. */ function parseNpmJsonOutput( output, description ) { - let parsed; try { - parsed = JSON.parse( output ); + return JSON.parse( output ); } catch { throw new Error( `Unable to parse npm registry ${ description }: ${ output }` ); } - /* npm v12 always wraps `view --json` in an array; older versions return the object. */ - return Array.isArray( parsed ) ? parsed[ 0 ] : parsed; } /** @@ -777,7 +774,7 @@ async function runNpmPublishPreflight( // TODO: Consider bounded concurrency here if this preflight becomes too slow. // Keep registry checks sequential so errors stay easy to read. for ( const { name, version } of releasePackages ) { - let registryPackage; + let registryOutput; try { const { stdout } = await commandFn( `npm view ${ name }@${ version } version gitHead dist-tags --json`, @@ -786,7 +783,7 @@ async function runNpmPublishPreflight( stdio: 'pipe', } ); - registryPackage = parseNpmJsonOutput( + registryOutput = parseNpmJsonOutput( stdout, `${ name }@${ version } metadata` ); @@ -797,11 +794,21 @@ async function runNpmPublishPreflight( throw error; } + /* npm v12 always returns an array; older versions return an object for a single match. */ + const registryPackages = Array.isArray( registryOutput ) + ? registryOutput + : [ registryOutput ]; + if ( registryPackages.length !== 1 ) { + throw new Error( + `Expected npm registry lookup for ${ name }@${ version } to return one version, got ${ registryPackages.length }.` + ); + } + const { version: registryVersion, gitHead: registryGitHead, 'dist-tags': distTags = {}, - } = registryPackage; + } = registryPackages[ 0 ]; if ( registryVersion !== version ) { throw new Error( `Expected npm registry lookup for ${ name }@${ version } to return version ${ version }, got ${ registryVersion }.` diff --git a/tools/release/commands/test/packages.js b/tools/release/commands/test/packages.js index 14572ca98b701f..3d1bd83497b743 100644 --- a/tools/release/commands/test/packages.js +++ b/tools/release/commands/test/packages.js @@ -584,6 +584,40 @@ describe( 'runNpmPublishPreflight', () => { } ); + it.each( [ + [ 'no versions', '[]', 0 ], + [ + 'more than one version', + '[{"version":"4.50.0","gitHead":"publish-sha"},{"version":"4.50.1","gitHead":"other-sha"}]', + 2, + ], + ] )( + 'fails when npm view returns %s', + async ( _description, stdout, count ) => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { stdout } ); + + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + `Expected npm registry lookup for @wordpress/a11y@4.50.0 to return one version, got ${ count }.` + ); + expect( console ).toHaveLogged(); + } + ); + it( 'fails when a published version came from another commit', async () => { const commandFn = vi .fn() diff --git a/tools/validation/read-npm-pack-result.mjs b/tools/validation/read-npm-pack-result.mjs new file mode 100644 index 00000000000000..c5f6fe76028299 --- /dev/null +++ b/tools/validation/read-npm-pack-result.mjs @@ -0,0 +1,28 @@ +/** + * Reads the single package result from `npm pack --json` output. + * + * @param {string} stdout The `npm pack --json` stdout. + * @param {string} packageName Package name for error messages. + * + * @return {Object} The pack result for the package. + */ +export function readNpmPackResult( stdout, packageName ) { + let output; + try { + output = JSON.parse( stdout ); + } catch { + throw new Error( + `Could not parse npm pack output for ${ packageName }:\n${ stdout }` + ); + } + /* npm v12 keys `pack --json` output by package name; older versions return an array. */ + const results = Array.isArray( output ) + ? output + : Object.values( output ?? {} ); + if ( results.length !== 1 ) { + throw new Error( + `Expected one npm pack result for ${ packageName }, got ${ results.length }.` + ); + } + return results[ 0 ]; +} diff --git a/tools/validation/read-npm-pack-result.test.js b/tools/validation/read-npm-pack-result.test.js new file mode 100644 index 00000000000000..d60d9ea5387658 --- /dev/null +++ b/tools/validation/read-npm-pack-result.test.js @@ -0,0 +1,31 @@ +import { expect, test } from 'vitest'; +import { readNpmPackResult } from './read-npm-pack-result.mjs'; + +const result = { name: 'test-package', filename: 'test-package-1.0.0.tgz' }; + +test.each( [ + [ 'an array, as npm v11 returns', [ result ] ], + [ + 'an object keyed by name, as npm v12 returns', + { 'test-package': result }, + ], +] )( 'reads %s', ( _description, output ) => { + expect( + readNpmPackResult( JSON.stringify( output ), 'test-package' ) + ).toEqual( result ); +} ); + +test.each( [ + [ 'no results', '[]', 0 ], + [ 'more than one result', JSON.stringify( [ result, result ] ), 2 ], +] )( 'fails on %s', ( _description, stdout, count ) => { + expect( () => readNpmPackResult( stdout, 'test-package' ) ).toThrow( + `Expected one npm pack result for test-package, got ${ count }.` + ); +} ); + +test( 'fails on output that is not JSON', () => { + expect( () => + readNpmPackResult( 'npm warn something', 'test-package' ) + ).toThrow( 'Could not parse npm pack output for test-package' ); +} ); diff --git a/tools/validation/test-vitest-consumers.mjs b/tools/validation/test-vitest-consumers.mjs index dcf4c7984d85bb..074a2438a584b1 100644 --- a/tools/validation/test-vitest-consumers.mjs +++ b/tools/validation/test-vitest-consumers.mjs @@ -15,6 +15,7 @@ import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { parseArgs } from 'node:util'; +import { readNpmPackResult } from './read-npm-pack-result.mjs'; const { values } = parseArgs( { options: { @@ -45,7 +46,7 @@ const env = { delete env.NODE_OPTIONS; delete env.NODE_PATH; -function run( command, args, cwd = consumer ) { +function execute( command, args, cwd ) { const result = spawnSync( command, args, { cwd, env, @@ -62,6 +63,10 @@ function run( command, args, cwd = consumer ) { result.stderr }` ); + return result; +} +function run( command, args, cwd = consumer ) { + const result = execute( command, args, cwd ); return result.stdout + result.stderr; } function write( name, content ) { @@ -104,24 +109,19 @@ function pack( name ) { path.join( staging, 'package.json' ), JSON.stringify( manifest ) ); - const output = JSON.parse( - run( - 'npm', - [ - 'pack', - staging, - '--json', - '--ignore-scripts', - '--pack-destination', - directory, - ], - directory - ) + const { stdout } = execute( + 'npm', + [ + 'pack', + staging, + '--json', + '--ignore-scripts', + '--pack-destination', + directory, + ], + directory ); - /* npm v12 keys `pack --json` output by package name; older versions return an array. */ - const [ packed ] = Array.isArray( output ) - ? output - : Object.values( output ); + const packed = readNpmPackResult( stdout, manifest.name ); return `file:${ path.join( directory, packed.filename ) }`; } const scripts = values.scripts ?? pack( 'scripts' ); diff --git a/tools/validation/validate-package-contents.mjs b/tools/validation/validate-package-contents.mjs index 8411006b931290..c7dd1b55511c5c 100755 --- a/tools/validation/validate-package-contents.mjs +++ b/tools/validation/validate-package-contents.mjs @@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { parseArgs } from 'node:util'; +import { readNpmPackResult } from './read-npm-pack-result.mjs'; const usage = 'Usage: node tools/validation/validate-package-contents.mjs [--disallow-path ]'; @@ -67,11 +68,7 @@ if ( packResult.status !== 0 ) { process.exit( packResult.status ?? 1 ); } -/* npm v12 keys `pack --json` output by package name; older versions return an array. */ -const packOutput = JSON.parse( packResult.stdout ); -const [ pack ] = Array.isArray( packOutput ) - ? packOutput - : Object.values( packOutput ); +const pack = readNpmPackResult( packResult.stdout, packageJson.name ); const packedPaths = pack.files.map( ( { path } ) => path ); const packedPathSet = new Set( packedPaths ); const disallowedPathPatterns = [ From e1c25151c7755bc68ff480b65b394c86561b68fa Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Mon, 5 Oct 2026 18:12:21 +0530 Subject: [PATCH 3/3] Release Tooling: Validate the npm view and pack result shape Reject an npm view entry that is not an object and an npm pack result without filename or files, naming the package. Run the preflight failure tests against both npm view shapes and cover the object shapes, null and malformed entries in the pack reader tests. --- tools/release/commands/packages.js | 11 +- tools/release/commands/test/packages.js | 230 ++++++++++-------- tools/validation/read-npm-pack-result.mjs | 13 +- tools/validation/read-npm-pack-result.test.js | 27 +- 4 files changed, 181 insertions(+), 100 deletions(-) diff --git a/tools/release/commands/packages.js b/tools/release/commands/packages.js index 52d03302bdb2d1..e7fa47580c9dce 100644 --- a/tools/release/commands/packages.js +++ b/tools/release/commands/packages.js @@ -804,11 +804,20 @@ async function runNpmPublishPreflight( ); } + const [ registryPackage ] = registryPackages; + if ( ! registryPackage || typeof registryPackage !== 'object' ) { + throw new Error( + `Expected npm registry lookup for ${ name }@${ version } to return package metadata, got ${ JSON.stringify( + registryPackage + ) }.` + ); + } + const { version: registryVersion, gitHead: registryGitHead, 'dist-tags': distTags = {}, - } = registryPackages[ 0 ]; + } = registryPackage; if ( registryVersion !== version ) { throw new Error( `Expected npm registry lookup for ${ name }@${ version } to return version ${ version }, got ${ registryVersion }.` diff --git a/tools/release/commands/test/packages.js b/tools/release/commands/test/packages.js index 3d1bd83497b743..d58ebed63221df 100644 --- a/tools/release/commands/test/packages.js +++ b/tools/release/commands/test/packages.js @@ -618,108 +618,148 @@ describe( 'runNpmPublishPreflight', () => { } ); - it( 'fails when a published version came from another commit', async () => { - const commandFn = vi - .fn() - .mockResolvedValueOnce( WHOAMI ) - .mockResolvedValueOnce( { - stdout: '{"version":"4.50.0","gitHead":"other-sha","dist-tags":{"latest":"4.50.0"}}', - } ); + it.each( [ 'null', '[null]' ] )( + 'fails when npm view returns %s', + async ( stdout ) => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { stdout } ); - await expect( - runNpmPublishPreflight( - { - distTag: 'latest', - gitWorkingDirectoryPath: '/repo', - publishCommit: 'publish-sha', - releasePackages: [ - { name: '@wordpress/a11y', version: '4.50.0' }, - ], - }, - { commandFn } - ) - ).rejects.toThrow( - '@wordpress/a11y@4.50.0 exists in the npm registry with gitHead other-sha, expected publish-sha.' - ); - expect( console ).toHaveLogged(); - } ); + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + 'Expected npm registry lookup for @wordpress/a11y@4.50.0 to return package metadata, got null.' + ); + expect( console ).toHaveLogged(); + } + ); - it( 'fails with an actionable error when a published version has no gitHead', async () => { - const commandFn = vi - .fn() - .mockResolvedValueOnce( WHOAMI ) - .mockResolvedValueOnce( { - stdout: '{"version":"4.50.0","dist-tags":{"latest":"4.50.0"}}', - } ); + describe.each( [ + [ 'an object, as npm v11 returns', ( json ) => json ], + [ 'an array, as npm v12 returns', ( json ) => `[${ json }]` ], + ] )( 'when npm view returns %s', ( _description, toStdout ) => { + it( 'fails when a published version came from another commit', async () => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { + stdout: toStdout( + '{"version":"4.50.0","gitHead":"other-sha","dist-tags":{"latest":"4.50.0"}}' + ), + } ); - await expect( - runNpmPublishPreflight( - { - distTag: 'latest', - gitWorkingDirectoryPath: '/repo', - publishCommit: 'publish-sha', - releasePackages: [ - { name: '@wordpress/a11y', version: '4.50.0' }, - ], - }, - { commandFn } - ) - ).rejects.toThrow( - '@wordpress/a11y@4.50.0 exists in the npm registry with gitHead nothing, expected publish-sha.' - ); - expect( console ).toHaveLogged(); - } ); + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + '@wordpress/a11y@4.50.0 exists in the npm registry with gitHead other-sha, expected publish-sha.' + ); + expect( console ).toHaveLogged(); + } ); - it( 'fails when a published version has the wrong dist-tag', async () => { - const commandFn = vi - .fn() - .mockResolvedValueOnce( WHOAMI ) - .mockResolvedValueOnce( { - stdout: '{"version":"4.50.0","gitHead":"publish-sha","dist-tags":{"latest":"4.49.0"}}', - } ); + it( 'fails with an actionable error when a published version has no gitHead', async () => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { + stdout: toStdout( + '{"version":"4.50.0","dist-tags":{"latest":"4.50.0"}}' + ), + } ); - await expect( - runNpmPublishPreflight( - { - distTag: 'latest', - gitWorkingDirectoryPath: '/repo', - publishCommit: 'publish-sha', - releasePackages: [ - { name: '@wordpress/a11y', version: '4.50.0' }, - ], - }, - { commandFn } - ) - ).rejects.toThrow( - '@wordpress/a11y@4.50.0 exists in the npm registry, but dist-tag "latest" points to 4.49.0. If another release moved the dist-tag, this prepared release is not safe to resume.' - ); - expect( console ).toHaveLogged(); - } ); + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + '@wordpress/a11y@4.50.0 exists in the npm registry with gitHead nothing, expected publish-sha.' + ); + expect( console ).toHaveLogged(); + } ); - it( 'fails when the registry returns a different version', async () => { - const commandFn = vi - .fn() - .mockResolvedValueOnce( WHOAMI ) - .mockResolvedValueOnce( { - stdout: '{"version":"4.49.0","gitHead":"publish-sha","dist-tags":{"latest":"4.49.0"}}', - } ); + it( 'fails when a published version has the wrong dist-tag', async () => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { + stdout: toStdout( + '{"version":"4.50.0","gitHead":"publish-sha","dist-tags":{"latest":"4.49.0"}}' + ), + } ); - await expect( - runNpmPublishPreflight( - { - distTag: 'latest', - gitWorkingDirectoryPath: '/repo', - publishCommit: 'publish-sha', - releasePackages: [ - { name: '@wordpress/a11y', version: '4.50.0' }, - ], - }, - { commandFn } - ) - ).rejects.toThrow( - 'Expected npm registry lookup for @wordpress/a11y@4.50.0 to return version 4.50.0, got 4.49.0.' - ); - expect( console ).toHaveLogged(); + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + '@wordpress/a11y@4.50.0 exists in the npm registry, but dist-tag "latest" points to 4.49.0. If another release moved the dist-tag, this prepared release is not safe to resume.' + ); + expect( console ).toHaveLogged(); + } ); + + it( 'fails when the registry returns a different version', async () => { + const commandFn = vi + .fn() + .mockResolvedValueOnce( WHOAMI ) + .mockResolvedValueOnce( { + stdout: toStdout( + '{"version":"4.49.0","gitHead":"publish-sha","dist-tags":{"latest":"4.49.0"}}' + ), + } ); + + await expect( + runNpmPublishPreflight( + { + distTag: 'latest', + gitWorkingDirectoryPath: '/repo', + publishCommit: 'publish-sha', + releasePackages: [ + { name: '@wordpress/a11y', version: '4.50.0' }, + ], + }, + { commandFn } + ) + ).rejects.toThrow( + 'Expected npm registry lookup for @wordpress/a11y@4.50.0 to return version 4.50.0, got 4.49.0.' + ); + expect( console ).toHaveLogged(); + } ); } ); } ); diff --git a/tools/validation/read-npm-pack-result.mjs b/tools/validation/read-npm-pack-result.mjs index c5f6fe76028299..a4eb0214938b0c 100644 --- a/tools/validation/read-npm-pack-result.mjs +++ b/tools/validation/read-npm-pack-result.mjs @@ -24,5 +24,16 @@ export function readNpmPackResult( stdout, packageName ) { `Expected one npm pack result for ${ packageName }, got ${ results.length }.` ); } - return results[ 0 ]; + const [ result ] = results; + if ( + typeof result?.filename !== 'string' || + ! Array.isArray( result.files ) + ) { + throw new Error( + `Unexpected npm pack result for ${ packageName }: ${ JSON.stringify( + result + ) }` + ); + } + return result; } diff --git a/tools/validation/read-npm-pack-result.test.js b/tools/validation/read-npm-pack-result.test.js index d60d9ea5387658..23e9fc5293264e 100644 --- a/tools/validation/read-npm-pack-result.test.js +++ b/tools/validation/read-npm-pack-result.test.js @@ -1,7 +1,11 @@ import { expect, test } from 'vitest'; import { readNpmPackResult } from './read-npm-pack-result.mjs'; -const result = { name: 'test-package', filename: 'test-package-1.0.0.tgz' }; +const result = { + name: 'test-package', + filename: 'test-package-1.0.0.tgz', + files: [ { path: 'package.json' } ], +}; test.each( [ [ 'an array, as npm v11 returns', [ result ] ], @@ -16,8 +20,15 @@ test.each( [ } ); test.each( [ - [ 'no results', '[]', 0 ], - [ 'more than one result', JSON.stringify( [ result, result ] ), 2 ], + [ 'an empty array', '[]', 0 ], + [ 'an array of several results', JSON.stringify( [ result, result ] ), 2 ], + [ 'an object with no keys', '{}', 0 ], + [ + 'an object with several keys', + JSON.stringify( { a: result, b: result } ), + 2, + ], + [ 'null', 'null', 0 ], ] )( 'fails on %s', ( _description, stdout, count ) => { expect( () => readNpmPackResult( stdout, 'test-package' ) ).toThrow( `Expected one npm pack result for test-package, got ${ count }.` @@ -29,3 +40,13 @@ test( 'fails on output that is not JSON', () => { readNpmPackResult( 'npm warn something', 'test-package' ) ).toThrow( 'Could not parse npm pack output for test-package' ); } ); + +test.each( [ + [ 'null', '[null]' ], + [ 'a result without files', '[{"filename":"test-package-1.0.0.tgz"}]' ], + [ 'a result without filename', '[{"files":[]}]' ], +] )( 'fails on %s', ( _description, stdout ) => { + expect( () => readNpmPackResult( stdout, 'test-package' ) ).toThrow( + 'Unexpected npm pack result for test-package' + ); +} );