Repository: semantic-release/semantic-release
Pull request #524 (2017-11-24 22:49:53)

Make semantic-release language agnostic
The last language specific thing `semantic-release` relies on is the `package.json`.
This PR remove this reliance by allowing to configure `semantic-release` with rc files in addition of the current `release` property in the `package.json`.

This way, with the proper plugins, `semantic-release` can release any type of project.
We should now recommend to install `semantic-release` globally:
```
npm install semantic-release -g
semantic-release
```
or with `npx`
```
npx semantic-release
```
or in Unix based CI:
```
if [[ `node -v | egrep -o '[0-9|\.]+'` > 8* ]]; then npm install semantic-release -g && semantic-release; fi
```

Here is the list of changes:
- Do not rely on `package.json` anymore
- Use `cosmiconfig` to load the configation. `semantic-release` can be configured:
  - via CLI options (including plugin names but not plugin options)
  - in the `release` property of `package.json` (as before)
  - in a `.releaserc.yml` or `.releaserc.js` or `.releaserc.js` or `release.config.js` file
  - in a `.releaserc` file containing `json`, `yaml` or `javascript` module
- Add the `repositoryUrl` options (used across `semantic-release` and plugins). The value is determined from CLi option, or option configuration, or package.json or the git remote url
- Verifies that `semantic-release` runs from a git repository
- `pkg` and `env` are not passed to plugin anymore
- `semantic-release` can be run both locally and globally. If ran globally with non default plugins, the plugins can be installed both globally or locally.

In addition with PR `semantic-release` now expect plugin to provide an async or `Promise` returning function rather than a function with a callback.

BREAKING CHANGE: `pkg` and `env` are not passed to plugin anymore.
Plugins relying on a `package.json` must verify the presence of a valid `package.json` and load it.
Plugins can use `process.env` instead of `env`.

BREAKING CHANGE: Each plugin is expected to return an async function or a Promise returning function. The callback parameter is not passed to plugins anymore.

This PR should be merged only after all the default plugin are updated. It currently uses the plugin directly from Github. So we'll have to change the `package.json` to point to the released version of the plugins.

Fix #526, Fix #527, Fix #528, Fix #529  

