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

Issue:
The `--no-ci` option is ignored
## Current behavior

When calling `semantic-release --no-ci`, the parameter is ignored.
`commander` set `options.noCi` as `undefined`, instead the `options.ci` is set to `true`.

In addition the `noCi` options from the config files is loaded only after checking `options.noCi`. So `noCi` from a config file is effectively ignored.

## Expected behavior

`semantic-release --no-ci` should be parsed properly. 

Pull request:
fix: fix the `--no-ci` arg parsing
The `noCi` is no properly set based on the `--no-ci` CLI arg and it is overwritten by the `noCi` option.

Fix #616

Patch:
diff --git a/cli.js b/cli.js
index 1f8e136..70f2490 100755
--- a/cli.js
+++ b/cli.js
@@ -48,8 +48,11 @@ module.exports = async () => {
       program.outputHelp();
       process.exitCode = 1;
     } else {
+      const opts = program.opts();
+      // Set the `noCi` options as commander.js sets the `ci` options instead (becasue args starts with `--no`)
+      opts.noCi = opts.ci === false ? true : undefined;
       // Remove option with undefined values, as commander.js sets non defined options as `undefined`
-      await require('.')(pickBy(program.opts(), value => !isUndefined(value)));
+      await require('.')(pickBy(opts, value => !isUndefined(value)));
     }
   } catch (err) {
     process.exitCode = 1;
diff --git a/index.js b/index.js
index fa829d1..bf9e4cf 100644
--- a/index.js
+++ b/index.js
@@ -9,13 +9,15 @@ const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
 
 module.exports = async opts => {
   const {isCi, branch, isPr} = envCi();
+  const config = await getConfig(opts, logger);
+  const {plugins, options} = config;
 
-  if (!isCi && !opts.dryRun && !opts.noCi) {
+  if (!isCi && !options.dryRun && !options.noCi) {
     logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
-    opts.dryRun = true;
+    options.dryRun = true;
   }
 
-  if (isCi && isPr && !opts.noCi) {
+  if (isCi && isPr && !options.noCi) {
     logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
     return;
   }
@@ -25,9 +27,6 @@ module.exports = async opts => {
     return;
   }
 
-  const config = await getConfig(opts, logger);
-  const {plugins, options} = config;
-
   if (branch !== options.branch) {
     logger.log(
       `This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${


Test patch:
diff --git a/test/index.test.js b/test/index.test.js
index 43afe5b..6bca7cc 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 and ignore "noCi" is not explicitly set', async t => {
+test.serial('Force a dry-run if not on a CI and "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
@@ -369,7 +369,7 @@ test.serial('Returns falsy value if not running from a git repository', async t
     './lib/logger': t.context.logger,
     'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
   });
-  t.falsy(await semanticRelease());
+  t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
   t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.');
 });
 
@@ -384,7 +384,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],
+    t.context.log.args[7][0],
     "This run was triggered by a pull request and therefore a new version won't be published."
   );
 });
diff --git a/test/integration.test.js b/test/integration.test.js
index a0e4bbc..11ff0d2 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -656,6 +656,77 @@ test.serial('Dry-run', async t => {
   await mockServer.verify(verifyMock);
 });
 
+test.serial('Allow local releases with "noCi" option', async t => {
+  delete process.env.TRAVIS;
+  delete process.env.CI;
+  const packageName = 'test-no-ci';
+  const owner = 'test-repo';
+  // Create a git repository, set the current working directory at the root of the repo
+  t.log('Create git repository and package.json');
+  await gitRepo();
+  // Create package.json in repository root
+  await writeJson('./package.json', {
+    name: packageName,
+    version: '0.0.0-dev',
+    repository: {url: `git+https://github.com/${owner}/${packageName}`},
+    publishConfig: {registry: npmRegistry.url},
+  });
+
+  /* Initial release */
+  const version = '1.0.0';
+  const verifyMock = await mockServer.mock(
+    `/repos/${owner}/${packageName}`,
+    {headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
+    {body: {permissions: {push: true}}, method: 'GET'}
+  );
+  const getRefMock = await mockServer.mock(
+    `/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
+    {},
+    {body: {}, statusCode: 404, method: 'GET'}
+  );
+  const createRefMock = await mockServer.mock(
+    `/repos/${owner}/${packageName}/git/refs`,
+    {
+      body: {ref: `refs/tags/v${version}`},
+      headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
+    },
+    {body: {ref: `refs/tags/${version}`}}
+  );
+  const createReleaseMock = await mockServer.mock(
+    `/repos/${owner}/${packageName}/releases`,
+    {
+      body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
+      headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
+    },
+    {body: {html_url: `release-url/${version}`}}
+  );
+
+  t.log('Commit a feature');
+  await gitCommits(['feat: Initial commit']);
+  t.log('$ semantic-release --no-ci');
+  const {stdout, code} = await execa(cli, ['--no-ci'], {env});
+  console.log(stdout);
+  t.regex(stdout, new RegExp(`Published GitHub release: release-url/${version}`));
+  t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
+  t.is(code, 0);
+
+  // Verify package.json and has been updated
+  t.is((await readJson('./package.json')).version, version);
+
+  // Retrieve the published package from the registry and check version and gitHead
+  const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
+    (await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
+  );
+  t.is(releasedVersion, version);
+  t.is(releasedGitHead, await gitHead());
+  t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
+
+  await mockServer.verify(verifyMock);
+  await mockServer.verify(getRefMock);
+  await mockServer.verify(createRefMock);
+  await mockServer.verify(createReleaseMock);
+});
+
 test.serial('Pass options via CLI arguments', async t => {
   const packageName = 'test-cli';
   const owner = 'test-repo';