Compare commits

...
7 Commits
22 changed files with 522 additions and 223 deletions
+2 -4
View File
@@ -105,13 +105,11 @@ After running the tests the command `semantic-release` will execute the followin
- [CI configurations](docs/recipes/README.md)
- [Package managers and languages](docs/recipes/README.md)
- Developer guide
- [JavaScript API](docs/developer-guide/js-api.md)
- [Plugins](docs/developer-guide/plugin.md)
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
- Resources
- [Videos](docs/resources.md#videos)
- [Articles](docs/resources.md#articles)
- [Tutorials](docs/resources.md#tutorials)
- Support
- [Resources](docs/support/resources.md)
- [Frequently Asked Questions](docs/support/FAQ.md)
- [Troubleshooting](docs/support/troubleshooting.md)
- [Node version requirement](docs/support/node-version.md)
+1
View File
@@ -22,6 +22,7 @@
- [Package managers and languages](docs/recipes/README.md)
## Developer guide
- [JavaScript API](docs/developer-guide/js-api.md)
- [Plugin](docs/developer-guide/plugin.md)
- [Shareable configuration](docs/developer-guide/shareable-configuration.md)
+1
View File
@@ -1,4 +1,5 @@
# Developer guide
- [JavaScript API](js-api.md)
- [Plugins](plugin.md)
- [Shareable configuration](shareable-configuration.md)
+270
View File
@@ -0,0 +1,270 @@
# JavaScript API
## Usage
```js
const semanticRelease = require('semantic-release');
const {WritableStreamBuffer} = require('stream-buffers');
const stdoutBuffer = WritableStreamBuffer();
const stderrBuffer = WritableStreamBuffer();
try {
const result = await semanticRelease({
// Core options
branch: 'master',
repositoryUrl: 'https://github.com/me/my-package.git',
// Shareable config
extends: 'my-shareable-config',
// Plugin options
githubUrl: 'https://my-ghe.com',
githubApiPathPrefix: '/api-prefix'
}, {
// Run semantic-release from `/path/to/git/repo/root` without having to change local process `cwd` with `process.chdir()`
cwd: '/path/to/git/repo/root',
// Pass the variable `MY_ENV_VAR` to semantic-release without having to modify the local `process.env`
env: {...process.env, MY_ENV_VAR: 'MY_ENV_VAR_VALUE'},
// Store stdout and stderr to use later instead of writing to `process.stdout` and `process.stderr`
stdout: stdoutBuffer,
stderr: stderrBuffer
});
if (result) {
const {lastRelease, commits, nextRelease, releases} = result;
console.log(`Published ${nextRelease.type} release version ${nextRelease.version} containing ${commits.length} commits.`);
if (lastRelease.version) {
console.log(`The last release was "${lastRelease.version}".`);
}
for (const release of releases) {
console.log(`The release was published with plugin "${pluginName}".`);
}
} else {
console.log('No release published.');
}
// Get stdout and stderr content
const logs = stdoutBuffer.getContentsAsString('utf8');
const errors = stderrBuffer.getContentsAsString('utf8');
} catch (err) {
console.error('The automated release failed with %O', err)
}
```
## API
### semanticRelease([options], [config]) => Promise<Result>
Run **semantic-release** and returns a `Promise` that resolves to a [Result](#result) object.
#### options
Type: `Object`
**semantic-release** options.
Can be used to set any [core option](../usage/configuration.md#configuration) or [plugin options](../usage/plugins.md#configuration).
Each option, will take precedence over options configured in the [configuration file](../usage/configuration.md#configuration) and [shareable configurations](../usage/configuration.md#extends).
#### config
Type: `Object`
**semantic-release** configuration specific for API usage.
##### cwd
Type: `String`<br>
Default: `process.cwd()`
The current working directory to use. It should be configured to the root of the Git repository to release from.
It allows to run **semantic-release** from a specific path without having to change the local process `cwd` with `process.chdir()`.
##### env
Type: `Object`<br>
Default: `process.env`
The environment variables to use.
It allows to run **semantic-release** with specific environment variables without having to modify the local `process.env`.
##### stdout
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
Default: `process.stdout`
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log information.
It allows to configure **semantic-release** to write logs to a specific stream rather than the local `process.stdout`.
##### stderr
Type: [`stream.Writable`](https://nodejs.org/api/stream.html#stream_writable_streams)<br>
Default: `process.stderr`
The [writable stream](https://nodejs.org/api/stream.html#stream_writable_streams) used to log errors.
It allows to configure **semantic-release** to write errors to a specific stream rather than the local `process.stderr`.
### Result
Type: `Object` `Boolean`<br>
And object with [`lastRelease`](#lastrelease), [`nextRelease`](#nextrelease), [`commits`](#commits) and [`releases`](#releases) if a release is published or `false` if no release was published.
#### lastRelease
Type: `Object`
Information related to the last release found:
| Name | Type | Description |
|---------|----------|----------------------------------------------------------------------------------------------------|
| version | `String` | The version of the last release. |
| gitHead | `String` | The sha of the last commit being part of the last release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the last release. |
**Notes**: If no previous release is found, `lastRelease` will be an empty `Object`.
Example:
```js
{
gitHead: 'da39a3ee5e6b4b0d3255bfef95601890afd80709',
version: '1.0.0',
gitTag: 'v1.0.0',
}
```
#### commits
Type: `Array<Object>`
The list of commit included in the new release.<br>
Each commit object has the following properties:
| Name | Type | Description |
|-----------------|----------|-------------------------------------------------|
| commit | `Object` | The commit abbreviated and full hash. |
| commit.long | `String` | The commit hash. |
| commit.short | `String` | The commit abbreviated hash. |
| tree | `Object` | The commit abbreviated and full tree hash. |
| tree.long | `String` | The commit tree hash. |
| tree.short | `String` | The commit abbreviated tree hash. |
| author | `Object` | The commit author information. |
| author.name | `String` | The commit author name. |
| author.email | `String` | The commit author email. |
| author.short | `String` | The commit author date. |
| committer | `Object` | The committer information. |
| committer.name | `String` | The committer name. |
| committer.email | `String` | The committer email. |
| committer.short | `String` | The committer date. |
| subject | `String` | The commit subject. |
| body | `String` | The commit body. |
| message | `String` | The commit full message (`subject` and `body`). |
| hash | `String` | The commit hash. |
| committerDate | `String` | The committer date. |
Example:
```js
[
{
commit: {
long: '68eb2c4d778050b0701136ca129f837d7ed494d2',
short: '68eb2c4'
},
tree: {
long: '7ab515d12bd2cf431745511ac4ee13fed15ab578',
short: '7ab515d'
},
author: {
name: 'Me',
email: 'me@email.com',
date: 2018-07-22T20:52:44.000Z
},
committer: {
name: 'Me',
email: 'me@email.com',
date: 2018-07-22T20:52:44.000Z
},
subject: 'feat: a new feature',
body: 'Description of the new feature',
hash: '68eb2c4d778050b0701136ca129f837d7ed494d2',
message: 'feat: a new feature\n\nDescription of the new feature',
committerDate: 2018-07-22T20:52:44.000Z
}
]
```
#### nextRelease
Type: `Object`
Information related to the newly published release:
| Name | Type | Description |
|---------|----------|---------------------------------------------------------------------------------------------------|
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
| version | `String` | The version of the new release. |
| gitHead | `String` | The sha of the last commit being part of the new release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the new release. |
| notes | `String` | The release notes for the new release. |
Example:
```js
{
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
}
```
#### releases
Type: `Array<Object>`
The list of releases published, one release per [publish plugin](../usage/plugins.md#publish-plugin).<br>
Each release object has the following properties:
| Name | Type | Description |
|------------|----------|-----------------------------------------------------------------------------------------------|
| name | `String` | **Optional.** The release name, only if set by the corresponding `publish` plugin. |
| url | `String` | **Optional.** The release URL, only if set by the corresponding `publish` plugin. |
| type | `String` | The [semver](https://semver.org) type of the release (`patch`, `minor` or `major`). |
| version | `String` | The version of the release. |
| gitHead | `String` | The sha of the last commit being part of the release. |
| gitTag | `String` | The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) associated with the release. |
| notes | `String` | The release notes for the release. |
| pluginName | `String` | The name of the plugin that published the release. |
Example:
```js
[
{
name: 'GitHub release',
url: 'https://github.com/me/my-package/releases/tag/v1.1.0',
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
pluginName: '@semantic-release/github'
},
{
name: 'npm package (@latest dist-tag)',
url: 'https://www.npmjs.com/package/my-package',
type: 'minor',
gitHead: '68eb2c4d778050b0701136ca129f837d7ed494d2',
version: '1.1.0',
gitTag: 'v1.1.0',
notes: 'Release notes for version 1.1.0...',
pluginName: '@semantic-release/npm'
}
]
```
+3
View File
@@ -40,6 +40,9 @@
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
- [semantic-release-gcr](https://github.com/carlos-cubas/semantic-release-gcr) Set of semantic-release plugins for publishing a docker image to Google Container Registry
- [verifyConditions](https://github.com/carlos-cubas/semantic-release-gcr#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
- [publish](https://github.com/carlos-cubas/semantic-release-gcr#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
- [semantic-release-vsce](https://github.com/raix/semantic-release-vsce) Set of semantic-release plugins for publishing Visual Studio Code extensions to the marketplace
- **verifyConditions** Verify the presence and the validity of the vsce authentication and release configuration
- **prepare** Create a `.vsix` for distribution
+25 -20
View File
@@ -1,5 +1,5 @@
const process = require('process');
const {template} = require('lodash');
const {template, pick} = require('lodash');
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
@@ -13,7 +13,7 @@ const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const {extractErrors} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const logger = require('./lib/logger');
const getLogger = require('./lib/get-logger');
const {fetch, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
const getError = require('./lib/get-error');
const {COMMIT_NAME, COMMIT_EMAIL} = require('./lib/definitions/constants');
@@ -42,7 +42,7 @@ async function run(context, plugins) {
if (isCi && isPr && !options.noCi) {
logger.log("This run was triggered by a pull request and therefore a new version won't be published.");
return;
return false;
}
if (ciBranch !== options.branch) {
@@ -53,6 +53,7 @@ async function run(context, plugins) {
);
return false;
}
logger.success(`Run automated release from branch ${ciBranch}`);
await verify(context);
@@ -63,16 +64,15 @@ async function run(context, plugins) {
} catch (err) {
if (!(await isBranchUpToDate(options.branch, {cwd, env}))) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
`The local branch ${options.branch} is behind the remote one, therefore a new version won't be published.`
);
return false;
}
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
logger.error(`The command "${err.cmd}" failed with the error message ${err.stderr}.`);
throw getError('EGITNOPERMISSION', {options});
}
logger.log('Run automated release from branch %s', options.branch);
logger.success(`Allowed to push to the Git repository`);
await plugins.verifyConditions(context);
@@ -85,7 +85,7 @@ async function run(context, plugins) {
if (!nextRelease.type) {
logger.log('There are no relevant changes, so no new version is released.');
return;
return false;
}
context.nextRelease = nextRelease;
nextRelease.version = getNextVersion(context);
@@ -95,35 +95,36 @@ async function run(context, plugins) {
if (options.dryRun) {
const notes = await plugins.generateNotes(context);
logger.log('Release note for version %s:\n', nextRelease.version);
logger.log(`Release note for version ${nextRelease.version}:`);
if (notes) {
logger.stdout(`${marked(notes)}\n`);
context.stdout.write(marked(notes));
}
} else {
nextRelease.notes = await plugins.generateNotes(context);
await plugins.prepare(context);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag, {cwd, env});
await push(options.repositoryUrl, options.branch, {cwd, env});
logger.success(`Created tag ${nextRelease.gitTag}`);
context.releases = await plugins.publish(context);
await plugins.success(context);
logger.log('Published release: %s', nextRelease.version);
logger.success(`Published release ${nextRelease.version}`);
}
return true;
return pick(context, ['lastRelease', 'commits', 'nextRelease', 'releases']);
}
function logErrors({logger}, err) {
function logErrors({logger, stderr}, err) {
const errors = extractErrors(err).sort(error => (error.semanticRelease ? -1 : 0));
for (const error of errors) {
if (error.semanticRelease) {
logger.log(`%s ${error.message}`, error.code);
logger.error(`${error.code} ${error.message}`);
if (error.details) {
logger.stderr(`${marked(error.details)}\n`);
stderr.write(marked(error.details));
}
} else {
logger.error('An error occurred while running semantic-release: %O', error);
@@ -142,10 +143,14 @@ async function callFail(context, plugins, error) {
}
}
module.exports = async (opts, {cwd = process.cwd(), env = process.env} = {}) => {
const context = {cwd, env, logger};
context.logger.log(`Running %s version %s`, pkg.name, pkg.version);
const {unhook} = hookStd({silent: false}, hideSensitive(context.env));
module.exports = async (opts = {}, {cwd = process.cwd(), env = process.env, stdout, stderr} = {}) => {
const {unhook} = hookStd(
{silent: false, streams: [process.stdout, process.stderr, stdout, stderr].filter(Boolean)},
hideSensitive(env)
);
const context = {cwd, env, stdout: stdout || process.stdout, stderr: stderr || process.stderr};
context.logger = getLogger(context);
context.logger.log(`Running ${pkg.name} version ${pkg.version}`);
try {
const {plugins, options} = await getConfig(context, opts);
context.options = options;
+1 -1
View File
@@ -24,7 +24,7 @@ module.exports = async ({cwd, env, lastRelease: {gitHead}, logger}) => {
commit.gitTags = commit.gitTags.trim();
return commit;
});
logger.log('Found %s commits since last release', commits.length);
logger.log(`Found ${commits.length} commits since last release`);
debug('Parsed commits: %o', commits);
return commits;
};
+1 -1
View File
@@ -42,7 +42,7 @@ module.exports = async ({cwd, env, options: {tagFormat}, logger}) => {
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag, {cwd, env}), {preserveOrder: true});
if (tag) {
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
logger.log(`Found git tag ${tag.gitTag} associated with version ${tag.version}`);
return {gitHead: await gitTagHead(tag.gitTag, {cwd, env}), ...tag};
}
+16
View File
@@ -0,0 +1,16 @@
const {Signale} = require('signale');
const figures = require('figures');
module.exports = ({stdout, stderr}) =>
new Signale({
config: {displayTimestamp: true, underlineMessage: true, displayLabel: false},
disabled: false,
interactive: false,
scope: 'semantic-release',
stream: [stdout],
types: {
error: {badge: figures.cross, color: 'red', label: '', stream: [stderr]},
log: {badge: figures.info, color: 'magenta', label: '', stream: [stdout]},
success: {badge: figures.tick, color: 'green', label: '', stream: [stdout]},
},
});
+2 -2
View File
@@ -5,10 +5,10 @@ module.exports = ({nextRelease: {type}, lastRelease, logger}) => {
let version;
if (lastRelease.version) {
version = semver.inc(lastRelease.version, type);
logger.log('The next release version is %s', version);
logger.log(`The next release version is ${version}`);
} else {
version = FIRST_RELEASE;
logger.log('There is no previous release, the next release version is %s', version);
logger.log(`There is no previous release, the next release version is ${version}`);
}
return version;
-29
View File
@@ -1,29 +0,0 @@
const chalk = require('chalk');
/**
* Logger with `log` and `error` function.
*/
module.exports = {
log(...args) {
const [format, ...rest] = args;
console.log(
`${chalk.grey('[Semantic release]:')}${
typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : ''
}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
error(...args) {
const [format, ...rest] = args;
console.error(
`${chalk.grey('[Semantic release]:')}${typeof format === 'string' ? ` ${chalk.red(format)}` : ''}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
stdout(...args) {
console.log(args);
},
stderr(...args) {
console.error(args);
},
};
+3 -2
View File
@@ -5,7 +5,8 @@ const PLUGINS_DEFINITIONS = require('../definitions/plugins');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
module.exports = ({cwd, options, logger}, pluginsPath) => {
module.exports = (context, pluginsPath) => {
const {options, logger} = context;
const errors = [];
const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
(
@@ -30,7 +31,7 @@ module.exports = ({cwd, options, logger}, pluginsPath) => {
}
const steps = castArray(pluginOpts).map(pluginOpt =>
normalize({cwd, options: omit(options, Object.keys(PLUGINS_DEFINITIONS)), logger}, type, pluginOpt, pluginsPath)
normalize({...context, options: omit(options, Object.keys(PLUGINS_DEFINITIONS))}, type, pluginOpt, pluginsPath)
);
plugins[type] = async input =>
+13 -10
View File
@@ -13,14 +13,6 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
const {path, ...config} = isString(pluginOpt) || isFunction(pluginOpt) ? {path: pluginOpt} : pluginOpt;
const pluginName = isFunction(path) ? `[Function: ${path.name}]` : path;
if (!isFunction(pluginOpt)) {
if (pluginsPath[path]) {
logger.log('Load plugin "%s" from %s in shareable config %s', type, path, pluginsPath[path]);
} else {
logger.log('Load plugin "%s" from %s', type, path);
}
}
const basePath = pluginsPath[path]
? dirname(resolveFrom.silent(__dirname, pluginsPath[path]) || resolveFrom(cwd, pluginsPath[path]))
: __dirname;
@@ -38,18 +30,29 @@ module.exports = ({cwd, options, logger}, type, pluginOpt, pluginsPath) => {
const validator = async input => {
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
try {
logger.log('Call plugin "%s"', type);
const result = await func(cloneDeep(input));
logger.log(`Start step "${type}" of plugin "${pluginName}"`);
const result = await func({...cloneDeep(input), logger: logger.scope(logger.scopeName, pluginName)});
if (outputValidator && !outputValidator(result)) {
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
}
logger.success(`Completed step "${type}" of plugin "${pluginName}"`);
return result;
} catch (err) {
logger.error(`Failed step "${type}" of plugin "${pluginName}"`);
extractErrors(err).forEach(err => Object.assign(err, {pluginName}));
throw err;
}
};
Reflect.defineProperty(validator, 'pluginName', {value: pluginName, writable: false, enumerable: true});
if (!isFunction(pluginOpt)) {
if (pluginsPath[path]) {
logger.success(`Loaded plugin "${type}" from "${path}" in shareable config "${pluginsPath[path]}"`);
} else {
logger.success(`Loaded plugin "${type}" from "${path}"`);
}
}
return validator;
};
+4 -3
View File
@@ -22,19 +22,19 @@
"@semantic-release/commit-analyzer": "^6.0.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "^5.0.0",
"@semantic-release/npm": "^4.0.0",
"@semantic-release/npm": "^4.0.1",
"@semantic-release/release-notes-generator": "^7.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
"cosmiconfig": "^5.0.1",
"debug": "^3.1.0",
"env-ci": "^2.0.0",
"execa": "^0.10.0",
"figures": "^2.0.0",
"find-versions": "^2.0.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^10.0.1",
"hook-std": "^1.0.1",
"hook-std": "^1.1.0",
"hosted-git-info": "^2.7.1",
"lodash": "^4.17.4",
"marked": "^0.4.0",
@@ -44,6 +44,7 @@
"read-pkg-up": "^4.0.0",
"resolve-from": "^4.0.0",
"semver": "^5.4.1",
"signale": "^1.2.1",
"yargs": "^12.0.0"
},
"devDependencies": {
+1 -1
View File
@@ -27,7 +27,7 @@ test('Get the highest non-prerelease valid tag', async t => {
const result = await getLastRelease({cwd, options: {tagFormat: `v\${version}`}, logger: t.context.logger});
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
t.deepEqual(t.context.log.args[0], ['Found git tag v2.0.0 associated with version 2.0.0']);
});
test('Get the highest tag in the history of the current branch', async t => {
+17
View File
@@ -0,0 +1,17 @@
import test from 'ava';
import {spy} from 'sinon';
import getLogger from '../lib/get-logger';
test('Expose "error", "success" and "log" functions', t => {
const stdout = spy();
const stderr = spy();
const logger = getLogger({stdout: {write: stdout}, stderr: {write: stderr}});
logger.log('test log');
logger.success('test success');
logger.error('test error');
t.regex(stdout.args[0][0], /.*test log/);
t.regex(stdout.args[1][0], /.*test success/);
t.regex(stderr.args[0][0], /.*test error/);
});
+9 -6
View File
@@ -4,10 +4,12 @@ import got from 'got';
import delay from 'delay';
import pRetry from 'p-retry';
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
const IMAGE = 'semanticrelease/npm-registry-docker:latest';
const SERVER_PORT = 15986;
const COUCHDB_PORT = 5984;
const SERVER_HOST = 'localhost';
const COUCHDB_USER = 'admin';
const COUCHDB_PASSWORD = 'password';
const NPM_USERNAME = 'integration';
const NPM_PASSWORD = 'suchsecure';
const NPM_EMAIL = 'integration@test.com';
@@ -15,7 +17,7 @@ const docker = new Docker();
let container;
/**
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
* Download the `npm-registry-docker` Docker image, create a new container and start it.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
@@ -24,10 +26,11 @@ async function start() {
Tty: true,
Image: IMAGE,
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
Env: [`COUCHDB_USER=${COUCHDB_USER}`, `COUCHDB_PASSWORD=${COUCHDB_PASSWORD}`],
});
await container.start();
await delay(3000);
await delay(4000);
try {
// Wait for the registry to be ready
@@ -37,13 +40,13 @@ async function start() {
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
throw new Error(`Couldn't start npm-registry-docker after 2 min`);
}
// Create user
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
json: true,
auth: 'admin:admin',
auth: `${COUCHDB_USER}:${COUCHDB_PASSWORD}`,
method: 'PUT',
body: {
_id: `org.couchdb.user:${NPM_USERNAME}`,
@@ -66,7 +69,7 @@ const authEnv = {
};
/**
* Stop and remote the `npm-docker-couchdb` Docker container.
* Stop and remote the `npm-registry-docker` Docker container.
*/
async function stop() {
await container.stop();
+101 -65
View File
@@ -22,9 +22,13 @@ test.beforeEach(t => {
// Stub the logger functions
t.context.log = spy();
t.context.error = spy();
t.context.stdout = spy();
t.context.stderr = spy();
t.context.logger = {log: t.context.log, error: t.context.error, stdout: t.context.stdout, stderr: t.context.stderr};
t.context.success = spy();
t.context.logger = {
log: t.context.log,
error: t.context.error,
success: t.context.success,
scope: () => t.context.logger,
};
});
test('Plugins are called with expected values', async t => {
@@ -68,10 +72,15 @@ test('Plugins are called with expected values', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, extendEnv: false, env}));
const result = await semanticRelease(options, {
cwd,
env,
stdout: {write: () => {}},
stderr: {write: () => {}},
});
t.is(verifyConditions1.callCount, 1);
t.deepEqual(verifyConditions1.args[0][0], config);
@@ -159,6 +168,16 @@ test('Plugins are called with expected values', async t => {
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
]);
t.deepEqual(result, {
lastRelease,
commits: [commits[0]],
nextRelease: {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`},
releases: [
{...release1, ...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: '[Function: proxy]'},
{...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`, pluginName: pluginNoop},
],
});
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag, {cwd}), nextRelease.gitHead);
@@ -193,10 +212,10 @@ test('Use custom tag format', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
// Verify the tag has been created on the local and remote repo and reference the gitHead
t.is(await gitTagHead(nextRelease.gitTag, {cwd}), nextRelease.gitHead);
@@ -237,11 +256,11 @@ test('Use new gitHead, and recreate release notes if a prepare plugin create a c
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
@@ -295,11 +314,11 @@ test('Call all "success" plugins even if one errors out', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
await t.throws(semanticRelease(options, {cwd, env: {}}));
await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(success1.callCount, 1);
t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
@@ -327,15 +346,17 @@ test('Log all "verifyConditions" errors', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
const errors = [
...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))),
];
t.deepEqual(errors, [error1, error2, error3]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR2 error 2']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR3 error 3']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 3], [
'An error occurred while running semantic-release: %O',
error1,
]);
@@ -371,14 +392,16 @@ test('Log all "verifyRelease" errors', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
const errors = [
...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))),
];
t.deepEqual(errors, [error1, error2]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
t.is(fail.callCount, 1);
t.deepEqual(fail.args[0][0], config);
t.deepEqual(fail.args[0][1].errors, [error1, error2]);
@@ -419,10 +442,10 @@ test('Dry-run skips publish and success', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
@@ -457,14 +480,16 @@ test('Dry-run skips fail', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = [...(await t.throws(semanticRelease(options, {cwd, env: {}})))];
const errors = [
...(await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))),
];
t.deepEqual(errors, [error1, error2]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 2], ['ERR1 error 1']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], ['ERR2 error 2']);
t.is(fail.callCount, 0);
});
@@ -504,10 +529,10 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: false, branch: 'master'}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
@@ -518,7 +543,7 @@ test('Force a dry-run if not on a CI and "noCi" is not explicitly set', async t
t.is(success.callCount, 0);
});
test.serial('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
test('Dry-run does not print changelog if "generateNotes" return "undefined"', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -547,12 +572,12 @@ test.serial('Dry-run does not print changelog if "generateNotes" return "undefin
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version %s:\n', '2.0.0']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version 2.0.0:']);
});
test('Allow local releases with "noCi" option', async t => {
@@ -591,10 +616,10 @@ test('Allow local releases with "noCi" option', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: false, branch: 'master', isPr: true}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.not(
@@ -643,10 +668,10 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(analyzeCommits.callCount, 1);
t.deepEqual(analyzeCommits.args[0][1].lastRelease, lastRelease);
@@ -665,23 +690,25 @@ test('Accept "undefined" value returned by the "generateNotes" plugins', async t
t.is(publish.args[0][1].nextRelease.notes, notes2);
});
test('Returns falsy value if triggered by a PR', async t => {
test('Returns false if triggered by a PR', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
});
t.falsy(await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}}));
t.false(
await semanticRelease({cwd, repositoryUrl}, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}})
);
t.is(
t.context.log.args[t.context.log.args.length - 1][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
});
test('Returns falsy value if triggered on an outdated clone', async t => {
test('Returns false if triggered on an outdated clone', async t => {
// Create a git repository, set the current working directory at the root of the repo
let {cwd, repositoryUrl} = await gitRepo(true);
const repoDir = cwd;
@@ -694,18 +721,22 @@ test('Returns falsy value if triggered on an outdated clone', async t => {
await gitPush(repositoryUrl, 'master', {cwd});
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease({repositoryUrl}, {cwd: repoDir, env: {}}));
t.false(
await semanticRelease(
{repositoryUrl},
{cwd: repoDir, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}
)
);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
"The local branch %s is behind the remote one, therefore a new version won't be published.",
'master',
"The local branch master is behind the remote one, therefore a new version won't be published.",
]);
});
test('Returns falsy value if not running from the configured branch', async t => {
test('Returns false if not running from the configured branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
const options = {
@@ -722,18 +753,18 @@ test('Returns falsy value if not running from the configured branch', async t =>
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
});
t.falsy(await semanticRelease(options, {cwd, env: {}}));
t.false(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(
t.context.log.args[1][0],
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.'
);
});
test('Returns falsy value if there is no relevant changes', async t => {
test('Returns false if there is no relevant changes', async t => {
// Create a git repository, set the current working directory at the root of the repo
const {cwd, repositoryUrl} = await gitRepo(true);
// Add commits to the master branch
@@ -759,11 +790,11 @@ test('Returns falsy value if there is no relevant changes', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease(options, {cwd, env: {}}));
t.false(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 0);
t.is(generateNotes.callCount, 0);
@@ -807,10 +838,10 @@ test('Exclude commits with [skip release] or [release skip] from analysis', asyn
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
await semanticRelease(options, {cwd, env: {}});
await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}});
t.is(analyzeCommits.callCount, 1);
t.is(analyzeCommits.args[0][1].commits.length, 2);
@@ -830,15 +861,15 @@ test('Log both plugins errors and errors thrown by "fail" plugin', async t => {
fail: [stub().rejects(failError1), stub().rejects(failError2)],
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
await t.throws(semanticRelease(options, {cwd, env: {}}));
await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError1);
t.is(t.context.error.args[t.context.error.args.length - 1][1], failError2);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s Plugin error', 'ERR']);
t.is(t.context.error.args[t.context.error.args.length - 1][0], 'ERR Plugin error');
t.is(t.context.error.args[t.context.error.args.length - 3][1], failError1);
t.is(t.context.error.args[t.context.error.args.length - 2][1], failError2);
});
test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
@@ -853,11 +884,11 @@ test('Call "fail" only if a plugin returns a SemanticReleaseError', async t => {
fail,
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
await t.throws(semanticRelease(options, {cwd, env: {}}));
await t.throws(semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.true(fail.notCalled);
t.is(t.context.error.args[t.context.error.args.length - 1][1], pluginError);
@@ -868,10 +899,12 @@ test('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found
const {cwd} = await gitRepo();
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = [...(await t.throws(semanticRelease({}, {cwd, env: {}})))];
const errors = [
...(await t.throws(semanticRelease({}, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}))),
];
// Verify error code and type
t.is(errors[0].code, 'ENOREPOURL');
@@ -902,10 +935,13 @@ test('Throw an Error if plugin returns an unexpected value', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const error = await t.throws(semanticRelease(options, {cwd, env: {}}), Error);
const error = await t.throws(
semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}),
Error
);
t.regex(error.details, /string/);
});
@@ -935,10 +971,10 @@ test('Get all commits including the ones not in the shallow clone', async t => {
};
const semanticRelease = requireNoCache('..', {
'./lib/logger': t.context.logger,
'./lib/get-logger': () => t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options, {cwd, env: {}}));
t.truthy(await semanticRelease(options, {cwd, env: {}, stdout: {write: () => {}}, stderr: {write: () => {}}}));
t.is(analyzeCommits.args[0][1].commits.length, 3);
});
+12 -10
View File
@@ -16,11 +16,9 @@ const requireNoCache = proxyquire.noPreserveCache();
// Environment variables used with semantic-release cli (similar to what a user would setup)
const env = {
...npmRegistry.authEnv,
GH_TOKEN: gitbox.gitCredential,
GITHUB_URL: mockServer.url,
NPM_EMAIL: 'integration@test.com',
NPM_USERNAME: 'integration',
NPM_PASSWORD: 'suchsecure',
TRAVIS: 'true',
CI: 'true',
TRAVIS_BRANCH: 'master',
@@ -29,9 +27,9 @@ const env = {
// Environment variables used only for the local npm command used to do verification
const testEnv = {
...process.env,
...npmRegistry.authEnv,
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'),
LEGACY_TOKEN: Buffer.from(`${env.NPM_USERNAME}:${env.NPM_PASSWORD}`, 'utf8').toString('base64'),
};
const cli = require.resolve('../bin/semantic-release');
@@ -467,6 +465,10 @@ test('Run via JS API', async t => {
version: '0.0.0-dev',
repository: {url: repositoryUrl},
publishConfig: {registry: npmRegistry.url},
release: {
fail: false,
success: false,
},
});
/* Initial release */
@@ -488,7 +490,7 @@ test('Run via JS API', async t => {
t.log('Commit a feature');
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ Call semantic-release via API');
await semanticRelease({fail: false, success: false}, {cwd, env});
await semanticRelease(undefined, {cwd, env, stdout: {write: () => {}}, stderr: {write: () => {}}});
// Verify package.json and has been updated
t.is((await readJson(path.resolve(cwd, 'package.json'))).version, version);
@@ -552,9 +554,9 @@ test('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
t.log('Commit a feature');
await gitCommits(['feat: Initial commit'], {cwd});
t.log('$ semantic-release');
const {stdout, code} = await execa(cli, [], {env, cwd, reject: false});
const {stderr, code} = await execa(cli, [], {env, cwd, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.regex(stderr, /EINHERITED Inherited error/);
t.is(code, 1);
});
@@ -570,13 +572,13 @@ test('Exit with 1 if missing permission to push to the remote repository', async
await gitCommits(['feat: Initial commit'], {cwd});
await gitPush('origin', 'master', {cwd});
t.log('$ semantic-release');
const {stdout, code} = await execa(
const {stderr, code} = await execa(
cli,
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, cwd, reject: false}
);
// Verify the type and message are logged
t.regex(stdout, /EGITNOPERMISSION/);
t.regex(stderr, /EGITNOPERMISSION/);
t.is(code, 1);
});
-51
View File
@@ -1,51 +0,0 @@
import test from 'ava';
import {stub} from 'sinon';
import logger from '../lib/logger';
test.beforeEach(t => {
t.context.log = stub(console, 'log');
t.context.error = stub(console, 'error');
});
test.afterEach.always(t => {
t.context.log.restore();
t.context.error.restore();
});
test.serial('Basic log', t => {
logger.log('test log');
logger.error('test error');
t.regex(t.context.log.args[0][0], /.*test log/);
t.regex(t.context.error.args[0][0], /.*test error/);
});
test.serial('Log object', t => {
const obj = {a: 1, b: '2'};
logger.log(obj);
logger.error(obj);
t.is(t.context.log.args[0][1], obj);
t.is(t.context.error.args[0][1], obj);
});
test.serial('Log with string formatting', t => {
logger.log('test log %s', 'log value');
logger.error('test error %s', 'error value');
t.regex(t.context.log.args[0][0], /.*test log/);
t.regex(t.context.error.args[0][0], /.*test error/);
t.is(t.context.log.args[0][1], 'log value');
t.is(t.context.error.args[0][1], 'error value');
});
test.serial('Log with error stacktrace and properties', t => {
const error = new Error('error message');
logger.error(error);
const otherError = new Error('other error message');
logger.error('test error %O', otherError);
t.is(t.context.error.args[0][1], error);
t.regex(t.context.error.args[1][0], /.*test error/);
t.is(t.context.error.args[1][1], otherError);
});
+38 -17
View File
@@ -8,7 +8,15 @@ const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
t.context.error = stub();
t.context.success = stub();
t.context.stderr = {write: stub()};
t.context.logger = {
log: t.context.log,
error: t.context.error,
success: t.context.success,
scope: () => t.context.logger,
};
});
test('Normalize and load plugin from string', t => {
@@ -21,7 +29,7 @@ test('Normalize and load plugin from string', t => {
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/plugin-noop']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/plugin-noop"']);
});
test('Normalize and load plugin from object', t => {
@@ -34,7 +42,7 @@ test('Normalize and load plugin from object', t => {
t.is(plugin.pluginName, './test/fixtures/plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'publish', './test/fixtures/plugin-noop']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "publish" from "./test/fixtures/plugin-noop"']);
});
test('Normalize and load plugin from a base file path', t => {
@@ -44,11 +52,8 @@ test('Normalize and load plugin from a base file path', t => {
t.is(plugin.pluginName, './plugin-noop');
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], [
'Load plugin "%s" from %s in shareable config %s',
'verifyConditions',
'./plugin-noop',
'./test/fixtures',
t.deepEqual(t.context.success.args[0], [
'Loaded plugin "verifyConditions" from "./plugin-noop" in shareable config "./test/fixtures"',
]);
});
@@ -90,12 +95,17 @@ test('Normalize and load plugin that retuns multiple functions', t => {
);
t.is(typeof plugin, 'function');
t.deepEqual(t.context.log.args[0], ['Load plugin "%s" from %s', 'verifyConditions', './test/fixtures/multi-plugin']);
t.deepEqual(t.context.success.args[0], ['Loaded plugin "verifyConditions" from "./test/fixtures/multi-plugin"']);
});
test('Wrap "analyzeCommits" plugin in a function that validate the output of the plugin', async t => {
const analyzeCommits = stub().resolves(2);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'analyzeCommits', analyzeCommits, {});
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'analyzeCommits',
analyzeCommits,
{}
);
const error = await t.throws(plugin());
@@ -108,7 +118,12 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the
test('Wrap "generateNotes" plugin in a function that validate the output of the plugin', async t => {
const generateNotes = stub().resolves(2);
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'generateNotes', generateNotes, {});
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'generateNotes',
generateNotes,
{}
);
const error = await t.throws(plugin());
@@ -120,11 +135,15 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the
});
test('Wrap "publish" plugin in a function that validate the output of the plugin', async t => {
const plugin = normalize({cwd, options: {}, logger: t.context.logger}, 'publish', './plugin-identity', {
'./plugin-identity': './test/fixtures',
});
const publish = stub().resolves(2);
const plugin = normalize(
{cwd, options: {}, stderr: t.context.stderr, logger: t.context.logger},
'publish',
publish,
{}
);
const error = await t.throws(plugin(2));
const error = await t.throws(plugin());
t.is(error.code, 'EPUBLISHOUTPUT');
t.is(error.name, 'SemanticReleaseError');
@@ -138,9 +157,11 @@ test('Plugin is called with "pluginConfig" (omitting "path", adding global confi
const pluginConf = {path: pluginFunction, conf: 'confValue'};
const options = {global: 'globalValue'};
const plugin = normalize({cwd, options, logger: t.context.logger}, '', pluginConf, {});
await plugin('param');
await plugin({param: 'param'});
t.true(pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, 'param'));
t.true(
pluginFunction.calledWith({conf: 'confValue', global: 'globalValue'}, {param: 'param', logger: t.context.logger})
);
});
test('Prevent plugins to modify "pluginConfig"', async t => {
+2 -1
View File
@@ -11,7 +11,8 @@ const cwd = process.cwd();
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
t.context.success = stub();
t.context.logger = {log: t.context.log, success: t.context.success, scope: () => t.context.logger};
});
test('Export default plugins', t => {