codecov[bot]: # [Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=h1) Report
> Merging [#524](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=desc) into [caribou](https://codecov.io/gh/semantic-release/semantic-release/commit/d4c7605f687d17a9fd5ab9a34e4bc4d541a07396?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/semantic-release/semantic-release/pull/524/graphs/tree.svg?width=650&height=150&src=pr&token=hENTgPdVFG)](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           caribou   #524   +/-   ##
======================================
  Coverage      100%   100%           
======================================
  Files           13     13           
  Lines          245    260   +15     
  Branches        48     54    +6     
======================================
+ Hits           245    260   +15
```


| [Impacted Files](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/plugins/normalize.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL3BsdWdpbnMvbm9ybWFsaXplLmpz) | `100% <100%> (ø)` | :arrow_up: |
| [cli.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-Y2xpLmpz) | `100% <100%> (ø)` | :arrow_up: |
| [index.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-aW5kZXguanM=) | `100% <100%> (ø)` | :arrow_up: |
| [lib/get-config.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL2dldC1jb25maWcuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [lib/git.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL2dpdC5qcw==) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=footer). Last update [d4c7605...7bec7fb](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov[bot]: # [Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=h1) Report
> Merging [#524](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=desc) into [caribou](https://codecov.io/gh/semantic-release/semantic-release/commit/d4c7605f687d17a9fd5ab9a34e4bc4d541a07396?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/semantic-release/semantic-release/pull/524/graphs/tree.svg?width=650&token=hENTgPdVFG&height=150&src=pr)](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree)

```diff
@@          Coverage Diff           @@
##           caribou   #524   +/-   ##
======================================
  Coverage      100%   100%           
======================================
  Files           13     13           
  Lines          245    260   +15     
  Branches        48     54    +6     
======================================
+ Hits           245    260   +15
```


| [Impacted Files](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/get-config.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL2dldC1jb25maWcuanM=) | `100% <100%> (ø)` | :arrow_up: |
| [cli.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-Y2xpLmpz) | `100% <100%> (ø)` | :arrow_up: |
| [lib/git.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL2dpdC5qcw==) | `100% <100%> (ø)` | :arrow_up: |
| [index.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-aW5kZXguanM=) | `100% <100%> (ø)` | :arrow_up: |
| [lib/plugins/normalize.js](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=tree#diff-bGliL3BsdWdpbnMvbm9ybWFsaXplLmpz) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=footer). Last update [d4c7605...a3f1596](https://codecov.io/gh/semantic-release/semantic-release/pull/524?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index f01d566..30ac378 100644
--- a/README.md
+++ b/README.md
@@ -167,6 +167,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.
 - `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes
 - `debug`: Output debugging information
 
@@ -206,12 +207,8 @@ module.exports = function (pluginConfig, config, callback) {}
 
 - `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then it’s this object.
 - `config`: A config object containing a lot of information to act upon.
-  - `env`: All environment variables
   - `options`: `semantic-release` options like `debug`, or `branch`
-  - `pkg`: Parsed `package.json`
   - For certain plugins the `config` object contains even more information. See below.
-- `callback`: If an error occurs pass it as first argument. Otherwise pass your result as second argument.
-
 
 ### `analyzeCommits`
 
diff --git a/cli.js b/cli.js
index 82be83e..60c0d66 100755
--- a/cli.js
+++ b/cli.js
@@ -10,6 +10,7 @@ module.exports = async () => {
     .name('semantic-release')
     .description('Run automated package publishing')
     .option('-b, --branch <branch>', 'Branch to release from')
+    .option('-r, --repositoryUrl <repositoryUrl>', 'Git repository URL')
     .option(
       '--verify-conditions <paths>',
       'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
@@ -41,7 +42,7 @@ module.exports = async () => {
       program.outputHelp();
       process.exitCode = 1;
     } else {
-      await require('./index')(program.opts());
+      await require('.')(program.opts());
     }
   } catch (err) {
     // If error is a SemanticReleaseError then it's an expected exception case (no release to be done, running on a PR etc..) and the cli will return with 0
diff --git a/index.js b/index.js
index 4fa04ef..ae22e27 100644
--- a/index.js
+++ b/index.js
@@ -1,32 +1,40 @@
 const marked = require('marked');
 const TerminalRenderer = require('marked-terminal');
 const SemanticReleaseError = require('@semantic-release/error');
-const {gitHead: getGitHead} = require('./lib/git');
 const getConfig = require('./lib/get-config');
 const getNextVersion = require('./lib/get-next-version');
 const getCommits = require('./lib/get-commits');
 const logger = require('./lib/logger');
+const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
 
 module.exports = async opts => {
+  if (!await isGitRepo()) {
+    throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO');
+  }
+
   const config = await getConfig(opts, logger);
-  const {plugins, env, options, pkg} = config;
+  const {plugins, options} = config;
+
+  if (!options.repositoryUrl) {
+    throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
+  }
 
-  logger.log('Run automated release for branch %s', options.branch);
+  logger.log('Run automated release from branch %s', options.name, options.branch);
 
   if (!options.dryRun) {
     logger.log('Call plugin %s', 'verify-conditions');
-    await plugins.verifyConditions({env, options, pkg, logger});
+    await plugins.verifyConditions({options, logger});
   }
 
   logger.log('Call plugin %s', 'get-last-release');
   const {commits, lastRelease} = await getCommits(
-    await plugins.getLastRelease({env, options, pkg, logger}),
+    await plugins.getLastRelease({options, logger}),
     options.branch,
     logger
   );
 
   logger.log('Call plugin %s', 'analyze-commits');
-  const type = await plugins.analyzeCommits({env, options, pkg, logger, lastRelease, commits});
+  const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
   if (!type) {
     throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
   }
@@ -34,9 +42,9 @@ module.exports = async opts => {
   const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
 
   logger.log('Call plugin %s', 'verify-release');
-  await plugins.verifyRelease({env, options, pkg, logger, lastRelease, commits, nextRelease});
+  await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease});
 
-  const generateNotesParam = {env, options, pkg, logger, lastRelease, commits, nextRelease};
+  const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
 
   if (options.dryRun) {
     logger.log('Call plugin %s', 'generate-notes');
@@ -49,7 +57,7 @@ module.exports = async opts => {
     nextRelease.notes = await plugins.generateNotes(generateNotesParam);
 
     logger.log('Call plugin %s', 'publish');
-    await plugins.publish({options, pkg, logger, lastRelease, commits, nextRelease}, async prevInput => {
+    await plugins.publish({options, logger, lastRelease, commits, nextRelease}, async prevInput => {
       const newGitHead = await getGitHead();
       // If previous publish plugin has created a commit (gitHead changed)
       if (prevInput.nextRelease.gitHead !== newGitHead) {
@@ -59,7 +67,7 @@ module.exports = async opts => {
         nextRelease.notes = await plugins.generateNotes(generateNotesParam);
       }
       // Call the next publish plugin with the updated `nextRelease`
-      return {options, pkg, logger, lastRelease, commits, nextRelease};
+      return {options, logger, lastRelease, commits, nextRelease};
     });
     logger.log('Published release: %s', nextRelease.version);
   }
diff --git a/lib/get-config.js b/lib/get-config.js
index 7770073..d7fd4c8 100644
--- a/lib/get-config.js
+++ b/lib/get-config.js
@@ -1,19 +1,27 @@
-const {readJson} = require('fs-extra');
+const readPkgUp = require('read-pkg-up');
 const {defaults} = require('lodash');
-const normalizeData = require('normalize-package-data');
+const cosmiconfig = require('cosmiconfig');
 const debug = require('debug')('semantic-release:config');
+const {repoUrl} = require('./git');
 const plugins = require('./plugins');
 
 module.exports = async (opts, logger) => {
-  const pkg = await readJson('./package.json');
-  normalizeData(pkg);
-  const options = defaults(opts, pkg.release, {branch: 'master'});
+  const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
+  const options = defaults(opts, config, {branch: 'master', repositoryUrl: (await pkgRepoUrl()) || (await repoUrl())});
+
+  debug('name: %O', options.name);
   debug('branch: %O', options.branch);
+  debug('repositoryUrl: %O', options.repositoryUrl);
   debug('analyzeCommits: %O', options.analyzeCommits);
   debug('generateNotes: %O', options.generateNotes);
   debug('verifyConditions: %O', options.verifyConditions);
   debug('verifyRelease: %O', options.verifyRelease);
   debug('publish: %O', options.publish);
 
-  return {env: process.env, pkg, options, plugins: await plugins(options, logger), logger};
+  return {options, plugins: await plugins(options, logger)};
 };
+
+async function pkgRepoUrl() {
+  const {pkg} = await readPkgUp();
+  return pkg && pkg.repository ? pkg.repository.url : null;
+}
diff --git a/lib/git.js b/lib/git.js
index d430035..dbff850 100644
--- a/lib/git.js
+++ b/lib/git.js
@@ -72,4 +72,20 @@ async function gitHead() {
   }
 }
 
-module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead};
+/**
+ * @return {string|null} The value of the remote git URL.
+ */
+async function repoUrl() {
+  return (await execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false})) || null;
+}
+
+/**
+ * @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
+ */
+async function isGitRepo() {
+  const shell = await execa('git', ['rev-parse', '--git-dir'], {reject: false});
+  debugShell('Check if the current working directory is a git repository', shell, debug);
+  return shell.code === 0;
+}
+
+module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl, isGitRepo};
diff --git a/lib/plugins/normalize.js b/lib/plugins/normalize.js
index 64b5974..3a6175f 100644
--- a/lib/plugins/normalize.js
+++ b/lib/plugins/normalize.js
@@ -1,4 +1,4 @@
-const {promisify, inspect} = require('util');
+const {inspect} = require('util');
 const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash');
 const importFrom = require('import-from');
 
@@ -14,9 +14,9 @@ module.exports = (pluginType, pluginConfig, logger, validator) => {
 
   let func;
   if (isFunction(plugin)) {
-    func = promisify(plugin.bind(null, cloneDeep(config)));
+    func = plugin.bind(null, cloneDeep(config));
   } else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
-    func = promisify(plugin[pluginType].bind(null, cloneDeep(config)));
+    func = plugin[pluginType].bind(null, cloneDeep(config));
   } else {
     throw new Error(
       `The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`
diff --git a/package.json b/package.json
index 55fa7c4..ab00d63 100644
--- a/package.json
+++ b/package.json
@@ -15,25 +15,25 @@
     }
   },
   "dependencies": {
-    "@semantic-release/commit-analyzer": "^4.0.0",
-    "@semantic-release/condition-travis": "^6.0.0",
+    "@semantic-release/commit-analyzer": "^5.0.0",
+    "@semantic-release/condition-travis": "^7.0.0",
     "@semantic-release/error": "^2.1.0",
-    "@semantic-release/github": "^1.0.0",
-    "@semantic-release/npm": "^1.0.0",
-    "@semantic-release/release-notes-generator": "^5.0.0",
+    "@semantic-release/github": "^2.0.0",
+    "@semantic-release/npm": "^2.0.0",
+    "@semantic-release/release-notes-generator": "^6.0.0",
     "chalk": "^2.3.0",
     "commander": "^2.11.0",
+    "cosmiconfig": "^3.1.0",
     "debug": "^3.1.0",
     "execa": "^0.8.0",
-    "fs-extra": "^4.0.2",
     "get-stream": "^3.0.0",
     "git-log-parser": "^1.2.0",
     "import-from": "^2.1.0",
     "lodash": "^4.0.0",
     "marked": "^0.3.6",
     "marked-terminal": "^2.0.0",
-    "normalize-package-data": "^2.3.4",
     "p-reduce": "^1.0.0",
+    "read-pkg-up": "^3.0.0",
     "semver": "^5.4.1"
   },
   "devDependencies": {
@@ -44,11 +44,13 @@
     "dockerode": "^2.5.2",
     "eslint-config-prettier": "^2.5.0",
     "eslint-plugin-prettier": "^2.3.0",
+    "file-url": "^2.0.2",
+    "fs-extra": "^4.0.2",
+    "js-yaml": "^3.10.0",
     "mockserver-client": "^2.0.0",
     "nock": "^9.0.2",
     "npm-registry-couchapp": "^2.6.12",
     "nyc": "^11.2.1",
-    "p-map-series": "^1.0.0",
     "prettier": "~1.8.0",
     "proxyquire": "^1.8.0",
     "sinon": "^4.0.0",
@@ -124,7 +126,8 @@
       "prettier"
     ],
     "rules": {
-      "prettier/prettier": 2
+      "prettier/prettier": 2,
+      "no-duplicate-imports": 2
     }
   }
 }


Test patch:
diff --git a/test/fixtures/multi-plugin.js b/test/fixtures/multi-plugin.js
index 55baaaa..1691941 100644
--- a/test/fixtures/multi-plugin.js
+++ b/test/fixtures/multi-plugin.js
@@ -1,21 +1,8 @@
 module.exports = {
-  verifyConditions(config, options, cb) {
-    cb();
-  },
-
-  getLastRelease(config, options, cb) {
-    cb();
-  },
-  analyzeCommits(config, options, cb) {
-    cb();
-  },
-  verifyRelease(config, options, cb) {
-    cb();
-  },
-  generateNotes(config, options, cb) {
-    cb();
-  },
-  publish(config, options, cb) {
-    cb();
-  },
+  verifyConditions: () => {},
+  getLastRelease: () => {},
+  analyzeCommits: () => {},
+  verifyRelease: () => {},
+  generateNotes: () => {},
+  publish: () => {},
 };
diff --git a/test/fixtures/plugin-error-inherited.js b/test/fixtures/plugin-error-inherited.js
index 12aad0f..bcc5a3e 100644
--- a/test/fixtures/plugin-error-inherited.js
+++ b/test/fixtures/plugin-error-inherited.js
@@ -9,6 +9,6 @@ class InheritedError extends SemanticReleaseError {
   }
 }
 
-module.exports = function(config, options, cb) {
-  cb(new InheritedError('Inherited error', 'EINHERITED'));
+module.exports = () => {
+  throw new InheritedError('Inherited error', 'EINHERITED');
 };
diff --git a/test/fixtures/plugin-error.js b/test/fixtures/plugin-error.js
index 2a4fe3d..2c3dee2 100644
--- a/test/fixtures/plugin-error.js
+++ b/test/fixtures/plugin-error.js
@@ -1,5 +1,5 @@
-module.exports = function(config, options, cb) {
+module.exports = () => {
   const error = new Error('a');
   error.errorProperty = 'errorProperty';
-  cb(error);
+  throw error;
 };
diff --git a/test/fixtures/plugin-noop.js b/test/fixtures/plugin-noop.js
index 28814df..cc40a46 100644
--- a/test/fixtures/plugin-noop.js
+++ b/test/fixtures/plugin-noop.js
@@ -1,3 +1,1 @@
-module.exports = (config, options, cb) => {
-  cb(null);
-};
+module.exports = () => {};
diff --git a/test/fixtures/plugin-result-config.js b/test/fixtures/plugin-result-config.js
index 7a6a598..141b1de 100644
--- a/test/fixtures/plugin-result-config.js
+++ b/test/fixtures/plugin-result-config.js
@@ -1,3 +1,1 @@
-module.exports = function(pluginConfig, options, cb) {
-  cb(null, {pluginConfig, options});
-};
+module.exports = (pluginConfig, options) => ({pluginConfig, options});
diff --git a/test/get-config.test.js b/test/get-config.test.js
index 3e8a5be..1b2d4d1 100644
--- a/test/get-config.test.js
+++ b/test/get-config.test.js
@@ -1,9 +1,9 @@
 import test from 'ava';
-import {writeJson} from 'fs-extra';
+import {writeFile, writeJson} from 'fs-extra';
 import proxyquire from 'proxyquire';
 import {stub} from 'sinon';
-import normalizeData from 'normalize-package-data';
-import {gitRepo} from './helpers/git-utils';
+import yaml from 'js-yaml';
+import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
 
 test.beforeEach(t => {
   // Save the current process.env
@@ -21,85 +21,175 @@ test.afterEach.always(t => {
   process.env = Object.assign({}, t.context.env);
 });
 
-test.serial('Default values', async t => {
-  const pkg = {name: 'package_name', release: {}};
+test.serial('Default values, reading repositoryUrl from package.json', async t => {
+  const pkg = {repository: 'git@package.com:owner/module.git'};
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
+  await gitCommits(['First']);
+  // Add remote.origin.url config
+  await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
   // Create package.json in repository root
   await writeJson('./package.json', pkg);
 
-  const result = await t.context.getConfig();
+  const {options} = await t.context.getConfig();
 
-  // Verify the normalized package is returned
-  normalizeData(pkg);
-  t.deepEqual(result.pkg, pkg);
   // Verify the default options are set
-  t.is(result.options.branch, 'master');
+  t.is(options.branch, 'master');
+  t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
 });
 
-test.serial('Read package.json configuration', async t => {
+test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Add remote.origin.url config
+  await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the default options are set
+  t.is(options.branch, 'master');
+  t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
+});
+
+test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
+  const pkg = {repository: 'git+https://hostname.com/owner/module.git'};
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Create package.json in repository root
+  await writeJson('./package.json', pkg);
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the default options are set
+  t.is(options.branch, 'master');
+  t.is(options.repositoryUrl, pkg.repository);
+});
+
+test.serial('Read options from package.json', async t => {
   const release = {
     analyzeCommits: 'analyzeCommits',
     generateNotes: 'generateNotes',
-    getLastRelease: {
-      path: 'getLastRelease',
-      param: 'getLastRelease_param',
-    },
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
     branch: 'test_branch',
+    repositoryUrl: 'git+https://hostname.com/owner/module.git',
   };
-  const pkg = {name: 'package_name', release};
 
   // Create a git repository, set the current working directory at the root of the repo
   await gitRepo();
   // Create package.json in repository root
-  await writeJson('./package.json', pkg);
+  await writeJson('./package.json', {release});
 
-  const result = await t.context.getConfig();
+  const {options} = await t.context.getConfig();
 
   // Verify the options contains the plugin config from package.json
-  t.is(result.options.analyzeCommits, release.analyzeCommits);
-  t.is(result.options.generateNotes, release.generateNotes);
-  t.deepEqual(result.options.getLastRelease, release.getLastRelease);
-  t.is(result.options.branch, release.branch);
+  t.deepEqual(options, release);
+  // Verify the plugins module is called with the plugin options from package.json
+  t.deepEqual(t.context.plugins.firstCall.args[0], release);
+});
 
+test.serial('Read options from .releaserc.yml', async t => {
+  const release = {
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
+    branch: 'test_branch',
+    repositoryUrl: 'git+https://hostname.com/owner/module.git',
+  };
+
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Create package.json in repository root
+  await writeFile('.releaserc.yml', yaml.safeDump(release));
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the options contains the plugin config from package.json
+  t.deepEqual(options, release);
   // Verify the plugins module is called with the plugin options from package.json
-  t.is(t.context.plugins.firstCall.args[0].analyzeCommits, release.analyzeCommits);
-  t.is(t.context.plugins.firstCall.args[0].generateNotes, release.generateNotes);
-  t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, release.getLastRelease);
-  t.is(t.context.plugins.firstCall.args[0].branch, release.branch);
+  t.deepEqual(t.context.plugins.firstCall.args[0], release);
 });
 
-test.serial('Prioritise cli parameters over package.json configuration', async t => {
+test.serial('Read options from .releaserc.json', async t => {
   const release = {
-    analyzeCommits: 'analyzeCommits',
-    generateNotes: 'generateNotes',
-    getLastRelease: {
-      path: 'getLastRelease',
-      param: 'getLastRelease_pkg',
-    },
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
+    branch: 'test_branch',
+    repositoryUrl: 'git+https://hostname.com/owner/module.git',
+  };
+
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Create package.json in repository root
+  await writeJson('.releaserc.json', release);
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the options contains the plugin config from package.json
+  t.deepEqual(options, release);
+  // Verify the plugins module is called with the plugin options from package.json
+  t.deepEqual(t.context.plugins.firstCall.args[0], release);
+});
+
+test.serial('Read options from .releaserc.js', async t => {
+  const release = {
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
+    branch: 'test_branch',
+    repositoryUrl: 'git+https://hostname.com/owner/module.git',
+  };
+
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Create package.json in repository root
+  await writeFile('.releaserc.js', `module.exports = ${JSON.stringify(release)}`);
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the options contains the plugin config from package.json
+  t.deepEqual(options, release);
+  // Verify the plugins module is called with the plugin options from package.json
+  t.deepEqual(t.context.plugins.firstCall.args[0], release);
+});
+
+test.serial('Read options from release.config.js', async t => {
+  const release = {
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
+    branch: 'test_branch',
+    repositoryUrl: 'git+https://hostname.com/owner/module.git',
+  };
+
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Create package.json in repository root
+  await writeFile('release.config.js', `module.exports = ${JSON.stringify(release)}`);
+
+  const {options} = await t.context.getConfig();
+
+  // Verify the options contains the plugin config from package.json
+  t.deepEqual(options, release);
+  // Verify the plugins module is called with the plugin options from package.json
+  t.deepEqual(t.context.plugins.firstCall.args[0], release);
+});
+
+test.serial('Prioritise cli parameters over file configuration and git repo', async t => {
+  const release = {
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_pkg'},
     branch: 'branch_pkg',
   };
   const options = {
-    getLastRelease: {
-      path: 'getLastRelease',
-      param: 'getLastRelease_cli',
-    },
+    getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_cli'},
     branch: 'branch_cli',
+    repositoryUrl: 'http://cli-url.com/owner/package',
   };
-  const pkg = {name: 'package_name', release};
-
+  const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
   // Create a git repository, set the current working directory at the root of the repo
-  await gitRepo();
+  const repo = await gitRepo();
+  await gitCommits(['First']);
+  // Create a clone
+  await gitShallowClone(repo);
   // Create package.json in repository root
   await writeJson('./package.json', pkg);
 
   const result = await t.context.getConfig(options);
 
   // Verify the options contains the plugin config from cli
-  t.deepEqual(result.options.getLastRelease, options.getLastRelease);
-  t.is(result.options.branch, options.branch);
-
+  t.deepEqual(result.options, options);
   // Verify the plugins module is called with the plugin options from cli
-  t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, options.getLastRelease);
-  t.is(t.context.plugins.firstCall.args[0].branch, options.branch);
+  t.deepEqual(t.context.plugins.firstCall.args[0], options);
 });
diff --git a/test/git.test.js b/test/git.test.js
index 5d19313..2409793 100644
--- a/test/git.test.js
+++ b/test/git.test.js
@@ -1,6 +1,15 @@
 import test from 'ava';
-import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead} from '../lib/git';
-import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitLog} from './helpers/git-utils';
+import fileUrl from 'file-url';
+import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl} from '../lib/git';
+import {
+  gitRepo,
+  gitCommits,
+  gitCheckout,
+  gitTagVersion,
+  gitShallowClone,
+  gitLog,
+  gitAddConfig,
+} from './helpers/git-utils';
 
 test.beforeEach(t => {
   // Save the current working diretory
@@ -93,3 +102,29 @@ test.serial('Get the commit sha for a given tag or "null" if the tag does not ex
   t.is((await gitTagHead('v1.0.0')).substring(0, 7), commits[0].hash);
   t.falsy(await gitTagHead('missing_tag'));
 });
+
+test.serial('Return git remote repository url from config', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+  // Add remote.origin.url config
+  await gitAddConfig('remote.origin.url', 'git@hostname.com:owner/package.git');
+
+  t.is(await repoUrl(), 'git@hostname.com:owner/package.git');
+});
+
+test.serial('Return git remote repository url set while cloning', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  const repo = await gitRepo();
+  await gitCommits(['First']);
+  // Create a clone
+  await gitShallowClone(repo);
+
+  t.is(await repoUrl(), fileUrl(repo));
+});
+
+test.serial('Return "null" if git repository url is not set', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+
+  t.is(await repoUrl(), null);
+});
diff --git a/test/helpers/git-utils.js b/test/helpers/git-utils.js
index 6d81de5..d63c758 100644
--- a/test/helpers/git-utils.js
+++ b/test/helpers/git-utils.js
@@ -1,6 +1,7 @@
 import tempy from 'tempy';
 import execa from 'execa';
-import pMapSeries from 'p-map-series';
+import fileUrl from 'file-url';
+import pReduce from 'p-reduce';
 
 /**
  * Commit message informations.
@@ -33,11 +34,16 @@ export async function gitRepo() {
  * @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
  */
 export async function gitCommits(messages) {
-  return (await pMapSeries(messages, async msg => {
-    const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
-    const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout);
-    return {branch, hash, message};
-  })).reverse();
+  return (await pReduce(
+    messages,
+    async (commits, msg) => {
+      const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
+      const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout);
+      commits.push({branch, hash, message});
+      return commits;
+    },
+    []
+  )).reverse();
 }
 
 /**
@@ -109,7 +115,7 @@ export async function gitShallowClone(origin, branch = 'master', depth = 1) {
   const dir = tempy.directory();
 
   process.chdir(dir);
-  await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, `file://${origin}`, dir]);
+  await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, fileUrl(origin), dir]);
   return dir;
 }
 
