-
Notifications
You must be signed in to change notification settings - Fork 36
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
Refactor: migrate to batching toposort for execution of releases #46
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -15,7 +15,7 @@ function hasChangedDeep(packages, ignore = []) { | |||
// 1. Any local dep package itself has changed | ||||
if (p._nextType) return true; | ||||
// 2. Any local dep package has local deps that have changed. | ||||
else if (hasChangedDeep(p._localDeps, [...ignore, ...packages])) return true; | ||||
else if (p._localDeps && hasChangedDeep(p._localDeps, [...ignore, ...packages])) return true; | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I came across this bug originally when doing a spike into why our API usage of multi-semantic-release didn't work, essentially, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. I've moved
|
||||
// Nope. | ||||
else return false; | ||||
}); | ||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,15 @@ | ||
const { dirname } = require("path"); | ||
const semanticRelease = require("semantic-release"); | ||
const batchingToposort = require("batching-toposort"); | ||
const { check } = require("./blork"); | ||
const getLogger = require("./getLogger"); | ||
const getSynchronizer = require("./getSynchronizer"); | ||
const getConfig = require("./getConfig"); | ||
const getConfigSemantic = require("./getConfigSemantic"); | ||
const getManifest = require("./getManifest"); | ||
const cleanPath = require("./cleanPath"); | ||
const RescopedStream = require("./RescopedStream"); | ||
const createInlinePluginCreator = require("./createInlinePluginCreator"); | ||
const stream = require("stream"); | ||
|
||
/** | ||
* The multirelease context. | ||
|
@@ -67,33 +68,91 @@ async function multiSemanticRelease( | |
const multiContext = { options, cwd, env, stdout, stderr }; | ||
|
||
// Load packages from paths. | ||
const packages = await Promise.all(paths.map((path) => getPackage(path, multiContext))); | ||
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, synchronizer, flags); | ||
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); | ||
const packages = await Promise.all( | ||
paths.map((path) => | ||
getPackage(path, multiContext).then((pkg) => { | ||
logger.success(`Loaded package ${pkg.name}`); | ||
return pkg; | ||
}) | ||
) | ||
); | ||
|
||
logger.complete(`Collected ${packages.length} packages! Starting release...`); | ||
|
||
// --- Toposort | ||
const packageNames = packages.map((pkg) => pkg.name); | ||
const packageDag = {}; | ||
|
||
// create the dag roots for each package: | ||
packages.forEach((pkg) => (packageDag[pkg.name] = [])); | ||
// populate the roots with that packages dependencies on other packages: | ||
packages.forEach((pkg) => { | ||
pkg.deps.forEach((dep) => { | ||
if (packageNames.includes(dep)) { | ||
packageDag[dep].push(pkg.name); | ||
} | ||
}); | ||
}); | ||
|
||
// Prepare the batches for releasing: | ||
const taskBatches = batchingToposort(packageDag); | ||
|
||
logger.complete(`Calculated ${taskBatches.length} batches of releases for optimal releasing power!`); | ||
|
||
const createInlinePlugin = createInlinePluginCreator(packages, multiContext, flags); | ||
|
||
const releaseTrain = new Promise((resolve, reject) => { | ||
Promise.resolve().then(async () => { | ||
for (const batch of taskBatches) { | ||
logger.log( | ||
`Starting batch #${taskBatches.indexOf(batch) + 1} containing ${batch.length} package${ | ||
batch.length > 1 ? "s" : "" | ||
}` | ||
); | ||
|
||
await Promise.all( | ||
batch.map((packageName) => { | ||
const pkg = packages.find((_pkg) => _pkg.name === packageName); | ||
|
||
// We probably don't really need to create separated streams, but | ||
// could be useful if we want to give a per-release log file or | ||
// something similar: | ||
pkg.stdout = stream.PassThrough(); | ||
pkg.stderr = stream.PassThrough(); | ||
|
||
pkg.stderr.pipe(process.stderr); | ||
pkg.stdout.pipe(process.stdout); | ||
|
||
// Start the release of the package: the idea of the catch/reject here | ||
// is to abort the entire release process if we fail to release a | ||
// package, but this may not actually work: | ||
releasePackage(pkg, createInlinePlugin, multiContext).catch(reject); | ||
|
||
return pkg.result; | ||
}) | ||
); | ||
} | ||
|
||
return releasePackage(pkg, createInlinePlugin, multiContext); | ||
resolve(packages); | ||
}); | ||
}); | ||
|
||
return releaseTrain | ||
.then(async (results) => { | ||
return Promise.all( | ||
results.map(async (pkg) => { | ||
pkg.result = await pkg.result; | ||
return pkg; | ||
}) | ||
); | ||
}) | ||
); | ||
const released = packages.filter((pkg) => pkg.result).length; | ||
.then((results) => { | ||
const released = results.filter((pkg) => pkg.status === "success").length; | ||
|
||
// Return packages list. | ||
logger.complete(`Released ${released} of ${packages.length} packages, semantically!`); | ||
return packages; | ||
// Return packages list. | ||
logger.complete(`Released ${released} of ${results.length} packages, semantically!`); | ||
return results; | ||
}); | ||
} | ||
|
||
// Exports. | ||
|
@@ -157,7 +216,7 @@ async function getPackage(path, { options: globalOptions, env, cwd, stdout, stde | |
async function releasePackage(pkg, createInlinePlugin, multiContext) { | ||
// Vars. | ||
const { options: pkgOptions, name, dir } = pkg; | ||
const { env, stdout, stderr } = multiContext; | ||
const { env } = multiContext; | ||
|
||
// Make an 'inline plugin' for this package. | ||
// The inline plugin is the only plugin we call semanticRelease() with. | ||
|
@@ -180,10 +239,29 @@ async function releasePackage(pkg, createInlinePlugin, multiContext) { | |
|
||
// Call semanticRelease() on the directory and save result to pkg. | ||
// Don't need to log out errors as semantic-release already does that. | ||
pkg.result = await semanticRelease(options, { | ||
pkg.result = semanticRelease(options, { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. by not awaiting here, we allow parallel releases of packages within a given batch; this does mean an interleaved log at the moment, but does mean that you should have faster releases. |
||
cwd: dir, | ||
env, | ||
stdout: new RescopedStream(stdout, name), | ||
stderr: new RescopedStream(stderr, name), | ||
}); | ||
stdout: new RescopedStream(pkg.stdout, name), | ||
stderr: new RescopedStream(pkg.stderr, name), | ||
}).then( | ||
(result) => { | ||
console.log({ | ||
lastRelease: result.lastRelease, | ||
nextRelease: result.nextRelease, | ||
releases: result.releases, | ||
}); | ||
|
||
pkg.isPending = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
pkg.status = "success"; | ||
return result; | ||
}, | ||
(error) => { | ||
pkg.isPending = false; | ||
pkg.status = "failure"; | ||
throw error; | ||
} | ||
); | ||
|
||
return pkg; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may not be needed, but I was noticing that our release notes contained a lot of unnecessary new lines