Repository: semantic-release/semantic-release (js)

Issue:
Extract agnostic CI verification as a default `condition-ci` plugin
The following additions in `v12.0.0` seems awfully restrictive, especially since it used to be optional (by not including `condition-travis`). 

```js
  if (!isCi && !opts.dryRun) {
    logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
    opts.dryRun = true;
  }
```
Forcing `dry-run` unless the CI env vars are set makes it more cumbersome to develop plugins.


```
  if (isCi && isPr) {
    logger.log('This run was triggered by a pull request and therefore a new version won’t be published.');
    return;
  }
```
Forcing the PR env variable makes it impossible to upgrade [`semantic-release-github-pr`](https://github.com/Updater/semantic-release-github-pr) to `v12.0.0` since it needs to run on PRs. 

What do you think of extracting it as a CI-agnostic `condition-ci` plugin?

Pull request description:
feat: allow to release from local machine
Doing releases outside a CI environment is not the recommended approach, but it might be useful in specific circumstances (for example #584 and [comment](https://github.com/semantic-release/semantic-release/pull/578#issuecomment-354579201)).

Since #578, `condition-travis` is not a default plugin anymore and verifying that `semantic-release` runs after all other CI jobs are successful is done externally, therefore allowing local releases is just a couple line change. So why not.

In addition the `Can I run this on my own machine rather than on a CI server?` FAQ mention it is possible even though it's not since several releases, so it was probably possible at some point. No real reason to remove (or more exactly continue to remove) a feature that was present before.

The FAQ clearly mention that even if it's possible, it's not the recommended approach.

Fix #584 and [comment](https://github.com/semantic-release/semantic-release/pull/578#issuecomment-354579201).

Patch:
diff --git a/README.md b/README.md
index a894689..0484c09 100644
--- a/README.md
+++ b/README.md
@@ -168,6 +168,7 @@ semantic-release
 These options are currently available:
 - `branch`: The branch on which releases should happen. Default: `'master'`
 - `repositoryUrl`: The git repository URL. Default: `repository` property in `package.json` or git origin url. Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. The Github shorthand URL is not supported.
+- `no-ci`: Skip Continuous Integration environment verifications, allowing to make releases from a local machine
 - `dry-run`: Dry-run mode, skip publishing, print next version and release notes
 - `extends`: Array of module or files path containing a shareable configuration. Options defined via CLI or in the `release` property will take precedence over the one defined in a shareable configuration.
 - `debug`: Output debugging information
@@ -269,7 +270,8 @@ If you run `npm run semantic-release` locally a dry run gets performed, which lo
 
 ### Can I run this on my own machine rather than on a CI server?
 
-Of course you can, but this doesn’t necessarily mean you should. Running your tests on an independent machine before releasing software is a crucial part of this workflow. Also it is a pain to set this up locally, with tokens lying around and everything. That said, you can run the scripts with `--debug=false` explicitly. You have to export `GH_TOKEN=<your_token>` and `NPM_TOKEN=<your_other_token>`.
+Yes, you can by explicitly setting the [`--no-ci` CLI option](#options), but this doesn’t necessarily mean you should. Running your tests on an independent machine before releasing software is a crucial part of this workflow.
+You will need to set all necessary authentication token (like `GH_TOKEN` and `NPM_TOKEN`) on your local machine.
 
 ### Can I manually trigger the release of a specific version?
 
diff --git a/cli.js b/cli.js
index 12d9743..1f8e136 100755
--- a/cli.js
+++ b/cli.js
@@ -27,6 +27,10 @@ module.exports = async () => {
     )
     .option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
     .option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list)
+    .option(
+      '--no-ci',
+      'Skip Continuous Integration environment verifications, allowing to make releases from a local machine'
+    )
     .option('--debug', 'Output debugging information')
     .option(
       '-d, --dry-run',
diff --git a/index.js b/index.js
index 964eba1..04bfbb4 100644
--- a/index.js
+++ b/index.js
@@ -10,13 +10,13 @@ const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
 module.exports = async opts => {
   const {isCi, branch, isPr} = envCi();
 
-  if (!isCi && !opts.dryRun) {
+  if (!isCi && !opts.dryRun && !opts.noCi) {
     logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
     opts.dryRun = true;
   }
 
-  if (isCi && isPr) {
-    logger.log('This run was triggered by a pull request and therefore a new version won’t be published.');
+  if (isCi && isPr && !opts.noCi) {
+    logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
     return;
   }
 


Test patch:
diff --git a/test/index.test.js b/test/index.test.js
index c256696..6541622 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -209,7 +209,7 @@ test.serial('Dry-run skips publish', async t => {
   t.is(publish.callCount, 0);
 });
 
-test.serial('Force a dry-run if not on a CI', async t => {
+test.serial('Force a dry-run if not on a CI and ignore "noCi" is not explicitly set', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
   // Add commits to the master branch
@@ -257,6 +257,58 @@ test.serial('Force a dry-run if not on a CI', async t => {
   t.is(publish.callCount, 0);
 });
 
+test.serial('Allow local releases with "noCi" option', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Add commits to the master branch
+  let commits = await gitCommits(['First']);
+  // Create the tag corresponding to version 1.0.0
+  await gitTagVersion('v1.0.0');
+  // Add new commits to the master branch
+  commits = (await gitCommits(['Second'])).concat(commits);
+
+  const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
+  const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
+  const notes = 'Release notes';
+
+  const verifyConditions = stub().resolves();
+  const getLastRelease = stub().resolves(lastRelease);
+  const analyzeCommits = stub().resolves(nextRelease.type);
+  const verifyRelease = stub().resolves();
+  const generateNotes = stub().resolves(notes);
+  const publish = stub().resolves();
+
+  const options = {
+    noCi: true,
+    branch: 'master',
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions,
+    getLastRelease,
+    analyzeCommits,
+    verifyRelease,
+    generateNotes,
+    publish,
+  };
+
+  const semanticRelease = proxyquire('..', {
+    './lib/logger': t.context.logger,
+    'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
+  });
+  t.truthy(await semanticRelease(options));
+
+  t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
+  t.not(
+    t.context.log.args[0][0],
+    "This run was triggered by a pull request and therefore a new version won't be published."
+  );
+  t.is(verifyConditions.callCount, 1);
+  t.is(getLastRelease.callCount, 1);
+  t.is(analyzeCommits.callCount, 1);
+  t.is(verifyRelease.callCount, 1);
+  t.is(generateNotes.callCount, 1);
+  t.is(publish.callCount, 1);
+});
+
 test.serial('Accept "undefined" values for the "getLastRelease" and "generateNotes" plugins', async t => {
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
@@ -333,7 +385,7 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
   t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
   t.is(
     t.context.log.args[0][0],
-    'This run was triggered by a pull request and therefore a new version won’t be published.'
+    "This run was triggered by a pull request and therefore a new version won't be published."
   );
 });