@@ -137,3 +143,13 @@ export async function gitDetachedHead(origin, head) {
 export async function gitPackRefs() {
   await execa('git', ['pack-refs', '--all']);
 }
+
+/**
+ * Add a new Git configuration.
+ *
+ * @param {string} name Config name.
+ * @param {string} value Config value.
+ */
+export async function gitAddConfig(name, value) {
+  await execa('git', ['config', '--add', name, value]);
+}
diff --git a/test/index.test.js b/test/index.test.js
index cd74f90..d0ff908 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -1,9 +1,8 @@
-import {callbackify} from 'util';
 import test from 'ava';
-import {writeJson} from 'fs-extra';
 import proxyquire from 'proxyquire';
 import {stub} from 'sinon';
-import normalizeData from 'normalize-package-data';
+import tempy from 'tempy';
+import SemanticReleaseError from '@semantic-release/error';
 import {gitHead as getGitHead} from '../lib/git';
 import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils';
 
@@ -42,11 +41,9 @@ test.serial('Plugins are called with expected values', async t => {
   // Add new commits to the master branch
   commits = (await gitCommits(['Second'])).concat(commits);
 
-  const name = 'package-name';
   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 verifyConditions1 = stub().resolves();
   const verifyConditions2 = stub().resolves();
   const getLastRelease = stub().resolves(lastRelease);
@@ -57,41 +54,34 @@ test.serial('Plugins are called with expected values', async t => {
 
   const options = {
     branch: 'master',
-    verifyConditions: [callbackify(verifyConditions1), callbackify(verifyConditions2)],
-    getLastRelease: callbackify(getLastRelease),
-    analyzeCommits: callbackify(analyzeCommits),
-    verifyRelease: callbackify(verifyRelease),
-    generateNotes: callbackify(generateNotes),
-    publish: callbackify(publish),
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions: [verifyConditions1, verifyConditions2],
+    getLastRelease,
+    analyzeCommits,
+    verifyRelease,
+    generateNotes,
+    publish,
   };
-  const pkg = {name, version: '0.0.0-dev'};
-  normalizeData(pkg);
-
-  await writeJson('./package.json', pkg);
 
   await t.context.semanticRelease(options);
 
   t.true(verifyConditions1.calledOnce);
-  t.deepEqual(verifyConditions1.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger});
+  t.deepEqual(verifyConditions1.firstCall.args[1], {options, logger: t.context.logger});
   t.true(verifyConditions2.calledOnce);
-  t.deepEqual(verifyConditions2.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger});
+  t.deepEqual(verifyConditions2.firstCall.args[1], {options, logger: t.context.logger});
 
   t.true(getLastRelease.calledOnce);
-  t.deepEqual(getLastRelease.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger});
+  t.deepEqual(getLastRelease.firstCall.args[1], {options, logger: t.context.logger});
 
   t.true(analyzeCommits.calledOnce);
-  t.deepEqual(analyzeCommits.firstCall.args[1].env, process.env);
   t.deepEqual(analyzeCommits.firstCall.args[1].options, options);
-  t.deepEqual(analyzeCommits.firstCall.args[1].pkg, pkg);
   t.deepEqual(analyzeCommits.firstCall.args[1].logger, t.context.logger);
   t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
   t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
   t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].message, commits[0].message);
 
   t.true(verifyRelease.calledOnce);
