Skip to content

Commit

Permalink
feat(publish): Validate npm registry and package access prerequisites
Browse files Browse the repository at this point in the history
Fixes #55
Fixes #1045
Fixes #1347
  • Loading branch information
evocateur committed Aug 8, 2018
1 parent 0097360 commit ebc8ba6
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 1 deletion.
2 changes: 2 additions & 0 deletions commands/publish/__tests__/publish-canary.test.js
Expand Up @@ -5,6 +5,8 @@ jest.unmock("@lerna/collect-updates");

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license");
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");

const fs = require("fs-extra");
const path = require("path");
Expand Down
2 changes: 2 additions & 0 deletions commands/publish/__tests__/publish-command.test.js
Expand Up @@ -2,6 +2,8 @@

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license");
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");
// FIXME: better mock for version command
jest.mock("../../version/lib/git-push");
jest.mock("../../version/lib/is-anything-committed");
Expand Down
2 changes: 2 additions & 0 deletions commands/publish/__tests__/publish-licenses.test.js
@@ -1,6 +1,8 @@
"use strict";

// local modules _must_ be explicitly mocked
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");
jest.mock("../lib/create-temp-licenses", () => jest.fn(() => Promise.resolve()));
jest.mock("../lib/remove-temp-licenses", () => jest.fn(() => Promise.resolve()));
// FIXME: better mock for version command
Expand Down
2 changes: 2 additions & 0 deletions commands/publish/__tests__/publish-lifecycle-scripts.test.js
Expand Up @@ -2,6 +2,8 @@

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license");
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");
// FIXME: better mock for version command
jest.mock("../../version/lib/git-push");
jest.mock("../../version/lib/is-anything-committed");
Expand Down
Expand Up @@ -5,6 +5,8 @@ jest.unmock("@lerna/collect-updates");

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license");
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");
// FIXME: better mock for version command
jest.mock("../../version/lib/git-push");
jest.mock("../../version/lib/is-anything-committed");
Expand Down
2 changes: 2 additions & 0 deletions commands/publish/__tests__/publish-tagging.test.js
Expand Up @@ -2,6 +2,8 @@

// local modules _must_ be explicitly mocked
jest.mock("../lib/get-packages-without-license");
jest.mock("../lib/verify-npm-package-access");
jest.mock("../lib/verify-npm-registry");
// FIXME: better mock for version command
jest.mock("../../version/lib/git-push");
jest.mock("../../version/lib/is-anything-committed");
Expand Down
12 changes: 11 additions & 1 deletion commands/publish/index.js
Expand Up @@ -25,6 +25,8 @@ const getTaggedPackages = require("./lib/get-tagged-packages");
const getPackagesWithoutLicense = require("./lib/get-packages-without-license");
const gitCheckout = require("./lib/git-checkout");
const removeTempLicenses = require("./lib/remove-temp-licenses");
const verifyNpmRegistry = require("./lib/verify-npm-registry");
const verifyNpmPackageAccess = require("./lib/verify-npm-package-access");

module.exports = factory;

Expand Down Expand Up @@ -89,7 +91,9 @@ class PublishCommand extends Command {
)
: [this.packagesToPublish];

return Promise.resolve().then(() => this.prepareLicenseActions());
return Promise.resolve()
.then(() => this.prepareRegistryActions())
.then(() => this.prepareLicenseActions());
});
}

Expand Down Expand Up @@ -232,6 +236,12 @@ class PublishCommand extends Command {
});
}

prepareRegistryActions() {
return Promise.resolve()
.then(() => verifyNpmRegistry(this.project.rootPath, this.npmConfig))
.then(() => verifyNpmPackageAccess(this.packagesToPublish, this.project.rootPath, this.npmConfig));
}

