From da8fe50875c17140069f10f6dd448f3201402887 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 27 Oct 2024 14:09:25 +0100 Subject: [PATCH 1/3] Fix (release-tools): The "publishPackages()" task should not throw an error after trying to publish packages after reaching an attempted limit. Instead, it should verify if the last try was successfully completed. If it wasn't, throw the error. Other (release-tools): Increased the attemptions limit from 3 to 5. --- .../lib/tasks/publishpackages.js | 76 +- .../tests/tasks/publishpackages.js | 752 ++++++++++-------- 2 files changed, 453 insertions(+), 375 deletions(-) diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js b/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js index 5b28d58d7..9e7c5327d 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/publishpackages.js @@ -15,19 +15,22 @@ import checkVersionAvailability from '../utils/checkversionavailability.js'; import findPathsToPackages from '../utils/findpathstopackages.js'; /** - * The purpose of the script is to validate the packages prepared for the release and then release them on npm. + * The purpose of the script is to publish the prepared packages. However, before, it executes a few checks that + * prevent from publishing an incomplete package. * - * The validation contains the following steps in each package: - * - User must be logged to npm on the specified account. - * - The package directory must contain `package.json` file. - * - All other files expected to be released must exist in the package directory. - * - The npm tag must match the tag calculated from the package version. + * The validation contains the following steps: + * + * - A user (a CLI session) must be logged to npm on the specified account (`npmOwner`). + * - A package directory must contain `package.json` file. + * - All files defined in the `optionalEntryPointPackages` option must exist in a package directory. + * - An npm tag (dist-tag) must match the tag calculated from the package version. + * A stable release can be also published as `next` or `staging. * * When the validation for each package passes, packages are published on npm. Optional callback is called for confirmation whether to * continue. * * If a package has already been published, the script does not try to publish it again. Instead, it treats the package as published. - * Whenever a communication between the script and npm fails, it tries to re-publish a package (up to three attempts). + * Whenever a communication between the script and npm fails, it tries to re-publish a package (up to five attempts). * * @param {object} options * @param {string} options.packagesDirectory Relative path to a location of packages to release. @@ -63,17 +66,36 @@ export default async function publishPackages( options ) { optionalEntryPointPackages = [], cwd = process.cwd(), concurrency = 2, - attempts = 3 + attempts = 5 } = options; - const remainingAttempts = attempts - 1; await assertNpmAuthorization( npmOwner ); + // Find packages that would be published... const packagePaths = await findPathsToPackages( cwd, packagesDirectory ); - await assertPackages( packagePaths, { requireEntryPoint, optionalEntryPointPackages } ); - await assertFilesToPublish( packagePaths, optionalEntries ); - await assertNpmTag( packagePaths, npmTag ); + // ...and filter out those that have already been processed. + // In other words, check whether a version per package (it's read from a `package.json` file) + // is not available. Otherwise, a package is ignored. + await removeAlreadyPublishedPackages( packagePaths ); + + // Once again, find packages to publish after the filtering operation. + const packagesToProcess = await findPathsToPackages( cwd, packagesDirectory ); + + if ( !packagesToProcess.length ) { + listrTask.output = 'All packages have been published.'; + + return Promise.resolve(); + } + + // No more attempts. Abort. + if ( attempts <= 0 ) { + throw new Error( 'Some packages could not be published.' ); + } + + await assertPackages( packagesToProcess, { requireEntryPoint, optionalEntryPointPackages } ); + await assertFilesToPublish( packagesToProcess, optionalEntries ); + await assertNpmTag( packagesToProcess, npmTag ); const shouldPublishPackages = confirmationCallback ? await confirmationCallback() : true; @@ -81,8 +103,6 @@ export default async function publishPackages( options ) { return Promise.resolve(); } - await removeAlreadyPublishedPackages( packagePaths ); - await executeInParallel( { cwd, packagesDirectory, @@ -95,39 +115,17 @@ export default async function publishPackages( options ) { concurrency } ); - const packagePathsAfterPublishing = await findPathsToPackages( cwd, packagesDirectory ); - - // All packages have been published. No need for re-executing. - if ( !packagePathsAfterPublishing.length ) { - return Promise.resolve(); - } - - // No more attempts. Abort. - if ( remainingAttempts <= 0 ) { - throw new Error( 'Some packages could not be published.' ); - } - listrTask.output = 'Let\'s give an npm a moment for taking a breath (~10 sec)...'; - // Let's give an npm a moment for taking a breath... await wait( 1000 * 10 ); - listrTask.output = 'Done. Let\'s continue.'; + listrTask.output = 'Done. Let\'s continue. Re-executing.'; // ...and try again. return publishPackages( { - packagesDirectory, - npmOwner, - listrTask, - signal, - npmTag, - optionalEntries, - requireEntryPoint, - optionalEntryPointPackages, - cwd, - concurrency, + ...options, confirmationCallback: null, // Do not ask again if already here. - attempts: remainingAttempts + attempts: attempts - 1 } ); } diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js b/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js index a1bd12a09..100a41173 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/publishpackages.js @@ -30,7 +30,17 @@ describe( 'publishPackages()', () => { beforeEach( () => { vi.spyOn( process, 'cwd' ).mockReturnValue( '/work/project' ); - vi.mocked( findPathsToPackages ).mockResolvedValue( [] ); + vi.mocked( findPathsToPackages ) + .mockResolvedValueOnce( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ) + .mockResolvedValueOnce( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ) + .mockResolvedValue( [] ); + vi.mocked( assertNpmAuthorization ).mockResolvedValue(); vi.mocked( assertPackages ).mockResolvedValue(); vi.mocked( assertNpmTag ).mockResolvedValue(); @@ -40,408 +50,444 @@ describe( 'publishPackages()', () => { vi.mocked( fs ).readJson.mockResolvedValue( { name: '', version: '' } ); vi.mocked( checkVersionAvailability ).mockResolvedValue( true ); + + vi.useFakeTimers(); } ); - it( 'should not throw if all assertion passes', async () => { - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ); + afterEach( () => { + vi.useRealTimers(); } ); - it( 'should read the package directory (default `cwd`)', async () => { - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + describe( 'a package verification', () => { + it( 'should not throw if all assertion passes', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); + + await vi.advanceTimersToNextTimerAsync(); + await promise; } ); - expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledWith( '/work/project', 'packages' ); - } ); + it( 'should read the package directory (default `cwd`)', async () => { + vi.mocked( findPathsToPackages ).mockReset().mockResolvedValue( [] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - it( 'should read the package directory (custom `cwd`)', async () => { - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - cwd: '/work/custom-dir' + expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledWith( '/work/project', 'packages' ); } ); - expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledWith( '/work/custom-dir', 'packages' ); - } ); + it( 'should read the package directory (custom `cwd`)', async () => { + vi.mocked( findPathsToPackages ).mockReset().mockResolvedValue( [] ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {}, + cwd: '/work/custom-dir' + } ); - it( 'should assert npm authorization', async () => { - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledWith( '/work/custom-dir', 'packages' ); } ); - expect( vi.mocked( assertNpmAuthorization ) ).toHaveBeenCalledExactlyOnceWith( 'pepe' ); - } ); + it( 'should assert npm authorization', async () => { + vi.mocked( findPathsToPackages ).mockReset().mockResolvedValue( [] ); - it( 'should throw if npm authorization assertion failed', async () => { - vi.mocked( assertNpmAuthorization ).mockRejectedValue( - new Error( 'You must be logged to npm as "pepe" to execute this release step.' ) - ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - await expect( publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'fake-pepe' - } ) ).rejects.toThrow( 'You must be logged to npm as "pepe" to execute this release step.' ); - } ); + expect( vi.mocked( assertNpmAuthorization ) ).toHaveBeenCalledExactlyOnceWith( 'pepe' ); + } ); - it( 'should assert that each found directory is a package', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + it( 'should throw if npm authorization assertion failed', async () => { + vi.mocked( assertNpmAuthorization ).mockRejectedValue( + new Error( 'You must be logged to npm as "pepe" to execute this release step.' ) + ); + + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'fake-pepe' + } ) ).rejects.toThrow( 'You must be logged to npm as "pepe" to execute this release step.' ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + expect( vi.mocked( assertNpmAuthorization ) ).toHaveBeenCalledExactlyOnceWith( 'fake-pepe' ); } ); - expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - { - requireEntryPoint: false, - optionalEntryPointPackages: [] - } - ); - } ); + it( 'should assert that each found directory is a package', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - // See: https://github.com/ckeditor/ckeditor5/issues/15127. - it( 'should allow enabling the "package entry point" validator', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - requireEntryPoint: true, - optionalEntryPointPackages: [ - 'ckeditor5-foo' - ] + expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + { + requireEntryPoint: false, + optionalEntryPointPackages: [] + } + ); } ); - expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - { + // See: https://github.com/ckeditor/ckeditor5/issues/15127. + it( 'should allow enabling the "package entry point" validator', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {}, requireEntryPoint: true, optionalEntryPointPackages: [ 'ckeditor5-foo' ] - } - ); - } ); + } ); - it( 'should throw if package assertion failed', async () => { - vi.mocked( assertPackages ).mockRejectedValue( - new Error( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ) - ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await expect( publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ) ).rejects.toThrow( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ); - } ); + expect( vi.mocked( assertPackages ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + { + requireEntryPoint: true, + optionalEntryPointPackages: [ + 'ckeditor5-foo' + ] + } + ); + } ); - it( 'should assert that each required file exists in the package directory (no optional entries)', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + it( 'should throw if package assertion failed', async () => { + vi.mocked( findPathsToPackages ) + .mockResolvedValue( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ); + + vi.mocked( assertPackages ).mockRejectedValue( + new Error( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ) + ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ) ).rejects.toThrow( 'The "package.json" file is missing in the "ckeditor5-foo" package.' ); } ); - expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - null - ); - } ); + it( 'should assert that each required file exists in the package directory (no optional entries)', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - it( 'should assert that each required file exists in the package directory (with optional entries)', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - optionalEntries: { - 'ckeditor5-foo': [ 'src' ] - } + expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + null + ); } ); - expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - { - 'ckeditor5-foo': [ 'src' ] - } - ); - } ); + it( 'should assert that each required file exists in the package directory (with optional entries)', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {}, + optionalEntries: { + 'ckeditor5-foo': [ 'src' ] + } + } ); - it( 'should throw if not all required files exist in the package directory', async () => { - vi.mocked( assertFilesToPublish ).mockRejectedValue( - new Error( 'Missing files in "ckeditor5-foo" package for entries: "src"' ) - ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await expect( publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ) ).rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src"' ); - } ); + expect( vi.mocked( assertFilesToPublish ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + { + 'ckeditor5-foo': [ 'src' ] + } + ); + } ); - it( 'should assert that version tag matches the npm tag (default npm tag)', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + it( 'should throw if not all required files exist in the package directory', async () => { + vi.mocked( assertFilesToPublish ).mockRejectedValue( + new Error( 'Missing files in "ckeditor5-foo" package for entries: "src"' ) + ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ) ).rejects.toThrow( 'Missing files in "ckeditor5-foo" package for entries: "src"' ); } ); - expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - 'staging' - ); - } ); + it( 'should assert that version tag matches the npm tag (default npm tag)', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - it( 'should assert that version tag matches the npm tag (custom npm tag)', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - npmTag: 'nightly' + expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + 'staging' + ); } ); - expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( - [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ], - 'nightly' - ); - } ); + it( 'should assert that version tag matches the npm tag (custom npm tag)', async () => { + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {}, + npmTag: 'nightly' + } ); - it( 'should throw if version tag does not match the npm tag', async () => { - vi.mocked( assertNpmTag ).mockRejectedValue( - new Error( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ) - ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await expect( publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ) ).rejects.toThrow( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ); - } ); + expect( vi.mocked( assertNpmTag ) ).toHaveBeenCalledExactlyOnceWith( + [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ], + 'nightly' + ); + } ); - it( 'should use two threads by default when publishing packages', async () => { - await publishPackages( {} ); + it( 'should throw if version tag does not match the npm tag', async () => { + vi.mocked( assertNpmTag ).mockRejectedValue( + new Error( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ) + ); - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { - concurrency: 2 - } ) ); + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ) ).rejects.toThrow( 'The version tag "rc" from "ckeditor5-foo" package does not match the npm tag "staging".' ); + } ); } ); - it( 'should pass parameters for publishing packages', async () => { - const listrTask = {}; - const abortController = new AbortController(); - - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - npmTag: 'nightly', - listrTask, - signal: abortController.signal, - concurrency: 3, - cwd: '/home/cwd' - } ); - - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledExactlyOnceWith( { - packagesDirectory: 'packages', - listrTask, - taskToExecute: publishPackageOnNpmCallback, - taskOptions: { npmTag: 'nightly' }, - signal: abortController.signal, - concurrency: 3, - cwd: '/home/cwd' + describe( 'publishing packages', () => { + it( 'should use two threads by default when publishing packages', async () => { + const promise = publishPackages( { + listrTask: {} + } ); + + await vi.advanceTimersToNextTimerAsync(); + await promise; + + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledExactlyOnceWith( expect.objectContaining( { + concurrency: 2 + } ) ); } ); - } ); - it( 'should publish packages on npm if confirmation callback is not set', async () => { - const listrTask = {}; + it( 'should pass parameters for publishing packages', async () => { + const listrTask = {}; + const abortController = new AbortController(); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - listrTask + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + npmTag: 'nightly', + listrTask, + signal: abortController.signal, + concurrency: 3, + cwd: '/home/cwd' + } ); + + await vi.advanceTimersToNextTimerAsync(); + await promise; + + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledExactlyOnceWith( { + packagesDirectory: 'packages', + listrTask, + taskToExecute: publishPackageOnNpmCallback, + taskOptions: { npmTag: 'nightly' }, + signal: abortController.signal, + concurrency: 3, + cwd: '/home/cwd' + } ); } ); - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); - } ); + it( 'should publish packages on npm if confirmation callback is not set', async () => { + const listrTask = {}; + + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask + } ); - it( 'should publish packages on npm if synchronous confirmation callback returns truthy value', async () => { - const confirmationCallback = vi.fn().mockReturnValue( true ); - const listrTask = {}; + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback, - listrTask + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); } ); - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); - } ); + it( 'should publish packages on npm if synchronous confirmation callback returns truthy value', async () => { + const confirmationCallback = vi.fn().mockReturnValue( true ); + const listrTask = {}; + + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback, + listrTask + } ); - it( 'should publish packages on npm if asynchronous confirmation callback returns truthy value', async () => { - const confirmationCallback = vi.fn().mockResolvedValue( true ); - const listrTask = {}; + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback, - listrTask + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); } ); - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); - } ); + it( 'should publish packages on npm if asynchronous confirmation callback returns truthy value', async () => { + const confirmationCallback = vi.fn().mockResolvedValue( true ); + const listrTask = {}; + + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback, + listrTask + } ); - it( 'should not publish packages on npm if synchronous confirmation callback returns falsy value', async () => { - const confirmationCallback = vi.fn().mockReturnValue( false ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledOnce(); } ); - expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); - expect( confirmationCallback ).toHaveBeenCalledOnce(); - } ); + it( 'should not publish packages on npm if synchronous confirmation callback returns falsy value', async () => { + const confirmationCallback = vi.fn().mockReturnValue( false ); - it( 'should not publish packages on npm if asynchronous confirmation callback returns falsy value', async () => { - const confirmationCallback = vi.fn().mockResolvedValue( false ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback + } ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe', - confirmationCallback + expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); + expect( confirmationCallback ).toHaveBeenCalledOnce(); } ); - expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); - expect( confirmationCallback ).toHaveBeenCalledOnce(); - } ); + it( 'should not publish packages on npm if asynchronous confirmation callback returns falsy value', async () => { + const confirmationCallback = vi.fn().mockResolvedValue( false ); - it( 'should throw if publishing packages on npm failed', async () => { - vi.mocked( executeInParallel ).mockRejectedValue( - new Error( 'Unable to publish "ckeditor5-foo" package.' ) - ); - - await expect( publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' - } ) ).rejects.toThrow( - 'Unable to publish "ckeditor5-foo" package.' - ); - } ); + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + confirmationCallback + } ); - it( 'should verify if given package can be published', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + expect( vi.mocked( executeInParallel ) ).not.toHaveBeenCalledOnce(); + expect( confirmationCallback ).toHaveBeenCalledOnce(); + } ); - vi.mocked( fs ).readJson - .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-foo', version: '1.0.0' } ) - .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-bar', version: '1.0.0' } ); + it( 'should throw if publishing packages on npm failed', async () => { + vi.mocked( executeInParallel ).mockRejectedValue( + new Error( 'Unable to publish "ckeditor5-foo" package.' ) + ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + await expect( publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe' + } ) ).rejects.toThrow( + 'Unable to publish "ckeditor5-foo" package.' + ); } ); - expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 2 ); - expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo/package.json' ); - expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-bar/package.json' ); + it( 'should verify if given package can be published', async () => { + vi.mocked( findPathsToPackages ) + .mockReset() + .mockResolvedValueOnce( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ) + .mockResolvedValue( [] ); - expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledTimes( 2 ); - expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( '1.0.0', '@ckeditor/ckeditor5-foo' ); - expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( '1.0.0', '@ckeditor/ckeditor5-bar' ); - } ); + vi.mocked( fs ).readJson + .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-foo', version: '1.0.0' } ) + .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-bar', version: '1.0.0' } ); - it( 'should remove a package if is already published', async () => { - vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ - '/work/project/packages/ckeditor5-foo', - '/work/project/packages/ckeditor5-bar' - ] ) - .mockResolvedValue( [] ); + const promise = publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); - vi.mocked( fs ).readJson - .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-foo', version: '1.0.0' } ) - .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-bar', version: '1.0.0' } ); + await vi.advanceTimersToNextTimerAsync(); + await promise; - vi.mocked( checkVersionAvailability ) - .mockResolvedValueOnce( false ) - .mockResolvedValueOnce( true ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo/package.json' ); + expect( vi.mocked( fs ).readJson ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-bar/package.json' ); - await publishPackages( { - packagesDirectory: 'packages', - npmOwner: 'pepe' + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledTimes( 2 ); + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( '1.0.0', '@ckeditor/ckeditor5-foo' ); + expect( vi.mocked( checkVersionAvailability ) ).toHaveBeenCalledWith( '1.0.0', '@ckeditor/ckeditor5-bar' ); } ); - expect( vi.mocked( fs ).remove ).toHaveBeenCalledTimes( 1 ); - expect( vi.mocked( fs ).remove ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo' ); + it( 'should remove a package if is already published', async () => { + vi.mocked( findPathsToPackages ) + .mockReset() + .mockResolvedValueOnce( [ + '/work/project/packages/ckeditor5-foo', + '/work/project/packages/ckeditor5-bar' + ] ) + .mockResolvedValue( [] ); + + vi.mocked( fs ).readJson + .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-foo', version: '1.0.0' } ) + .mockResolvedValueOnce( { name: '@ckeditor/ckeditor5-bar', version: '1.0.0' } ); + + vi.mocked( checkVersionAvailability ) + .mockResolvedValueOnce( false ) + .mockResolvedValueOnce( true ); + + await publishPackages( { + packagesDirectory: 'packages', + npmOwner: 'pepe', + listrTask: {} + } ); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( fs ).remove ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo' ); + } ); } ); describe( 're-publish packages that could not be published', () => { beforeEach( () => { - vi.useFakeTimers(); - } ); - - afterEach( () => { - vi.useRealTimers(); + vi.mocked( findPathsToPackages ).mockReset(); } ); it( 'should not execute the specified `confirmationCallback` when re-publishing packages', async () => { @@ -531,8 +577,11 @@ describe( 'publishPackages()', () => { vi.mocked( fs ).readJson.mockResolvedValue( {} ); + const messages = []; const listrTask = { - output: '' + set output( value ) { + messages.push( value ); + } }; const confirmationCallback = vi.fn().mockReturnValue( true ); @@ -543,38 +592,48 @@ describe( 'publishPackages()', () => { listrTask } ); - await vi.advanceTimersByTimeAsync( 0 ); - expect( listrTask.output ).not.toEqual( '' ); - await vi.advanceTimersToNextTimerAsync(); - expect( listrTask.output ).toEqual( 'Done. Let\'s continue.' ); - await promise; + + expect( messages[ 0 ] ).toEqual( 'Let\'s give an npm a moment for taking a breath (~10 sec)...' ); + expect( messages[ 1 ] ).toEqual( 'Done. Let\'s continue. Re-executing.' ); } ); - it( 'should try to publish packages thrice before rejecting a promise', async () => { + it( 'should try to publish packages five times before rejecting a promise', async () => { + // We want to simulate that the last call published the package. The previous attempts had failed. + for ( let i = 0; i < 5; ++i ) { + vi.mocked( findPathsToPackages ) + .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) + .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ); + } + + // The last call before throwing an error. After comparing results with the registry, + // there is nothing to publish. vi.mocked( findPathsToPackages ) - .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) - .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) - .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) - .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-bar' ] ) .mockResolvedValue( [] ); vi.mocked( fs ).readJson.mockResolvedValue( {} ); + const listrTask = { + output: '' + }; + const promise = publishPackages( { packagesDirectory: 'packages', npmOwner: 'pepe', - listrTask: {} + listrTask } ); - // Needed twice because the third attempt does not setup a timeout. - await vi.advanceTimersToNextTimerAsync(); - await vi.advanceTimersToNextTimerAsync(); + // Each execution sets its own timer. + for ( let i = 0; i < 5; ++i ) { + await vi.advanceTimersToNextTimerAsync(); + } + await promise; - expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledTimes( 3 ); + expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledTimes( 5 ); + expect( listrTask.output ).toEqual( 'All packages have been published.' ); } ); it( 'should execute itself and publish the non-published packages again (integration)', async () => { @@ -584,7 +643,7 @@ describe( 'publishPackages()', () => { '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' ] ) - // Check for failed packages. + // All packages must be published. .mockResolvedValueOnce( [ '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' @@ -594,7 +653,11 @@ describe( 'publishPackages()', () => { '/work/project/packages/ckeditor5-foo', '/work/project/packages/ckeditor5-bar' ] ) - // Repeat execution: Check for failed packages. + // Check for failed packages. + .mockResolvedValueOnce( [ + '/work/project/packages/ckeditor5-foo' + ] ) + // Repeat execution: look for packages to release. .mockResolvedValue( [] ); vi.mocked( fs ).readJson.mockImplementation( input => { @@ -621,6 +684,7 @@ describe( 'publishPackages()', () => { listrTask: {} } ); + await vi.advanceTimersToNextTimerAsync(); await vi.advanceTimersToNextTimerAsync(); await promise; @@ -628,10 +692,10 @@ describe( 'publishPackages()', () => { expect( vi.mocked( fs ).remove ).toHaveBeenCalledWith( '/work/project/packages/ckeditor5-foo' ); expect( vi.mocked( executeInParallel ) ).toHaveBeenCalledTimes( 2 ); - expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledTimes( 4 ); + expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledTimes( 6 ); } ); - it( 'should reject a promise if cannot publish packages and there is no more attempting', async () => { + it( 'should reject a promise if cannot publish packages and there are no more attempts', async () => { vi.mocked( findPathsToPackages ).mockResolvedValue( [ '/work/project/packages/ckeditor5-bar' ] ); vi.mocked( fs ).readJson.mockResolvedValue( { @@ -639,11 +703,18 @@ describe( 'publishPackages()', () => { version: '1.0.0' } ); - await expect( publishPackages( { + const promise = safeReject( publishPackages( { packagesDirectory: 'packages', npmOwner: 'pepe', - attempts: 1 - } ) ).rejects.toThrow( 'Some packages could not be published.' ); + attempts: 1, + listrTask: {} + } ) ); + + await vi.advanceTimersToNextTimerAsync(); + + await expect( promise ).rejects.toThrow( 'Some packages could not be published.' ); + + expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledTimes( 4 ); } ); it( 'should reject a promise if cannot publish packages and there is no more attempting (a negative attempts value)', async () => { @@ -659,6 +730,15 @@ describe( 'publishPackages()', () => { npmOwner: 'pepe', attempts: -5 } ) ).rejects.toThrow( 'Some packages could not be published.' ); + + expect( vi.mocked( findPathsToPackages ) ).toHaveBeenCalledTimes( 2 ); } ); } ); } ); + +// To mute the "PromiseRejectionHandledWarning" warning. +function safeReject( promise ) { + promise.catch( vi.fn() ); + + return promise; +} From d34a626d79897c834ad5a65191561a34ba87050e Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 27 Oct 2024 14:39:54 +0100 Subject: [PATCH 2/3] Other (release-tools): Created a decorated version of utils exposes by the "pacote" package ("manifest()", "packument()"). It prevents from using any cache when checking the npm registry. --- .../lib/utils/pacotecacheless.js | 33 +++ .../tests/utils/pacotecacheless.js | 198 ++++++++++++++++++ 2 files changed, 231 insertions(+) create mode 100644 packages/ckeditor5-dev-release-tools/lib/utils/pacotecacheless.js create mode 100644 packages/ckeditor5-dev-release-tools/tests/utils/pacotecacheless.js diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/pacotecacheless.js b/packages/ckeditor5-dev-release-tools/lib/utils/pacotecacheless.js new file mode 100644 index 000000000..f9581e7c0 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/lib/utils/pacotecacheless.js @@ -0,0 +1,33 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import os from 'os'; +import { randomUUID } from 'crypto'; +import upath from 'upath'; +import fs from 'fs-extra'; +import pacote from 'pacote'; + +export const manifest = cacheLessPacoteFactory( pacote.manifest ); +export const packument = cacheLessPacoteFactory( pacote.packument ); + +function cacheLessPacoteFactory( callback ) { + return async ( description, options = {} ) => { + const uuid = randomUUID(); + const cacheDir = upath.join( os.tmpdir(), `pacote--${ uuid }` ); + + await fs.ensureDir( cacheDir ); + + try { + return await callback( description, { + ...options, + cache: cacheDir, + memoize: false, + preferOnline: true + } ); + } finally { + await fs.remove( cacheDir ); + } + }; +} diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/pacotecacheless.js b/packages/ckeditor5-dev-release-tools/tests/utils/pacotecacheless.js new file mode 100644 index 000000000..d8ec6d429 --- /dev/null +++ b/packages/ckeditor5-dev-release-tools/tests/utils/pacotecacheless.js @@ -0,0 +1,198 @@ +/** + * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md. + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import os from 'os'; +import { randomUUID } from 'crypto'; +import fs from 'fs-extra'; +import pacote from 'pacote'; +import { manifest, packument } from '../../lib/utils/pacotecacheless.js'; + +vi.mock( 'os' ); +vi.mock( 'crypto' ); +vi.mock( 'fs-extra' ); +vi.mock( 'pacote' ); + +describe( 'pacote (no cache)', () => { + beforeEach( () => { + vi.mocked( os ).tmpdir.mockReturnValue( '/tmp' ); + vi.mocked( randomUUID ).mockReturnValue( 'a-1-b-2' ); + + vi.mocked( fs ).ensureDir.mockResolvedValue(); + vi.mocked( fs ).remove.mockResolvedValue(); + } ); + + describe( 'manifest()', () => { + it( 'should be a function', () => { + expect( manifest ).toBeTypeOf( 'function' ); + } ); + + it( 'should create a temporary cache directory', async () => { + await manifest( 'foo' ); + + expect( vi.mocked( os ).tmpdir ).toHaveBeenCalledOnce(); + expect( vi.mocked( randomUUID ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledExactlyOnceWith( '/tmp/pacote--a-1-b-2' ); + } ); + + it( 'must create a cache directory before executing `pacote.manifest()`', async () => { + await manifest( 'foo' ); + + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledBefore( vi.mocked( pacote ).manifest ); + } ); + + it( 'should pass a temporary cache directory to `pacote.manifest()`', async () => { + await manifest( 'foo' ); + + expect( vi.mocked( pacote ).manifest ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.objectContaining( { + cache: '/tmp/pacote--a-1-b-2', + memoize: false, + preferOnline: true + } ) ); + } ); + + it( 'should pass arguments to `pacote.manifest()`', async () => { + await manifest( 'foo', { foo: true, bar: false } ); + + expect( vi.mocked( pacote ).manifest ).toHaveBeenCalledExactlyOnceWith( + 'foo', + expect.objectContaining( { + foo: true, + bar: false + } ) ); + } ); + + it( 'should not allow overriding the cache parameters when executing `pacote.manifest()`', async () => { + await manifest( 'foo', { + cache: null, + memoize: 1, + preferOnline: 'never' + } ); + + expect( vi.mocked( pacote ).manifest ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.not.objectContaining( { + cache: null, + memoize: 1, + preferOnline: 'never' + } ) ); + } ); + + it( 'should resolve with a value returned by `pacote.manifest()', async () => { + const value = { + status: 'success', + data: { + done: true + } + }; + + vi.mocked( pacote ).manifest.mockResolvedValue( value ); + + await expect( manifest( 'foo' ) ).resolves.toEqual( value ); + } ); + + it( 'must remove a cache directory after executing `pacote.manifest()` (when resolved)', async () => { + await manifest( 'foo' ); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledAfter( vi.mocked( pacote ).manifest ); + } ); + + it( 'must remove a cache directory after executing `pacote.manifest()` (when rejected)', async () => { + vi.mocked( pacote ).manifest.mockRejectedValue( 'null' ); + + await expect( manifest( 'foo' ) ).rejects.toThrow(); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledAfter( vi.mocked( pacote ).manifest ); + } ); + } ); + + describe( 'packument()', () => { + it( 'should be a function', () => { + expect( packument ).toBeTypeOf( 'function' ); + } ); + + it( 'should create a temporary cache directory', async () => { + await packument( 'foo' ); + + expect( vi.mocked( os ).tmpdir ).toHaveBeenCalledOnce(); + expect( vi.mocked( randomUUID ) ).toHaveBeenCalledOnce(); + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledExactlyOnceWith( '/tmp/pacote--a-1-b-2' ); + } ); + + it( 'must create a cache directory before executing `pacote.packument()`', async () => { + await packument( 'foo' ); + + expect( vi.mocked( fs ).ensureDir ).toHaveBeenCalledBefore( vi.mocked( pacote ).packument ); + } ); + + it( 'should pass a temporary cache directory to `pacote.packument()`', async () => { + await packument( 'foo' ); + + expect( vi.mocked( pacote ).packument ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.objectContaining( { + cache: '/tmp/pacote--a-1-b-2', + memoize: false, + preferOnline: true + } ) ); + } ); + + it( 'should pass arguments to `pacote.packument()`', async () => { + await packument( 'foo', { foo: true, bar: false } ); + + expect( vi.mocked( pacote ).packument ).toHaveBeenCalledExactlyOnceWith( + 'foo', + expect.objectContaining( { + foo: true, + bar: false + } ) ); + } ); + + it( 'should not allow overriding the cache parameters when executing `pacote.packument()`', async () => { + await packument( 'foo', { + cache: null, + memoize: 1, + preferOnline: 'never' + } ); + + expect( vi.mocked( pacote ).packument ).toHaveBeenCalledExactlyOnceWith( + expect.any( String ), + expect.not.objectContaining( { + cache: null, + memoize: 1, + preferOnline: 'never' + } ) ); + } ); + + it( 'should resolve with a value returned by `pacote.packument()', async () => { + const value = { + status: 'success', + data: { + done: true + } + }; + + vi.mocked( pacote ).packument.mockResolvedValue( value ); + + await expect( packument( 'foo' ) ).resolves.toEqual( value ); + } ); + + it( 'must remove a cache directory after executing `pacote.packument()` (when resolved)', async () => { + await packument( 'foo' ); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledAfter( vi.mocked( pacote ).packument ); + } ); + + it( 'must remove a cache directory after executing `pacote.packument()` (when rejected)', async () => { + vi.mocked( pacote ).packument.mockRejectedValue( 'null' ); + + await expect( packument( 'foo' ) ).rejects.toThrow(); + + expect( vi.mocked( fs ).remove ).toHaveBeenCalledAfter( vi.mocked( pacote ).packument ); + } ); + } ); +} ); From 0f2a58fd32aa26a1760fa2e1ad222c16ff71920e Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Sun, 27 Oct 2024 14:51:07 +0100 Subject: [PATCH 3/3] Internal: Replaced internal "pacote" calls with the decorated functions. --- .../lib/utils/checkversionavailability.js | 6 ++-- .../lib/utils/isversionpublishablefortag.js | 4 +-- .../lib/utils/versions.js | 4 +-- .../tests/utils/checkversionavailability.js | 10 +++--- .../tests/utils/isversionpublishablefortag.js | 14 ++++---- .../tests/utils/versions.js | 36 +++++++++---------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js b/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js index 47d634686..a44057f27 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/checkversionavailability.js @@ -3,7 +3,7 @@ * For licensing, see LICENSE.md. */ -import pacote from 'pacote'; +import { manifest } from './pacotecacheless.js'; /** * Checks if the provided version for the package exists in the npm registry. @@ -15,9 +15,9 @@ import pacote from 'pacote'; * @returns {Promise} */ export default async function checkVersionAvailability( version, packageName ) { - return pacote.manifest( `${ packageName }@${ version }`, { cache: null, preferOnline: true } ) + return manifest( `${ packageName }@${ version }` ) .then( () => { - // If `pacote.manifest` resolves, a package with the given version exists. + // If `manifest` resolves, a package with the given version exists. return false; } ) .catch( () => { diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js b/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js index 4c12983f9..a95fc6c77 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/isversionpublishablefortag.js @@ -4,7 +4,7 @@ */ import semver from 'semver'; -import pacote from 'pacote'; +import { manifest } from './pacotecacheless.js'; /** * This util aims to verify if the given `packageName` can be published with the given `version` on the `npmTag`. @@ -15,7 +15,7 @@ import pacote from 'pacote'; * @returns {Promise.} */ export default async function isVersionPublishableForTag( packageName, version, npmTag ) { - const npmVersion = await pacote.manifest( `${ packageName }@${ npmTag }`, { cache: null, preferOnline: true } ) + const npmVersion = await manifest( `${ packageName }@${ npmTag }` ) .then( ( { version } ) => version ) // An `npmTag` does not exist, or it's a first release of a package. .catch( () => null ); diff --git a/packages/ckeditor5-dev-release-tools/lib/utils/versions.js b/packages/ckeditor5-dev-release-tools/lib/utils/versions.js index 56a401863..004dbbd9f 100644 --- a/packages/ckeditor5-dev-release-tools/lib/utils/versions.js +++ b/packages/ckeditor5-dev-release-tools/lib/utils/versions.js @@ -4,7 +4,7 @@ */ import { tools } from '@ckeditor/ckeditor5-dev-utils'; -import pacote from 'pacote'; +import { packument } from './pacotecacheless.js'; import getChangelog from './getchangelog.js'; import getPackageJson from './getpackagejson.js'; @@ -38,7 +38,7 @@ export function getLastFromChangelog( cwd = process.cwd() ) { export function getLastPreRelease( releaseIdentifier, cwd = process.cwd() ) { const packageName = getPackageJson( cwd ).name; - return pacote.packument( packageName, { cache: null, preferOnline: true } ) + return packument( packageName ) .then( result => { const lastVersion = Object.keys( result.versions ) .filter( version => version.startsWith( releaseIdentifier ) ) diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js b/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js index a5831fea7..0a0badc4c 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/checkversionavailability.js @@ -4,21 +4,21 @@ */ import { describe, expect, it, vi } from 'vitest'; -import pacote from 'pacote'; import checkVersionAvailability from '../../lib/utils/checkversionavailability.js'; +import { manifest } from '../../lib/utils/pacotecacheless.js'; -vi.mock( 'pacote' ); +vi.mock( '../../lib/utils/pacotecacheless.js' ); describe( 'checkVersionAvailability()', () => { it( 'should resolve to true if version does not exist', async () => { - vi.mocked( pacote.manifest ).mockRejectedValue( 'E404' ); + vi.mocked( manifest ).mockRejectedValue( 'E404' ); await expect( checkVersionAvailability( '1.0.1', 'stub-package' ) ).resolves.toBe( true ); - expect( pacote.manifest ).toHaveBeenCalledExactlyOnceWith( 'stub-package@1.0.1', expect.any( Object ) ); + expect( manifest ).toHaveBeenCalledExactlyOnceWith( 'stub-package@1.0.1' ); } ); it( 'should resolve to false if version exists', async () => { - pacote.manifest.mockResolvedValue( '1.0.1' ); + manifest.mockResolvedValue( '1.0.1' ); await expect( checkVersionAvailability( '1.0.1', 'stub-package' ) ).resolves.toBe( false ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js b/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js index 6ca3572f7..e64d34cab 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/isversionpublishablefortag.js @@ -4,18 +4,18 @@ */ import { describe, expect, it, vi } from 'vitest'; -import pacote from 'pacote'; import semver from 'semver'; import isVersionPublishableForTag from '../../lib/utils/isversionpublishablefortag.js'; +import { manifest } from '../../lib/utils/pacotecacheless.js'; -vi.mock( 'pacote' ); +vi.mock( '../../lib/utils/pacotecacheless.js' ); vi.mock( 'semver' ); describe( 'isVersionPublishableForTag()', () => { it( 'should return false if given version is not available', async () => { vi.mocked( semver.lte ).mockReturnValue( true ); - vi.mocked( pacote.manifest ).mockResolvedValue( ( { + vi.mocked( manifest ).mockResolvedValue( ( { version: '1.0.0' } ) ); @@ -23,13 +23,13 @@ describe( 'isVersionPublishableForTag()', () => { expect( result ).to.equal( false ); expect( semver.lte ).toHaveBeenCalledExactlyOnceWith( '1.0.0', '1.0.0' ); - expect( pacote.manifest ).toHaveBeenCalledExactlyOnceWith( 'package-name@latest', expect.any( Object ) ); + expect( manifest ).toHaveBeenCalledExactlyOnceWith( 'package-name@latest' ); } ); it( 'should return false if given version is not higher than the latest published', async () => { vi.mocked( semver.lte ).mockReturnValue( true ); - vi.mocked( pacote.manifest ).mockResolvedValue( ( { + vi.mocked( manifest ).mockResolvedValue( ( { version: '1.0.1' } ) ); @@ -40,12 +40,12 @@ describe( 'isVersionPublishableForTag()', () => { } ); it( 'should return true if given npm tag is not published yet', async () => { - vi.mocked( pacote.manifest ).mockRejectedValue( 'E404' ); + vi.mocked( manifest ).mockRejectedValue( 'E404' ); const result = await isVersionPublishableForTag( 'package-name', '1.0.0', 'alpha' ); expect( result ).to.equal( true ); expect( semver.lte ).not.toHaveBeenCalled(); - expect( pacote.manifest ).toHaveBeenCalledExactlyOnceWith( 'package-name@alpha', expect.any( Object ) ); + expect( manifest ).toHaveBeenCalledExactlyOnceWith( 'package-name@alpha' ); } ); } ); diff --git a/packages/ckeditor5-dev-release-tools/tests/utils/versions.js b/packages/ckeditor5-dev-release-tools/tests/utils/versions.js index f4c62e412..b04d75cd4 100644 --- a/packages/ckeditor5-dev-release-tools/tests/utils/versions.js +++ b/packages/ckeditor5-dev-release-tools/tests/utils/versions.js @@ -5,9 +5,9 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { tools } from '@ckeditor/ckeditor5-dev-utils'; -import pacote from 'pacote'; import getChangelog from '../../lib/utils/getchangelog.js'; import getPackageJson from '../../lib/utils/getpackagejson.js'; +import { packument } from '../../lib/utils/pacotecacheless.js'; import { getLastFromChangelog, @@ -21,7 +21,7 @@ import { } from '../../lib/utils/versions.js'; vi.mock( '@ckeditor/ckeditor5-dev-utils' ); -vi.mock( 'pacote' ); +vi.mock( '../../lib/utils/pacotecacheless.js' ); vi.mock( '../../lib/utils/getchangelog.js' ); vi.mock( '../../lib/utils/getpackagejson.js' ); @@ -106,20 +106,20 @@ describe( 'versions', () => { } ); it( 'asks npm for all versions of a package', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: {} } ); return getLastPreRelease( '42.0.0-alpha' ) .then( () => { - expect( vi.mocked( pacote ).packument ).toHaveBeenCalledTimes( 1 ); - expect( vi.mocked( pacote ).packument ).toHaveBeenCalledWith( 'ckeditor5', expect.any( Object ) ); + expect( vi.mocked( packument ) ).toHaveBeenCalledTimes( 1 ); + expect( vi.mocked( packument ) ).toHaveBeenCalledWith( 'ckeditor5' ); } ); } ); it( 'returns null if there is no version for a package', () => { - vi.mocked( pacote ).packument.mockRejectedValue(); + vi.mocked( packument ).mockRejectedValue(); return getLastPreRelease( '42.0.0-alpha' ) .then( result => { @@ -128,7 +128,7 @@ describe( 'versions', () => { } ); it( 'returns null if there is no pre-release version matching the release identifier', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -146,7 +146,7 @@ describe( 'versions', () => { } ); it( 'returns last pre-release version matching the release identifier', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -164,7 +164,7 @@ describe( 'versions', () => { } ); it( 'returns last pre-release version matching the release identifier (non-chronological versions order)', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -183,7 +183,7 @@ describe( 'versions', () => { } ); it( 'returns last pre-release version matching the release identifier (sequence numbers greater than 10)', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -203,7 +203,7 @@ describe( 'versions', () => { } ); it( 'returns last nightly version', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230614.0': {}, @@ -225,7 +225,7 @@ describe( 'versions', () => { } ); it( 'returns last nightly version from a specified day', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230614.0': {}, @@ -253,7 +253,7 @@ describe( 'versions', () => { } ); it( 'returns last nightly pre-release version', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230613.0': {}, @@ -279,7 +279,7 @@ describe( 'versions', () => { } ); it( 'returns pre-release version with id = 0 if pre-release version was never published for the package yet', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -295,7 +295,7 @@ describe( 'versions', () => { } ); it( 'returns pre-release version with incremented id if older pre-release version was already published', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -311,7 +311,7 @@ describe( 'versions', () => { } ); it( 'returns nightly version with incremented id if older nightly version was already published', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.5': {}, @@ -340,7 +340,7 @@ describe( 'versions', () => { } ); it( 'asks for a last nightly pre-release version', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-nightly-20230615.0': {}, @@ -369,7 +369,7 @@ describe( 'versions', () => { } ); it( 'asks for a last internal pre-release version', () => { - vi.mocked( pacote ).packument.mockResolvedValue( { + vi.mocked( packument ).mockResolvedValue( { name: 'ckeditor5', versions: { '0.0.0-internal-20230615.0': {},