-  t.deepEqual(verifyRelease.firstCall.args[1].env, process.env);
   t.deepEqual(verifyRelease.firstCall.args[1].options, options);
-  t.deepEqual(verifyRelease.firstCall.args[1].pkg, pkg);
   t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger);
   t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
   t.deepEqual(verifyRelease.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@@ -99,9 +89,7 @@ test.serial('Plugins are called with expected values', async t => {
   t.deepEqual(verifyRelease.firstCall.args[1].nextRelease, nextRelease);
 
   t.true(generateNotes.calledOnce);
-  t.deepEqual(generateNotes.firstCall.args[1].env, process.env);
   t.deepEqual(generateNotes.firstCall.args[1].options, options);
-  t.deepEqual(generateNotes.firstCall.args[1].pkg, pkg);
   t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger);
   t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
   t.deepEqual(generateNotes.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@@ -110,7 +98,6 @@ test.serial('Plugins are called with expected values', async t => {
 
   t.true(publish.calledOnce);
   t.deepEqual(publish.firstCall.args[1].options, options);
-  t.deepEqual(publish.firstCall.args[1].pkg, pkg);
   t.deepEqual(publish.firstCall.args[1].logger, t.context.logger);
   t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
   t.deepEqual(publish.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
@@ -140,15 +127,15 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
 
   const options = {
     branch: 'master',
-    verifyConditions: callbackify(stub().resolves()),
-    getLastRelease: callbackify(stub().resolves(lastRelease)),
-    analyzeCommits: callbackify(stub().resolves(nextRelease.type)),
-    verifyRelease: callbackify(stub().resolves()),
-    generateNotes: callbackify(generateNotes),
-    publish: [callbackify(publish1), callbackify(publish2)],
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions: stub().resolves(),
+    getLastRelease: stub().resolves(lastRelease),
+    analyzeCommits: stub().resolves(nextRelease.type),
+    verifyRelease: stub().resolves(),
+    generateNotes,
+    publish: [publish1, publish2],
   };
 
-  await writeJson('./package.json', {});
   await t.context.semanticRelease(options);
 
   t.true(generateNotes.calledTwice);
@@ -173,7 +160,6 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
   // Add new commits to the master branch
   commits = (await gitCommits(['Second'])).concat(commits);
 
-  const name = 'package-name';
   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';
@@ -188,17 +174,14 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
   const options = {
     dryRun: true,
     branch: 'master',
-    verifyConditions: callbackify(verifyConditions),
-    getLastRelease: callbackify(getLastRelease),
-    analyzeCommits: callbackify(analyzeCommits),
-    verifyRelease: callbackify(verifyRelease),
-    generateNotes: callbackify(generateNotes),
-    publish: callbackify(publish),
+    repositoryUrl: 'git@hostname.com:owner/module.git',
+    verifyConditions,
+    getLastRelease,
+    analyzeCommits,
+    verifyRelease,
+    generateNotes,
+    publish,
   };
-  const pkg = {name, version: '0.0.0-dev'};
-  normalizeData(pkg);
-
-  await writeJson('./package.json', pkg);
 
   await t.context.semanticRelease(options);
 
@@ -209,3 +192,25 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
   t.true(generateNotes.calledOnce);
   t.true(publish.notCalled);
 });
+
+test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
+  // Set the current working directory to a temp directory
+  process.chdir(tempy.directory());
+
+  const error = await t.throws(t.context.semanticRelease());
+
+  // Verify error code and type
+  t.is(error.code, 'ENOGITREPO');
+  t.true(error instanceof SemanticReleaseError);
+});
+
+test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be found', async t => {
+  // Create a git repository, set the current working directory at the root of the repo
+  await gitRepo();
+
+  const error = await t.throws(t.context.semanticRelease());
+
+  // Verify error code and type
+  t.is(error.code, 'ENOREPOURL');
+  t.true(error instanceof SemanticReleaseError);
+});
diff --git a/test/integration.test.js b/test/integration.test.js
index fc23e46..954bc70 100644
--- a/test/integration.test.js
+++ b/test/integration.test.js
@@ -7,7 +7,7 @@ import registry from './helpers/registry';
 import mockServer from './helpers/mockserver';
 import semanticRelease from '..';
 
