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 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 :(
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)
- 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

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

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

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

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

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));
28 changes: 7 additions & 21 deletions bin/runner.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,18 @@
const get = require("lodash.get");

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();
}

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

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

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

// Add a writable stream checker.
add(
"stream",
// 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"
);

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

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

// 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.
Expand All @@ -32,15 +46,11 @@ function createInlinePluginCreator(packages, multiContext) {
* @internal
*/
const updateManifestDeps = (pkg, path) => {
const dbg = console.log;

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

dbg("name=", manifest.name);
dbg("path=", path);
// Loop through localDeps to update dependencies/devDependencies/peerDependencies in manifest.
pkg._localDeps.forEach(d => {
pkg._localDeps.forEach((d) => {
// Get version of dependency.
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.devDependencies.hasOwnProperty(d.name)) manifest.devDependencies[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.
Expand Down Expand Up @@ -91,7 +99,7 @@ function createInlinePluginCreator(packages, multiContext) {
*
* @internal
*/
async function analyzeCommits(pluginOptions, context) {
const analyzeCommits = async (pluginOptions, context) => {
// Filter commits by directory.
commits = await getCommitsFiltered(cwd, dir, context.lastRelease.gitHead);

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

// Make a list of local dependencies.
// 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.
pkg._nextType = await plugins.analyzeCommits(context);

// 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.
if (!pkg._nextType && hasChangedDeep(pkg._localDeps)) pkg._nextType = "patch";

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

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

// 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.
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 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) {
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"));
}

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

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

// Call other plugins.
const prepare = async (pluginOptions, context) => {
await plugins.prepare(context);

// Package is prepared.
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.
const verifyConditions = (pluginOptions, context) => plugins.verifyConditions(context);
const verifyRelease = (pluginOptions, context) => plugins.verifyRelease(context);
const publish = async (pluginOptions, context) => {
const result = await plugins.publish(context);

// istanbul ignore next
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
const fail = (pluginOptions, context) => plugins.fail(context);
const addChannel = (pluginOptions, context) => plugins.addChannel(context);

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

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

This file was deleted.

Loading