Compare commits

...
17 Commits
Author SHA1 Message Date
Pierre Vanduynslager 4053d8f7fb fix: log plugin type in addition of path 2017-12-10 04:03:32 -05:00
greenkeeper[bot]andPierre Vanduynslager 88f33ec482 chore(package): update mockserver-client to version 5.1.1 2017-12-06 16:29:19 -05:00
Pierre Vanduynslager 0761ef378a style: update to prettier@1.9.0 2017-12-05 10:03:18 -05:00
greenkeeper[bot]andPierre Vanduynslager f30fb6c8af chore(package): update prettier to version 1.9.0 2017-12-05 10:03:18 -05:00
Hutson BettsandGregor Martynus 0113db2e06 docs(README): add node support policy
Add a _Node Support Policy_ section to the project's `README.md` file
to indicate what this project, and its core team, are promising in terms
of Node runtime support.

Establishing a support policy provides reassurance to the community that
they can expect a level of functionality from `semantic-release`, while
providing guidance to the core maintainers, and everyone else that
contributes, the level of support they should espire to.

Closes #485
2017-12-01 15:39:21 -08:00
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
24 changed files with 287 additions and 185 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)
+16
View File
@@ -287,6 +287,8 @@ Being able to write code for just the most recent node versions greatly simplifi
For a special purpose tool like `semantic-release`, that's only meant to be used in controlled CI environments, we think it's okay to have such a high version requirement. As `semantic-release` handles package publishing we expect almost every project to have at least one build job running node 8 already – and that's all it takes. Even if that's not that case `semantic-release` can still be executed with the help of [npx](https://www.npmjs.com/package/npx) (`npx -p node@8 npm run semantic-release`).
Please see our [Node Support Policy](#node-support-policy) for our long-term promise for supporting Node.
## Badge
Use this in one of your projects? Include one of these badges in your README.md to let people know that your package is published using `semantic-release`.
@@ -309,6 +311,20 @@ Use this in one of your projects? Include one of these badges in your README.md
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
```
## Node Support Policy
We only support [Long-Term Support](https://github.com/nodejs/Release) versions of Node starting with [Node 8.9.0 (LTS)](https://nodejs.org/en/blog/release/v8.9.0/).
We specifically limit our support to LTS versions of Node, not because this package won't work on other versions, but because we have a limited amount of time, and supporting LTS offers the greatest return on that investment.
It's possible this package will work correctly on newer versions of Node. It may even be possible to use this package on older versions of Node, though that's more unlikely as we'll make every effort to take advantage of features available in the oldest LTS version we support.
As each Node LTS version reaches its end-of-life we will remove that version from the node engines property of our package's package.json file. Removing a Node version is considered a breaking change and will entail the publishing of a new major version of this package. We will not accept any requests to support an end-of-life version of Node. Any merge requests or issues supporting an end-of-life version of Node will be closed.
We will accept code that allows this package to run on newer, non-LTS, versions of Node. Furthermore, we will attempt to ensure our own changes work on the latest version of Node. To help in that commitment, our continuous integration setup runs against all LTS versions of Node in addition the most recent Node release; called current.
JavaScript package managers should allow you to install this package with any version of Node, with, at most, a warning if your version of Node does not fall within the range specified by our node engines property. If you encounter issues installing this package, please report the issue to your package manager.
## License
MIT License
+1 -1
View File
@@ -19,7 +19,7 @@ module.exports = async opts => {
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
}
logger.log('Run automated release from branch %s', options.name, options.branch);
logger.log('Run automated release from branch %s', options.branch);
if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions');
+5 -10
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 {
@@ -83,24 +84,18 @@ function noGitHeadMessage(branch, version) {
This means semantic-release can not extract the commits between now and then.
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
You can recover from this error by creating a tag for the version "${
version
}" on the commit corresponding to this release:
You can recover from this error by creating a tag for the version "${version}" on the commit corresponding to this release:
$ git tag -f v${version} <commit sha1 corresponding to last release>
$ git push -f --tags origin ${branch}
`;
}
function notInHistoryMessage(gitHead, branch, version) {
return `The commit the last release of this package was derived from is not in the direct history of the "${
branch
}" branch.
return `The commit the last release of this package was derived from is not in the direct history of the "${branch}" branch.
This means semantic-release can not extract the commits between now and then.
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
You can recover from this error by restoring the commit "${gitHead}" or by creating a tag for the version "${
version
}" on the commit corresponding to this release:
You can recover from this error by restoring the commit "${gitHead}" or by creating a tag for the version "${version}" on the commit corresponding to this release:
$ git tag -f v${version || '<version>'} <commit sha1 corresponding to last release>
$ git push -f --tags origin ${branch}
`;
-4
View File
@@ -1,13 +1,9 @@
const semver = require('semver');
const SemanticReleaseError = require('@semantic-release/error');
module.exports = (type, lastRelease, logger) => {
let version;
if (lastRelease.version) {
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);
} else {
version = '1.0.0';
+3 -3
View File
@@ -38,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: {
@@ -57,8 +57,8 @@ module.exports = {
'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: {
+2 -2
View File
@@ -8,7 +8,7 @@ module.exports = (pluginType, pluginConfig, logger, validator) => {
}
const {path, ...config} = isString(pluginConfig) || isFunction(pluginConfig) ? {path: pluginConfig} : pluginConfig;
if (!isFunction(pluginConfig)) {
logger.log('Load plugin %s', path);
logger.log('Load plugin %s from %s', pluginType, path);
}
const plugin = isFunction(path) ? path : importFrom.silent(__dirname, path) || importFrom(process.cwd(), path);
@@ -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;
};
+6 -6
View File
@@ -37,7 +37,7 @@
"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",
@@ -46,12 +46,13 @@
"eslint-plugin-prettier": "^2.3.0",
"file-url": "^2.0.2",
"fs-extra": "^4.0.2",
"got": "^8.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^2.0.0",
"mockserver-client": "^5.1.1",
"nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12",
"nyc": "^11.2.1",
"prettier": "~1.8.0",
"p-retry": "^1.0.0",
"prettier": "~1.9.0",
"proxyquire": "^1.8.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1",
@@ -126,8 +127,7 @@
"prettier"
],
"rules": {
"prettier/prettier": 2,
"no-duplicate-imports": 2
"prettier/prettier": 2
}
}
}
+5 -6
View File
@@ -1,6 +1,5 @@
import test from 'ava';
import {stub} from 'sinon';
import SemanticReleaseError from '@semantic-release/error';
import getCommits from '../lib/get-commits';
import {
gitRepo,
@@ -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
-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);
});
+1 -1
View File
@@ -36,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 => {
+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
+79 -4
View File
@@ -2,7 +2,7 @@ import test from 'ava';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import tempy from 'tempy';
import SemanticReleaseError from '@semantic-release/error';
import DEFINITIONS from '../lib/plugins/definitions';
import {gitHead as getGitHead} from '../lib/git';
import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils';
@@ -193,6 +193,54 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
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());
@@ -201,10 +249,10 @@ test.serial('Throw SemanticReleaseError if not running from a git repository', a
// Verify error code and type
t.is(error.code, 'ENOGITREPO');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be found', async t => {
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();
@@ -212,5 +260,32 @@ test.serial('Throw SemanticReleaseError if repositoryUrl is not set and canot be
// Verify error code and type
t.is(error.code, 'ENOREPOURL');
t.true(error instanceof SemanticReleaseError);
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'/);
});
+50 -29
View File
@@ -3,30 +3,35 @@ 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 '..';
/* eslint camelcase: ["error", {properties: "never"}] */
// Environment variables used with cli
// 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 () => {
// Start the local NPM registry
await npmRegistry.start();
// Start Mock Server
await mockServer.start();
// Start the local NPM registry
await registry.start();
});
test.beforeEach(t => {
@@ -34,7 +39,24 @@ test.beforeEach(t => {
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');
@@ -46,7 +68,7 @@ test.afterEach.always(t => {
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();
@@ -54,9 +76,10 @@ test.afterEach.always(t => {
});
test.after.always(async () => {
await mockServer.stop();
// 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 => {
@@ -71,10 +94,10 @@ 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 */
@@ -125,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());
@@ -170,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());
@@ -215,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());
@@ -260,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());
@@ -284,7 +307,7 @@ 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 */
@@ -319,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);
@@ -360,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);
@@ -399,7 +422,7 @@ 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 */
@@ -434,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);
@@ -469,9 +492,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.regex(
stderr,
new RegExp(
`You can recover from this error by restoring the commit "${head}" or by creating a tag for the version "${
version
}" on the commit corresponding to this release`
`You can recover from this error by restoring the commit "${head}" or by creating a tag for the version "${version}" on the commit corresponding to this release`
)
);
@@ -509,7 +530,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);
@@ -528,7 +549,7 @@ test.serial('Dry-run', 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 */
@@ -557,7 +578,7 @@ 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 */
@@ -578,7 +599,7 @@ test.serial('Pass options via CLI arguments', async t => {
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
@@ -597,7 +618,7 @@ 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 */
@@ -640,7 +661,7 @@ test.serial('Run via JS API', async t => {
// Retrieve the published package from the registry and check version and gitHead
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
+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'));
});
+8 -8
View File
@@ -10,17 +10,17 @@ test.beforeEach(t => {
});
test('Normalize and load plugin from string', t => {
const plugin = normalize('', './test/fixtures/plugin-noop', t.context.logger);
const plugin = normalize('verifyConditions', './test/fixtures/plugin-noop', t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-noop'));
t.true(t.context.log.calledWith(match.string, 'verifyConditions', './test/fixtures/plugin-noop'));
});
test('Normalize and load plugin from object', t => {
const plugin = normalize('', {path: './test/fixtures/plugin-noop'}, t.context.logger);
const plugin = normalize('publish', {path: './test/fixtures/plugin-noop'}, t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-noop'));
t.true(t.context.log.calledWith(match.string, 'publish', './test/fixtures/plugin-noop'));
});
test('Normalize and load plugin from function', t => {
@@ -33,20 +33,20 @@ test('Normalize and load plugin that retuns multiple functions', t => {
const plugin = normalize('verifyConditions', './test/fixtures/multi-plugin', t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, './test/fixtures/multi-plugin'));
t.true(t.context.log.calledWith(match.string, 'verifyConditions', './test/fixtures/multi-plugin'));
});
test('Wrap plugin in a function that validate the output of the plugin', async t => {
const pluginFunction = stub().resolves(1);
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');
});
@@ -104,7 +104,7 @@ test('Always pass a defined "pluginConfig" for plugin defined with path', async
});
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));
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));
+5 -2
View File
@@ -49,7 +49,7 @@ test('Use default when only options are passed for a single plugin', t => {
});
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger));
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error);
t.is(
error.message,
@@ -58,7 +58,10 @@ test('Throw an error if plugin configuration is missing a path for plugin pipeli
});
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));
const error = t.throws(
() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger),
Error
);
t.is(
error.message,