-/* eslint-disable camelcase */
+/* eslint camelcase: ["error", {properties: "never"}] */
 
 // Environment variables used with cli
 const env = {
diff --git a/test/plugins/normalize.test.js b/test/plugins/normalize.test.js
index 67f4c42..7667e03 100644
--- a/test/plugins/normalize.test.js
+++ b/test/plugins/normalize.test.js
@@ -1,4 +1,3 @@
-import {callbackify} from 'util';
 import test from 'ava';
 import {noop} from 'lodash';
 import {stub, match} from 'sinon';
@@ -39,7 +38,7 @@ test('Normalize and load plugin that retuns multiple functions', t => {
 
 test('Wrap plugin in a function that validate the output of the plugin', async t => {
   const pluginFunction = stub().resolves(1);
-  const plugin = normalize('', callbackify(pluginFunction), t.context.logger, {
+  const plugin = normalize('', pluginFunction, t.context.logger, {
     validator: output => output === 1,
     message: 'The output must be 1',
   });
@@ -53,7 +52,7 @@ test('Wrap plugin in a function that validate the output of the plugin', async t
 
 test('Plugin is called with "pluginConfig" (omitting "path") and input', async t => {
   const pluginFunction = stub().resolves();
-  const conf = {path: callbackify(pluginFunction), conf: 'confValue'};
+  const conf = {path: pluginFunction, conf: 'confValue'};
   const plugin = normalize('', conf, t.context.logger);
   await plugin('param');
 
@@ -61,9 +60,8 @@ test('Plugin is called with "pluginConfig" (omitting "path") and input', async t
 });
 
 test('Prevent plugins to modify "pluginConfig"', async t => {
-  const pluginFunction = stub().callsFake((pluginConfig, options, cb) => {
+  const pluginFunction = stub().callsFake(pluginConfig => {
     pluginConfig.conf.subConf = 'otherConf';
-    cb();
   });
   const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
   const plugin = normalize('', conf, t.context.logger);
@@ -73,9 +71,8 @@ test('Prevent plugins to modify "pluginConfig"', async t => {
 });
 
 test('Prevent plugins to modify its input', async t => {
-  const pluginFunction = stub().callsFake((pluginConfig, options, cb) => {
+  const pluginFunction = stub().callsFake((pluginConfig, options) => {
     options.param.subParam = 'otherParam';
-    cb();
   });
   const input = {param: {subParam: 'originalSubParam'}};
   const plugin = normalize('', pluginFunction, t.context.logger);