Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: publish updated packages deps #2

Merged
merged 13 commits into from
May 20, 2020
Merged
2 changes: 1 addition & 1 deletion README.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ The integration with semantic release is pretty janky — this is a quick summar
- Had to duplicate the internal cosmiconfig setup from semantic release to get this working :( - Had to duplicate the internal cosmiconfig setup from semantic release to get this working :(
4. I found Git getting itself into weird states because e.g. `git tag` is done asynchronously 4. I found Git getting itself into weird states because e.g. `git tag` is done asynchronously
- To get around this I had to stagger package publishing so they were done one at a time (which slows things down) - To get around this I had to stagger package publishing so they were done one at a time (which slows things down)
- I think calls to `execa()` in semantic release should be replaced with `execa.sync()` to ensure Git's internal state is atomic. For an experiment, you may add `--execasync` CLI flag that makes all calls synchronous through [ritm-hook](https://github.com/elastic/require-in-the-middle). - I think calls to `execa()` in semantic release should be replaced with `execa.sync()` to ensure Git's internal state is atomic.


### Git tags ### Git tags


Expand Down
23 changes: 8 additions & 15 deletions bin/cli.js
Original file line number Original file line Diff line number Diff line change
@@ -1,36 +1,29 @@
#!/usr/bin/env node #!/usr/bin/env node


const meow = require("meow"); const meow = require("meow");
const pairs = require("lodash.pairs"); const { toPairs, set } = require("lodash");
const set = require("lodash.set");
const runner = require("./runner"); const runner = require("./runner");
const cli = meow( const cli = meow(
` `
Usage Usage
$ multi-semantic-release $ multi-semantic-release


Options Options
--sync, Forces all execa calls to be synchronous 'none'
--debug, Enables all additional logging
--debug.spawn Turns on logging for process.spawn


Examples Examples
$ multi-semantic-release --sync --debug $ multi-semantic-release
$ multi-semantic-release --debug.spawn
`, `,
{ {
flags: { flags: {
sync: { /*execasync: {
type: "boolean", type: "boolean",
alias: "execasync" // Legacy alias: "sync",
}, },*/
debug: { },
type: "boolean"
}
}
} }
); );


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


runner(processFlags(cli.flags)); runner(processFlags(cli.flags));
28 changes: 7 additions & 21 deletions bin/runner.js
Original file line number Original file line Diff line number Diff line change
@@ -1,32 +1,18 @@
const get = require("lodash.get"); module.exports = (flags) => {

module.exports = flags => {
const getLogger = require("../lib/getLogger");
const logger = getLogger({ stdout: process.stdout, stderr: process.stderr });
const debug = require("../lib/debug");

logger.log("flags=", flags);

debug.config(flags.debug);

if (flags.watchspawn || get(flags, "debug.spawn")) {
require("../lib/spawnHook").hook();
}

// Execa hook.
if (flags.sync) {
require("../lib/execaHook").hook();
}

// Imports. // Imports.
const getWorkspacesYarn = require("../lib/getWorkspacesYarn"); const getWorkspacesYarn = require("../lib/getWorkspacesYarn");
const multiSemanticRelease = require("../lib/multiSemanticRelease"); const multiSemanticRelease = require("../lib/multiSemanticRelease");
const multisemrelPkgJson = require("../package.json");
const semrelPkgJson = require("semantic-release/package.json");


// Get directory. // Get directory.
const cwd = process.cwd(); const cwd = process.cwd();


// Catch errors. // Catch errors.
try { try {
console.log(`multi-semantic-release version: ${multisemrelPkgJson.version}`);
console.log(`semantic-release version: ${semrelPkgJson.version}`);

// Get list of package.json paths according to Yarn workspaces. // Get list of package.json paths according to Yarn workspaces.
const paths = getWorkspacesYarn(cwd); const paths = getWorkspacesYarn(cwd);
console.log("yarn paths", paths); console.log("yarn paths", paths);
Expand All @@ -37,7 +23,7 @@ module.exports = flags => {
// Success. // Success.
process.exit(0); process.exit(0);
}, },
error => { (error) => {
// Log out errors. // Log out errors.
console.error(`[multi-semantic-release]:`, error); console.error(`[multi-semantic-release]:`, error);
process.exit(1); process.exit(1);
Expand Down
4 changes: 2 additions & 2 deletions lib/blork.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const isString = checker("string");
// Add a directory checker. // Add a directory checker.
add( add(
"directory", "directory",
v => isAbsolute(v) && existsSync(v) && lstatSync(v).isDirectory(), (v) => isAbsolute(v) && existsSync(v) && lstatSync(v).isDirectory(),
"directory that exists in the filesystem" "directory that exists in the filesystem"
); );


// Add a writable stream checker. // Add a writable stream checker.
add( add(
"stream", "stream",
// istanbul ignore next (not important) // istanbul ignore next (not important)
v => v instanceof Writable || v instanceof WritableStreamBuffer, (v) => v instanceof Writable || v instanceof WritableStreamBuffer,
"instance of stream.Writable or WritableStreamBuffer" "instance of stream.Writable or WritableStreamBuffer"
); );


Expand Down
100 changes: 60 additions & 40 deletions lib/createInlinePluginCreator.js
Original file line number Original file line Diff line number Diff line change
@@ -1,11 +1,8 @@
const { writeFileSync } = require("fs"); const { writeFileSync } = require("fs");
// const { resolve } = require("path");
const { check } = require("./blork");
const wait = require("./wait");
const getCommitsFiltered = require("./getCommitsFiltered"); const getCommitsFiltered = require("./getCommitsFiltered");
const getManifest = require("./getManifest"); const getManifest = require("./getManifest");
const hasChangedDeep = require("./hasChangedDeep"); const hasChangedDeep = require("./hasChangedDeep");
const debug = require("./debug"); const EventEmitter = require("promise-events");


/** /**
* Create an inline plugin creator for a multirelease. * Create an inline plugin creator for a multirelease.
Expand All @@ -22,7 +19,24 @@ function createInlinePluginCreator(packages, multiContext) {
const { cwd } = multiContext; const { cwd } = multiContext;


// List of packages which are still todo (don't yet have a result). // List of packages which are still todo (don't yet have a result).
const todo = () => packages.filter(p => p.result === undefined); 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)));

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

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

return promise;
};


/** /**
* Update pkg deps. * Update pkg deps.
Expand All @@ -32,15 +46,11 @@ function createInlinePluginCreator(packages, multiContext) {
* @internal * @internal
*/ */
const updateManifestDeps = (pkg, path) => { const updateManifestDeps = (pkg, path) => {
const dbg = console.log;

// Get and parse manifest file contents. // Get and parse manifest file contents.
const manifest = getManifest(path); const manifest = getManifest(path);


dbg("name=", manifest.name);
dbg("path=", path);
// Loop through localDeps to update dependencies/devDependencies/peerDependencies in manifest. // Loop through localDeps to update dependencies/devDependencies/peerDependencies in manifest.
pkg._localDeps.forEach(d => { pkg._localDeps.forEach((d) => {
// Get version of dependency. // Get version of dependency.
const release = d._nextRelease || d._lastRelease; const release = d._nextRelease || d._lastRelease;


Expand All @@ -52,8 +62,6 @@ function createInlinePluginCreator(packages, multiContext) {
if (manifest.dependencies.hasOwnProperty(d.name)) manifest.dependencies[d.name] = release.version; if (manifest.dependencies.hasOwnProperty(d.name)) manifest.dependencies[d.name] = release.version;
if (manifest.devDependencies.hasOwnProperty(d.name)) manifest.devDependencies[d.name] = release.version; if (manifest.devDependencies.hasOwnProperty(d.name)) manifest.devDependencies[d.name] = release.version;
if (manifest.peerDependencies.hasOwnProperty(d.name)) manifest.peerDependencies[d.name] = release.version; if (manifest.peerDependencies.hasOwnProperty(d.name)) manifest.peerDependencies[d.name] = release.version;

dbg("dep.name=", d.name, "dep.version=", release.version);
}); });


// Write package.json back out. // Write package.json back out.
Expand Down Expand Up @@ -91,7 +99,7 @@ function createInlinePluginCreator(packages, multiContext) {
* *
* @internal * @internal
*/ */
async function analyzeCommits(pluginOptions, context) { const analyzeCommits = async (pluginOptions, context) => {
// Filter commits by directory. // Filter commits by directory.
commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead); commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead);


Expand All @@ -103,20 +111,20 @@ function createInlinePluginCreator(packages, multiContext) {


// Make a list of local dependencies. // Make a list of local dependencies.
// Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array. // Map dependency names (e.g. my-awesome-dep) to their actual package objects in the packages array.
pkg._localDeps = deps.map(d => packages.find(p => d === p.name)).filter(Boolean); pkg._localDeps = deps.map((d) => packages.find((p) => d === p.name)).filter(Boolean);


// Set nextType for package from plugins. // Set nextType for package from plugins.
pkg._nextType = await plugins.analyzeCommits(context); pkg._nextType = await plugins.analyzeCommits(context);


// Wait until all todo packages have been analyzed. // Wait until all todo packages have been analyzed.
await wait(() => todo().every(p => p.hasOwnProperty("_nextType"))); await waitFor("_nextType");


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


// Return type. // Return type.
return pkg._nextType; return pkg._nextType;
} };


/** /**
* Generate notes step (after). * Generate notes step (after).
Expand Down Expand Up @@ -144,12 +152,20 @@ function createInlinePluginCreator(packages, multiContext) {
* *
* @internal * @internal
*/ */
async function generateNotes(pluginOptions, context) { const generateNotes = async (pluginOptions, context) => {
// Set nextRelease for package. // Set nextRelease for package.
pkg._nextRelease = context.nextRelease; pkg._nextRelease = context.nextRelease;


// Wait until all todo packages are ready to generate notes. // Wait until all todo packages are ready to generate notes.
await wait(() => todo().every(p => p.hasOwnProperty("_nextRelease"))); await waitFor("_nextRelease");

if (packages[0] !== pkg) {
await pkg._readyForRelease;
}

// Update pkg deps.
updateManifestDeps(pkg, path);
pkg._depsUpdated = true;


// Vars. // Vars.
const notes = []; const notes = [];
Expand All @@ -165,16 +181,16 @@ function createInlinePluginCreator(packages, multiContext) {
if (subs) notes.push(subs.replace(/^(#+) (\[?\d+\.\d+\.\d+\]?)/, `$1 ${name} $2`)); if (subs) notes.push(subs.replace(/^(#+) (\[?\d+\.\d+\.\d+\]?)/, `$1 ${name} $2`));


// If it has upgrades add an upgrades section. // If it has upgrades add an upgrades section.
const upgrades = pkg._localDeps.filter(d => d._nextRelease); const upgrades = pkg._localDeps.filter((d) => d._nextRelease);
if (upgrades.length) { if (upgrades.length) {
notes.push(`### Dependencies`); notes.push(`### Dependencies`);
const bullets = upgrades.map(d => `* **${d.name}:** upgraded to ${d._nextRelease.version}`); const bullets = upgrades.map((d) => `* **${d.name}:** upgraded to ${d._nextRelease.version}`);
notes.push(bullets.join("\n")); notes.push(bullets.join("\n"));
} }


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


/** /**
* Prepare step. * Prepare step.
Expand All @@ -188,38 +204,41 @@ function createInlinePluginCreator(packages, multiContext) {
* *
* @internal * @internal
*/ */
async function prepare(pluginOptions, context) { const prepare = async (pluginOptions, context) => {
// Update pkg deps.
updateManifestDeps(pkg, path);

// Call other plugins.
await plugins.prepare(context); await plugins.prepare(context);


// Package is prepared. // Package is prepared.
pkg._prepared = true; pkg._prepared = true;

};
// Wait until all todo packages are prepared (make sure no releases happen if any package errors before here).
await wait(() => {
return todo().every(p => p.hasOwnProperty("_prepared"));
});

// Serialize the releases so only one publishes at once by waiting until this one to be next in the todo list (packages are spliced out of todo when they return a result).
// Need this because: when semanticRelease() does several `git push` simultaneously some will fail due to refs not being locked.
// (semantic-release should probably use `execa.sync()` to ensure Git operations are atomic — if they do there should be no issues with doing several releases at once).
await wait(() => todo()[0] === pkg);
}


// These steps just passthrough to plugins. // These steps just passthrough to plugins.
const verifyConditions = (pluginOptions, context) => plugins.verifyConditions(context); const verifyConditions = (pluginOptions, context) => plugins.verifyConditions(context);
const verifyRelease = (pluginOptions, context) => plugins.verifyRelease(context); const verifyRelease = (pluginOptions, context) => plugins.verifyRelease(context);
const publish = async (pluginOptions, context) => { const publish = async (pluginOptions, context) => {
const result = await plugins.publish(context); const result = await plugins.publish(context);

// istanbul ignore next // istanbul ignore next
return result.length ? result[0] : {}; return result.length ? result[0] : {};
}; };
const success = (pluginOptions, context) => plugins.success(context);
const postProcess = async (context, cb) => {
pkg._processed = true;
const nextToPublish = todo().find((p) => !p._processed);

if (nextToPublish) {
context.logger.log("next package to process", nextToPublish.name);
ee.emit(nextToPublish.name);
} else {
context.logger.log("looks like there's no package to publish next");
}

return cb(context);
};
const success = async (pluginOptions, context) => postProcess(context, plugins.success);
// istanbul ignore next
const fail = (pluginOptions, context) => postProcess(context, plugins.fail);
// istanbul ignore next // istanbul ignore next
const fail = (pluginOptions, context) => plugins.fail(context); const addChannel = (pluginOptions, context) => plugins.addChannel(context);


// Exports. // Exports.
return { return {
Expand All @@ -230,7 +249,8 @@ function createInlinePluginCreator(packages, multiContext) {
prepare, prepare,
publish, publish,
success, success,
fail fail,
addChannel,
}; };
} }


Expand Down
19 changes: 0 additions & 19 deletions lib/debug.js

This file was deleted.

Loading