Compare commits

...
16 Commits
Author SHA1 Message Date
Pierre Vanduynslager eec4e68615 test: Isolate environment variables passed to semantic-release
`npm` is called through `semantic-release` and in the integration test to make some assertion.
By isolating the the environment variables passed to `semantic-release` it's easier to avoid a mistake that would pass the test environment that contains is own auth variables.
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 8371a03da0 test: Verify Error instance type and SemanticReleaseError name 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 2491032a99 test: Add test to check unexpected plugin return values 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 02ddf34f21 Fix: Remove redundant test of commitAnalyzer plugin output 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 685959107b chore: Clean up mistakenly added configs 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 613a646363 fix: Typo in error messages 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager d7b323dd00 fix: Accept undefined values for the getLastRelease and generateNotes plugins
Adapt tests and default value to the plugin definitions (`definitions.js`)
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager a7359bffb6 test: Improve docker start/stop script
- Simplify the loop that check availability
- Increase timeout
- Remove the container after stopping
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager fcb832bbe1 fix: remove name from log as it's not an existing option anymore 2017-11-29 18:06:14 -05:00
greenkeeper[bot]andPierre Vanduynslager 89f5e07408 chore(package): update ava to version 0.24.0 2017-11-27 15:22:17 -05:00
Pierre Vanduynslager ce3841f553 test: Ignore the ~/.npmrc file during tests
When the test are run npm read the ~/.npmrc and ./.npmrc files and set some environment variables starting with `npm_`.
When the test create temporary folders and created a `.npmrc` the values there are ignored as the environment variables take precedence.
This commit remove this environment variable (from the local `process.env`) before starting the integrations test and restore them after. This way the `./.npmrc` files created in temp directory for the test are correctly used.
2017-11-25 14:50:44 -05:00
Pierre Vanduynslager 40c58c9b42 ci: Run npm registry in Docker 2017-11-25 14:50:44 -05:00
Pierre Vanduynslager 0c67ba517f 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`.
2017-11-24 21:56:15 -05:00
Pierre Vanduynslager 5bec59b26b feat: Expect plugins to return Promises
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.
2017-11-24 21:56:15 -05:00
Pierre Vanduynslager d4c7605f68 fix: Use default plugin if the path property is missing from a single plugin configuration 2017-11-22 19:47:09 -05:00
Pierre Vanduynslager facdadaddb ci: Lint with XO 2017-11-21 20:55:47 -05:00
37 changed files with 675 additions and 410 deletions
-4
View File
@@ -129,7 +129,3 @@ $RECYCLE.BIN/
package-lock.json
yarn.lock
# Registry tests
test/helpers/registry/couch
test/helpers/registry/data
+3 -1
View File
@@ -1,11 +1,13 @@
language: node_js
services:
- couchdb
- docker
notifications:
email: false
node_js:
- 9
- 8
os:
- linux
# Trigger a push build on caribou and greenkeeper branches + PRs build on every branches
# Avoid double build on PRs (See https://github.com/travis-ci/travis-ci/issues/1147)
+1 -4
View File
@@ -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`
+2 -1
View File
@@ -3,6 +3,7 @@
// Bad news: We have to write plain ES5 in this file
// Good news: It's the only file of the entire project
// eslint-disable-next-line no-var
var semver = require('semver');
if (semver.lt(process.version, '8.0.0')) {
@@ -22,7 +23,7 @@ npx is bundled with npm >= 5.4, or available via npm. More info: npm.im/npx`
process.exit(1);
}
// node 8+ from this point on
// Node 8+ from this point on
require('../cli')().catch(() => {
process.exitCode = 1;
});
+2 -1
View File
@@ -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
+19 -11
View File
@@ -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.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);
}
+2 -1
View File
@@ -18,6 +18,7 @@ const getVersionHead = require('./get-version-head');
* @typedef {Object} LastRelease
* @property {string} version The version number of the last release.
* @property {string} [gitHead] The commit sha used to make the last release.
* @property {string} [gitTag] The tag used to make the last release.
*/
/**
@@ -45,7 +46,7 @@ const getVersionHead = require('./get-version-head');
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `branch`.
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`.
*/
module.exports = async ({version, gitHead}, branch, logger) => {
module.exports = async ({version, gitHead} = {}, branch, logger) => {
let gitTag;
if (gitHead || version) {
try {
+14 -6
View File
@@ -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;
}
+4 -8
View File
@@ -1,17 +1,13 @@
const semver = require('semver');
const SemanticReleaseError = require('@semantic-release/error');
module.exports = (type, lastRelease, logger) => {
let version;
if (!lastRelease.version) {
if (lastRelease.version) {
version = semver.inc(lastRelease.version, type);
logger.log('The next release version is %s', version);
} else {
version = '1.0.0';
logger.log('There is no previous release, the next release version is %s', version);
} else {
version = semver.inc(lastRelease.version, type);
if (!version) {
throw new SemanticReleaseError(`Invalid release type ${type}`, 'EINVALIDTYPE');
}
logger.log('The next release version is %s', version);
}
return version;
+17 -1
View File
@@ -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};
+9 -15
View File
@@ -1,16 +1,12 @@
const {isString, isObject, isFunction, isArray} = require('lodash');
const semver = require('semver');
const conditionTravis = require('@semantic-release/condition-travis');
const commitAnalyzer = require('@semantic-release/commit-analyzer');
const releaseNotesGenerator = require('@semantic-release/release-notes-generator');
const npm = require('@semantic-release/npm');
const github = require('@semantic-release/github');
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
module.exports = {
verifyConditions: {
default: [npm.verifyConditions, github.verifyConditions, conditionTravis],
default: ['@semantic-release/npm', '@semantic-release/github', '@semantic-release/condition-travis'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
@@ -18,7 +14,7 @@ module.exports = {
},
},
getLastRelease: {
default: npm.getLastRelease,
default: '@semantic-release/npm',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
message:
@@ -34,7 +30,7 @@ module.exports = {
},
},
analyzeCommits: {
default: commitAnalyzer,
default: '@semantic-release/commit-analyzer',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
message:
@@ -42,7 +38,7 @@ module.exports = {
},
output: {
validator: output => !output || RELEASE_TYPE.includes(output),
message: 'The "analyzeCommits" plugin output must be either undefined or a valid semver release type.',
message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.',
},
},
verifyRelease: {
@@ -54,19 +50,19 @@ module.exports = {
},
},
generateNotes: {
default: releaseNotesGenerator,
default: '@semantic-release/release-notes-generator',
config: {
validator: conf => !conf || validatePluginConfig(conf),
message:
'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
},
output: {
validator: output => isString(output),
message: 'The "generateNotes" plugin output must be a string.',
validator: output => !output || isString(output),
message: 'The "generateNotes" plugin output, if defined, must be a string.',
},
},
publish: {
default: [npm.publish, github.publish],
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
@@ -74,5 +70,3 @@ module.exports = {
},
},
};
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
+5 -1
View File
@@ -1,4 +1,4 @@
const {isArray} = require('lodash');
const {isArray, isObject} = require('lodash');
const DEFINITIONS = require('./definitions');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
@@ -8,6 +8,10 @@ module.exports = (options, logger) =>
const {config, output, default: def} = DEFINITIONS[pluginType];
let pluginConfs;
if (options[pluginType]) {
// If an object is passed and the path is missing, set the default one for single plugins
if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
options[pluginType].path = def;
}
if (config && !config.validator(options[pluginType])) {
throw new Error(config.message);
}
+4 -4
View File
@@ -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}.`
@@ -27,7 +27,7 @@ module.exports = (pluginType, pluginConfig, logger, validator) => {
const result = await func(cloneDeep(input));
if (validator && !validator.validator(result)) {
throw new Error(`${validator.message}. Received: ${inspect(result)}`);
throw new Error(`${validator.message} Received: ${inspect(result)}`);
}
return result;
};
+28 -33
View File
@@ -15,68 +15,53 @@
}
},
"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": {
"ava": "^0.23.0",
"ava": "^0.24.0",
"codecov": "^3.0.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"dockerode": "^2.5.2",
"eslint": "^4.7.0",
"eslint-config-prettier": "^2.5.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-prettier": "^2.3.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"file-url": "^2.0.2",
"fs-extra": "^4.0.2",
"got": "^8.0.0",
"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",
"p-retry": "^1.0.0",
"prettier": "~1.8.0",
"proxyquire": "^1.8.0",
"rimraf": "^2.5.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1"
"tempy": "^0.2.1",
"xo": "^0.18.2"
},
"engines": {
"node": ">=4",
"npm": ">=2"
},
"eslintConfig": {
"extends": [
"standard",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
},
"files": [
"bin",
"lib",
@@ -127,12 +112,22 @@
"url": "git+https://github.com/semantic-release/semantic-release.git"
},
"scripts": {
"clean": "rimraf coverage && rimraf .nyc_output",
"cm": "git-cz",
"codecov": "codecov -f coverage/coverage-final.json",
"lint": "eslint index.js cli.js lib test",
"pretest": "npm run clean && npm run lint",
"lint": "xo",
"pretest": "npm run lint",
"semantic-release": "./bin/semantic-release.js",
"test": "nyc ava -v"
},
"xo": {
"extends": [
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
}
}
+6 -19
View File
@@ -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: () => {},
};
+2 -2
View File
@@ -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');
};
+2 -2
View File
@@ -1,5 +1,5 @@
module.exports = function(config, options, cb) {
module.exports = () => {
const error = new Error('a');
error.errorProperty = 'errorProperty';
cb(error);
throw error;
};
+1 -3
View File
@@ -1,3 +1 @@
module.exports = (config, options, cb) => {
cb(null);
};
module.exports = () => {};
+1 -3
View File
@@ -1,3 +1 @@
module.exports = function(pluginConfig, options, cb) {
cb(null, {pluginConfig, options});
};
module.exports = (pluginConfig, options) => ({pluginConfig, options});
+6 -7
View File
@@ -1,6 +1,6 @@
import test from 'ava';
import {stub} from 'sinon';
import SemanticReleaseError from '@semantic-release/error';
import getCommits from '../lib/get-commits';
import {
gitRepo,
gitCommits,
@@ -11,7 +11,6 @@ import {
gitLog,
gitDetachedHead,
} from './helpers/git-utils';
import getCommits from '../lib/get-commits';
test.beforeEach(t => {
// Save the current working diretory
@@ -409,7 +408,7 @@ test.serial('Throws ENOGITHEAD error if the gitHead of the last release cannot b
// Verify error code and type
t.is(error.code, 'ENOGITHEAD');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message explaining the error
t.regex(
t.context.error.firstCall.args[0],
@@ -428,7 +427,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in history', async t =
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -453,7 +452,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in branch history but
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -482,7 +481,7 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in detached head but p
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -508,7 +507,7 @@ test.serial('Throws ENOTINHISTORY error when a tag is not in branch history but
const error = await t.throws(getCommits({version: '1.0.0'}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
+136 -46
View File
@@ -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);
});
-7
View File
@@ -1,6 +1,5 @@
import test from 'ava';
import {stub} from 'sinon';
import SemanticReleaseError from '@semantic-release/error';
import getNextVersion from '../lib/get-next-version';
test.beforeEach(t => {
@@ -28,9 +27,3 @@ test('Return 1.0.0 if there is no previous release', t => {
const version = getNextVersion('minor', {}, t.context.logger);
t.is(version, '1.0.0');
});
test('Return an error if the release type is invalid', t => {
const error = t.throws(() => getNextVersion('invalid', {version: '1.0.0'}, t.context.logger));
t.is(error.code, 'EINVALIDTYPE');
t.true(error instanceof SemanticReleaseError);
});
+40 -5
View File
@@ -1,6 +1,15 @@
import test from 'ava';
import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitLog} from './helpers/git-utils';
import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead} from '../lib/git';
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
@@ -27,7 +36,7 @@ test.serial('Throw error if the last commit sha cannot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await t.throws(gitHead());
await t.throws(gitHead(), Error);
});
test.serial('Unshallow repository', async t => {
@@ -74,7 +83,7 @@ test.serial('Get the tag associated with a commit sha or "null" if the commit do
// 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']);
const commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
@@ -86,10 +95,36 @@ test.serial('Get the commit sha for a given tag or "null" if the tag does not ex
// 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']);
const commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
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);
});
+25 -9
View File
@@ -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();
}
/**
@@ -87,14 +93,14 @@ export async function gitTagVersion(tagName, sha) {
* @return {Array<string>} The list of tags from the current git repository.
*/
export async function gitTags() {
return (await execa('git', ['tag'])).stdout.split('\n').filter(tag => !!tag);
return (await execa('git', ['tag'])).stdout.split('\n').filter(tag => Boolean(tag));
}
/**
* @return {Array<string>} The list of commit sha from the current git repository.
*/
export async function gitLog() {
return (await execa('git', ['log', '--format=format:%H'])).stdout.split('\n').filter(sha => !!sha);
return (await execa('git', ['log', '--format=format:%H'])).stdout.split('\n').filter(sha => Boolean(sha));
}
/**
@@ -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]);
}
+1 -1
View File
@@ -7,7 +7,7 @@ import nock from 'nock';
* @param {String} [githubUrl='https://api.github.com'] The url on which to intercept http requests.
* @return {Object} A `nock` object ready to respond to a github authentication request.
*/
export function authenticate(
export default function authenticate(
{githubToken = 'GH_TOKEN', githubUrl = 'https://api.github.com', githubApiPathPrefix = ''} = {}
) {
return nock(`${githubUrl}/${githubApiPathPrefix}`, {reqheaders: {Authorization: `token ${githubToken}`}});
+21 -5
View File
@@ -1,7 +1,10 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import pRetry from 'p-retry';
import {mockServerClient} from 'mockserver-client';
const IMAGE = 'jamesdbloom/mockserver:latest';
const MOCK_SERVER_PORT = 1080;
const MOCK_SERVER_HOST = 'localhost';
const docker = new Docker();
@@ -13,22 +16,35 @@ let container;
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull('jamesdbloom/mockserver:latest'));
await getStream(await docker.pull(IMAGE));
container = await docker.createContainer({
Image: 'jamesdbloom/mockserver',
Tty: true,
Image: IMAGE,
PortBindings: {[`${MOCK_SERVER_PORT}/tcp`]: [{HostPort: `${MOCK_SERVER_PORT}`}]},
});
return container.start();
await container.start();
try {
// Wait for the mock server to be ready
await pRetry(() => got.put(`http://${MOCK_SERVER_HOST}:${MOCK_SERVER_PORT}/status`, {cache: false}), {
retries: 7,
minTimeout: 1000,
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start mock-server after 2 min`);
}
}
/**
* Stop the `mockserver` Docker container.
* Stop and remote the `mockserver` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
return container.stop();
await container.stop();
await container.remove();
}
/**
+78
View File
@@ -0,0 +1,78 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import pRetry from 'p-retry';
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
const SERVER_PORT = 15986;
const COUCHDB_PORT = 5984;
const SERVER_HOST = 'localhost';
const NPM_USERNAME = 'integration';
const NPM_PASSWORD = 'suchsecure';
const NPM_EMAIL = 'integration@test.com';
const docker = new Docker();
let container;
/**
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
*
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
container = await docker.createContainer({
Tty: true,
Image: IMAGE,
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
});
await container.start();
try {
// Wait for the registry to be ready
await pRetry(() => got(`http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app`, {cache: false}), {
retries: 7,
minTimeout: 1000,
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
}
// Create user
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
json: true,
auth: 'admin:admin',
method: 'PUT',
body: {
_id: `org.couchdb.user:${NPM_USERNAME}`,
name: NPM_USERNAME,
roles: [],
type: 'user',
password: NPM_PASSWORD,
email: NPM_EMAIL,
},
});
}
const url = `http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app/_rewrite/`;
const authEnv = {
npm_config_registry: url, // eslint-disable-line camelcase
NPM_USERNAME,
NPM_PASSWORD,
NPM_EMAIL,
};
/**
* Stop and remote the `npm-docker-couchdb` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
await container.stop();
await container.remove();
}
export default {start, stop, authEnv, url};
-14
View File
@@ -1,14 +0,0 @@
import execa from 'execa';
const opts = {cwd: __dirname};
const uri = 'http://localhost:' + (process.env.TRAVIS === 'true' ? 5984 : 15986) + '/registry/_design/app/_rewrite/';
function start() {
return execa('./start.sh', opts);
}
function stop() {
return execa('./stop.sh', opts);
}
export default {start, stop, uri};
-20
View File
@@ -1,20 +0,0 @@
[couchdb]
database_dir = data
view_index_dir = data
delayed_commits = false
uuid = bf4ecd84a7c89d60b5b2540fdf8c322c
[couch_httpd_auth]
public_fields = appdotnet, avatar, avatarMedium, avatarLarge, date, email, fields, freenode, fullname, github, homepage, name, roles, twitter, type, _id, _rev
users_db_public = true
[httpd]
port = 15986
bind_address = 127.0.0.1
secure_rewrites = false
[log]
file = couch/couch.log
[admins]
admin = -pbkdf2-89582b49cd2e8443e29a841f5a76d367956a8e58,1afa2f1531a17ac97f2ac9e334293753,10
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
# exit if an error occurs
set -e
cd $(dirname $0)
mkdir -p couch
if [[ $TRAVIS = true ]]
then
COUCH=http://admin:password@127.0.0.1:5984
curl -X PUT http://127.0.0.1:5984/_config/admins/admin -d '"password"'
curl -X PUT $COUCH/_config/couchdb/delayed_commits -d '"false"'
curl -X PUT $COUCH/_config/couch_httpd_auth/users_db_public -d '"true"'
curl -X PUT $COUCH/_config/couch_httpd_auth/public_fields -d '"appdotnet, avatar, avatarMedium, avatarLarge, date, email, fields, freenode, fullname, github, homepage, name, roles, twitter, type, _id, _rev"'
curl -X PUT $COUCH/_config/httpd/secure_rewrites -d '"false"'
else
COUCH=http://admin:password@127.0.0.1:15986
couchdb -b -a local.ini -p couch/pid -o couch/stdout.log -e couch/stderr.log
# wait for couch to start
sleep 1
fi
# create "registry" database
curl -X PUT $COUCH/registry
# create sample npm user
curl -X PUT $COUCH/_users/org.couchdb.user:integration -H Content-Type:application/json --data-binary '{"_id": "org.couchdb.user:integration", "name": "integration", "roles": [], "type": "user", "password": "suchsecure", "email": "integration@test.com"}'
# npm-registry-couchpp needs this variable set to run
export DEPLOY_VERSION=nope
# setup npm-registry-couchapp
npm explore npm-registry-couchapp -- npm start --npm-registry-couchapp:couch=$COUCH/registry
npm explore npm-registry-couchapp -- npm run load --npm-registry-couchapp:couch=$COUCH/registry
export NO_PROMPT=yes
npm explore npm-registry-couchapp -- npm run copy --npm-registry-couchapp:couch=$COUCH/registry
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
# close couchdb background process
couchdb -d
# delete data and logs
cd $(dirname $0)
cat couch/{couch,stdout,stderr}.log
cat couch/pid | xargs kill
rm -rf couch
rm -rf data
+123 -43
View File
@@ -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 DEFINITIONS from '../lib/plugins/definitions';
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,100 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
t.true(generateNotes.calledOnce);
t.true(publish.notCalled);
});
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();
// Add commits to the master branch
await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
const lastRelease = {gitHead: undefined, gitTag: undefined, version: undefined};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
await t.context.semanticRelease(options);
t.true(getLastRelease.calledOnce);
t.true(analyzeCommits.calledOnce);
t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
t.true(verifyRelease.calledOnce);
t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
t.true(generateNotes.calledOnce);
t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
t.true(publish.calledOnce);
t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
t.falsy(publish.firstCall.args[1].nextRelease.notes);
});
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.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot 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.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw an Error if returns an unexpected value', 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
await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves('string');
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
};
const error = await t.throws(t.context.semanticRelease(options), Error);
// Verify error message
t.regex(error.message, new RegExp(DEFINITIONS.getLastRelease.output.message));
t.regex(error.message, /Received: 'string'/);
});
+85 -59
View File
@@ -3,58 +3,83 @@ import {writeJson, readJson} from 'fs-extra';
import {stub} from 'sinon';
import execa from 'execa';
import {gitRepo, gitCommits, gitHead, gitTagVersion, gitPackRefs, gitAmmendCommit} from './helpers/git-utils';
import registry from './helpers/registry';
import mockServer from './helpers/mockserver';
import npmRegistry from './helpers/npm-registry';
import semanticRelease from '..';
// Environment variables used with cli
/* eslint camelcase: ["error", {properties: "never"}] */
// Environment variables used with semantic-release cli (similar to what a user would setup)
const env = {
npm_config_registry: registry.uri,
GH_TOKEN: 'github_token',
GITHUB_URL: mockServer.url,
NPM_EMAIL: 'integration@test.com',
NPM_USERNAME: 'integration',
NPM_PASSWORD: 'suchsecure',
};
// Environment variables used only for the local npm command used to do verification
const testEnv = Object.assign({}, process.env, {
npm_config_registry: npmRegistry.url,
NPM_EMAIL: 'integration@test.com',
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
});
const cli = require.resolve('../bin/semantic-release');
const pluginError = require.resolve('./fixtures/plugin-error');
const pluginInheritedError = require.resolve('./fixtures/plugin-error-inherited');
test.before(async t => {
test.before(async () => {
// Start the local NPM registry
await npmRegistry.start();
// Start Mock Server
await mockServer.start();
// Start the local NPM registry
await registry.start();
});
test.beforeEach(async t => {
test.beforeEach(t => {
// Save the current process.env
t.context.env = Object.assign({}, process.env);
// Save the current working diretory
t.context.cwd = process.cwd();
// Delete env paramaters that could have been set on the machine running the tests
delete process.env.NPM_TOKEN;
delete process.env.NPM_USERNAME;
delete process.env.NPM_PASSWORD;
delete process.env.NPM_EMAIL;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GH_URL;
delete process.env.GITHUB_URL;
delete process.env.GH_PREFIX;
delete process.env.GITHUB_PREFIX;
// Delete all `npm_config` environment variable set by CI as they take precedence over the `.npmrc` because the process that runs the tests is started before the `.npmrc` is created
for (let i = 0, keys = Object.keys(process.env); i < keys.length; i++) {
if (keys[i].startsWith('npm_config')) {
delete process.env[keys[i]];
}
}
// Disable logs during tests
t.context.log = stub(console, 'log');
t.context.error = stub(console, 'error');
t.context.stdout = stub(process.stdout, 'write');
t.context.stderr = stub(process.stderr, 'write');
});
test.afterEach.always(async t => {
test.afterEach.always(t => {
// Restore process.env
process.env = Object.assign({}, t.context.env);
// Restore the current working directory
process.chdir(t.context.cwd);
// Restore the logs
t.context.log.restore();
t.context.error.restore();
t.context.stdout.restore();
t.context.stderr.restore();
});
test.after.always(async t => {
await mockServer.stop();
test.after.always(async () => {
// Stop the local NPM registry
await registry.stop();
await npmRegistry.stop();
// Stop Mock Server
await mockServer.stop();
});
test.serial('Release patch, minor and major versions', async t => {
@@ -69,12 +94,12 @@ test.serial('Release patch, minor and major versions', async t => {
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: registry.uri},
publishConfig: {registry: npmRegistry.url},
});
// Create a npm-shrinkwrap.json file
await execa('npm', ['shrinkwrap'], {env});
await execa('npm', ['shrinkwrap'], {env: testEnv});
/** No release **/
/* No release */
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -88,7 +113,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.regex(stdout, /There are no relevant changes, so no new version is released/);
t.is(code, 0);
/** Initial release **/
/* Initial release */
let version = '1.0.0';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -123,7 +148,7 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
@@ -133,7 +158,7 @@ test.serial('Release patch, minor and major versions', async t => {
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Patch release **/
/* Patch release */
version = '1.0.1';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -168,7 +193,7 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
@@ -178,7 +203,7 @@ test.serial('Release patch, minor and major versions', async t => {
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Minor release **/
/* Minor release */
version = '1.1.0';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -213,7 +238,7 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
@@ -223,7 +248,7 @@ test.serial('Release patch, minor and major versions', async t => {
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Major release **/
/* Major release */
version = '2.0.0';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -258,7 +283,7 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
@@ -282,10 +307,10 @@ test.serial('Release versions from a packed git repository, using tags to determ
version: '0.0.0-dev',
repository: {url: `git@github.com:${owner}/${packageName}.git`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: registry.uri},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
let version = '1.0.0';
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -317,7 +342,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
// Verify package.json has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version
let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
@@ -327,7 +352,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
await gitTagVersion(`v${version}`);
t.log(`Create git tag v${version}`);
/** Patch release **/
/* Patch release */
version = '1.0.1';
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -358,7 +383,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
@@ -397,10 +422,10 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: registry.uri},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
let version = '1.0.0';
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
@@ -432,7 +457,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
const head = await gitHead();
t.is(releasedGitHead, head);
@@ -446,12 +471,12 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
await gitTagVersion(`v${version}`);
t.log(`Create git tag v${version}`);
/** Rewrite sha of commit used for release **/
/* Rewrite sha of commit used for release */
t.log('Amend release commit');
const {hash} = await gitAmmendCommit('feat: Initial commit');
/** Patch release **/
/* Patch release */
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
@@ -473,7 +498,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
)
);
/** Create a tag to recover and redo release **/
/* Create a tag to recover and redo release */
t.log(`Create git tag v${version} to recover`);
await gitTagVersion(`v${version}`, hash);
@@ -507,7 +532,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
@@ -526,14 +551,15 @@ test.serial('Dry-run', async t => {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release -d');
let {stdout, code} = await execa(cli, ['-d'], {env});
const {stdout, code} = await execa(cli, ['-d'], {env});
t.regex(stdout, new RegExp(`There is no previous release, the next release version is ${version}`));
t.regex(stdout, new RegExp(`Release note for version ${version}`));
t.regex(stdout, /Initial commit/);
@@ -554,15 +580,15 @@ test.serial('Pass options via CLI arguments', async t => {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: registry.uri},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
let version = '1.0.0';
/* Initial release */
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stdout, code} = await execa(
const {stdout, code} = await execa(
cli,
['--verify-conditions', '@semantic-release/npm', '--publish', '@semantic-release/npm', '--debug'],
{env}
@@ -574,8 +600,8 @@ test.serial('Pass options via CLI arguments', async t => {
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
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());
@@ -594,22 +620,22 @@ test.serial('Run via JS API', async t => {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: registry.uri},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
let version = '1.0.0';
let verifyMock = await mockServer.mock(
/* Initial release */
const version = '1.0.0';
const verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${githubToken}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let createRefMock = await mockServer.mock(
const createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${githubToken}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = await mockServer.mock(
const createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
@@ -636,8 +662,8 @@ test.serial('Run via JS API', async t => {
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
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());
@@ -662,11 +688,11 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
release: {verifyConditions: pluginError},
});
/** Initial release **/
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stderr, code} = await execa(cli, [], {env, reject: false});
const {stderr, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stderr, /Error: a/);
// Verify the the stacktrace is logged
@@ -690,11 +716,11 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async
release: {verifyConditions: pluginInheritedError},
});
/** Initial release **/
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stdout, code} = await execa(cli, [], {env, reject: false});
const {stdout, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.is(code, 0);
@@ -702,21 +728,21 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async
test.serial('CLI returns error code and prints help if called with a command', async t => {
t.log('$ semantic-release pre');
let {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
const {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 1);
});
test.serial('CLI prints help if called with --help', async t => {
t.log('$ semantic-release --help');
let {stdout, code} = await execa(cli, ['--help'], {env});
const {stdout, code} = await execa(cli, ['--help'], {env});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 0);
});
test.serial('CLI returns error code with invalid option', async t => {
t.log('$ semantic-release --unknown-option');
let {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
const {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
t.regex(stderr, /unknown option/);
t.is(code, 1);
});
+3 -3
View File
@@ -89,12 +89,12 @@ test('The "analyzeCommits" plugin output must be either undefined or a valid sem
t.true(definitions.analyzeCommits.output.validator('major'));
});
test('The "generateNotes" plugin output must be a string', t => {
t.false(definitions.generateNotes.output.validator());
t.false(definitions.generateNotes.output.validator(null));
test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.false(definitions.generateNotes.output.validator(1));
t.false(definitions.generateNotes.output.validator({}));
t.true(definitions.generateNotes.output.validator());
t.true(definitions.generateNotes.output.validator(null));
t.true(definitions.generateNotes.output.validator(''));
t.true(definitions.generateNotes.output.validator('string'));
});
+9 -12
View File
@@ -1,4 +1,3 @@
import {callbackify} from 'util';
import test from 'ava';
import {noop} from 'lodash';
import {stub, match} from 'sinon';
@@ -39,21 +38,21 @@ 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',
message: 'The output must be 1.',
});
await t.notThrows(plugin());
pluginFunction.resolves(2);
const error = await t.throws(plugin());
const error = await t.throws(plugin(), Error);
t.is(error.message, 'The output must be 1. Received: 2');
});
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);
@@ -84,7 +81,7 @@ test('Prevent plugins to modify its input', async t => {
t.is(input.param.subParam, 'originalSubParam');
});
test('Return noop if the plugin is not defined', async t => {
test('Return noop if the plugin is not defined', t => {
const plugin = normalize();
t.is(plugin, noop);
@@ -106,8 +103,8 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Throws an error if the plugin return an object without the expected plugin function', async t => {
const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger));
test('Throws an error if the plugin return an object without the expected plugin function', t => {
const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger), Error);
t.is(
error.message,
+1 -1
View File
@@ -32,7 +32,7 @@ test('Stop execution and throw error is a step rejects', async t => {
const step2 = stub().throws(new Error('test error'));
const step3 = stub().resolves(3);
const error = await t.throws(pipeline([step1, step2, step3])(0));
const error = await t.throws(pipeline([step1, step2, step3])(0), Error);
t.is(error.message, 'test error');
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
+25 -4
View File
@@ -9,8 +9,8 @@ test.beforeEach(t => {
});
test('Export default plugins', t => {
// Call the plugin module
const plugins = getPlugins({}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
@@ -21,7 +21,6 @@ test('Export default plugins', t => {
});
test('Export plugins based on config', t => {
// Call the plugin module
const plugins = getPlugins(
{
verifyConditions: ['./test/fixtures/plugin-noop', {path: './test/fixtures/plugin-noop'}],
@@ -31,6 +30,7 @@ test('Export plugins based on config', t => {
},
t.context.logger
);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
@@ -40,8 +40,29 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.publish, 'function');
});
test('Throw an error if plugin configuration is invalid', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger));
test('Use default when only options are passed for a single plugin', t => {
const plugins = getPlugins({getLastRelease: {}, analyzeCommits: {}}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
});
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error);
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
);
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(
() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger),
Error
);
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'