Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions tools/release/commands/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -774,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`,
Expand All @@ -783,7 +783,7 @@ async function runNpmPublishPreflight(
stdio: 'pipe',
}
);
registryPackage = parseNpmJsonOutput(
registryOutput = parseNpmJsonOutput(
stdout,
`${ name }@${ version } metadata`
);
Expand All @@ -794,6 +794,25 @@ 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 [ 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,
Expand Down
320 changes: 202 additions & 118 deletions tools/release/commands/test/packages.js
Original file line number Diff line number Diff line change
Expand Up @@ -549,133 +549,217 @@ 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( [
Comment thread
manzoorwanijk marked this conversation as resolved.
[
'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
.fn()
.mockResolvedValueOnce( WHOAMI )
.mockResolvedValueOnce( {
stdout: '{"version":"4.50.0","gitHead":"other-sha","dist-tags":{"latest":"4.50.0"}}',
} );
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(
'@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 one version, got ${ count }.`
);
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"}}',
} );
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 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(
'Expected npm registry lookup for @wordpress/a11y@4.50.0 to return package metadata, got null.'
);
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"}}',
} );
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, 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 other-sha, 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 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(
'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 with gitHead nothing, 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: 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(
'@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();
} );
} );
} );

Expand Down
Loading
Loading