diff --git a/bin/cli.js b/bin/cli.js index 949b4b2..b5647ca 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -9,7 +9,7 @@ const cli = meow( $ multi-semantic-release Options - 'none' + --sequential-init Avoid hypothetical concurrent initialization collisions. Examples $ multi-semantic-release diff --git a/bin/runner.js b/bin/runner.js index c93b656..e89070c 100644 --- a/bin/runner.js +++ b/bin/runner.js @@ -18,7 +18,7 @@ module.exports = (flags) => { console.log("yarn paths", paths); // Do multirelease (log out any errors). - multiSemanticRelease(paths, {}, { cwd }).then( + multiSemanticRelease(paths, {}, { cwd }, flags).then( () => { // Success. process.exit(0); diff --git a/lib/createInlinePluginCreator.js b/lib/createInlinePluginCreator.js index fcc2b7c..fbe4d40 100644 --- a/lib/createInlinePluginCreator.js +++ b/lib/createInlinePluginCreator.js @@ -22,6 +22,7 @@ function createInlinePluginCreator(packages, multiContext, synchronizer) { // Announcement of readiness for release. announceForAll("_readyToGenerateNotes"); + announceForAll("_readyForRelease"); /** * Update pkg deps. @@ -85,6 +86,12 @@ function createInlinePluginCreator(packages, multiContext, synchronizer) { // And bind actual logger. Object.assign(pkg.loggerRef, context.logger); + pkg._ready = true; + emit( + "_readyForRelease", + todo().find((p) => !p._ready) + ); + return plugins.verifyConditions(context); }; @@ -198,11 +205,11 @@ function createInlinePluginCreator(packages, multiContext, synchronizer) { const publish = async (pluginOptions, context) => { pkg._prepared = true; - const nextPkgToProcess = todo().find((p) => p._nextType && !p._prepared); - if (nextPkgToProcess) { - emit("_readyToGenerateNotes", nextPkgToProcess); - } + emit( + "_readyToGenerateNotes", + todo().find((p) => p._nextType && !p._prepared) + ); // Wait for all packages to be `prepare`d and tagged by `semantic-release` await waitForAll("_prepared", (p) => p._nextType); diff --git a/lib/multiSemanticRelease.js b/lib/multiSemanticRelease.js index 337d41d..00e1613 100644 --- a/lib/multiSemanticRelease.js +++ b/lib/multiSemanticRelease.js @@ -41,12 +41,14 @@ const createInlinePluginCreator = require("./createInlinePluginCreator"); * @param {string[]} paths An array of paths to package.json files. * @param {Object} inputOptions An object containing semantic-release options. * @param {Object} settings An object containing: cwd, env, stdout, stderr (mainly for configuring tests). + * @param {Object} flags Argv flags. * @returns {Promise} Promise that resolves to a list of package objects with `result` property describing whether it released or not. */ async function multiSemanticRelease( paths, inputOptions = {}, - { cwd = process.cwd(), env = process.env, stdout = process.stdout, stderr = process.stderr } = {} + { cwd = process.cwd(), env = process.env, stdout = process.stdout, stderr = process.stderr } = {}, + flags = {} ) { // Check params. check(paths, "paths: string[]"); @@ -72,10 +74,22 @@ async function multiSemanticRelease( // Shared signal bus. const synchronizer = getSynchronizer(packages); + const { getLucky } = synchronizer; // Release all packages. const createInlinePlugin = createInlinePluginCreator(packages, multiContext, synchronizer); - await Promise.all(packages.map((pkg) => releasePackage(pkg, createInlinePlugin, multiContext))); + await Promise.all( + packages.map(async (pkg) => { + // Avoid hypothetical concurrent initialization collisions. + // https://github.com/dhoulb/multi-semantic-release/issues/24 + if (flags.sequentialInit) { + getLucky("_readyForRelease", pkg); + await pkg._readyForRelease; + } + + return releasePackage(pkg, createInlinePlugin, multiContext); + }) + ); const released = packages.filter((pkg) => pkg.result).length; // Return packages list. diff --git a/test/lib/multiSemanticRelease.test.js b/test/lib/multiSemanticRelease.test.js index 677d46f..8db0061 100644 --- a/test/lib/multiSemanticRelease.test.js +++ b/test/lib/multiSemanticRelease.test.js @@ -329,6 +329,62 @@ describe("multiSemanticRelease()", () => { }, }); }); + test("Changes in some packages (sequential-init)", async () => { + // Create Git repo. + const cwd = gitInit(); + // Initial commit. + copyDirectory(`test/fixtures/yarnWorkspaces/`, cwd); + const sha1 = gitCommitAll(cwd, "feat: Initial release"); + gitTag(cwd, "msr-test-a@1.0.0"); + gitTag(cwd, "msr-test-b@1.0.0"); + gitTag(cwd, "msr-test-c@1.0.0"); + gitTag(cwd, "msr-test-d@1.0.0"); + // Second commit. + writeFileSync(`${cwd}/packages/a/aaa.txt`, "AAA"); + const sha2 = gitCommitAll(cwd, "feat(aaa): Add missing text file"); + const url = gitInitOrigin(cwd); + gitPush(cwd); + + // Capture output. + const stdout = new WritableStreamBuffer(); + const stderr = new WritableStreamBuffer(); + + // Call multiSemanticRelease() + // Doesn't include plugins that actually publish. + const multiSemanticRelease = require("../../"); + const result = await multiSemanticRelease( + [ + `packages/c/package.json`, + `packages/d/package.json`, + `packages/b/package.json`, + `packages/a/package.json`, + ], + {}, + { cwd, stdout, stderr }, + { sequentialInit: true } + ); + + // Check manifests. + expect(require(`${cwd}/packages/a/package.json`)).toMatchObject({ + peerDependencies: { + "msr-test-c": "1.0.1", + }, + }); + expect(require(`${cwd}/packages/b/package.json`)).toMatchObject({ + dependencies: { + "msr-test-a": "1.1.0", + }, + devDependencies: { + "msr-test-c": "1.0.1", + }, + }); + expect(require(`${cwd}/packages/c/package.json`)).toMatchObject({ + devDependencies: { + "msr-test-b": "1.0.1", + "msr-test-d": "1.0.0", + }, + }); + }); test("Error if release's local deps have no version number", async () => { // Create Git repo with copy of Yarn workspaces fixture. const cwd = gitInit();