Skip to content

Commit

Permalink
feat: run publish step in parallel
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed May 21, 2020
2 parents 561a8e6 + bc9e617 commit 0ecaddf
Show file tree
Hide file tree
Showing 26 changed files with 3,606 additions and 3,122 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017,
"ecmaVersion": 2019,
"experimentalObjectRestSpread": true
},
"extends": [
Expand Down
4 changes: 2 additions & 2 deletions .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"tabWidth": 4,
"useTabs": true,
"printWidth": 120,
"parser": "babylon",
"parser": "babel",
"proseWrap": "never"
}
}
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@ language: node_js

branches:
only:
- master
- master

cache: yarn

jobs:
include:
- stage: test
node_js: "8"
- stage: test
node_js: "10"
- stage: release
Expand All @@ -18,4 +16,4 @@ jobs:
deploy:
provider: script
skip_cleanup: true
script: yarn run semantic-release
script: yarn run semantic-release
35 changes: 2 additions & 33 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,5 @@
#!/usr/bin/env node

// Execa hook.
if (process.argv.includes("--execasync")) {
require("../lib/execaHook").hook();
}
const runner = require("./runner");

// Imports.
const getWorkspacesYarn = require("../lib/getWorkspacesYarn");
const multiSemanticRelease = require("../lib/multiSemanticRelease");

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

// Catch errors.
try {
// Get list of package.json paths according to Yarn workspaces.
const paths = getWorkspacesYarn(cwd);

// Do multirelease (log out any errors).
multiSemanticRelease(paths, {}, { cwd }).then(
() => {
// Success.
process.exit(0);
},
error => {
// Log out errors.
console.error(`[multi-semantic-release]:`, error);
process.exit(1);
}
);
} catch (error) {
// Log out errors.
console.error(`[multi-semantic-release]:`, error);
process.exit(1);
}
runner(process.argv);
37 changes: 37 additions & 0 deletions bin/runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module.exports = (argv) => {
// 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);

// Do multirelease (log out any errors).
multiSemanticRelease(paths, {}, { cwd }).then(
() => {
// Success.
process.exit(0);
},
(error) => {
// Log out errors.
console.error(`[multi-semantic-release]:`, error);
process.exit(1);
}
);
} catch (error) {
// Log out errors.
console.error(`[multi-semantic-release]:`, error);
process.exit(1);
}
};
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
150 changes: 75 additions & 75 deletions lib/createInlinePluginCreator.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
const { writeFileSync } = require("fs");
const { check } = require("./blork");
const wait = require("./wait");
const getCommitsFiltered = require("./getCommitsFiltered");
const getManifest = require("./getManifest");
const hasChangedDeep = require("./hasChangedDeep");
const EventEmitter = require("promise-events");

/**
* Create an inline plugin creator for a multirelease.
Expand All @@ -20,7 +19,54 @@ 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.
* @param {Package} pkg The package this function is being called on.
* @param {string} path Path to package.json file
* @returns {undefined}
* @internal
*/
const updateManifestDeps = (pkg, path) => {
// Get and parse manifest file contents.
const manifest = getManifest(path);

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

// Cannot establish version.
if (!release || !release.version)
throw Error(`Cannot release because dependency ${d.name} has not been released`);

// Update version of dependency in manifest.
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;
});

// Write package.json back out.
writeFileSync(path, JSON.stringify(manifest));
};

/**
* Create an inline plugin for an individual package in a multirelease.
Expand Down Expand Up @@ -53,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 @@ -65,20 +111,21 @@ 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")));
pkg._analyzed = true;
await waitFor("_analyzed");

// 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 @@ -106,12 +153,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 @@ -127,91 +182,36 @@ 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.
* Responsible for preparing the release, for example creating or updating files such as package.json, CHANGELOG.md, documentation or compiled assets and pushing a commit.
*
* In multirelease: Writes current version of local dependencies to package.json, and serialises the publishing so it's not happening simultaneously.
*
* @param {object} pluginOptions Options to configure this plugin.
* @param {object} context The semantic-release context.
* @returns {Promise<void>} Promise that resolves when done.
*
* @internal
*/
async function prepare(pluginOptions, context) {
// Get and parse manifest file contents.
const manifest = getManifest(path);

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

// Cannot establish version.
if (!release || !release.version)
throw Error(`Cannot release because dependency ${d.name} has not been released`);

// Update version of dependency in manifest.
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;
});

// Write package.json back out.
writeFileSync(path, JSON.stringify(manifest));

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

// Package is prepared.
const publish = async () => {
pkg._prepared = true;
const nextPkgToProcess = todo().find((p) => !p._prepared);

// 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"));
});
if (nextPkgToProcess) {
ee.emit(nextPkgToProcess.name);
}

// 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);
}
// Wait for all packages to be `prepare`d and tagged by `semantic-release`
await waitFor("_prepared");

// 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] : {};
return {};
};
const success = (pluginOptions, context) => plugins.success(context);
// istanbul ignore next
const fail = (pluginOptions, context) => plugins.fail(context);

// Exports.
return {
verifyConditions,
analyzeCommits,
verifyRelease,
generateNotes,
prepare,
publish,
success,
fail
};
}

Expand Down
Loading

0 comments on commit 0ecaddf

Please sign in to comment.