From 0c67ba517fd6dd42959ee263ad50a6c7c30d57af Mon Sep 17 00:00:00 2001 From: Pierre Vanduynslager Date: Thu, 23 Nov 2017 17:38:33 -0500 Subject: [PATCH] feat: Make semantic-release language agnostic - 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. 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`. --- README.md | 3 +- cli.js | 3 +- index.js | 30 ++++--- lib/get-config.js | 20 +++-- lib/git.js | 18 +++- package.json | 21 +++-- test/get-config.test.js | 182 ++++++++++++++++++++++++++++---------- test/git.test.js | 39 +++++++- test/helpers/git-utils.js | 30 +++++-- test/index.test.js | 54 ++++++----- test/integration.test.js | 2 +- 11 files changed, 292 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index e9ba6c0d..30ac378f 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,9 +207,7 @@ 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. ### `analyzeCommits` diff --git a/cli.js b/cli.js index 82be83ef..60c0d664 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 to release from') + .option('-r, --repositoryUrl ', 'Git repository URL') .option( '--verify-conditions ', '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 4fa04efe..ae22e27e 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 => { - const config = await getConfig(opts, logger); - const {plugins, env, options, pkg} = config; + if (!await isGitRepo()) { + throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO'); + } - logger.log('Run automated release for branch %s', options.branch); + const config = await getConfig(opts, logger); + const {plugins, options} = config; + + if (!options.repositoryUrl) { + throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL'); + } + + 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 77700730..d7fd4c85 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 d4300356..dbff8509 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/package.json b/package.json index 55fa7c41..ab00d634 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 } } } diff --git a/test/get-config.test.js b/test/get-config.test.js index 3e8a5be6..1b2d4d17 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 {options} = await t.context.getConfig(); + + // Verify the default options are set + t.is(options.branch, 'master'); + t.is(options.repositoryUrl, 'git@package.com:owner/module.git'); +}); + +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 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, pkg.repository); }); -test.serial('Read package.json configuration', async t => { +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.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.yml', 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 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.deepEqual(t.context.plugins.firstCall.args[0], release); +}); + +test.serial('Read options from .releaserc.json', 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 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 5d193139..24097936 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 6d81de53..d63c758e 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} 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 d69f8ea0..d0ff9084 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,8 +1,8 @@ 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'; @@ -41,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); @@ -56,6 +54,7 @@ test.serial('Plugins are called with expected values', async t => { const options = { branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', verifyConditions: [verifyConditions1, verifyConditions2], getLastRelease, analyzeCommits, @@ -63,34 +62,26 @@ test.serial('Plugins are called with expected values', async t => { 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); @@ -98,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); @@ -109,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); @@ -139,6 +127,7 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre const options = { branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', verifyConditions: stub().resolves(), getLastRelease: stub().resolves(lastRelease), analyzeCommits: stub().resolves(nextRelease.type), @@ -147,7 +136,6 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre publish: [publish1, publish2], }; - await writeJson('./package.json', {}); await t.context.semanticRelease(options); t.true(generateNotes.calledTwice); @@ -172,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'; @@ -187,6 +174,7 @@ test.serial('Dry-run skips verifyConditions and publish', async t => { const options = { dryRun: true, branch: 'master', + repositoryUrl: 'git@hostname.com:owner/module.git', verifyConditions, getLastRelease, analyzeCommits, @@ -194,10 +182,6 @@ test.serial('Dry-run skips verifyConditions and publish', async t => { generateNotes, publish, }; - const pkg = {name, version: '0.0.0-dev'}; - normalizeData(pkg); - - await writeJson('./package.json', pkg); await t.context.semanticRelease(options); @@ -208,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 fc23e468..954bc700 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 = {