Skip to content

Commit

Permalink
feat: add debug messages, separate synchronizer layer
Browse files Browse the repository at this point in the history
closes #24
  • Loading branch information
antongolub authored Jul 21, 2020
1 parent cd0ee38 commit 6930cb6
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 220 deletions.
30 changes: 29 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
#!/usr/bin/env node

const meow = require("meow");
const { toPairs, set } = require("lodash");
const runner = require("./runner");
const cli = meow(
`
Usage
$ multi-semantic-release
runner(process.argv);
Options
--sequential-init Avoid hypothetical concurrent initialization collisions.
--debug Output debugging information.
--help Help info.
Examples
$ multi-semantic-release
`,
{
flags: {
sequentialInit: {
type: "boolean",
},
debug: {
type: "boolean",
},
},
}
);

const processFlags = (flags) => toPairs(flags).reduce((m, [k, v]) => set(m, k, v), {});

runner(processFlags(cli.flags));
9 changes: 7 additions & 2 deletions bin/runner.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module.exports = (argv) => {
module.exports = (flags) => {
if (flags.debug) {
require("debug").enable("msr:*");
}

// Imports.
const getWorkspacesYarn = require("../lib/getWorkspacesYarn");
const multiSemanticRelease = require("../lib/multiSemanticRelease");
Expand All @@ -12,13 +16,14 @@ module.exports = (argv) => {
try {
console.log(`multi-semantic-release version: ${multisemrelPkgJson.version}`);
console.log(`semantic-release version: ${semrelPkgJson.version}`);
console.log(`flags: ${JSON.stringify(flags, null, 2)}`);

// Get list of package.json paths according to Yarn workspaces.
const paths = getWorkspacesYarn(cwd);
console.log("yarn paths", paths);

// Do multirelease (log out any errors).
multiSemanticRelease(paths, {}, { cwd }).then(
multiSemanticRelease(paths, {}, { cwd }, flags).then(
() => {
// Success.
process.exit(0);
Expand Down
73 changes: 33 additions & 40 deletions lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { writeFileSync } = require("fs");
const { identity, once } = require("lodash");
const EventEmitter = require("promise-events");
const debug = require("debug")("msr:inlinePlugin");
const getCommitsFiltered = require("./getCommitsFiltered");
const { getManifest, getIndent } = require("./getManifest");
const hasChangedDeep = require("./hasChangedDeep");
Expand All @@ -11,40 +10,15 @@ const hasChangedDeep = require("./hasChangedDeep");
*
* @param {Package[]} packages The multi-semantic-release context.
* @param {MultiContext} multiContext The multi-semantic-release context.
* @param {Synchronizer} synchronizer Shared synchronization assets
* @returns {Function} A function that creates an inline package.
*
* @internal
*/
function createInlinePluginCreator(packages, multiContext) {
function createInlinePluginCreator(packages, multiContext, synchronizer) {
// Vars.
const { cwd } = multiContext;

// List of packages which are still todo (don't yet have a result).
const todo = () => packages.filter((p) => p.result === undefined);

// Shared signal bus.
const ee = new EventEmitter();

// Announcement of readiness for release.
todo().forEach((p) => (p._readyForRelease = ee.once(p.name)));

// The first lucky package to be released is marked as `readyForRelease`
const ignition = once((name) => ee.emit(name));

// Status sync point.
const waitFor = (prop, filter = identity) => {
const promise = ee.once(prop);

if (
todo()
.filter(filter)
.every((p) => p.hasOwnProperty(prop))
) {
ee.emit(prop);
}

return promise;
};
const { todo, waitFor, waitForAll, emit, getLucky } = synchronizer;

/**
* Update pkg deps.
Expand Down Expand Up @@ -108,7 +82,17 @@ function createInlinePluginCreator(packages, multiContext) {
// And bind actual logger.
Object.assign(pkg.loggerRef, context.logger);

return plugins.verifyConditions(context);
pkg._ready = true;
emit(
"_readyForRelease",
todo().find((p) => !p._ready)
);

const res = await plugins.verifyConditions(context);

debug("verified conditions: %s", pkg.name);

return res;
};

/**
Expand Down Expand Up @@ -144,11 +128,14 @@ function createInlinePluginCreator(packages, multiContext) {

// Wait until all todo packages have been analyzed.
pkg._analyzed = true;
await waitFor("_analyzed");
await waitForAll("_analyzed");

// Make sure type is "patch" if the package has any deps that have changed.
if (!pkg._nextType && hasChangedDeep(pkg._localDeps)) pkg._nextType = "patch";

debug("commits analyzed: %s", pkg.name);
debug("release type: %s", pkg._nextType);

// Return type.
return pkg._nextType;
};
Expand Down Expand Up @@ -184,11 +171,11 @@ function createInlinePluginCreator(packages, multiContext) {
pkg._nextRelease = context.nextRelease;

// Wait until all todo packages are ready to generate notes.
await waitFor("_nextRelease", (p) => p._nextType);
await waitForAll("_nextRelease", (p) => p._nextType);

// Wait until the current pkg is ready to generate notes
ignition(pkg.name);
await pkg._readyForRelease;
getLucky("_readyToGenerateNotes", pkg);
await waitFor("_readyToGenerateNotes", pkg);

// Update pkg deps.
updateManifestDeps(pkg, path);
Expand All @@ -215,23 +202,27 @@ function createInlinePluginCreator(packages, multiContext) {
notes.push(bullets.join("\n"));
}

debug("notes generated: %s", pkg.name);

// Return the notes.
return notes.join("\n\n");
};

const publish = async (pluginOptions, context) => {
pkg._prepared = true;
const nextPkgToProcess = todo().find((p) => p._nextType && !p._prepared);

if (nextPkgToProcess) {
ee.emit(nextPkgToProcess.name);
}
emit(
"_readyToGenerateNotes",
todo().find((p) => p._nextType && !p._prepared)
);

// Wait for all packages to be `prepare`d and tagged by `semantic-release`
await waitFor("_prepared", (p) => p._nextType);
await waitForAll("_prepared", (p) => p._nextType);

const res = await plugins.publish(context);

debug("published: %s", pkg.name);

// istanbul ignore next
return res.length ? res[0] : {};
};
Expand All @@ -252,6 +243,8 @@ function createInlinePluginCreator(packages, multiContext) {
})
);

debug("inlinePlugin created: %s", pkg.name);

return inlinePlugin;
}

Expand Down
89 changes: 89 additions & 0 deletions lib/getSynchronizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const EventEmitter = require("promise-events");
const { identity } = require("lodash");
const debug = require("debug")("msr:synchronizer");

/**
* Cross-packages synchronization context.
* @typedef Synchronizer
* @param {EventEmitter} ee Shared event emitter class.
* @param {Function} todo Gets the list of packages which are still todo
* @param {Function} once Memoized event subscriber.
* @param {Function} emit Memoized event emitter.
* @params {Function} waitFor Function returns a promise that waits until the package has target probe value.
* @params {Function} waitForAll Function returns a promise that waits until all the packages have the same target probe value.
*/

/**
* Creates shared signal bus and its assets.
* @param {Package[]} packages The multi-semantic-release context.
* @returns {Synchronizer} Shared sync assets.
*/
const getSynchronizer = (packages) => {
const ee = new EventEmitter();
const getEventName = (probe, pkg) => `${probe}${pkg ? ":" + pkg.name : ""}`;

// List of packages which are still todo (don't yet have a result).
const todo = () => packages.filter((p) => p.result === undefined);

// Emitter with memo: next subscribers will receive promises from the past if exists.
const store = {
evt: {},
subscr: {},
};

const emit = (probe, pkg) => {
const name = getEventName(probe, pkg);
debug("ready: %s", name);

return store.evt[name] || (store.evt[name] = ee.emit(name));
};

const once = (probe, pkg) => {
const name = getEventName(probe, pkg);
return store.evt[name] || store.subscr[name] || (store.subscr[name] = ee.once(name));
};

const waitFor = (probe, pkg) => {
const name = getEventName(probe, pkg);
return pkg[name] || (pkg[name] = once(probe, pkg));
};

// Status sync point.
const waitForAll = (probe, filter = identity) => {
const promise = once(probe);

if (
todo()
.filter(filter)
.every((p) => p.hasOwnProperty(probe))
) {
debug("ready: %s", probe);
emit(probe);
}

return promise;
};

// Only the first lucky package passes the probe.
const getLucky = (probe, pkg) => {
if (getLucky[probe]) {
return;
}
const name = getEventName(probe, pkg);
debug("lucky: %s", name);

getLucky[probe] = emit(probe, pkg);
};

return {
ee,
emit,
once,
todo,
waitFor,
waitForAll,
getLucky,
};
};

module.exports = getSynchronizer;
24 changes: 21 additions & 3 deletions lib/multiSemanticRelease.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const { dirname } = require("path");
const semanticRelease = require("semantic-release");
const { check } = require("./blork");
const getLogger = require("./getLogger");
const getSynchronizer = require("./getSynchronizer");
const getConfig = require("./getConfig");
const getConfigSemantic = require("./getConfigSemantic");
const { getManifest } = require("./getManifest");
Expand Down Expand Up @@ -39,12 +40,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<Package[]>} 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[]");
Expand All @@ -68,9 +71,24 @@ async function multiSemanticRelease(
packages.forEach((pkg) => logger.success(`Loaded package ${pkg.name}`));
logger.complete(`Queued ${packages.length} packages! Starting release...`);

// Shared signal bus.
const synchronizer = getSynchronizer(packages);
const { getLucky, waitFor } = synchronizer;

// Release all packages.
const createInlinePlugin = createInlinePluginCreator(packages, multiContext);
await Promise.all(packages.map((pkg) => releasePackage(pkg, createInlinePlugin, multiContext)));
const createInlinePlugin = createInlinePluginCreator(packages, multiContext, synchronizer);
await Promise.all(
packages.map(async (pkg) => {
// Avoid hypothetical concurrent initialization collisions / throttling issues.
// https://github.com/dhoulb/multi-semantic-release/issues/24
if (flags.sequentialInit) {
getLucky("_readyForRelease", pkg);
await waitFor("_readyForRelease", pkg);
}

return releasePackage(pkg, createInlinePlugin, multiContext);
})
);
const released = packages.filter((pkg) => pkg.result).length;

// Return packages list.
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,23 @@
"bash-glob": "^2.0.0",
"blork": "^9.2.2",
"cosmiconfig": "^6.0.0",
"execa": "^4.0.3",
"get-stream": "^5.1.0",
"git-log-parser": "^1.2.0",
"lodash": "^4.17.19",
"meow": "^7.0.1",
"promise-events": "^0.1.8",
"semantic-release": "^17.1.1",
"semver": "^7.3.2",
"signale": "^1.4.0",
"stream-buffers": "^3.0.2",
"tempy": "^0.5.0",
"execa": "^4.0.3"
"tempy": "^0.6.0"
},
"devDependencies": {
"@commitlint/config-conventional": "^9.1.1",
"commitlint": "^9.1.0",
"coveralls": "^3.1.0",
"eslint": "^7.4.0",
"eslint": "^7.5.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"file-url": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/yarnWorkspaces/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@
],
"noCi": true
}
}
}
Loading

0 comments on commit 6930cb6

Please sign in to comment.