updateCanaryVersions() {
const publishableUpdates = this.updates.filter(node => !node.pkg.private);

Expand Down
4 changes: 4 additions & 0 deletions commands/publish/lib/__mocks__/verify-npm-package-access.js
@@ -0,0 +1,4 @@
"use strict";

// to mock user modules, you _must_ call `jest.mock('./path/to/module')`
module.exports = jest.fn(() => Promise.resolve());
4 changes: 4 additions & 0 deletions commands/publish/lib/__mocks__/verify-npm-registry.js
@@ -0,0 +1,4 @@
"use strict";

// to mock user modules, you _must_ call `jest.mock('./path/to/module')`
module.exports = jest.fn(() => Promise.resolve());
71 changes: 71 additions & 0 deletions commands/publish/lib/verify-npm-package-access.js
@@ -0,0 +1,71 @@
"use strict";

const log = require("npmlog");
const childProcess = require("@lerna/child-process");
const getExecOpts = require("@lerna/get-npm-exec-opts");
const ValidationError = require("@lerna/validation-error");

module.exports = verifyNpmPackageAccess;

function verifyNpmPackageAccess(packages, location, { registry }) {
log.silly("verifyNpmPackageAccess");

const args = [
"access",
"ls-packages",
// immediate feedback from request errors, not excruciatingly slow retries
// @see https://docs.npmjs.com/misc/config#fetch-retries
"--fetch-retries=0",
// including http requests makes raw logging easier to debug for end users
"--loglevel=http",
];
const opts = getExecOpts({ location }, registry);

// we do not need special log handling
delete opts.pkg;

return childProcess.exec("npm", args, opts).then(
result => {
const permission = JSON.parse(result.stdout);

for (const pkg of packages) {
if (permission[pkg.name] !== "read-write") {
throw new ValidationError(
"EACCESS",
"You do not have write permission required to publish %j",
pkg.name
);
}
}
},
// only catch npm error, not validation error above
({ stderr }) => {
// pass if registry does not support ls-packages endpoint
if (/E500/.test(stderr) && /ECONNREFUSED/.test(stderr)) {
// most likely a private registry (npm Enterprise, verdaccio, etc)
log.warn(
"EREGISTRY",
"Registry %j does not support `npm access ls-packages`, skipping permission checks...",
registry
);

// don't log redundant errors
return;
}

if (/ENEEDAUTH/.test(stderr)) {
throw new ValidationError(
"ENEEDAUTH",
"You must be logged in to publish packages. Use `npm login` and try again."
);
}

// Log the error cleanly to stderr, it already has npmlog decorations
log.pause();
console.error(stderr); // eslint-disable-line no-console
log.resume();

throw new ValidationError("EWHOAMI", "Authentication error. Use `npm whoami` to troubleshoot.");
}
);
}
34 changes: 34 additions & 0 deletions commands/publish/lib/verify-npm-registry.js
@@ -0,0 +1,34 @@
"use strict";

const log = require("npmlog");
const childProcess = require("@lerna/child-process");
const getExecOpts = require("@lerna/get-npm-exec-opts");
const ValidationError = require("@lerna/validation-error");

module.exports = verifyNpmRegistry;

function verifyNpmRegistry(location, { registry }) {
log.silly("verifyNpmRegistry");

const args = [
"ping",
// immediate feedback from request errors, not excruciatingly slow retries
// @see https://docs.npmjs.com/misc/config#fetch-retries
"--fetch-retries=0",
// including http requests makes raw logging easier to debug for end users
"--loglevel=http",
];
const opts = getExecOpts({ location }, registry);

// we do not need special log handling
delete opts.pkg;

return childProcess.exec("npm", args, opts).catch(({ stderr }) => {
// Log the error cleanly to stderr, it already has npmlog decorations
log.pause();
console.error(stderr); // eslint-disable-line no-console
log.resume();

throw new ValidationError("EREGISTRY", "Connection to npm registry failed");
});
}
2 changes: 2 additions & 0 deletions commands/publish/package.json
Expand Up @@ -36,11 +36,13 @@
"@lerna/child-process": "file:../../core/child-process",
"@lerna/collect-updates": "file:../../utils/collect-updates",
"@lerna/command": "file:../../core/command",
"@lerna/get-npm-exec-opts": "file:../../utils/get-npm-exec-opts",
"@lerna/npm-dist-tag": "file:../../utils/npm-dist-tag",
"@lerna/npm-publish": "file:../../utils/npm-publish",
"@lerna/output": "file:../../utils/output",
"@lerna/run-lifecycle": "file:../../utils/run-lifecycle",
"@lerna/run-parallel-batches": "file:../../utils/run-parallel-batches",
"@lerna/validation-error": "file:../../core/validation-error",
"@lerna/version": "file:../version",
"fs-extra": "^6.0.1",
"npm-package-arg": "^6.0.0",
Expand Down
2 changes: 2 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ebc8ba6

Please sign in to comment.