diff --git a/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js b/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js index 59a0129fd..d21db461e 100644 --- a/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js +++ b/packages/ckeditor5-dev-release-tools/lib/tasks/reassignnpmtags.js @@ -12,10 +12,13 @@ const chalk = require( 'chalk' ); const columns = require( 'cli-columns' ); const { tools } = require( '@ckeditor/ckeditor5-dev-utils' ); +const util = require( 'util' ); +const exec = util.promisify( require( 'child_process' ).exec ); const assertNpmAuthorization = require( '../utils/assertnpmauthorization' ); /** * Used to switch the tags from `staging` to `latest` for specified array of packages. + * Each operation will be retried up to 3 times in case of failure. * * @param {Object} options * @param {String} options.npmOwner User that is authorized to release packages. @@ -33,30 +36,29 @@ module.exports = async function reassignNpmTags( { npmOwner, version, packages } const counter = tools.createSpinner( 'Reassigning npm tags...', { total: packages.length } ); counter.start(); - for ( const packageName of packages ) { - const latestVersion = await exec( `npm show ${ packageName }@latest version` ) - .then( version => version.trim() ) - .catch( error => { - errors.push( trimErrorMessage( error.message ) ); - } ); - - if ( latestVersion === version ) { - packagesSkipped.push( packageName ); - - continue; - } - - await exec( `npm dist-tag add ${ packageName }@${ version } latest` ) - .then( () => { - packagesUpdated.push( packageName ); + const updateTagPromises = packages.map( async packageName => { + const updateLatestTagRetryable = retry( () => exec( `npm dist-tag add ${ packageName }@${ version } latest` ) ); + await updateLatestTagRetryable() + .then( response => { + if ( response.stdout ) { + packagesUpdated.push( packageName ); + } else if ( response.stderr ) { + throw new Error( response.stderr ); + } } ) .catch( error => { - errors.push( trimErrorMessage( error.message ) ); + if ( error.message.includes( 'is already set to version' ) ) { + packagesSkipped.push( packageName ); + } else { + errors.push( trimErrorMessage( error.message ) ); + } } ) .finally( () => { counter.increase(); } ); - } + } ); + + await Promise.allSettled( updateTagPromises ); counter.finish(); @@ -85,9 +87,24 @@ function trimErrorMessage( message ) { } /** - * @param {String} command - * @returns {Promise.} + * @param {Function} callback + * @param {Number} times + * @returns {RetryCallback} */ -async function exec( command ) { - return tools.shExec( command, { verbosity: 'silent', async: true } ); +function retry( callback, times = 3 ) { + return ( ...args ) => Promise.resolve() + .then( () => callback( ...args ) ) + .catch( err => { + if ( times > 0 ) { + return retry( callback, times - 1 )( ...args ); + } + + throw err; + } ); } + +/** + * @callback RetryCallback + * @param {...*} args + * @returns {Promise} + */ diff --git a/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js b/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js index ed60506da..a227936a7 100644 --- a/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js +++ b/packages/ckeditor5-dev-release-tools/tests/tasks/reassignnpmtags.js @@ -21,7 +21,6 @@ describe( 'reassignNpmTags()', () => { stubs = { tools: { - shExec: sinon.stub(), createSpinner: sinon.stub().callsFake( () => { return stubs.spinner; } ) @@ -43,15 +42,18 @@ describe( 'reassignNpmTags()', () => { columns: sinon.stub(), console: { log: sinon.stub( console, 'log' ) - } + }, + util: { + promisify: sinon.stub().callsFake( () => stubs.exec ) + }, + exec: sinon.stub() }; - stubs.tools.shExec.withArgs( sinon.match( 'npm show' ) ).resolves( '1.0.0' ); - mockery.registerMock( '@ckeditor/ckeditor5-dev-utils', { tools: stubs.tools } ); mockery.registerMock( '../utils/assertnpmauthorization', stubs.assertNpmAuthorization ); mockery.registerMock( 'cli-columns', stubs.columns ); mockery.registerMock( 'chalk', stubs.chalk ); + mockery.registerMock( 'util', stubs.util ); reassignNpmTags = require( '../../lib/tasks/reassignnpmtags' ); } ); @@ -64,7 +66,7 @@ describe( 'reassignNpmTags()', () => { it( 'should throw an error when assertNpmAuthorization throws error', async () => { stubs.assertNpmAuthorization.throws( new Error( 'User not logged in error' ) ); - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); try { await reassignNpmTags( { npmOwner: 'correct-npm-user', version: '1.0.1', packages: [ 'package1' ] } ); @@ -76,25 +78,18 @@ describe( 'reassignNpmTags()', () => { expect( npmDistTagAdd.callCount ).to.equal( 0 ); } ); - it( 'should still try to update tags when can not obtain package version from npm', async () => { - stubs.tools.shExec.withArgs( sinon.match( 'npm show' ) ).throws( new Error( 'Can not obtain package version.' ) ); - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); - - await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); - - expect( npmDistTagAdd.callCount ).to.equal( 2 ); - } ); - it( 'should skip updating tags when provided version matches existing version for tag latest', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + stubs.columns.returns( 'package1 | package2' ); + stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).throws( new Error( 'is already set to version' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.0', packages: [ 'package1', 'package2' ] } ); - expect( npmDistTagAdd.callCount ).to.equal( 0 ); + expect( stubs.console.log.firstCall.args[ 0 ] ).to.equal( '⬇️ Packages skipped:' ); + expect( stubs.console.log.secondCall.args[ 0 ] ).to.deep.equal( 'package1 | package2' ); } ); it( 'should update tags when tag latest for provided version does not yet exist', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).resolves( { stdout: '+latest' } ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); @@ -103,7 +98,7 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should continue updating packages even if first package update fails', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); @@ -131,7 +126,7 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should increase the spinner counter after failure processing a package', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } ); @@ -140,7 +135,7 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should finish the spinner once all packages have been processed', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); @@ -154,6 +149,7 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should display skipped packages in a column', async () => { + stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).throws( new Error( 'is already set to version' ) ); stubs.columns.returns( '1 | 2 | 3' ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.0', packages: [ 'package1', 'package2' ] } ); @@ -168,6 +164,7 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should display processed packages in a column', async () => { + stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ).resolves( { stdout: '+latest' } ); stubs.columns.returns( '1 | 2 | 3' ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1', 'package2' ] } ); @@ -182,8 +179,8 @@ describe( 'reassignNpmTags()', () => { } ); it( 'should display errors found during processing a package', async () => { - const npmDistTagAdd = stubs.tools.shExec.withArgs( sinon.match( 'npm dist-tag add' ) ); - npmDistTagAdd.onFirstCall().throws( new Error( 'Npm error while updating tag.' ) ); + const npmDistTagAdd = stubs.exec.withArgs( sinon.match( 'npm dist-tag add' ) ); + npmDistTagAdd.throws( new Error( 'Npm error while updating tag.' ) ); await reassignNpmTags( { npmOwner: 'authorized-user', version: '1.0.1', packages: [ 'package1' ] } );