diff --git a/commands/publish/README.md b/commands/publish/README.md
index f86b27c7812..b5c282c6e07 100644
--- a/commands/publish/README.md
+++ b/commands/publish/README.md
@@ -65,6 +65,7 @@ This is useful when a previous `lerna publish` failed to publish all packages to
- [`--tag-version-prefix`](#--tag-version-prefix)
- [`--temp-tag`](#--temp-tag)
- [`--yes`](#--yes)
+- [`--summary-file
`](#--summary-file)
### `--canary`
@@ -291,6 +292,27 @@ lerna publish --canary --yes
When run with this flag, `lerna publish` will skip all confirmation prompts.
Useful in [Continuous integration (CI)](https://en.wikipedia.org/wiki/Continuous_integration) to automatically answer the publish confirmation prompt.
+### `--summary-file`
+
+```sh
+lerna publish --canary --yes --summary-file ./output.json
+```
+
+When run with this flag, once a successfully publish it will create a json summary report(see below for an example).
+
+```json
+[
+ {
+ "packageName": "package1",
+ "version": "v1.0.1-alpha"
+ },
+ {
+ "packageName": "package2",
+ "version": "v2.0.1-alpha"
+ }
+]
+```
+
## Deprecated Options
### `--skip-npm`
diff --git a/commands/publish/__tests__/publish-command.test.js b/commands/publish/__tests__/publish-command.test.js
index 75a50df3e93..a50e79df5ab 100644
--- a/commands/publish/__tests__/publish-command.test.js
+++ b/commands/publish/__tests__/publish-command.test.js
@@ -34,6 +34,8 @@ const initFixture = require("@lerna-test/init-fixture")(__dirname);
const path = require("path");
const fs = require("fs-extra");
+const fsmain = require("fs");
+
// file under test
const lernaPublish = require("@lerna-test/command-runner")(require("../command"));
@@ -306,6 +308,31 @@ Map {
});
});
+ describe("--summary-file", () => {
+ it("skips creating the summary file", async () => {
+ const cwd = await initFixture("normal");
+ const fsSpy = jest.spyOn(fs, "writeFileSync");
+ await lernaPublish(cwd);
+
+ expect(fsSpy).not.toHaveBeenCalled();
+ });
+
+ it("creates the summary file", async () => {
+ const cwd = await initFixture("normal");
+ const fsSpy = jest.spyOn(fsmain, "writeFileSync");
+ await lernaPublish(cwd)("--summary-file", "./output.json");
+
+ const expectedJsonResponse = [
+ { packageName: "package-1", version: "1.0.1" },
+ { packageName: "package-2", version: "1.0.1" },
+ { packageName: "package-3", version: "1.0.1" },
+ { packageName: "package-4", version: "1.0.1" },
+ ];
+ expect(fsSpy).toHaveBeenCalled();
+ expect(fsSpy).toHaveBeenCalledWith("./output.json", JSON.stringify(expectedJsonResponse));
+ });
+ });
+
describe("--no-verify-access", () => {
it("skips package access verification", async () => {
const cwd = await initFixture("normal");
diff --git a/commands/publish/command.js b/commands/publish/command.js
index ec2d3e9b0d5..39f2b6a50b2 100644
--- a/commands/publish/command.js
+++ b/commands/publish/command.js
@@ -110,6 +110,11 @@ exports.builder = (yargs) => {
hidden: true,
type: "boolean",
},
+ "summary-file": {
+ // Json output.
+ hidden: true,
+ type: "string",
+ },
// y: {
// describe: "Skip all confirmation prompts.",
// alias: "yes",
diff --git a/commands/publish/index.js b/commands/publish/index.js
index 16d12a2c883..a6d357cbe4e 100644
--- a/commands/publish/index.js
+++ b/commands/publish/index.js
@@ -1,6 +1,7 @@
"use strict";
const os = require("os");
+const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const pMap = require("p-map");
@@ -154,10 +155,10 @@ class PublishCommand extends Command {
// don't execute recursively if run from a poorly-named script
this.runRootLifecycle = /^(pre|post)?publish$/.test(process.env.npm_lifecycle_event)
- ? (stage) => {
+ ? stage => {
this.logger.warn("lifecycle", "Skipping root %j because it has already been called", stage);
}
- : (stage) => this.runPackageLifecycle(this.project.manifest, stage);
+ : stage => this.runPackageLifecycle(this.project.manifest, stage);
let chain = Promise.resolve();
@@ -171,7 +172,7 @@ class PublishCommand extends Command {
chain = chain.then(() => versionCommand(this.argv));
}
- return chain.then((result) => {
+ return chain.then(result => {
if (!result) {
// early return from nested VersionCommand
return false;
@@ -185,10 +186,10 @@ class PublishCommand extends Command {
}
// (occasionally) redundant private filtering necessary to handle nested VersionCommand
- this.updates = result.updates.filter((node) => !node.pkg.private);
+ this.updates = result.updates.filter(node => !node.pkg.private);
this.updatesVersions = new Map(result.updatesVersions);
- this.packagesToPublish = this.updates.map((node) => node.pkg);
+ this.packagesToPublish = this.updates.map(node => node.pkg);
if (this.options.contents) {
// globally override directory to publish
@@ -237,10 +238,29 @@ class PublishCommand extends Command {
return chain.then(() => {
const count = this.packagesToPublish.length;
- const message = this.packagesToPublish.map((pkg) => ` - ${pkg.name}@${pkg.version}`);
output("Successfully published:");
- output(message.join(os.EOL));
+
+ if (this.options.summaryFile) {
+ // create a json object and output it to a file location.
+ const filePath = this.options.summaryFile || "./output.json";
+ const jsonObject = this.packagesToPublish.map(pkg => {
+ return {
+ packageName: pkg.name,
+ version: pkg.version,
+ };
+ });
+ output(jsonObject);
+ try {
+ fs.writeFileSync(filePath, JSON.stringify(jsonObject));
+ output("Locate Summary Report Here: ", filePath);
+ } catch (error) {
+ output("Failed to create the summary report", error);
+ }
+ } else {
+ const message = this.packagesToPublish.map(pkg => ` - ${pkg.name}@${pkg.version}`);
+ output(message.join(os.EOL));
+ }
this.logger.success("published", "%d %s", count, count === 1 ? "package" : "packages");
});
@@ -259,7 +279,7 @@ class PublishCommand extends Command {
chain = chain.then(() => this.verifyWorkingTreeClean());
chain = chain.then(() => getCurrentTags(this.execOpts, matchingPattern));
- chain = chain.then((taggedPackageNames) => {
+ chain = chain.then(taggedPackageNames => {
if (!taggedPackageNames.length) {
this.logger.notice("from-git", "No tagged release found");
@@ -267,17 +287,17 @@ class PublishCommand extends Command {
}
if (this.project.isIndependent()) {
- return taggedPackageNames.map((name) => this.packageGraph.get(name));
+ return taggedPackageNames.map(name => this.packageGraph.get(name));
}
return getTaggedPackages(this.packageGraph, this.project.rootPath, this.execOpts);
});
// private packages are never published, full stop.
- chain = chain.then((updates) => updates.filter((node) => !node.pkg.private));
+ chain = chain.then(updates => updates.filter(node => !node.pkg.private));
- return chain.then((updates) => {
- const updatesVersions = updates.map((node) => [node.name, node.version]);
+ return chain.then(updates => {
+ const updatesVersions = updates.map(node => [node.name, node.version]);
return {
updates,
@@ -293,7 +313,7 @@ class PublishCommand extends Command {
// attempting to publish a release with local changes is not allowed
chain = chain
.then(() => this.verifyWorkingTreeClean())
- .catch((err) => {
+ .catch(err => {
// an execa error is thrown when git suffers a fatal error (such as no git repository present)
if (err.failed && /git describe/.test(err.command)) {
// (we tried)
@@ -307,7 +327,7 @@ class PublishCommand extends Command {
// private packages are already omitted by getUnpublishedPackages()
chain = chain.then(() => getUnpublishedPackages(this.packageGraph, this.conf.snapshot));
- chain = chain.then((unpublished) => {
+ chain = chain.then(unpublished => {
if (!unpublished.length) {
this.logger.notice("from-package", "No unpublished release found");
}
@@ -315,8 +335,8 @@ class PublishCommand extends Command {
return unpublished;
});
- return chain.then((updates) => {
- const updatesVersions = updates.map((node) => [node.name, node.version]);
+ return chain.then(updates => {
+ const updatesVersions = updates.map(node => [node.name, node.version]);
return {
updates,
@@ -352,10 +372,10 @@ class PublishCommand extends Command {
forcePublish,
includeMergedTags,
// private packages are never published, don't bother describing their refs.
- }).filter((node) => !node.pkg.private)
+ }).filter(node => !node.pkg.private)
);
- const makeVersion = (fallback) => ({ lastVersion = fallback, refCount, sha }) => {
+ const makeVersion = fallback => ({ lastVersion = fallback, refCount, sha }) => {
// the next version is bumped without concern for preid or current index
const nextVersion = semver.inc(lastVersion.replace(this.tagPrefix, ""), release.replace("pre", ""));
@@ -366,8 +386,8 @@ class PublishCommand extends Command {
if (this.project.isIndependent()) {
// each package is described against its tags only
- chain = chain.then((updates) =>
- pMap(updates, (node) =>
+ chain = chain.then(updates =>
+ pMap(updates, node =>
describeRef(
{
match: `${node.name}@*`,
@@ -377,15 +397,15 @@ class PublishCommand extends Command {
)
// an unpublished package will have no reachable git tag
.then(makeVersion(node.version))
- .then((version) => [node.name, version])
- ).then((updatesVersions) => ({
+ .then(version => [node.name, version])
+ ).then(updatesVersions => ({
updates,
updatesVersions,
}))
);
} else {
// all packages are described against the last tag
- chain = chain.then((updates) =>
+ chain = chain.then(updates =>
describeRef(
{
match: `${this.tagPrefix}*.*.*`,
@@ -395,8 +415,8 @@ class PublishCommand extends Command {
)
// a repo with no tags should default to whatever lerna.json claims
.then(makeVersion(this.project.version))
- .then((version) => updates.map((node) => [node.name, version]))
- .then((updatesVersions) => ({
+ .then(version => updates.map(node => [node.name, version]))
+ .then(updatesVersions => ({
updates,
updatesVersions,
}))
@@ -413,7 +433,7 @@ class PublishCommand extends Command {
confirmPublish() {
const count = this.packagesToPublish.length;
const message = this.packagesToPublish.map(
- (pkg) => ` - ${pkg.name} => ${this.updatesVersions.get(pkg.name)}`
+ pkg => ` - ${pkg.name} => ${this.updatesVersions.get(pkg.name)}`
);
output("");
@@ -432,11 +452,11 @@ class PublishCommand extends Command {
prepareLicenseActions() {
return Promise.resolve()
.then(() => getPackagesWithoutLicense(this.project, this.packagesToPublish))
- .then((packagesWithoutLicense) => {
+ .then(packagesWithoutLicense => {
if (packagesWithoutLicense.length && !this.project.licensePath) {
this.packagesToBeLicensed = [];
- const names = packagesWithoutLicense.map((pkg) => pkg.name);
+ const names = packagesWithoutLicense.map(pkg => pkg.name);
const noun = names.length > 1 ? "Packages" : "Package";
const verb = names.length > 1 ? "are" : "is";
const list =
@@ -480,7 +500,7 @@ class PublishCommand extends Command {
// validate user has valid npm credentials first,
// by far the most common form of failed execution
chain = chain.then(() => getNpmUsername(this.conf.snapshot));
- chain = chain.then((username) => {
+ chain = chain.then(username => {
// if no username was retrieved, don't bother validating
if (username) {
return verifyNpmPackageAccess(this.packagesToPublish, username, this.conf.snapshot);
@@ -489,7 +509,7 @@ class PublishCommand extends Command {
// read profile metadata to determine if account-level 2FA is enabled
chain = chain.then(() => getTwoFactorAuthRequired(this.conf.snapshot));
- chain = chain.then((isRequired) => {
+ chain = chain.then(isRequired => {
// notably, this still doesn't handle package-level 2FA requirements
this.twoFactorAuthRequired = isRequired;
});
@@ -499,7 +519,7 @@ class PublishCommand extends Command {
}
updateCanaryVersions() {
- return pMap(this.updates, (node) => {
+ return pMap(this.updates, node => {
node.pkg.set("version", this.updatesVersions.get(node.name));
for (const [depName, resolved] of node.localDependencies) {
@@ -516,11 +536,11 @@ class PublishCommand extends Command {
resolveLocalDependencyLinks() {
// resolve relative file: links to their actual version range
- const updatesWithLocalLinks = this.updates.filter((node) =>
- Array.from(node.localDependencies.values()).some((resolved) => resolved.type === "directory")
+ const updatesWithLocalLinks = this.updates.filter(node =>
+ Array.from(node.localDependencies.values()).some(resolved => resolved.type === "directory")
);
- return pMap(updatesWithLocalLinks, (node) => {
+ return pMap(updatesWithLocalLinks, node => {
for (const [depName, resolved] of node.localDependencies) {
// regardless of where the version comes from, we can't publish "file:../sibling-pkg" specs
const depVersion = this.updatesVersions.get(depName) || this.packageGraph.get(depName).pkg.version;
@@ -554,7 +574,7 @@ class PublishCommand extends Command {
}
serializeChanges() {
- return pMap(this.packagesToPublish, (pkg) => pkg.serialize());
+ return pMap(this.packagesToPublish, pkg => pkg.serialize());
}
resetChanges() {
@@ -566,9 +586,9 @@ class PublishCommand extends Command {
};
const dirtyManifests = [this.project.manifest]
.concat(this.packagesToPublish)
- .map((pkg) => path.relative(cwd, pkg.manifestLocation));
+ .map(pkg => path.relative(cwd, pkg.manifestLocation));
- return gitCheckout(dirtyManifests, gitOpts, this.execOpts).catch((err) => {
+ return gitCheckout(dirtyManifests, gitOpts, this.execOpts).catch(err => {
this.logger.silly("EGITCHECKOUT", err.message);
this.logger.notice("FYI", "Unable to reset working tree changes, this probably isn't a git repo.");
});
@@ -590,7 +610,7 @@ class PublishCommand extends Command {
removeTempLicensesOnError(error) {
return Promise.resolve()
.then(() =>
- removeTempLicenses(this.packagesToBeLicensed).catch((removeError) => {
+ removeTempLicenses(this.packagesToBeLicensed).catch(removeError => {
this.logger.error(
"licenses",
"error removing temporary license files",
@@ -612,7 +632,7 @@ class PublishCommand extends Command {
return Promise.resolve()
.then(() => getOneTimePassword("Enter OTP:"))
- .then((otp) => {
+ .then(otp => {
this.otpCache.otp = otp;
});
}
@@ -652,10 +672,10 @@ class PublishCommand extends Command {
const opts = this.conf.snapshot;
const mapper = pPipe(
...[
- this.options.requireScripts && ((pkg) => this.execScript(pkg, "prepublish")),
+ this.options.requireScripts && (pkg => this.execScript(pkg, "prepublish")),
- (pkg) =>
- pulseTillDone(packDirectory(pkg, pkg.location, opts)).then((packed) => {
+ pkg =>
+ pulseTillDone(packDirectory(pkg, pkg.location, opts)).then(packed => {
tracker.verbose("packed", path.relative(this.project.rootPath, pkg.contents));
tracker.completeWork(1);
@@ -673,7 +693,7 @@ class PublishCommand extends Command {
chain = chain.then(() => removeTempLicenses(this.packagesToBeLicensed));
// remove temporary license files if _any_ error occurs _anywhere_ in the promise chain
- chain = chain.catch((error) => this.removeTempLicensesOnError(error));
+ chain = chain.catch(error => this.removeTempLicensesOnError(error));
if (!this.hasRootedLeaf) {
chain = chain.then(() => this.runPackageLifecycle(this.project.manifest, "postpack"));
@@ -702,7 +722,7 @@ class PublishCommand extends Command {
const mapper = pPipe(
...[
- (pkg) => {
+ pkg => {
const preDistTag = this.getPreDistTag(pkg);
const tag = !this.options.tempTag && preDistTag ? preDistTag : opts.tag;
const pkgOpts = Object.assign({}, opts, { tag });
@@ -717,7 +737,7 @@ class PublishCommand extends Command {
});
},
- this.options.requireScripts && ((pkg) => this.execScript(pkg, "postpublish")),
+ this.options.requireScripts && (pkg => this.execScript(pkg, "postpublish")),
].filter(Boolean)
);
@@ -741,14 +761,14 @@ class PublishCommand extends Command {
let chain = Promise.resolve();
const opts = this.conf.snapshot;
- const getDistTag = (publishConfig) => {
+ const getDistTag = publishConfig => {
if (opts.tag === "latest" && publishConfig && publishConfig.tag) {
return publishConfig.tag;
}
return opts.tag;
};
- const mapper = (pkg) => {
+ const mapper = pkg => {
const spec = `${pkg.name}@${pkg.version}`;
const preDistTag = this.getPreDistTag(pkg);
const distTag = preDistTag || getDistTag(pkg.get("publishConfig"));