Compare commits

...
34 Commits
Author SHA1 Message Date
greenkeeper[bot]andPierre Vanduynslager d3c7232b70 fix(package): update git-url-parse to version 10.0.1 2018-07-16 00:02:59 -04:00
Pierre Vanduynslager 89e584713d docs: migration to new GitBook version 2018-07-14 18:00:06 -04:00
greenkeeper[bot]andPierre Vanduynslager ed6a3818dc chore(package): update clear-module to version 3.0.0 2018-07-11 14:20:05 -04:00
Pierre Vanduynslager d8e59cce53 fix: set default path to generateNotes object config 2018-07-10 15:50:24 -04:00
Pierre Vanduynslager 24ce560065 refactor: build plugin pipeline parameters at initialization
In addition, factorize the pipeline config function to avoid code duplication.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager eb26254b00 refactor: use Object.entries rather than Object.keys 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 50061bb267 refactor: remove unnecessary object destructuring 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 5989989452 feat: allow to define multiple generateNotes plugins
Each `generateNotes` plugin will be called in the order defined and will receive the concatenation of the previous one in `nextRelease.notes`.
That gives each plugin the ability to test if there is a notes part that will precede it's own.
Each plugin is expected to return it's own part of the release notes only. **semantic-release** will take care of concatenating all the notes parts.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 576eb6027f refactor: simplify plugin validation 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager f7f4aabe9e refactor: use the lastInput arg to compute the prepare pipeline next input
Use the `getNextInput`'s provided argument rather than relying on the closure.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 12de6282dd refactor: fix incorrect comments in lib/plugins/pipeline.js 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager d3032868d8 docs: fix default value for analyzeCommits plugin 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager ed9c456f5e refactor: always return an Array of results/errors from a plugin pipeline
Always return an `Array` from a pipeline simplify the function utilization as it's more deterministic. Previously, it would return/throw a single value/error when called with a single input and an `Array` of results/errors when called with an `Array` of input.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager cac48823f1 docs: clarify verifyRelease plugin description 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 09348f1b8a style: disable max-params warning for lib/plugins/normalize.js 2018-07-10 13:18:58 -04:00
Pierre Vanduynslager f93eeb799b fix: do not set path to plugin config defined as a Function or an Array
If a plugin hook was defined as a `Function` or an `Array` the `path` property would be set to the default value.
Even if this bug had no actual negative impact, it should be fixed so the code would perform as intended.
2018-07-10 13:18:58 -04:00
Pierre Vanduynslager 071dccea4b fix: use unauthenticated URL to check if branch is up to date
In case the authentication token provided is unauthorized the call to `isBranchUpToDate` will fail due to lack of read permission if that URL is used. As a result the error about outdated local branch will be reported instead of the one about missing permission.
By using the original (unauthenticated) URL `isBranchUpToDate` shouldn't fail due to permission as it requires only read permissions, that are necessarly present as the CI wass able to clone the repo.
2018-07-10 13:07:00 -04:00
Pierre Vanduynslager eb22f9998d docs: FAQ for reverting a release 2018-07-10 11:52:39 -04:00
Pierre Vanduynslager 5847514fcc fix: allow empty release notes in dry-run mode 2018-07-10 11:42:22 -04:00
Pierre Vanduynslager a39ccb8f6c docs: add missing link in GitBook summary 2018-07-08 01:16:21 -04:00
Pierre VanduynslagerandGitHub 0862480cf0 fix(package): update hosted-git-info to version 2.7.1 2018-07-06 21:47:50 -04:00
Pierre Vanduynslager 45eee4acdd fix: fetch all tags even if the repo is not shallow 2018-07-02 18:38:24 -04:00
Pierre Vanduynslager 2d3a5e53e9 test: harmonize git-utils functions name 2018-07-02 18:38:24 -04:00
Pierre Vanduynslager 4abda31f83 fix: add debug log for git fetch command 2018-07-02 16:50:41 -04:00
Pierre Vanduynslager 3602716c0b docs: update semantic-release and travis-deploy-once versions in examples 2018-07-02 11:23:37 -04:00
Pierre Vanduynslager d4f68a5680 fix(package): update yargs to version 12.0.0 2018-06-26 15:46:19 -04:00
Pierre Vanduynslager 4454d57e02 feat: allow to disable the publish plugin hook 2018-06-19 15:03:43 -04:00
greenkeeper[bot]andPierre Vanduynslager 9137f8537b fix(package): update read-pkg-up to version 4.0.0 2018-06-18 11:15:16 -04:00
Trevor RichardsonandPierre Vanduynslager 7615fdc9bc Fix documented explanation in plugins.md
`verifyConditions` explanation was wrong previously.
2018-06-18 10:52:27 -04:00
Pierre Vanduynslager 2b6378f26f fix: use git ls-remote to verify if the remote branch is ahead 2018-06-15 16:16:55 -04:00
Pierre Vanduynslager 24a8052038 refactor: simply EGITNOPERMISSION error parameters 2018-06-15 16:16:55 -04:00
greenkeeper[bot]andPierre Vanduynslager 0ab0426075 fix(package): update p-locate to version 3.0.0 2018-06-15 12:22:47 -04:00
Pierre Vanduynslager f9d9144e3d docs: Add a troubleshooting section about squashed commits 2018-06-11 11:52:23 -04:00
greenkeeper[bot]andPierre Vanduynslager 11cef46c48 chore(package): update sinon to version 6.0.0 2018-06-11 11:33:14 -04:00
34 changed files with 482 additions and 405 deletions
+3 -9
View File
@@ -1,9 +1,5 @@
# Summary
## About
- [Highlights](README.md#highlights)
- [How does it work?](README.md#how-does-it-work)
## Usage
- [Installation](docs/usage/installation.md#installation)
- [CI Configuration](docs/usage/ci-configuration.md#ci-configuration)
@@ -21,18 +17,16 @@
- [Travis CI](docs/recipes/travis.md)
- [Travis CI with build stages](docs/recipes/travis-build-stages.md)
- [GitLab CI](docs/recipes/gitlab-ci.md)
- [Git hosted services](docs/recipes/README.md)
- [Git authentication with SSH keys](docs/recipes/git-auth-ssh-keys.md)
- [Package managers and languages](docs/recipes/README.md)
## Developer guide
- [Plugin](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)
-8
View File
@@ -1,8 +0,0 @@
{
"plugins" : ["github", "anchors"],
"pluginsConfig": {
"github": {
"url": "https://github.com/semantic-release/semantic-release"
}
}
}
+7 -9
View File
@@ -1,5 +1,3 @@
const {pickBy, isUndefined} = require('lodash');
const stringList = {
type: 'string',
array: true,
@@ -13,6 +11,7 @@ module.exports = async () => {
const cli = require('yargs')
.command('$0', 'Run automated package publishing', yargs => {
yargs.demandCommand(0, 0).usage(`Run automated package publishing
Usage:
semantic-release [options] [plugins]`);
})
@@ -20,7 +19,7 @@ Usage:
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: undefined, type: 'boolean', group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
@@ -29,16 +28,15 @@ Usage:
.option('publish', {...stringList, group: 'Plugins'})
.option('success', {...stringList, group: 'Plugins'})
.option('fail', {...stringList, group: 'Plugins'})
.option('debug', {describe: 'Output debugging information', default: undefined, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: undefined, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', default: undefined, group: 'Options'})
.option('v', {alias: 'version', default: undefined, group: 'Options'})
.option('debug', {describe: 'Output debugging information', type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', group: 'Options'})
.option('v', {alias: 'version', group: 'Options'})
.strict(false)
.exitProcess(false);
try {
// Remove option with undefined values, as yargs sets non defined options as `undefined`
const {help, version, ...opts} = pickBy(cli.argv, value => !isUndefined(value));
const {help, version, ...opts} = cli.argv;
if (Boolean(help) || Boolean(version)) {
process.exitCode = 0;
-1
View File
@@ -4,5 +4,4 @@
- [Extending](extending/README.md)- Extending **semantic-release** with plugins and shareable configurations
- [Recipes](recipes/README.md) - Community written recipes for common **semantic-release** use-cases
- [Developer Guide](developer-guide/README.md) - The essentials of writing a **semantic-release** plugin or shareable configurations
- [Resources](resources.md) - Videos, articles and tutorials
- [Support](support/README.md) - FAQ and troubleshooting
+1 -1
View File
@@ -59,7 +59,7 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
"semantic-release": "^15.0.0"
}
}
```
+1 -1
View File
@@ -51,7 +51,7 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
"semantic-release": "^15.0.0"
}
}
```
+1 -1
View File
@@ -48,7 +48,7 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
"semantic-release": "^15.0.0"
}
}
```
+2 -2
View File
@@ -83,8 +83,8 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
```json
{
"devDependencies": {
"semantic-release": "^12.0.0",
"travis-deploy-once": "^4.0.0"
"semantic-release": "^15.0.0",
"travis-deploy-once": "^5.0.0"
}
}
```
+14
View File
@@ -124,6 +124,20 @@ Yes, the publishing to the npm registry can be disabled with the [`npmPublish`](
See the [`@semantic-release/npm`](https://github.com/semantic-release/npm#semantic-releasenpm) plugin documentation for more details.
## How can I revert a release?
If you have introduced a breaking bug in a release you have 2 options:
- If you have a fix immediately ready, commit and push it (or merge it via a pull request) to the release branch
- Otherwise [revert the commit](https://git-scm.com/docs/git-revert) that introduced the bug and push the revert commit (or merge it via a pull request) to the release branch
In both cases **semantic-release** will publish a new release, so your package users' will get the fixed/reverted version.
Depending on the package manager you are using, you might be able to un-publish or deprecate a release, in order to prevent users to download it by accident. For example npm allows you to [un-publish](https://docs.npmjs.com/cli/unpublish) in [next 72 hours](https://www.npmjs.com/policies/unpublish) after releasing or to [deprecate](https://docs.npmjs.com/cli/deprecate) a release.
In any case **do not remove the Git tag associated with the buggy version**, otherwise **semantic-release** will later try to republish that version. Publishing a version after un-publishing is not supported by most package managers.
**Note**: If you are using the default [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines) be aware that it uses a different revert commit format than the standard one created by [git revert](https://git-scm.com/docs/git-revert), contrary to what is [claimed in the convention](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#revert). Therefore, if you revert a commit with [`git revert`](https://git-scm.com/docs/git-revert), use the [`--edit` option](https://git-scm.com/docs/git-revert#git-revert---edit) to format the message according to the [Angular revert commit message format](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#revert). See [conventional-changelog/conventional-changelog#348](https://github.com/conventional-changelog/conventional-changelog/issues/348) for more details.
## Can I use `.npmrc` options?
Yes, all the [npm configuration options](https://docs.npmjs.com/misc/config) are supported via the [`.npmrc`](https://docs.npmjs.com/files/npmrc) file at the root of your repository.
+1
View File
@@ -1,5 +1,6 @@
# Support
- [Resources](resources.md) - Videos, articles and tutorials
- [Frequently Asked Questions](FAQ.md)
- [Troubleshooting](troubleshooting.md)
- [Node version requirement](node-version.md)
+10
View File
@@ -44,3 +44,13 @@ npm ERR! You do not have permission to publish "<package-name>". Are you logged
This message is a little unclear, and might not have anything to with your `NPM_TOKEN` or authentication method. It might instead be related to the package name itself. If there is already a package with the same name as yours or, there is a very close match, it could trigger this error.
Best way to be sure, is to search [npmjs.org](https://www.npmjs.com/)) using your package name. If there is a name conflict, rename your package in your `package.json`
## Squashed commits are ignored by **semantic-release**
**semantic-release** parses commits according to a [commit message convention](https://github.com/semantic-release/semantic-release#commit-message-format) to figure out how they affect the codebase. Commits that doesn't follow the project's commit message convention are simply ignored.
When [squashing commits](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History#_squashing) most Git tools will by default generate a new commit message with a summary of the squashed commits. This commit message will most likely not be compliant with the project's commit message convention and therefore will be ignored by **semantic-release**.
When squashing commits make sure to rewrite the resulting commit message to be compliant with the project's commit message convention.
**Note**: if the resulting squashed commit would encompasses multiple changes (for example multiple unrelated features or fixes) then it's probably not a good idea to squash those commits together. A commit should contain exactly one self-contained functional change and a functional change should be contained in exactly one commit. See [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit).
+3 -3
View File
@@ -145,7 +145,7 @@ See [Plugins configuration](plugins.md#configuration) for more details.
Type: `String`, `Object`
Default: `['@semantic-release/commit-analyzer']`
Default: `'@semantic-release/commit-analyzer'`
CLI argument: `--analyze-commits`
@@ -167,13 +167,13 @@ See [Plugins configuration](plugins.md#configuration) for more details.
### generateNotes
Type: `String`, `Object`
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/release-notes-generator']`
CLI argument: `--generate-notes`
Define the [generate notes plugin](plugins.md#generatenotes-plugin).
Define the [generate notes plugins](plugins.md#generatenotes-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
+2 -2
View File
@@ -24,6 +24,6 @@ For other type of projects we recommend installing **semantic-release** directly
$ npx semantic-release
```
**Note:** For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@12`, or `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@15`, or `npm install -g semantic-release@15`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
**Note:** `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
+3 -3
View File
@@ -20,13 +20,13 @@ Default implementation: [@semantic-release/commit-analyzer](https://github.com/s
### verifyRelease plugin
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
Default implementation: none.
### generateNotes plugin
Responsible for generating release notes.
Responsible for generating release notes. If multiple `generateNotes` plugins are defined, the release notes will be the result of the concatenation of plugin output.
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
@@ -91,7 +91,7 @@ For example:
With this configuration:
- the `custom-plugin` npm module will be used to [analyze commits](#analyzecommits-plugin)
- the `./build/my-plugin.js` script will be used to [generate release notes](#generatenotes-plugin)
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugins will be used to [verify conditions](#verifyconditions-plugin)
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec), [`@semantic-release/npm`](https://github.com/semantic-release/npm) and [`@semantic-release/github`](https://github.com/semantic-release/github) plugins will be used to [verify conditions](#verifyconditions-plugin)
- the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin will be used to [verify the release](#verifyrelease-plugin)
- the `cmd` option will be set to `verify-conditions.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify conditions](#verifyconditions-plugin)
- the `cmd` option will be set to `verify-release.sh` only for the [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin used to [verify the release](#verifyrelease-plugin)
+21 -58
View File
@@ -1,4 +1,4 @@
const {template, isPlainObject, castArray} = require('lodash');
const {template} = require('lodash');
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
@@ -13,7 +13,7 @@ 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 {unshallow, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
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');
@@ -54,42 +54,32 @@ async function run(options, plugins) {
await verify(options);
const {repositoryUrl} = options;
options.repositoryUrl = await getGitAuthUrl(options);
if (!(await isBranchUpToDate(options.branch))) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
);
return false;
}
try {
await verifyAuth(options.repositoryUrl, options.branch);
} catch (err) {
if (!(await isBranchUpToDate(options.branch))) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
);
return false;
}
logger.error(`The command "${err.cmd}" failed with the error message %s.`, err.stderr);
throw getError('EGITNOPERMISSION', {options, repositoryUrl});
throw getError('EGITNOPERMISSION', {options});
}
logger.log('Run automated release from branch %s', options.branch);
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger}, {settleAll: true});
await plugins.verifyConditions({options, logger});
// Unshallow the repo in order to get all the tags
await unshallow(options.repositoryUrl);
await fetch(options.repositoryUrl);
const lastRelease = await getLastRelease(options.tagFormat, logger);
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({
options,
logger,
lastRelease,
commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)),
});
const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
if (!type) {
logger.log('There are no relevant changes, so no new version is released.');
return;
@@ -97,55 +87,28 @@ async function run(options, plugins) {
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
logger.log('Call plugin %s', 'verify-release');
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, {settleAll: true});
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease});
const generateNotesParam = {options, logger, lastRelease, commits, nextRelease};
if (options.dryRun) {
logger.log('Call plugin %s', 'generate-notes');
const notes = await plugins.generateNotes(generateNotesParam);
logger.log('Release note for version %s:\n', nextRelease.version);
process.stdout.write(`${marked(notes)}\n`);
if (notes) {
process.stdout.write(`${marked(notes)}\n`);
}
} else {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
logger.log('Call plugin %s', 'prepare');
await plugins.prepare(
{options, logger, lastRelease, commits, nextRelease},
{
getNextInput: async lastResult => {
const newGitHead = await getGitHead();
// If previous prepare plugin has created a commit (gitHead changed)
if (lastResult.nextRelease.gitHead !== newGitHead) {
nextRelease.gitHead = newGitHead;
// Regenerate the release notes
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
}
// Call the next publish plugin with the updated `nextRelease`
return {options, logger, lastRelease, commits, nextRelease};
},
}
);
await plugins.prepare({options, logger, lastRelease, commits, nextRelease});
// 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);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
{options, logger, lastRelease, commits, nextRelease},
// Add nextRelease and plugin properties to published release
{transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step})}
);
const releases = await plugins.publish({options, logger, lastRelease, commits, nextRelease});
await plugins.success(
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
{settleAll: true}
);
await plugins.success({options, logger, lastRelease, commits, nextRelease, releases});
logger.log('Published release: %s', nextRelease.version);
}
@@ -170,7 +133,7 @@ async function callFail(plugins, options, error) {
const errors = extractErrors(error).filter(error => error.semanticRelease);
if (errors.length > 0) {
try {
await plugins.fail({options, logger, errors}, {settleAll: true});
await plugins.fail({options, logger, errors});
} catch (err) {
logErrors(err);
}
+3 -1
View File
@@ -6,4 +6,6 @@ const COMMIT_NAME = 'semantic-release-bot';
const COMMIT_EMAIL = 'semantic-release-bot@martynus.net';
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL};
const RELEASE_NOTES_SEPARATOR = '\n\n';
module.exports = {RELEASE_TYPE, FIRST_RELEASE, COMMIT_NAME, COMMIT_EMAIL, RELEASE_NOTES_SEPARATOR};
+14 -14
View File
@@ -4,7 +4,7 @@ const {toLower, isString} = require('lodash');
const pkg = require('../../package.json');
const {RELEASE_TYPE} = require('./constants');
const homepage = url.format({...url.parse(pkg.homepage), ...{hash: null}});
const homepage = url.format({...url.parse(pkg.homepage), hash: null});
const stringify = obj => (isString(obj) ? obj : inspect(obj, {breakLength: Infinity, depth: 2, maxArrayLength: 5}));
const linkify = file => `${homepage}/blob/caribou/${file}`;
@@ -27,11 +27,11 @@ Please make sure to add the \`repositoryUrl\` to the [semantic-release configura
'docs/usage/configuration.md'
)}).`,
}),
EGITNOPERMISSION: ({options, repositoryUrl}) => ({
EGITNOPERMISSION: ({options}) => ({
message: 'The push permission to the Git repository is required.',
details: `**semantic-release** cannot push the version tag to the branch \`${
options.branch
}\` on remote Git repository with URL \`${repositoryUrl}\`.
}\` on remote Git repository with URL \`${options.repositoryUrl}\`.
Please refer to the [authentication configuration documentation](${linkify(
'docs/usage/ci-configuration.md#authentication'
@@ -55,25 +55,25 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
}),
EPLUGINCONF: ({pluginType, pluginConf}) => ({
message: `The \`${pluginType}\` plugin configuration is invalid.`,
details: `The [${pluginType} plugin configuration](${linkify(
`docs/usage/plugins.md#${toLower(pluginType)}-plugin`
EPLUGINCONF: ({type, pluginConf}) => ({
message: `The \`${type}\` plugin configuration is invalid.`,
details: `The [${type} plugin configuration](${linkify(
`docs/usage/plugins.md#${toLower(type)}-plugin`
)}) if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a \`path\` property.
Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`,
Your configuration for the \`${type}\` plugin is \`${stringify(pluginConf)}\`.`,
}),
EPLUGIN: ({pluginName, pluginType}) => ({
message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`,
details: `A valid \`${pluginType}\` **semantic-release** plugin must be a function or an object with a function in the property \`${pluginType}\`.
EPLUGIN: ({pluginName, type}) => ({
message: `A plugin configured in the step ${type} is not a valid semantic-release plugin.`,
details: `A valid \`${type}\` **semantic-release** plugin must be a function or an object with a function in the property \`${type}\`.
The plugin \`${pluginName}\` doesn't have the property \`${pluginType}\` and cannot be used for the \`${pluginType}\` step.
The plugin \`${pluginName}\` doesn't have the property \`${type}\` and cannot be used for the \`${type}\` step.
Please refer to the \`${pluginName}\` and [semantic-release plugins configuration](${linkify(
'docs/usage/plugins.md'
)}) documentation for more details.`,
}),
EANALYZEOUTPUT: ({result, pluginName}) => ({
EANALYZECOMMITSOUTPUT: ({result, pluginName}) => ({
message: 'The `analyzeCommits` plugin returned an invalid value. It must return a valid semver release type.',
details: `The \`analyzeCommits\` plugin must return a valid [semver](https://semver.org) release type. The valid values are: ${RELEASE_TYPE.map(
type => `\`${type}\``
@@ -89,7 +89,7 @@ We recommend to report the issue to the \`${pluginName}\` authors, providing the
'docs/developer-guide/plugin.md'
)})`,
}),
ERELEASENOTESOUTPUT: ({result, pluginName}) => ({
EGENERATENOTESOUTPUT: ({result, pluginName}) => ({
message: 'The `generateNotes` plugin returned an invalid value. It must return a `String`.',
details: `The \`generateNotes\` plugin must return a \`String\`.
+55 -38
View File
@@ -1,67 +1,84 @@
const {isString, isFunction, isArray, isPlainObject} = require('lodash');
const {RELEASE_TYPE} = require('./constants');
const {gitHead} = require('../git');
const {RELEASE_TYPE, RELEASE_NOTES_SEPARATOR} = require('./constants');
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
module.exports = {
verifyConditions: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
pipelineConfig: () => ({settleAll: true}),
},
analyzeCommits: {
default: '@semantic-release/commit-analyzer',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
},
output: {
validator: output => !output || RELEASE_TYPE.includes(output),
error: 'EANALYZEOUTPUT',
},
configValidator: conf => Boolean(conf) && validatePluginConfig(conf),
outputValidator: output => !output || RELEASE_TYPE.includes(output),
preprocess: ({commits, ...inputs}) => ({
...inputs,
commits: commits.filter(commit => !/\[skip\s+release\]|\[release\s+skip\]/i.test(commit.message)),
}),
postprocess: ([result]) => result,
},
verifyRelease: {
default: false,
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
pipelineConfig: () => ({settleAll: true}),
},
generateNotes: {
default: '@semantic-release/release-notes-generator',
config: {
validator: conf => !conf || validatePluginConfig(conf),
},
output: {
validator: output => !output || isString(output),
error: 'ERELEASENOTESOUTPUT',
},
default: ['@semantic-release/release-notes-generator'],
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
outputValidator: output => !output || isString(output),
pipelineConfig: () => ({
getNextInput: ({nextRelease, ...generateNotesParam}, notes) => ({
...generateNotesParam,
nextRelease: {
...nextRelease,
notes: `${nextRelease.notes ? `${nextRelease.notes}${RELEASE_NOTES_SEPARATOR}` : ''}${notes}`,
},
}),
}),
postprocess: results => results.filter(Boolean).join(RELEASE_NOTES_SEPARATOR),
},
prepare: {
default: ['@semantic-release/npm'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
pipelineConfig: ({generateNotes}, logger) => ({
getNextInput: async ({nextRelease, ...prepareParam}) => {
const newGitHead = await gitHead();
// If previous prepare plugin has created a commit (gitHead changed)
if (nextRelease.gitHead !== newGitHead) {
nextRelease.gitHead = newGitHead;
// Regenerate the release notes
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await generateNotes({nextRelease, ...prepareParam});
}
// Call the next publish plugin with the updated `nextRelease`
return {...prepareParam, nextRelease};
},
}),
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
output: {
validator: output => !output || isPlainObject(output),
error: 'EPUBLISHOUTPUT',
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
outputValidator: output => !output || isPlainObject(output),
pipelineConfig: () => ({
// Add `nextRelease` and plugin properties to published release
transform: (release, step, {nextRelease}) => ({
...(isPlainObject(release) ? release : {}),
...nextRelease,
...step,
}),
}),
},
success: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
pipelineConfig: () => ({settleAll: true}),
},
fail: {
default: ['@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
configValidator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
pipelineConfig: () => ({settleAll: true}),
},
};
+2 -2
View File
@@ -34,9 +34,9 @@ module.exports = async (opts, logger) => {
// For each plugin defined in a shareable config, save in `pluginsPath` the extendable config path,
// so those plugin will be loaded relatively to the config file
Object.keys(extendsOpts).reduce((pluginsPath, option) => {
Object.entries(extendsOpts).reduce((pluginsPath, [option, value]) => {
if (PLUGINS_DEFINITIONS[option]) {
castArray(extendsOpts[option])
castArray(value)
.filter(plugin => isString(plugin) || (isPlainObject(plugin) && isString(plugin.path)))
.map(plugin => (isString(plugin) ? plugin : plugin.path))
.forEach(plugin => {
+1 -4
View File
@@ -35,10 +35,7 @@ module.exports = async ({repositoryUrl, branch}) => {
// Replace `git+https` and `git+http` with `https` or `http`
if (protocols.includes('http') || protocols.includes('https')) {
repositoryUrl = format({
...parse(repositoryUrl),
...{protocol: protocols.includes('https') ? 'https' : 'http'},
});
repositoryUrl = format({...parse(repositoryUrl), protocol: protocols.includes('https') ? 'https' : 'http'});
}
}
+15 -5
View File
@@ -49,12 +49,16 @@ async function isRefInHistory(ref) {
}
/**
* Unshallow the git repository (retriving every commits and tags).
* Unshallow the git repository if necessary and fetch all the tags.
*
* @param {String} repositoryUrl The remote repository URL.
*/
async function unshallow(repositoryUrl) {
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], {reject: false});
async function fetch(repositoryUrl) {
try {
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl]);
} catch (err) {
await execa('git', ['fetch', '--tags', repositoryUrl]);
}
}
/**
@@ -146,14 +150,20 @@ async function verifyTagName(tagName) {
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, falsy otherwise.
*/
async function isBranchUpToDate(branch) {
return isRefInHistory(await execa.stdout('git', ['rev-parse', `origin/${branch}`]));
try {
return await isRefInHistory(
(await execa.stdout('git', ['ls-remote', '--heads', 'origin', branch])).match(/^(\w+)?/)[1]
);
} catch (err) {
debug(err);
}
}
module.exports = {
gitTagHead,
gitTags,
isRefInHistory,
unshallow,
fetch,
gitHead,
repoUrl,
isGitRepo,
+28 -22
View File
@@ -1,4 +1,4 @@
const {isArray, isObject, omit, castArray, isUndefined} = require('lodash');
const {identity, isPlainObject, omit, castArray, isUndefined} = require('lodash');
const AggregateError = require('aggregate-error');
const getError = require('../get-error');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
@@ -7,32 +7,38 @@ const normalize = require('./normalize');
module.exports = (options, pluginsPath, logger) => {
const errors = [];
const plugins = Object.keys(PLUGINS_DEFINITIONS).reduce((plugins, pluginType) => {
const {config, default: def} = PLUGINS_DEFINITIONS[pluginType];
let pluginConfs;
const plugins = Object.entries(PLUGINS_DEFINITIONS).reduce(
(
plugins,
[type, {configValidator, default: def, pipelineConfig, postprocess = identity, preprocess = identity}]
) => {
let pluginConfs;
if (isUndefined(options[pluginType])) {
pluginConfs = def;
} else {
// If an object is passed and the path is missing, set the default one for single plugins
if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
options[pluginType].path = def;
if (isUndefined(options[type])) {
pluginConfs = def;
} else {
const defaultPaths = castArray(def);
// If an object is passed and the path is missing, set the default one for single plugins
if (isPlainObject(options[type]) && !options[type].path && defaultPaths.length === 1) {
[options[type].path] = defaultPaths;
}
if (configValidator && !configValidator(options[type])) {
errors.push(getError('EPLUGINCONF', {type, pluginConf: options[type]}));
return plugins;
}
pluginConfs = options[type];
}
if (config && !config.validator(options[pluginType])) {
errors.push(getError('EPLUGINCONF', {pluginType, pluginConf: options[pluginType]}));
return plugins;
}
pluginConfs = options[pluginType];
}
const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS));
const globalOpts = omit(options, Object.keys(PLUGINS_DEFINITIONS));
const steps = castArray(pluginConfs).map(conf => normalize(type, pluginsPath, globalOpts, conf, logger));
plugins[pluginType] = pipeline(
castArray(pluginConfs).map(conf => normalize(pluginType, pluginsPath, globalOpts, conf, logger))
);
plugins[type] = async input =>
postprocess(await pipeline(steps, pipelineConfig && pipelineConfig(plugins, logger))(await preprocess(input)));
return plugins;
}, {});
return plugins;
},
{}
);
if (errors.length > 0) {
throw new AggregateError(errors);
}
+12 -9
View File
@@ -5,7 +5,9 @@ const getError = require('../get-error');
const {extractErrors} = require('../utils');
const PLUGINS_DEFINITIONS = require('../definitions/plugins');
module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
/* eslint max-params: ["error", 5] */
module.exports = (type, pluginsPath, globalOpts, pluginOpts, logger) => {
if (!pluginOpts) {
return noop;
}
@@ -15,9 +17,9 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
if (!isFunction(pluginOpts)) {
if (pluginsPath[path]) {
logger.log('Load plugin "%s" from %s in shareable config %s', pluginType, path, 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', pluginType, path);
logger.log('Load plugin "%s" from %s', type, path);
}
}
@@ -31,18 +33,19 @@ module.exports = (pluginType, pluginsPath, globalOpts, pluginOpts, logger) => {
let func;
if (isFunction(plugin)) {
func = plugin.bind(null, cloneDeep({...globalOpts, ...config}));
} else if (isPlainObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
func = plugin[pluginType].bind(null, cloneDeep({...globalOpts, ...config}));
} else if (isPlainObject(plugin) && plugin[type] && isFunction(plugin[type])) {
func = plugin[type].bind(null, cloneDeep({...globalOpts, ...config}));
} else {
throw getError('EPLUGIN', {pluginType, pluginName});
throw getError('EPLUGIN', {type, pluginName});
}
const validator = async input => {
const definition = PLUGINS_DEFINITIONS[pluginType];
const {outputValidator} = PLUGINS_DEFINITIONS[type] || {};
try {
logger.log('Call plugin "%s"', type);
const result = await func(cloneDeep(input));
if (definition && definition.output && !definition.output.validator(result)) {
throw getError(PLUGINS_DEFINITIONS[pluginType].output.error, {result, pluginName});
if (outputValidator && !outputValidator(result)) {
throw getError(`E${type.toUpperCase()}OUTPUT`, {result, pluginName});
}
return result;
} catch (err) {
+12 -11
View File
@@ -8,10 +8,6 @@ const {extractErrors} = require('../utils');
*
* @typedef {Function} Pipeline
* @param {Any} input Argument to pass to the first step in the pipeline.
* @param {Object} options Pipeline options.
* @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects.
* @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last and current step results; the returned value will be used as the argument of the next step.
* @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result and the step function; the returned value will be saved in the pipeline results.
*
* @return {Array<*>|*} An Array with the result of each step in the pipeline; if there is only 1 step in the pipeline, the result of this step is returned directly.
*
@@ -22,18 +18,23 @@ const {extractErrors} = require('../utils');
* Create a Pipeline with a list of Functions.
*
* @param {Array<Function>} steps The list of Function to execute.
* @param {Object} options Pipeline options.
* @param {Boolean} [options.settleAll=false] If `true` all the steps in the pipeline are executed, even if one rejects, if `false` the execution stops after a steps rejects.
* @param {Function} [options.getNextInput=identity] Function called after each step is executed, with the last step input and the current current step result; the returned value will be used as the input of the next step.
* @param {Function} [options.transform=identity] Function called after each step is executed, with the current step result, the step function and the last step input; the returned value will be saved in the pipeline results.
*
* @return {Pipeline} A Function that execute the `steps` sequencially
*/
module.exports = steps => async (input, {settleAll = false, getNextInput = identity, transform = identity} = {}) => {
module.exports = (steps, {settleAll = false, getNextInput = identity, transform = identity} = {}) => async input => {
const results = [];
const errors = [];
await pReduce(
steps,
async (lastResult, step) => {
async (lastInput, step) => {
let result;
try {
// Call the step with the input computed at the end of the previous iteration and save intermediary result
result = await transform(await step(lastResult), step);
result = await transform(await step(lastInput), step, lastInput);
results.push(result);
} catch (err) {
if (settleAll) {
@@ -43,13 +44,13 @@ module.exports = steps => async (input, {settleAll = false, getNextInput = ident
throw err;
}
}
// Prepare input for the next step, passing the result of the last iteration (or initial parameter for the first iteration) and the current one
return getNextInput(lastResult, result);
// Prepare input for the next step, passing the input of the last iteration (or initial parameter for the first iteration) and the result of the current one
return getNextInput(lastInput, result);
},
input
);
if (errors.length > 0) {
throw errors.length === 1 ? errors[0] : new AggregateError(errors);
throw new AggregateError(errors);
}
return results.length <= 1 ? results[0] : results;
return results;
};
+7 -7
View File
@@ -33,22 +33,22 @@
"find-versions": "^2.0.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^9.0.0",
"git-url-parse": "^10.0.1",
"hook-std": "^1.0.1",
"hosted-git-info": "^2.6.0",
"hosted-git-info": "^2.7.1",
"lodash": "^4.17.4",
"marked": "^0.4.0",
"marked-terminal": "^3.0.0",
"p-locate": "^2.0.0",
"p-locate": "^3.0.0",
"p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"read-pkg-up": "^4.0.0",
"resolve-from": "^4.0.0",
"semver": "^5.4.1",
"yargs": "^11.0.0"
"yargs": "^12.0.0"
},
"devDependencies": {
"ava": "^0.25.0",
"clear-module": "^2.1.0",
"clear-module": "^3.0.0",
"codecov": "^3.0.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
@@ -63,7 +63,7 @@
"nyc": "^12.0.1",
"p-retry": "^2.0.0",
"proxyquire": "^2.0.0",
"sinon": "^5.0.1",
"sinon": "^6.0.0",
"tempy": "^0.2.1",
"xo": "^0.21.0"
},
+82 -86
View File
@@ -1,133 +1,129 @@
import test from 'ava';
import plugins from '../../lib/definitions/plugins';
import errors from '../../lib/definitions/errors';
import {RELEASE_NOTES_SEPARATOR} from '../../lib/definitions/constants';
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyConditions.config.validator({}));
t.false(plugins.verifyConditions.config.validator({path: null}));
t.false(plugins.verifyConditions.configValidator({}));
t.false(plugins.verifyConditions.configValidator({path: null}));
t.true(plugins.verifyConditions.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyConditions.config.validator());
t.true(plugins.verifyConditions.config.validator('plugin-path.js'));
t.true(plugins.verifyConditions.config.validator(() => {}));
t.true(plugins.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.verifyConditions.configValidator({path: 'plugin-path.js'}));
t.true(plugins.verifyConditions.configValidator());
t.true(plugins.verifyConditions.configValidator('plugin-path.js'));
t.true(plugins.verifyConditions.configValidator(() => {}));
t.true(plugins.verifyConditions.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
t.false(plugins.analyzeCommits.config.validator({}));
t.false(plugins.analyzeCommits.config.validator({path: null}));
t.false(plugins.analyzeCommits.config.validator([]));
t.false(plugins.analyzeCommits.config.validator());
t.false(plugins.analyzeCommits.configValidator({}));
t.false(plugins.analyzeCommits.configValidator({path: null}));
t.false(plugins.analyzeCommits.configValidator([]));
t.false(plugins.analyzeCommits.configValidator());
t.true(plugins.analyzeCommits.config.validator({path: 'plugin-path.js'}));
t.true(plugins.analyzeCommits.config.validator('plugin-path.js'));
t.true(plugins.analyzeCommits.config.validator(() => {}));
t.true(plugins.analyzeCommits.configValidator({path: 'plugin-path.js'}));
t.true(plugins.analyzeCommits.configValidator('plugin-path.js'));
t.true(plugins.analyzeCommits.configValidator(() => {}));
});
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.false(plugins.verifyRelease.configValidator({}));
t.false(plugins.verifyRelease.configValidator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.configValidator());
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
t.true(plugins.verifyRelease.configValidator(() => {}));
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
t.false(plugins.generateNotes.config.validator({}));
t.false(plugins.generateNotes.config.validator({path: null}));
t.false(plugins.generateNotes.config.validator([]));
test('The "generateNotes" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.generateNotes.configValidator({}));
t.false(plugins.generateNotes.configValidator({path: null}));
t.true(plugins.generateNotes.config.validator());
t.true(plugins.generateNotes.config.validator({path: 'plugin-path.js'}));
t.true(plugins.generateNotes.config.validator('plugin-path.js'));
t.true(plugins.generateNotes.config.validator(() => {}));
t.true(plugins.generateNotes.configValidator({path: 'plugin-path.js'}));
t.true(plugins.generateNotes.configValidator());
t.true(plugins.generateNotes.configValidator('plugin-path.js'));
t.true(plugins.generateNotes.configValidator(() => {}));
t.true(plugins.generateNotes.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.false(plugins.verifyRelease.configValidator({}));
t.false(plugins.verifyRelease.configValidator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.verifyRelease.configValidator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.configValidator());
t.true(plugins.verifyRelease.configValidator('plugin-path.js'));
t.true(plugins.verifyRelease.configValidator(() => {}));
t.true(plugins.verifyRelease.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(plugins.publish.config.validator({}));
t.false(plugins.publish.config.validator({path: null}));
t.false(plugins.publish.config.validator());
t.false(plugins.publish.configValidator({}));
t.false(plugins.publish.configValidator({path: null}));
t.true(plugins.publish.config.validator({path: 'plugin-path.js'}));
t.true(plugins.publish.config.validator('plugin-path.js'));
t.true(plugins.publish.config.validator(() => {}));
t.true(plugins.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.publish.configValidator({path: 'plugin-path.js'}));
t.true(plugins.publish.configValidator());
t.true(plugins.publish.configValidator('plugin-path.js'));
t.true(plugins.publish.configValidator(() => {}));
t.true(plugins.publish.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "success" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.success.config.validator({}));
t.false(plugins.success.config.validator({path: null}));
t.false(plugins.success.configValidator({}));
t.false(plugins.success.configValidator({path: null}));
t.true(plugins.success.config.validator({path: 'plugin-path.js'}));
t.true(plugins.success.config.validator());
t.true(plugins.success.config.validator('plugin-path.js'));
t.true(plugins.success.config.validator(() => {}));
t.true(plugins.success.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.success.configValidator({path: 'plugin-path.js'}));
t.true(plugins.success.configValidator());
t.true(plugins.success.configValidator('plugin-path.js'));
t.true(plugins.success.configValidator(() => {}));
t.true(plugins.success.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "fail" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.fail.config.validator({}));
t.false(plugins.fail.config.validator({path: null}));
t.false(plugins.fail.configValidator({}));
t.false(plugins.fail.configValidator({path: null}));
t.true(plugins.fail.config.validator({path: 'plugin-path.js'}));
t.true(plugins.fail.config.validator());
t.true(plugins.fail.config.validator('plugin-path.js'));
t.true(plugins.fail.config.validator(() => {}));
t.true(plugins.fail.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
t.true(plugins.fail.configValidator({path: 'plugin-path.js'}));
t.true(plugins.fail.configValidator());
t.true(plugins.fail.configValidator('plugin-path.js'));
t.true(plugins.fail.configValidator(() => {}));
t.true(plugins.fail.configValidator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
t.false(plugins.analyzeCommits.output.validator('invalid'));
t.false(plugins.analyzeCommits.output.validator(1));
t.false(plugins.analyzeCommits.output.validator({}));
t.false(plugins.analyzeCommits.outputValidator('invalid'));
t.false(plugins.analyzeCommits.outputValidator(1));
t.false(plugins.analyzeCommits.outputValidator({}));
t.true(plugins.analyzeCommits.output.validator());
t.true(plugins.analyzeCommits.output.validator(null));
t.true(plugins.analyzeCommits.output.validator('major'));
t.true(plugins.analyzeCommits.outputValidator());
t.true(plugins.analyzeCommits.outputValidator(null));
t.true(plugins.analyzeCommits.outputValidator('major'));
});
test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.false(plugins.generateNotes.output.validator(1));
t.false(plugins.generateNotes.output.validator({}));
t.false(plugins.generateNotes.outputValidator(1));
t.false(plugins.generateNotes.outputValidator({}));
t.true(plugins.generateNotes.output.validator());
t.true(plugins.generateNotes.output.validator(null));
t.true(plugins.generateNotes.output.validator(''));
t.true(plugins.generateNotes.output.validator('string'));
t.true(plugins.generateNotes.outputValidator());
t.true(plugins.generateNotes.outputValidator(null));
t.true(plugins.generateNotes.outputValidator(''));
t.true(plugins.generateNotes.outputValidator('string'));
});
test('The "publish" plugin output, if defined, must be an object', t => {
t.false(plugins.publish.output.validator(1));
t.false(plugins.publish.output.validator('string'));
t.false(plugins.publish.outputValidator(1));
t.false(plugins.publish.outputValidator('string'));
t.true(plugins.publish.output.validator({}));
t.true(plugins.publish.output.validator());
t.true(plugins.publish.output.validator(null));
t.true(plugins.publish.output.validator(''));
t.true(plugins.publish.outputValidator({}));
t.true(plugins.publish.outputValidator());
t.true(plugins.publish.outputValidator(null));
t.true(plugins.publish.outputValidator(''));
});
test('The "analyzeCommits" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.analyzeCommits.output.error));
});
test('The "generateNotes" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.generateNotes.output.error));
});
test('The "publish" plugin output definition return an existing error code', t => {
t.true(Object.keys(errors).includes(plugins.publish.output.error));
test('The "generateNotes" plugins output are concatenated with separator', t => {
t.is(plugins.generateNotes.postprocess(['note 1', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
t.is(plugins.generateNotes.postprocess(['', 'note']), 'note');
t.is(plugins.generateNotes.postprocess([undefined, 'note']), 'note');
t.is(plugins.generateNotes.postprocess(['note 1', '', 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
t.is(plugins.generateNotes.postprocess(['note 1', undefined, 'note 2']), `note 1${RELEASE_NOTES_SEPARATOR}note 2`);
});
+32 -11
View File
@@ -3,7 +3,7 @@ import tempy from 'tempy';
import {
gitTagHead,
isRefInHistory,
unshallow,
fetch,
gitHead,
repoUrl,
tag,
@@ -23,8 +23,8 @@ import {
gitAddConfig,
gitCommitTag,
gitRemoteTagHead,
push as pushUtil,
reset,
gitPush,
gitDetachedHead,
} from './helpers/git-utils';
// Save the current working diretory
@@ -53,7 +53,7 @@ test.serial('Throw error if the last commit sha cannot be found', async t => {
await t.throws(gitHead(), Error);
});
test.serial('Unshallow repository', async t => {
test.serial('Unshallow and fetch repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
// Add commits to the master branch
@@ -64,7 +64,7 @@ test.serial('Unshallow repository', async t => {
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits()).length, 1);
await unshallow(repo);
await fetch(repo);
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits()).length, 2);
@@ -75,7 +75,24 @@ test.serial('Do not throw error when unshallow a complete repository', async t =
const repo = await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
await t.notThrows(unshallow(repo));
await t.notThrows(fetch(repo));
});
test.serial('Fetch all tags on a detached head repository', async t => {
const repo = await gitRepo(true);
await gitCommits(['First']);
await gitTagVersion('v1.0.0');
await gitCommits(['Second']);
await gitTagVersion('v1.0.1');
const [commit] = await gitCommits(['Third']);
await gitTagVersion('v1.1.0');
await gitPush();
await gitDetachedHead(repo, commit.hash);
await fetch(repo);
t.deepEqual((await gitTags()).sort(), ['v1.0.0', 'v1.0.1', 'v1.1.0'].sort());
});
test.serial('Verify if the commit `sha` is in the direct history of the current branch', async t => {
@@ -191,20 +208,24 @@ test.serial('Throws error if obtaining the tags fails', async t => {
test.serial('Return "true" if repository is up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
await gitPush();
t.true(await isBranchUpToDate('master'));
});
test.serial('Return falsy if repository is not up to date', async t => {
await gitRepo(true);
const repositoryUrl = await gitRepo(true);
const repoDir = process.cwd();
await gitCommits(['First']);
await gitCommits(['Second']);
await pushUtil();
await gitPush();
t.true(await isBranchUpToDate('master'));
await reset();
await gitShallowClone(repositoryUrl);
await gitCommits(['Third']);
await gitPush();
process.chdir(repoDir);
t.falsy(await isBranchUpToDate('master'));
});
@@ -212,7 +233,7 @@ test.serial('Return falsy if repository is not up to date', async t => {
test.serial('Return "true" if local repository is ahead', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
await gitPush();
await gitCommits(['Second']);
t.true(await isBranchUpToDate('master'));
+2 -11
View File
@@ -151,7 +151,7 @@ export async function gitDetachedHead(repositoryUrl, head) {
process.chdir(dir);
await execa('git', ['init']);
await execa('git', ['remote', 'add', 'origin', repositoryUrl]);
await execa('git', ['fetch']);
await execa('git', ['fetch', repositoryUrl]);
await execa('git', ['checkout', head]);
return dir;
}
@@ -209,15 +209,6 @@ export async function gitCommitTag(gitHead) {
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
export async function push(repositoryUrl = 'origin', branch = 'master') {
export async function gitPush(repositoryUrl = 'origin', branch = 'master') {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* Reset repository to a commit.
*
* @param {String} [commit='HEAD~1'] Commit reference to reset the repo to.
*/
export async function reset(commit = 'HEAD~1') {
await execa('git', ['reset', commit]);
}
+113 -53
View File
@@ -4,7 +4,6 @@ import {spy, stub} from 'sinon';
import clearModule from 'clear-module';
import AggregateError from 'aggregate-error';
import SemanticReleaseError from '@semantic-release/error';
import DEFINITIONS from '../lib/definitions/plugins';
import {COMMIT_NAME, COMMIT_EMAIL} from '../lib/definitions/constants';
import {
gitHead as getGitHead,
@@ -13,9 +12,8 @@ import {
gitCommits,
gitTagVersion,
gitRemoteTagHead,
push,
gitPush,
gitShallowClone,
reset,
} from './helpers/git-utils';
// Save the current process.env
@@ -59,16 +57,20 @@ test.serial('Plugins are called with expected values', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
await gitPush();
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
const notes1 = 'Release notes 1';
const notes2 = 'Release notes 2';
const notes3 = 'Release notes 3';
const verifyConditions1 = stub().resolves();
const verifyConditions2 = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const generateNotes1 = stub().resolves(notes1);
const generateNotes2 = stub().resolves(notes2);
const generateNotes3 = stub().resolves(notes3);
const release1 = {name: 'Release 1', url: 'https://release1.com'};
const prepare = stub().resolves();
const publish1 = stub().resolves(release1);
@@ -80,7 +82,7 @@ test.serial('Plugins are called with expected values', async t => {
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
verifyRelease,
generateNotes,
generateNotes: [generateNotes1, generateNotes2, generateNotes3],
prepare,
publish: [publish1, pluginNoop],
success,
@@ -115,14 +117,32 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(verifyRelease.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(verifyRelease.args[0][1].nextRelease, nextRelease);
t.is(generateNotes.callCount, 1);
t.deepEqual(generateNotes.args[0][0], config);
t.deepEqual(generateNotes.args[0][1].options, options);
t.deepEqual(generateNotes.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease);
t.deepEqual(generateNotes.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(generateNotes1.callCount, 1);
t.deepEqual(generateNotes1.args[0][0], config);
t.deepEqual(generateNotes1.args[0][1].options, options);
t.deepEqual(generateNotes1.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes1.args[0][1].lastRelease, lastRelease);
t.deepEqual(generateNotes1.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes1.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes1.args[0][1].nextRelease, nextRelease);
t.is(generateNotes2.callCount, 1);
t.deepEqual(generateNotes2.args[0][0], config);
t.deepEqual(generateNotes2.args[0][1].options, options);
t.deepEqual(generateNotes2.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes2.args[0][1].lastRelease, lastRelease);
t.deepEqual(generateNotes2.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes2.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes2.args[0][1].nextRelease, {...nextRelease, notes: notes1});
t.is(generateNotes3.callCount, 1);
t.deepEqual(generateNotes3.args[0][0], config);
t.deepEqual(generateNotes3.args[0][1].options, options);
t.deepEqual(generateNotes3.args[0][1].logger, t.context.logger);
t.deepEqual(generateNotes3.args[0][1].lastRelease, lastRelease);
t.deepEqual(generateNotes3.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(generateNotes3.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes3.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}`});
t.is(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
@@ -131,7 +151,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][0], config);
@@ -140,7 +160,7 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(publish1.args[0][1].lastRelease, lastRelease);
t.deepEqual(publish1.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(publish1.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.is(success.callCount, 1);
t.deepEqual(success.args[0][0], config);
@@ -149,10 +169,10 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(success.args[0][1].lastRelease, lastRelease);
t.deepEqual(success.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(success.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(success.args[0][1].nextRelease, {...nextRelease, notes: `${notes1}\n\n${notes2}\n\n${notes3}`});
t.deepEqual(success.args[0][1].releases, [
{...release1, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
{...nextRelease, ...{notes}, ...{pluginName: pluginNoop}},
{...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
@@ -171,7 +191,7 @@ test.serial('Use custom tag format', async t => {
await gitCommits(['First']);
await gitTagVersion('test-1.0.0');
await gitCommits(['Second']);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
const notes = 'Release notes';
@@ -208,7 +228,7 @@ test.serial('Use new gitHead, and recreate release notes if a prepare plugin cre
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -243,16 +263,16 @@ test.serial('Use new gitHead, and recreate release notes if a prepare plugin cre
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(prepare1.callCount, 1);
t.deepEqual(prepare1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(prepare1.args[0][1].nextRelease, {...nextRelease, notes});
nextRelease.gitHead = await getGitHead();
t.deepEqual(generateNotes.args[1][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(generateNotes.args[1][1].nextRelease, {...nextRelease, notes});
t.is(prepare2.callCount, 1);
t.deepEqual(prepare2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(prepare2.args[0][1].nextRelease, {...nextRelease, notes});
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, notes});
// Verify the tag has been created on the local and remote repo and reference the last gitHead
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
@@ -268,7 +288,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -301,14 +321,10 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
t.is(success1.callCount, 1);
t.deepEqual(success1.args[0][0], config);
t.deepEqual(success1.args[0][1].releases, [
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
]);
t.deepEqual(success1.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
t.is(success2.callCount, 1);
t.deepEqual(success2.args[0][1].releases, [
{...release, ...nextRelease, ...{notes}, ...{pluginName: '[Function: proxy]'}},
]);
t.deepEqual(success2.args[0][1].releases, [{...release, ...nextRelease, notes, pluginName: '[Function: proxy]'}]);
});
test.serial('Log all "verifyConditions" errors', async t => {
@@ -316,7 +332,7 @@ test.serial('Log all "verifyConditions" errors', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
await gitPush();
const error1 = new Error('error 1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -359,7 +375,7 @@ test.serial('Log all "verifyRelease" errors', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -396,7 +412,7 @@ test.serial('Dry-run skips publish and success', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -445,7 +461,7 @@ test.serial('Dry-run skips fail', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -480,7 +496,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -521,6 +537,43 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
t.is(success.callCount, 0);
});
test.serial('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 repositoryUrl = await gitRepo(true);
// 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']);
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const analyzeCommits = stub().resolves(nextRelease.type);
const generateNotes = stub().resolves();
const options = {
dryRun: true,
branch: 'master',
repositoryUrl,
verifyConditions: false,
analyzeCommits,
verifyRelease: false,
generateNotes,
prepare: false,
publish: false,
success: false,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['Release note for version %s:\n', '2.0.0']);
});
test.serial('Allow local releases with "noCi" option', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
@@ -530,7 +583,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -584,13 +637,15 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
await gitPush();
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const generateNotes1 = stub().resolves();
const notes2 = 'Release notes 2';
const generateNotes2 = stub().resolves(notes2);
const publish = stub().resolves();
const options = {
@@ -599,7 +654,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
verifyConditions: stub().resolves(),
analyzeCommits,
verifyRelease,
generateNotes,
generateNotes: [generateNotes1, generateNotes2],
prepare: stub().resolves(),
publish,
success: stub().resolves(),
@@ -618,12 +673,15 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
t.is(verifyRelease.callCount, 1);
t.deepEqual(verifyRelease.args[0][1].lastRelease, lastRelease);
t.is(generateNotes.callCount, 1);
t.deepEqual(generateNotes.args[0][1].lastRelease, lastRelease);
t.is(generateNotes1.callCount, 1);
t.deepEqual(generateNotes1.args[0][1].lastRelease, lastRelease);
t.is(generateNotes2.callCount, 1);
t.deepEqual(generateNotes2.args[0][1].lastRelease, lastRelease);
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].lastRelease, lastRelease);
t.falsy(publish.args[0][1].nextRelease.notes);
t.is(publish.args[0][1].nextRelease.notes, notes2);
});
test.serial('Returns falsy value if triggered by a PR', async t => {
@@ -645,11 +703,15 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
test.serial('Returns falsy value if triggered on an outdated clone', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
const repoDir = process.cwd();
// Add commits to the master branch
await gitCommits(['First']);
await gitCommits(['Second']);
await push();
await reset();
await gitPush();
await gitShallowClone(repositoryUrl);
await gitCommits(['Third']);
await gitPush();
process.chdir(repoDir);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
@@ -696,7 +758,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
await gitPush();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
@@ -746,7 +808,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
'Test commit\n\n commit body\n[skip release]',
'Test commit\n\n commit body\n[release skip]',
]);
await push();
await gitPush();
const analyzeCommits = stub().resolves();
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
const options = {
@@ -871,7 +933,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
await gitPush();
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves('string');
@@ -891,8 +953,6 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
});
const error = await t.throws(semanticRelease(options), Error);
// Verify error message
t.regex(error.message, new RegExp(DEFINITIONS.analyzeCommits.output.message));
t.regex(error.details, /string/);
});
@@ -900,7 +960,7 @@ test.serial('Get all commits including the ones not in the shallow clone', async
const repositoryUrl = await gitRepo(true);
await gitTagVersion('v1.0.0');
await gitCommits(['First', 'Second', 'Third']);
await push(repositoryUrl, 'master');
await gitPush(repositoryUrl, 'master');
await gitShallowClone(repositoryUrl);
+3 -2
View File
@@ -2,7 +2,7 @@ import test from 'ava';
import {writeJson, readJson} from 'fs-extra';
import {stub} from 'sinon';
import execa from 'execa';
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead} from './helpers/git-utils';
import {gitHead as getGitHead, gitTagHead, gitRepo, gitCommits, gitRemoteTagHead, gitPush} from './helpers/git-utils';
import gitbox from './helpers/gitbox';
import mockServer from './helpers/mockserver';
import npmRegistry from './helpers/npm-registry';
@@ -609,11 +609,12 @@ test.serial('Exit with 1 if missing permission to push to the remote repository'
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
await gitPush();
t.log('$ semantic-release');
const {stdout, code} = await execa(
cli,
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
{env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false}
{env: {...env, GH_TOKEN: 'user:wrong_pass'}, reject: false}
);
// Verify the type and message are logged
t.regex(stdout, /EGITNOPERMISSION/);
+10 -2
View File
@@ -94,8 +94,10 @@ test('Wrap "analyzeCommits" plugin in a function that validate the output of the
const error = await t.throws(plugin());
t.is(error.code, 'EANALYZEOUTPUT');
t.is(error.code, 'EANALYZECOMMITSOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
@@ -105,8 +107,10 @@ test('Wrap "generateNotes" plugin in a function that validate the output of the
const error = await t.throws(plugin());
t.is(error.code, 'ERELEASENOTESOUTPUT');
t.is(error.code, 'EGENERATENOTESOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
@@ -123,6 +127,8 @@ test('Wrap "publish" plugin in a function that validate the output of the plugin
t.is(error.code, 'EPUBLISHOUTPUT');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
t.regex(error.details, /2/);
});
@@ -187,6 +193,8 @@ test('Throws an error if the plugin return an object without the expected plugin
t.is(error.code, 'EPLUGIN');
t.is(error.name, 'SemanticReleaseError');
t.truthy(error.message);
t.truthy(error.details);
});
test('Throws an error if the plugin is not found', t => {
+8 -26
View File
@@ -18,24 +18,6 @@ test('Execute each function in series passing the same input', async t => {
t.true(step2.calledBefore(step3));
});
test('With one step, returns the step values rather than an Array ', async t => {
const step1 = stub().resolves(1);
const result = await pipeline([step1])(0);
t.deepEqual(result, 1);
t.true(step1.calledWith(0));
});
test('With one step, throws the error rather than an AggregateError ', async t => {
const error = new Error('test error 1');
const step1 = stub().rejects(error);
const thrown = await t.throws(pipeline([step1])(0));
t.is(error, thrown);
});
test('Execute each function in series passing a transformed input from "getNextInput"', async t => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
@@ -43,7 +25,7 @@ test('Execute each function in series passing a transformed input from "getNextI
const step4 = stub().resolves(4);
const getNextInput = (lastResult, result) => lastResult + result;
const result = await pipeline([step1, step2, step3, step4])(0, {settleAll: false, getNextInput});
const result = await pipeline([step1, step2, step3, step4], {settleAll: false, getNextInput})(0);
t.deepEqual(result, [1, 2, 3, 4]);
t.true(step1.calledWith(0));
@@ -62,7 +44,7 @@ test('Execute each function in series passing the "lastResult" and "result" to "
const step4 = stub().resolves(4);
const getNextInput = stub().returnsArg(0);
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: false, getNextInput});
const result = await pipeline([step1, step2, step3, step4], {settleAll: false, getNextInput})(5);
t.deepEqual(result, [1, 2, 3, 4]);
t.deepEqual(getNextInput.args, [[5, 1], [5, 2], [5, 3], [5, 4]]);
@@ -76,7 +58,7 @@ test('Execute each function in series calling "transform" to modify the results'
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const result = await pipeline([step1, step2, step3, step4])(5, {getNextInput, transform});
const result = await pipeline([step1, step2, step3, step4], {getNextInput, transform})(5);
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
@@ -90,13 +72,13 @@ test('Execute each function in series calling "transform" to modify the results
const getNextInput = stub().returnsArg(0);
const transform = stub().callsFake(result => result + 1);
const result = await pipeline([step1, step2, step3, step4])(5, {settleAll: true, getNextInput, transform});
const result = await pipeline([step1, step2, step3, step4], {settleAll: true, getNextInput, transform})(5);
t.deepEqual(result, [1 + 1, 2 + 1, 3 + 1, 4 + 1]);
t.deepEqual(getNextInput.args, [[5, 1 + 1], [5, 2 + 1], [5, 3 + 1], [5, 4 + 1]]);
});
test('Stop execution and throw error is a step rejects', async t => {
test('Stop execution and throw error if a step rejects', async t => {
const step1 = stub().resolves(1);
const step2 = stub().rejects(new Error('test error'));
const step3 = stub().resolves(3);
@@ -131,7 +113,7 @@ test('Execute all even if a Promise rejects', async t => {
const step2 = stub().rejects(error1);
const step3 = stub().rejects(error2);
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
const errors = await t.throws(pipeline([step1, step2, step3], {settleAll: true})(0));
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
@@ -147,7 +129,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
const step1 = stub().rejects(new AggregateError([error1, error2]));
const step2 = stub().rejects(new AggregateError([error3, error4]));
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
const errors = await t.throws(pipeline([step1, step2], {settleAll: true})(0));
t.deepEqual([...errors], [error1, error2, error3, error4]);
t.true(step1.calledWith(0));
@@ -163,7 +145,7 @@ test('Execute each function in series passing a transformed input even if a step
const step4 = stub().resolves(4);
const getNextInput = (prevResult, result) => prevResult + result;
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
const errors = await t.throws(pipeline([step1, step2, step3, step4], {settleAll: true, getNextInput})(0));
t.deepEqual([...errors], [error2, error3]);
t.true(step1.calledWith(0));
+14 -3
View File
@@ -116,11 +116,22 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
});
test('Use default when only options are passed for a single plugin', t => {
const plugins = getPlugins({generateNotes: {}, analyzeCommits: {}}, {}, t.context.logger);
const analyzeCommits = {};
const generateNotes = {};
const success = () => {};
const fail = [() => {}];
const plugins = getPlugins({analyzeCommits, generateNotes, success, fail}, {}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
// Verify only the plugins defined as an object with no `path` are set to the default value
t.falsy(success.path);
t.falsy(fail.path);
});
test('Merge global options with plugin options', async t => {
@@ -134,7 +145,7 @@ test('Merge global options with plugin options', async t => {
t.context.logger
);
const result = await plugins.verifyRelease();
const [result] = await plugins.verifyRelease();
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
});