Compare commits

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

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

Closes #485
2017-12-01 15:39:21 -08:00
Pierre Vanduynslager eec4e68615 test: Isolate environment variables passed to semantic-release
`npm` is called through `semantic-release` and in the integration test to make some assertion.
By isolating the the environment variables passed to `semantic-release` it's easier to avoid a mistake that would pass the test environment that contains is own auth variables.
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 8371a03da0 test: Verify Error instance type and SemanticReleaseError name 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 2491032a99 test: Add test to check unexpected plugin return values 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 02ddf34f21 Fix: Remove redundant test of commitAnalyzer plugin output 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 685959107b chore: Clean up mistakenly added configs 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager 613a646363 fix: Typo in error messages 2017-11-29 18:06:14 -05:00
Pierre Vanduynslager d7b323dd00 fix: Accept undefined values for the getLastRelease and generateNotes plugins
Adapt tests and default value to the plugin definitions (`definitions.js`)
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager a7359bffb6 test: Improve docker start/stop script
- Simplify the loop that check availability
- Increase timeout
- Remove the container after stopping
2017-11-29 18:06:14 -05:00
Pierre Vanduynslager fcb832bbe1 fix: remove name from log as it's not an existing option anymore 2017-11-29 18:06:14 -05:00
greenkeeper[bot]andPierre Vanduynslager 89f5e07408 chore(package): update ava to version 0.24.0 2017-11-27 15:22:17 -05:00
Pierre Vanduynslager ce3841f553 test: Ignore the ~/.npmrc file during tests
When the test are run npm read the ~/.npmrc and ./.npmrc files and set some environment variables starting with `npm_`.
When the test create temporary folders and created a `.npmrc` the values there are ignored as the environment variables take precedence.
This commit remove this environment variable (from the local `process.env`) before starting the integrations test and restore them after. This way the `./.npmrc` files created in temp directory for the test are correctly used.
2017-11-25 14:50:44 -05:00
Pierre Vanduynslager 40c58c9b42 ci: Run npm registry in Docker 2017-11-25 14:50:44 -05:00
Pierre Vanduynslager 0c67ba517f feat: Make semantic-release language agnostic
- Do not rely on `package.json` anymore
- Use `cosmiconfig` to load the configation. `semantic-release` can be configured:
  - via CLI options (including plugin names but not plugin options)
  - in the `release` property of `package.json` (as before)
  - in a `.releaserc.yml` or `.releaserc.js` or `.releaserc.js` or `release.config.js` file
  - in a `.releaserc` file containing `json`, `yaml` or `javascript` module
- Add the `repositoryUrl` options (used across `semantic-release` and plugins). The value is determined from CLi option, or option configuration, or package.json or the git remote url
- Verifies that `semantic-release` runs from a git repository
- `pkg` and `env` are not passed to plugin anymore
- `semantic-release` can be run both locally and globally. If ran globally with non default plugins, the plugins can be installed both globally or locally.

BREAKING CHANGE: `pkg` and `env` are not passed to plugin anymore.
Plugins relying on a `package.json` must verify the presence of a valid `package.json` and load it.
Plugins can use `process.env` instead of `env`.
2017-11-24 21:56:15 -05:00
Pierre Vanduynslager 5bec59b26b feat: Expect plugins to return Promises
BREAKING CHANGE: Each plugin is expected to return an async function or a Promise returning function. The callback parameter is not passed to plugins anymore.
2017-11-24 21:56:15 -05:00
Pierre Vanduynslager d4c7605f68 fix: Use default plugin if the path property is missing from a single plugin configuration 2017-11-22 19:47:09 -05:00
Pierre Vanduynslager facdadaddb ci: Lint with XO 2017-11-21 20:55:47 -05:00
Pierre Vanduynslager d548edcf37 feat: Extract npm and github publish to plugins
- Add a new plugin type: `publish`
- Add support for multi-plugin. A plugin module can now return an object with a property for each plugin type
- Uses by default [npm](https://github.com/semantic-release/npm) and [github](https://github.com/semantic-release/github) in addition of Travis for the verify condition plugin
- Uses by default [npm](https://github.com/semantic-release/npm) and [github](https://github.com/semantic-release/github) for the publish plugin
- `gitTag` if one can be found is passed to `generateNotes` for both `lastRelease` and `nextRelease`
- `semantic-release` now verifies the plugin configuration (in the `release` property of `package.json`) and throws an error if it's invalid
- `semantic-release` now verifies each plugin output and will throw an error if a plugin returns an unexpected value.

BREAKING CHANGE: `githubToken`, `githubUrl` and `githubApiPathPrefix` have to be set at the [github](https://github.com/semantic-release/github) plugin level. They can be set via `GH_TOKEN`, `GH_URL` and `GH_PREFIX` environment variables.

BREAKING CHANGE: the `npm` parameter is not passed to any plugin anymore. Each plugin have to read `.npmrc` if they needs to (with https://github.com/kevva/npm-conf for example).
2017-11-21 16:41:04 -05:00
Pierre Vanduynslager 991a7b5f97 refactor: Simplify file tree 2017-11-21 16:41:04 -05:00
Gregor MartynusandGitHub 332608378a docs(README) fix plugin anchor links (#504) 2017-11-21 10:15:01 -08:00
Matt ZiemerandGregor Martynus 018835f56d docs(README): typo (#516) 2017-11-21 10:03:16 -08:00
greenkeeper[bot]andPierre Vanduynslager c8748ad0e2 chore(package): update dependencies 2017-11-21 12:50:10 -05:00
Josh DuffandPierre Vanduynslager a4951b7c30 fix(docs): remove unnecessary backticks in readme 2017-11-10 12:31:53 -05:00
Pierre Vanduynslager 17a600672f style: Update style for Prettier 1.8.0 2017-11-07 00:34:16 -05:00
greenkeeper[bot]andPierre Vanduynslager 470697c01d chore(package): update prettier to version 1.8.0 2017-11-07 00:34:16 -05:00
Gregor MartynusandGitHub 94b00e21c7 chore(airtable-crm): initial configuration 2017-11-03 16:02:10 -07:00
Pierre VanduynslagerandGregor Martynus d0180c4348 feat: Additional commit information
Add `author`, `commiter`, `gitTags` and `committerDate` to the commit object passed to plugin
2017-10-29 20:01:16 -07:00
60 changed files with 1830 additions and 1481 deletions
+1
View File
@@ -0,0 +1 @@
base: app0ZOxG0FnHmOiuU
-4
View File
@@ -129,7 +129,3 @@ $RECYCLE.BIN/
package-lock.json
yarn.lock
# Registry tests
test/helpers/registry/couch
test/helpers/registry/data
+3 -1
View File
@@ -1,11 +1,13 @@
language: node_js
services:
- couchdb
- docker
notifications:
email: false
node_js:
- 9
- 8
os:
- linux
# Trigger a push build on caribou and greenkeeper branches + PRs build on every branches
# Avoid double build on PRs (See https://github.com/travis-ci/travis-ci/issues/1147)
+40 -26
View File
@@ -54,7 +54,7 @@ This removes the immediate connection between human emotions and version numbers
“We fail to follow SemVer and why it neednt matter”
</th>
<th>
“semantic-release Q&A with Kent C. Dodds”
“semantic-release Q&amp;A with Kent C. Dodds”
</th>
</tr>
<tr>
@@ -87,15 +87,12 @@ When pushing new commits with `git push` a CI build is triggered. After running
| Step | Description |
| ------------------ | ------------------------------------------------------------------------------------------- |
| Verify Conditions` | Run the [verifyConditions](#verifyConditions) plugin) |
| Get last release` | Obtain last release with the [getLastRelease](#getLastRelease) plugin |
| Analyze commits | Determine the type of release to do with the [analyzeCommits](#analyzeCommits) plugin |
| Verify release | Call the [verifyRelease](#verifyRelease) plugin |
| npm publish | Update the version in `package.json` and call `npm publish` |
| Generate notes | Generate release notes with plugin [generateNotes](#generateNotes) |
| Github release | A git tag and [Github release](https://help.github.com/articles/about-releases/) is created |
_Note:_ The current release/tag implementation is tied to GitHub, but could be opened up to Bitbucket, GitLab, et al. Feel free to send PRs for these services.
| Verify Conditions | Run the [verifyConditions](#verifyconditions) plugin |
| Get last release | Obtain last release with the [getLastRelease](#getlastrelease) plugin |
| Analyze commits | Determine the type of release to do with the [analyzeCommits](#analyzecommits) plugin |
| Verify release | Call the [verifyRelease](#verifyrelease) plugin |
| Generate notes | Generate release notes with plugin [generateNotes](#generatenotes) |
| Publish | Call the [publish](#publish) plugin |
## Default Commit Message Format
@@ -170,11 +167,9 @@ semantic-release
These options are currently available:
- `branch`: The branch on which releases should happen. Default: `'master'`
- `repositoryUrl`: The git repository URL. Default: `repository` property in `package.json` or git origin url. Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. The Github shorthand URL is not supported.
- `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes
- `debug`: Output debugging information
- `githubToken`: The token used to authenticate with GitHub. Default: `process.env.GH_TOKEN`
- `githubUrl`: Optional. Pass your GitHub Enterprise endpoint.
- `githubApiPathPrefix`: Optional. The path prefix for your GitHub Enterprise API.
_A few notes on `npm` config_:
1. The `npm` token can only be defined in the environment as `NPM_TOKEN`, because thats where `npm` itself is going to read it from.
@@ -183,8 +178,6 @@ _A few notes on `npm` config_:
3. If you want to use another dist-tag for your publishes than `'latest'` you can specify that inside the `package.json`s [`publishConfig`](https://docs.npmjs.com/files/package.json#publishconfig) field.
4. `semantic-release` generally tries to orientate itself towards `npm` it inherits the loglevel for example.
## Plugins
There are numerous steps where you can customize `semantic-release`s behaviour using plugins. A plugin is a regular [option](#options), but passed inside the `release` block of `package.json`:
@@ -214,13 +207,8 @@ module.exports = function (pluginConfig, config, callback) {}
- `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then its this object.
- `config`: A config object containing a lot of information to act upon.
- `env`: All environment variables
- `npm`: Select npm configuration bits like `registry`, `tag` and `auth`
- `options`: `semantic-release` options like `debug`, or `branch`
- `pkg`: Parsed `package.json`
- For certain plugins the `config` object contains even more information. See below.
- `callback`: If an error occurs pass it as first argument. Otherwise pass your result as second argument.
### `analyzeCommits`
@@ -230,14 +218,13 @@ While it may be tempting to use `'prepatch'`, `'preminor'` & `'prerelease'` as p
Have a look at the [default implementation](https://github.com/semantic-release/commit-analyzer/).
### `generateNotes`
This plugin is responsible for generating release notes. Call the callback with the notes as a string. Have a look at the [default implementation](https://github.com/semantic-release/release-notes-generator/).
It receives a `commits` array, the `lastRelease` and `nextRelease` inside `config`.
### `verifyConditions`
This plugins is responsible for verifying that a release should happen in the first place. For example, the [default implementation](https://github.com/semantic-release/condition-travis/) verifies that the publish is happening on Travis, that its the right branch, and that all other build jobs succeeded. There are more use cases for this, e.g. verifying that test coverage is above a certain threshold or that there are no [vulnerabilities](https://nodesecurity.io/) in your dependencies. Be creative.
This plugins is responsible for verifying that a release should happen in the first place.
The default implementations are:
- [travis](https://github.com/semantic-release/condition-travis/): verifies that the publish is happening on Travis, that its the right branch, and that all other build jobs succeeded.
- [github](https://github.com/semantic-release/github/): verifies a Github authentication is set and valid.
- [npm](https://github.com/semantic-release/npm/): verifies an npm authentication is set and valid.
Passing an array of plugins will run them in series.
@@ -251,6 +238,17 @@ Passing an array of plugins will run them in series.
This plugin is responsible for determining a packages last release version. The [default implementation](https://github.com/semantic-release/last-release-npm) uses the last published version on a npm registry.
### `generateNotes`
This plugin is responsible for generating release notes. Call the callback with the notes as a string. Have a look at the [default implementation](https://github.com/semantic-release/release-notes-generator/).
It receives a `commits` array, the `lastRelease` and `nextRelease` inside `config`.
### `publish`
This plugins is responsible for publishing the release. The default implementations publish on [npm](https://github.com/semantic-release/npm) and [github](https://github.com/semantic-release/github).
Passing an array of plugins will run them in series.
## ITYM*FAQ*LT
> I think you might frequently ask questions like these
@@ -289,6 +287,8 @@ Being able to write code for just the most recent node versions greatly simplifi
For a special purpose tool like `semantic-release`, that's only meant to be used in controlled CI environments, we think it's okay to have such a high version requirement. As `semantic-release` handles package publishing we expect almost every project to have at least one build job running node 8 already and that's all it takes. Even if that's not that case `semantic-release` can still be executed with the help of [npx](https://www.npmjs.com/package/npx) (`npx -p node@8 npm run semantic-release`).
Please see our [Node Support Policy](#node-support-policy) for our long-term promise for supporting Node.
## Badge
Use this in one of your projects? Include one of these badges in your README.md to let people know that your package is published using `semantic-release`.
@@ -311,6 +311,20 @@ Use this in one of your projects? Include one of these badges in your README.md
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg?style=flat-square)](https://github.com/semantic-release/semantic-release)
```
## Node Support Policy
We only support [Long-Term Support](https://github.com/nodejs/Release) versions of Node starting with [Node 8.9.0 (LTS)](https://nodejs.org/en/blog/release/v8.9.0/).
We specifically limit our support to LTS versions of Node, not because this package won't work on other versions, but because we have a limited amount of time, and supporting LTS offers the greatest return on that investment.
It's possible this package will work correctly on newer versions of Node. It may even be possible to use this package on older versions of Node, though that's more unlikely as we'll make every effort to take advantage of features available in the oldest LTS version we support.
As each Node LTS version reaches its end-of-life we will remove that version from the node engines property of our package's package.json file. Removing a Node version is considered a breaking change and will entail the publishing of a new major version of this package. We will not accept any requests to support an end-of-life version of Node. Any merge requests or issues supporting an end-of-life version of Node will be closed.
We will accept code that allows this package to run on newer, non-LTS, versions of Node. Furthermore, we will attempt to ensure our own changes work on the latest version of Node. To help in that commitment, our continuous integration setup runs against all LTS versions of Node in addition the most recent Node release; called current.
JavaScript package managers should allow you to install this package with any version of Node, with, at most, a warning if your version of Node does not fall within the range specified by our node engines property. If you encounter issues installing this package, please report the issue to your package manager.
## License
MIT License
+3 -2
View File
@@ -3,6 +3,7 @@
// Bad news: We have to write plain ES5 in this file
// Good news: It's the only file of the entire project
// eslint-disable-next-line no-var
var semver = require('semver');
if (semver.lt(process.version, '8.0.0')) {
@@ -22,7 +23,7 @@ npx is bundled with npm >= 5.4, or available via npm. More info: npm.im/npx`
process.exit(1);
}
// node 8+ from this point on
require('../src/cli')().catch(() => {
// Node 8+ from this point on
require('../cli')().catch(() => {
process.exitCode = 1;
});
+5 -6
View File
@@ -10,22 +10,21 @@ module.exports = async () => {
.name('semantic-release')
.description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from')
.option('--github-token <token>', 'Token to authenticate with Github API')
.option('--github-url <url>', 'GitHub Enterprise endpoint')
.option('--github-api-path-prefix <prefix>', 'Prefix of the GitHub Enterprise endpoint')
.option('-r, --repositoryUrl <repositoryUrl>', 'Git repository URL')
.option(
'--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin',
'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
list
)
.option('--get-last-release <path>', 'Path or package name for the getLastRelease plugin')
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
.option(
'--verify-release <paths>',
'Comma separated list of paths or packages name for the verifyRelease plugin',
'Comma separated list of paths or packages name for the verifyRelease plugin(s)',
list
)
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
.option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list)
.option('--debug', 'Output debugging information')
.option(
'-d, --dry-run',
@@ -43,7 +42,7 @@ module.exports = async () => {
program.outputHelp();
process.exitCode = 1;
} else {
await require('./index')(program);
await require('.')(program.opts());
}
} catch (err) {
// If error is a SemanticReleaseError then it's an expected exception case (no release to be done, running on a PR etc..) and the cli will return with 0
+74
View File
@@ -0,0 +1,74 @@
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error');
const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const logger = require('./lib/logger');
const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
module.exports = async opts => {
if (!await isGitRepo()) {
throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO');
}
const config = await getConfig(opts, logger);
const {plugins, options} = config;
if (!options.repositoryUrl) {
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
}
logger.log('Run automated release from branch %s', options.branch);
if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger});
}
logger.log('Call plugin %s', 'get-last-release');
const {commits, lastRelease} = await getCommits(
await plugins.getLastRelease({options, logger}),
options.branch,
logger
);
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
}
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
logger.log('Call plugin %s', 'verify-release');
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);
marked.setOptions({renderer: new TerminalRenderer()});
logger.log('Release note for version %s:\n', nextRelease.version);
process.stdout.write(`${marked(notes)}\n`);
} else {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
logger.log('Call plugin %s', 'publish');
await plugins.publish({options, logger, lastRelease, commits, nextRelease}, async prevInput => {
const newGitHead = await getGitHead();
// If previous publish plugin has created a commit (gitHead changed)
if (prevInput.nextRelease.gitHead !== newGitHead) {
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};
});
logger.log('Published release: %s', nextRelease.version);
}
};
View File
+25 -35
View File
@@ -1,12 +1,12 @@
const execa = require('execa');
const gitLogParser = require('git-log-parser');
const getStream = require('get-stream');
const debug = require('debug')('semantic-release:get-commits');
const {unshallow} = require('./git');
const getVersionHead = require('./get-version-head');
const {debugShell} = require('./debug');
const logger = require('./logger');
/**
* Commit message.
*
*
* @typedef {Object} Commit
* @property {string} hash The commit hash.
* @property {string} message The commit message.
@@ -14,15 +14,16 @@ const logger = require('./logger');
/**
* Last release.
*
*
* @typedef {Object} LastRelease
* @property {string} version The version number of the last release.
* @property {string} [gitHead] The commit sha used to make the last release.
* @property {string} [gitTag] The tag used to make the last release.
*/
/**
* Result object.
*
*
* @typedef {Object} Result
* @property {Array<Commit>} commits The list of commits since the last release.
* @property {LastRelease} lastRelease The updated lastRelease.
@@ -30,7 +31,7 @@ const logger = require('./logger');
/**
* Retrieve the list of commits on the current branch since the last released version, or all the commits of the current branch if there is no last released version.
*
*
* The commit correspoding to the last released version is determined as follow:
* - Use `lastRelease.gitHead` if defined and present in `branch` history.
* - If `lastRelease.gitHead` is not in the `branch` history, unshallow the repository and try again.
@@ -39,16 +40,17 @@ const logger = require('./logger');
* @param {LastRelease} lastRelease The lastRelease object obtained from the getLastRelease plugin.
* @param {string} branch The branch to release from.
* @param {Object} logger Global logger.
*
*
* @return {Promise<Result>} The list of commits on the branch `branch` since the last release and the updated lastRelease with the gitHead used to retrieve the commits.
*
*
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `lastRelease.gitHead` or the commit sha derived from `config.lastRelease.version` is not in the direct history of `branch`.
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `lastRelease.gitHead` is undefined and no commit sha can be found for the `config.lastRelease.version`.
*/
module.exports = async ({version, gitHead}, branch) => {
module.exports = async ({version, gitHead} = {}, branch, logger) => {
let gitTag;
if (gitHead || version) {
try {
gitHead = await getVersionHead(gitHead, version, branch);
({gitHead, gitTag} = await getVersionHead(gitHead, version, branch));
} catch (err) {
if (err.code === 'ENOTINHISTORY') {
logger.error(notInHistoryMessage(err.gitHead, branch, version));
@@ -61,32 +63,20 @@ module.exports = async ({version, gitHead}, branch) => {
} else {
logger.log('No previous release found, retrieving all commits');
// If there is no gitHead nor a version, there is no previous release. Unshallow the repo in order to retrieve all commits
const shell = await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
debugShell('Unshallow repo', shell, debug);
await unshallow();
}
try {
const shell = await execa('git', [
'log',
'--format=format:%H==SPLIT==%B==END==',
`${gitHead ? gitHead + '..' : ''}HEAD`,
]);
debugShell('Get commits', shell, debug);
const commits = shell.stdout
.split('==END==')
.filter(raw => !!raw.trim())
.map(raw => {
const [hash, message] = raw.trim().split('==SPLIT==');
return {hash, message};
});
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead}};
} catch (err) {
debug(err);
logger.log('Found no commit since last release');
return {commits: [], lastRelease: {version, gitHead}};
}
Object.assign(gitLogParser.fields, {hash: 'H', message: 'B', gitTags: 'd', committerDate: {key: 'ci', type: Date}});
const commits = (await getStream.array(gitLogParser.parse({_: `${gitHead ? gitHead + '..' : ''}HEAD`}))).map(
commit => {
commit.message = commit.message.trim();
commit.gitTags = commit.gitTags.trim();
return commit;
}
);
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead, gitTag}};
};
function noGitHeadMessage(branch, version) {
+27
View File
@@ -0,0 +1,27 @@
const readPkgUp = require('read-pkg-up');
const {defaults} = require('lodash');
const cosmiconfig = require('cosmiconfig');
const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const plugins = require('./plugins');
module.exports = async (opts, logger) => {
const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
const options = defaults(opts, config, {branch: 'master', repositoryUrl: (await pkgRepoUrl()) || (await repoUrl())});
debug('name: %O', options.name);
debug('branch: %O', options.branch);
debug('repositoryUrl: %O', options.repositoryUrl);
debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease);
debug('publish: %O', options.publish);
return {options, plugins: await plugins(options, logger)};
};
async function pkgRepoUrl() {
const {pkg} = await readPkgUp();
return pkg && pkg.repository ? pkg.repository.url : null;
}
@@ -1,18 +1,13 @@
const semver = require('semver');
const SemanticReleaseError = require('@semantic-release/error');
const logger = require('./logger');
module.exports = (type, lastRelease) => {
module.exports = (type, lastRelease, logger) => {
let version;
if (!lastRelease.version) {
if (lastRelease.version) {
version = semver.inc(lastRelease.version, type);
logger.log('The next release version is %s', version);
} else {
version = '1.0.0';
logger.log('There is no previous release, the next release version is %s', version);
} else {
version = semver.inc(lastRelease.version, type);
if (!version) {
throw new SemanticReleaseError(`Invalid release type ${type}`, 'EINVALIDTYPE');
}
logger.log('The next release version is %s', version);
}
return version;
@@ -1,47 +1,15 @@
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const SemanticReleaseError = require('@semantic-release/error');
const {debugShell} = require('./debug');
/**
* Get the commit sha for a given tag.
*
* @param {string} tagName Tag name for which to retrieve the commit sha.
*
* @return {string} The commit sha of the tag in parameter or `null`.
*/
async function gitTagHead(tagName) {
try {
const shell = await execa('git', ['rev-list', '-1', '--tags', tagName]);
debugShell('Get git tag head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return null;
}
}
/**
* Verify if the commist `sha` is in the direct history of the current branch.
*
* @param {string} sha The sha of the commit to look for.
*
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
*/
async function isCommitInHistory(sha) {
const shell = await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false});
debugShell('Check if commit is in history', shell, debug);
return shell.code === 0;
}
const {gitTagHead, gitCommitTag, isCommitInHistory, unshallow} = require('./git');
/**
* Get the commit sha for a given version, if it's contained in the given branch.
*
* @param {string} gitHead The commit sha to look for.
* @param {string} version The version corresponding to the commit sha to look for. Used to search in git tags.
*
* @return {Promise<string>} A Promise that resolves to the commit sha of the version, either `gitHead` of the commit associated with the `version` tag.
*
*
* @return {Promise<Object>} A Promise that resolves to an object with the `gitHead` and `gitTag` for the the `version`.
*
* @throws {SemanticReleaseError} with code `ENOTINHISTORY` if `gitHead` or the commit sha dereived from `version` is not in the direct history of `branch`.
* @throws {SemanticReleaseError} with code `ENOGITHEAD` if `gitHead` is undefined and no commit sha can be found for the `version`.
*/
@@ -49,17 +17,15 @@ module.exports = async (gitHead, version) => {
// Check if gitHead is defined and exists in release branch
if (gitHead && (await isCommitInHistory(gitHead))) {
debug('Use gitHead: %s', gitHead);
return gitHead;
return {gitHead, gitTag: await gitCommitTag(gitHead)};
}
// Ushallow the repository
const shell = await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
debugShell('Unshallow repo', shell, debug);
await unshallow();
// Check if gitHead is defined and exists in release branch again
if (gitHead && (await isCommitInHistory(gitHead))) {
debug('Use gitHead: %s', gitHead);
return gitHead;
return {gitHead, gitTag: await gitCommitTag(gitHead)};
}
let tagHead;
@@ -70,7 +36,7 @@ module.exports = async (gitHead, version) => {
// Check if tagHead is found and exists in release branch again
if (tagHead && (await isCommitInHistory(tagHead))) {
debug('Use tagHead: %s', tagHead);
return tagHead;
return {gitHead: tagHead, gitTag: await gitCommitTag(tagHead)};
}
}
+91
View File
@@ -0,0 +1,91 @@
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const {debugShell} = require('./debug');
/**
* Get the commit sha for a given tag.
*
* @param {string} tagName Tag name for which to retrieve the commit sha.
*
* @return {string} The commit sha of the tag in parameter or `null`.
*/
async function gitTagHead(tagName) {
try {
const shell = await execa('git', ['rev-list', '-1', tagName]);
debugShell('Get git tag head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return null;
}
}
/**
* Get the tag associated with a commit sha.
*
* @param {string} gitHead The commit sha for which to retrieve the associated tag.
*
* @return {string} The tag associatedwith the sha in parameter or `null`.
*/
async function gitCommitTag(gitHead) {
try {
const shell = await execa('git', ['describe', '--tags', '--exact-match', gitHead]);
debugShell('Get git commit tag', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
return null;
}
}
/**
* Verify if the commit `sha` is in the direct history of the current branch.
*
* @param {string} sha The sha of the commit to look for.
*
* @return {boolean} `true` if the commit `sha` is in the history of the current branch, `false` otherwise.
*/
async function isCommitInHistory(sha) {
const shell = await execa('git', ['merge-base', '--is-ancestor', sha, 'HEAD'], {reject: false});
debugShell('Check if commit is in history', shell, debug);
return shell.code === 0;
}
/**
* Unshallow the git repository (retriving every commits and tags).
*/
async function unshallow() {
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
}
/**
* @return {string} the sha of the HEAD commit.
*/
async function gitHead() {
try {
const shell = await execa('git', ['rev-parse', 'HEAD']);
debugShell('Get git head', shell, debug);
return shell.stdout;
} catch (err) {
debug(err);
throw new Error(err.stderr);
}
}
/**
* @return {string|null} The value of the remote git URL.
*/
async function repoUrl() {
return (await execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false})) || null;
}
/**
* @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
*/
async function isGitRepo() {
const shell = await execa('git', ['rev-parse', '--git-dir'], {reject: false});
debugShell('Check if the current working directory is a git repository', shell, debug);
return shell.code === 0;
}
module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl, isGitRepo};
+3 -3
View File
@@ -7,9 +7,9 @@ module.exports = {
log(...args) {
const [format, ...rest] = args;
console.log(
`${chalk.grey('[Semantic release]:')}${typeof format === 'string'
? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}`
: ''}`,
`${chalk.grey('[Semantic release]:')}${
typeof format === 'string' ? ` ${format.replace(/%[^%]/g, seq => chalk.magenta(seq))}` : ''
}`,
...(typeof format === 'string' ? [] : [format]).concat(rest)
);
},
+72
View File
@@ -0,0 +1,72 @@
const {isString, isObject, isFunction, isArray} = require('lodash');
const semver = require('semver');
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
module.exports = {
verifyConditions: {
default: ['@semantic-release/npm', '@semantic-release/github', '@semantic-release/condition-travis'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.',
},
},
getLastRelease: {
default: '@semantic-release/npm',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
message:
'The "getLastRelease" plugin is mandatory, and must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
},
output: {
validator: output =>
!output ||
(isObject(output) && !output.version) ||
(isString(output.version) && Boolean(semver.valid(semver.clean(output.version)))),
message:
'The "getLastRelease" plugin output if defined, must be an object with an optionnal valid semver version in the "version" property.',
},
},
analyzeCommits: {
default: '@semantic-release/commit-analyzer',
config: {
validator: conf => Boolean(conf) && validatePluginConfig(conf),
message:
'The "analyzeCommits" plugin is mandatory, and must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
},
output: {
validator: output => !output || RELEASE_TYPE.includes(output),
message: 'The "analyzeCommits" plugin output, if defined, must be a valid semver release type.',
},
},
verifyRelease: {
default: false,
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
'The "verifyRelease" 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.',
},
},
generateNotes: {
default: '@semantic-release/release-notes-generator',
config: {
validator: conf => !conf || validatePluginConfig(conf),
message:
'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
},
output: {
validator: output => !output || isString(output),
message: 'The "generateNotes" plugin output, if defined, must be a string.',
},
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => Boolean(conf) && (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
'The "publish" plugin is mandatory, and must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.',
},
},
};
+28
View File
@@ -0,0 +1,28 @@
const {isArray, isObject} = require('lodash');
const DEFINITIONS = require('./definitions');
const pipeline = require('./pipeline');
const normalize = require('./normalize');
module.exports = (options, logger) =>
Object.keys(DEFINITIONS).reduce((plugins, pluginType) => {
const {config, output, default: def} = DEFINITIONS[pluginType];
let pluginConfs;
if (options[pluginType]) {
// If an object is passed and the path is missing, set the default one for single plugins
if (isObject(options[pluginType]) && !options[pluginType].path && !isArray(def)) {
options[pluginType].path = def;
}
if (config && !config.validator(options[pluginType])) {
throw new Error(config.message);
}
pluginConfs = options[pluginType];
} else {
pluginConfs = def;
}
plugins[pluginType] = isArray(pluginConfs)
? pipeline(pluginConfs.map(conf => normalize(pluginType, conf, logger, output)))
: normalize(pluginType, pluginConfs, logger, output);
return plugins;
}, {});
+34
View File
@@ -0,0 +1,34 @@
const {inspect} = require('util');
const {isString, isObject, isFunction, noop, cloneDeep} = require('lodash');
const importFrom = require('import-from');
module.exports = (pluginType, pluginConfig, logger, validator) => {
if (!pluginConfig) {
return noop;
}
const {path, ...config} = isString(pluginConfig) || isFunction(pluginConfig) ? {path: pluginConfig} : pluginConfig;
if (!isFunction(pluginConfig)) {
logger.log('Load plugin %s from %s', pluginType, path);
}
const plugin = isFunction(path) ? path : importFrom.silent(__dirname, path) || importFrom(process.cwd(), path);
let func;
if (isFunction(plugin)) {
func = plugin.bind(null, cloneDeep(config));
} else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
func = plugin[pluginType].bind(null, cloneDeep(config));
} else {
throw new Error(
`The ${pluginType} plugin must be a function, or an object with a function in the property ${pluginType}.`
);
}
return async input => {
const result = await func(cloneDeep(input));
if (validator && !validator.validator(result)) {
throw new Error(`${validator.message} Received: ${inspect(result)}`);
}
return result;
};
};
+19
View File
@@ -0,0 +1,19 @@
const {identity} = require('lodash');
const pReduce = require('p-reduce');
module.exports = steps => async (input, getNextInput = identity) => {
const results = [];
await pReduce(
steps,
async (prevResult, nextStep) => {
// Call the next step with the input computed at the end of the previous iteration
const result = await nextStep(prevResult);
// Save intermediary result
results.push(result);
// Prepare input for next step, passing the result of the previous iteration and the current one
return getNextInput(prevResult, result);
},
input
);
return results;
};
+40 -44
View File
@@ -15,74 +15,58 @@
}
},
"dependencies": {
"@semantic-release/commit-analyzer": "^4.0.0",
"@semantic-release/condition-travis": "^6.0.0",
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/condition-travis": "^7.0.0",
"@semantic-release/error": "^2.1.0",
"@semantic-release/last-release-npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^5.0.0",
"@semantic-release/github": "^2.0.0",
"@semantic-release/npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^3.1.0",
"debug": "^3.1.0",
"execa": "^0.8.0",
"fs-extra": "^4.0.2",
"git-head": "^1.2.1",
"github": "^12.0.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"import-from": "^2.1.0",
"lodash": "^4.0.0",
"marked": "^0.3.6",
"marked-terminal": "^2.0.0",
"nerf-dart": "^1.0.0",
"normalize-package-data": "^2.3.4",
"npm-conf": "^1.1.2",
"p-series": "^1.0.0",
"parse-github-repo-url": "^1.3.0",
"require-relative": "^0.8.7",
"p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"semver": "^5.4.1"
},
"devDependencies": {
"ava": "^0.23.0",
"ava": "^0.24.0",
"codecov": "^3.0.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"dockerode": "^2.5.2",
"eslint": "^4.7.0",
"eslint-config-prettier": "^2.5.0",
"eslint-config-standard": "^10.2.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-prettier": "^2.3.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"get-stream": "^3.0.0",
"mockserver-client": "^1.0.16",
"file-url": "^2.0.2",
"fs-extra": "^4.0.2",
"got": "^8.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^5.1.1",
"nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12",
"nyc": "^11.2.1",
"p-map-series": "^1.0.0",
"prettier": "^1.7.0",
"p-retry": "^1.0.0",
"prettier": "~1.9.0",
"proxyquire": "^1.8.0",
"rimraf": "^2.5.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1"
"tempy": "^0.2.1",
"xo": "^0.18.2"
},
"engines": {
"node": ">=4",
"npm": ">=2"
},
"eslintConfig": {
"extends": [
"standard",
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
},
"files": [
"bin",
"src"
"lib",
"index.js",
"cli.js"
],
"homepage": "https://github.com/semantic-release/semantic-release#readme",
"keywords": [
@@ -100,7 +84,9 @@
"main": "index.js",
"nyc": {
"include": [
"src/**/*.js"
"lib/**/*.js",
"index.js",
"cli.js"
],
"reporter": [
"json",
@@ -126,12 +112,22 @@
"url": "git+https://github.com/semantic-release/semantic-release.git"
},
"scripts": {
"clean": "rimraf coverage && rimraf .nyc_output",
"cm": "git-cz",
"codecov": "codecov -f coverage/coverage-final.json",
"lint": "eslint .",
"pretest": "npm run clean && npm run lint",
"lint": "xo",
"pretest": "npm run lint",
"semantic-release": "./bin/semantic-release.js",
"test": "nyc ava -v"
},
"xo": {
"extends": [
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
}
}
-60
View File
@@ -1,60 +0,0 @@
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error');
const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version');
const verifyPkg = require('./lib/verify-pkg');
const verifyAuth = require('./lib/verify-auth');
const getCommits = require('./lib/get-commits');
const publishNpm = require('./lib/publish-npm');
const githubRelease = require('./lib/github-release');
const logger = require('./lib/logger');
module.exports = async opts => {
const config = await getConfig(opts);
const {plugins, env, options, pkg, npm} = config;
logger.log('Run automated release for %s on branch %s', pkg.name, options.branch);
verifyPkg(pkg);
if (!options.dryRun) {
verifyAuth(options, env);
}
if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({env, options, pkg, npm, logger});
}
logger.log('Call plugin %s', 'get-last-release');
const {commits, lastRelease} = await getCommits(
await plugins.getLastRelease({env, options, pkg, npm, logger}),
options.branch
);
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({env, options, pkg, npm, logger, lastRelease, commits});
if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
}
const nextRelease = {type, version: getNextVersion(type, lastRelease)};
logger.log('Call plugin %s', 'verify-release');
await plugins.verifyRelease({env, options, pkg, npm, logger, lastRelease, commits, nextRelease});
if (!options.dryRun) {
await publishNpm(pkg, npm, nextRelease);
}
logger.log('Call plugin %s', 'generate-notes');
const notes = await plugins.generateNotes({env, options, pkg, npm, logger, lastRelease, commits, nextRelease});
if (options.dryRun) {
marked.setOptions({renderer: new TerminalRenderer()});
logger.log('Release note for version %s:\n', nextRelease.version);
console.log(marked(notes));
} else {
const releaseUrl = await githubRelease(pkg, notes, nextRelease.version, options);
logger.log('Published Github release: %s', releaseUrl);
}
};
-43
View File
@@ -1,43 +0,0 @@
const url = require('url');
const {readJson} = require('fs-extra');
const {defaults} = require('lodash');
const npmConf = require('npm-conf');
const normalizeData = require('normalize-package-data');
const debug = require('debug')('semantic-release:config');
const logger = require('./logger');
const getPlugins = require('./plugins');
const getRegistry = require('./get-registry');
module.exports = async opts => {
const pkg = await readJson('./package.json');
const {GH_TOKEN, GITHUB_TOKEN, GH_URL} = process.env;
normalizeData(pkg);
const options = defaults(opts, pkg.release, {
branch: 'master',
fallbackTags: {next: 'latest'},
githubToken: GH_TOKEN || GITHUB_TOKEN,
githubUrl: GH_URL,
});
debug('branch: %O', options.branch);
debug('fallbackTags: %O', options.fallbackTags);
debug('analyzeCommits: %O', options.analyzeCommits);
debug('generateNotes: %O', options.generateNotes);
debug('verifyConditions: %O', options.verifyConditions);
debug('verifyRelease: %O', options.verifyRelease);
const plugins = await getPlugins(options);
const conf = npmConf();
const npm = {
auth: {token: process.env.NPM_TOKEN},
registry: getRegistry(pkg, conf),
tag: (pkg.publishConfig || {}).tag || conf.get('tag'),
conf,
};
// normalize trailing slash
npm.registry = url.format(url.parse(npm.registry));
debug('npm registry: %O', npm.registry);
debug('npm tag: %O', npm.tag);
return {env: process.env, pkg, options, plugins, npm, logger};
};
-11
View File
@@ -1,11 +0,0 @@
module.exports = ({publishConfig, name}, conf) => {
if (publishConfig && publishConfig.registry) {
return publishConfig.registry;
}
if (name[0] !== '@') {
return conf.get('registry') || 'https://registry.npmjs.org/';
}
return conf.get(`${name.split('/')[0]}/registry`) || conf.get('registry') || 'https://registry.npmjs.org/';
};
-36
View File
@@ -1,36 +0,0 @@
const {promisify} = require('util');
const url = require('url');
const gitHead = require('git-head');
const GitHubApi = require('github');
const parseSlug = require('parse-github-repo-url');
const debug = require('debug')('semantic-release:github-release');
module.exports = async (pkg, notes, version, {branch, githubUrl, githubToken, githubApiPathPrefix}) => {
const [owner, repo] = parseSlug(pkg.repository.url);
let {port, protocol, hostname: host} = githubUrl ? url.parse(githubUrl) : {};
protocol = (protocol || '').split(':')[0] || null;
const pathPrefix = githubApiPathPrefix || null;
const github = new GitHubApi({port, protocol, host, pathPrefix});
debug('Github host: %o', host);
debug('Github port: %o', port);
debug('Github protocol: %o', protocol);
debug('Github pathPrefix: %o', pathPrefix);
github.authenticate({type: 'token', token: githubToken});
const name = `v${version}`;
const release = {owner, repo, tag_name: name, name, target_commitish: branch, body: notes};
debug('release owner: %o', owner);
debug('release repo: %o', repo);
debug('release name: %o', name);
debug('release branch: %o', branch);
const sha = await promisify(gitHead)();
const ref = `refs/tags/${name}`;
debug('Create git tag %o with commit %o', ref, sha);
await github.gitdata.createReference({owner, repo, ref, sha});
const {data: {html_url: releaseUrl}} = await github.repos.createRelease(release);
return releaseUrl;
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = (config, options, cb) => {
cb(null);
};
-45
View File
@@ -1,45 +0,0 @@
const {promisify} = require('util');
const relative = require('require-relative');
const pSeries = require('p-series');
const logger = require('./logger');
module.exports = options => {
const plugins = {
analyzeCommits: normalize(options.analyzeCommits, '@semantic-release/commit-analyzer'),
generateNotes: normalize(options.generateNotes, '@semantic-release/release-notes-generator'),
getLastRelease: normalize(options.getLastRelease, '@semantic-release/last-release-npm'),
};
['verifyConditions', 'verifyRelease'].forEach(plugin => {
if (!Array.isArray(options[plugin])) {
plugins[plugin] = normalize(
options[plugin],
plugin === 'verifyConditions' ? '@semantic-release/condition-travis' : './plugin-noop'
);
} else {
plugins[plugin] = async pluginOptions => {
return pSeries(
options[plugin].map(step => {
return () => normalize(step, './plugin-noop')(pluginOptions);
})
);
};
}
});
return plugins;
};
const normalize = (pluginConfig, fallback) => {
if (typeof pluginConfig === 'string') {
logger.log('Load plugin %s', pluginConfig);
return promisify(relative(pluginConfig).bind(null, {}));
}
if (pluginConfig && typeof pluginConfig.path === 'string') {
logger.log('Load plugin %s', pluginConfig.path);
return promisify(relative(pluginConfig.path).bind(null, pluginConfig));
}
return promisify(require(fallback).bind(null, pluginConfig || {}));
};
module.exports.normalize = normalize;
-35
View File
@@ -1,35 +0,0 @@
const {appendFile, readJson, writeJson, pathExists} = require('fs-extra');
const execa = require('execa');
const nerfDart = require('nerf-dart');
const debug = require('debug')('semantic-release:publish-npm');
const {debugShell} = require('./debug');
const logger = require('./logger');
module.exports = async (pkg, {conf, registry, auth}, {version}) => {
const pkgFile = await readJson('./package.json');
if (await pathExists('./npm-shrinkwrap.json')) {
const shrinkwrap = await readJson('./npm-shrinkwrap.json');
shrinkwrap.version = version;
await writeJson('./npm-shrinkwrap.json', shrinkwrap);
logger.log('Wrote version %s to npm-shrinkwrap.json', version);
}
await writeJson('./package.json', Object.assign(pkgFile, {version}));
logger.log('Wrote version %s to package.json', version);
if (process.env.NPM_OLD_TOKEN && process.env.NPM_EMAIL) {
// Using the old auth token format is not considered part of the public API
// This might go away anytime (i.e. once we have a better testing strategy)
await appendFile('./.npmrc', `_auth = \${NPM_OLD_TOKEN}\nemail = \${NPM_EMAIL}`);
logger.log('Wrote NPM_OLD_TOKEN and NPM_EMAIL to .npmrc.');
} else {
await appendFile('./.npmrc', `${nerfDart(registry)}:_authToken = \${NPM_TOKEN}`);
logger.log('Wrote NPM_TOKEN to .npmrc.');
}
logger.log('Publishing version %s to npm registry %s', version, registry);
const shell = await execa('npm', ['publish']);
console.log(shell.stdout);
debugShell('Publishing on npm', shell, debug);
};
-11
View File
@@ -1,11 +0,0 @@
const SemanticReleaseError = require('@semantic-release/error');
module.exports = (options, env) => {
if (!options.githubToken) {
throw new SemanticReleaseError('No github token specified.', 'ENOGHTOKEN');
}
if (!(env.NPM_TOKEN || (env.NPM_OLD_TOKEN && env.NPM_EMAIL))) {
throw new SemanticReleaseError('No npm token specified.', 'ENONPMTOKEN');
}
};
-11
View File
@@ -1,11 +0,0 @@
const SemanticReleaseError = require('@semantic-release/error');
module.exports = pkg => {
if (!pkg.name) {
throw new SemanticReleaseError('No "name" found in package.json.', 'ENOPKGNAME');
}
if (!pkg.repository || !pkg.repository.url) {
throw new SemanticReleaseError('No "repository" found in package.json.', 'ENOPKGREPO');
}
};
+8
View File
@@ -0,0 +1,8 @@
module.exports = {
verifyConditions: () => {},
getLastRelease: () => {},
analyzeCommits: () => {},
verifyRelease: () => {},
generateNotes: () => {},
publish: () => {},
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function(config, options, cb) {
cb(new Error('b'));
};
+2 -2
View File
@@ -9,6 +9,6 @@ class InheritedError extends SemanticReleaseError {
}
}
module.exports = function(config, options, cb) {
cb(new InheritedError('Inherited error', 'EINHERITED'));
module.exports = () => {
throw new InheritedError('Inherited error', 'EINHERITED');
};
@@ -1,5 +1,5 @@
module.exports = function(config, options, cb) {
module.exports = () => {
const error = new Error('a');
error.errorProperty = 'errorProperty';
cb(error);
throw error;
};
+1
View File
@@ -0,0 +1 @@
module.exports = () => {};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function(config, options, cb) {
cb(null, 'a');
};
-3
View File
@@ -1,3 +0,0 @@
module.exports = function(config, options, cb) {
cb(null, 'b');
};
+1 -3
View File
@@ -1,3 +1 @@
module.exports = function(pluginConfig, options, cb) {
cb(null, {pluginConfig, options});
};
module.exports = (pluginConfig, options) => ({pluginConfig, options});
+125 -27
View File
@@ -1,7 +1,6 @@
import test from 'ava';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import SemanticReleaseError from '@semantic-release/error';
import getCommits from '../lib/get-commits';
import {
gitRepo,
gitCommits,
@@ -19,9 +18,7 @@ test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
t.context.getCommits = proxyquire('../src/lib/get-commits', {
'./logger': {log: t.context.log, error: t.context.error},
});
t.context.logger = {log: t.context.log, error: t.context.error};
});
test.afterEach.always(t => {
@@ -36,18 +33,53 @@ test.serial('Get all commits when there is no last release', async t => {
const commits = await gitCommits(['First', 'Second']);
// Retrieve the commits with the commits module
const result = await t.context.getCommits({}, 'master');
const result = await getCommits({}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.falsy(result.lastRelease.gitHead);
t.falsy(result.lastRelease.version);
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits with gitTags', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
let commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
// Retrieve the commits with the commits module
const result = await getCommits({}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[0].gitTags, '(HEAD -> master)');
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
t.is(result.commits[1].gitTags, '(tag: v1.0.0)');
});
test.serial('Get all commits when there is no last release, including the ones not in the shallow clone', async t => {
@@ -62,18 +94,25 @@ test.serial('Get all commits when there is no last release, including the ones n
t.is((await gitLog()).length, 1);
// Retrieve the commits with the commits module
const result = await t.context.getCommits({}, 'master');
const result = await getCommits({}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.falsy(result.lastRelease.gitHead);
t.falsy(result.lastRelease.version);
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits since gitHead (from lastRelease)', async t => {
@@ -83,18 +122,25 @@ test.serial('Get all commits since gitHead (from lastRelease)', async t => {
const commits = await gitCommits(['First', 'Second', 'Third']);
// Retrieve the commits with the commits module, since commit 'First'
const result = await t.context.getCommits({gitHead: commits[commits.length - 1].hash}, 'master');
const result = await getCommits({gitHead: commits[commits.length - 1].hash}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead, commits[commits.length - 1].hash);
t.falsy(result.lastRelease.version);
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits since gitHead (from lastRelease) on a detached head repo', async t => {
@@ -106,16 +152,20 @@ test.serial('Get all commits since gitHead (from lastRelease) on a detached head
await gitDetachedHead(repo, commits[1].hash);
// Retrieve the commits with the commits module, since commit 'First'
const result = await t.context.getCommits({gitHead: commits[commits.length - 1].hash}, 'master');
const result = await getCommits({gitHead: commits[commits.length - 1].hash}, 'master', t.context.logger);
// Verify the module retrieved only the commit 'feat: Second' (included in the detached and after 'fix: First')
t.is(result.commits.length, 1);
t.is(result.commits[0].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[0].message, commits[1].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead, commits[commits.length - 1].hash);
t.falsy(result.lastRelease.version);
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits since gitHead (from tag) ', async t => {
@@ -129,17 +179,24 @@ test.serial('Get all commits since gitHead (from tag) ', async t => {
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
const result = await t.context.getCommits({version: '1.0.0'}, 'master');
const result = await getCommits({version: '1.0.0'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.gitTag, '1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
@@ -156,15 +213,19 @@ test.serial('Get all commits since gitHead (from tag) on a detached head repo',
await gitDetachedHead(repo, commits[1].hash);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag 1.0.0)
const result = await t.context.getCommits({version: '1.0.0'}, 'master');
const result = await getCommits({version: '1.0.0'}, 'master', t.context.logger);
// Verify the module retrieved only the commit 'feat: Second' (included in the detached and after 'fix: First')
t.is(result.commits.length, 1);
t.is(result.commits[0].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[0].message, commits[1].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.gitTag, '1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
@@ -179,17 +240,24 @@ test.serial('Get all commits since gitHead (from tag formatted like v<version>)
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
const result = await t.context.getCommits({version: '1.0.0'}, 'master');
const result = await getCommits({version: '1.0.0'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.gitTag, 'v1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
@@ -204,17 +272,24 @@ test.serial('Get commits when last release gitHead is missing but a tag match th
commits = (await gitCommits(['Second', 'Third'])).concat(commits);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
const result = await t.context.getCommits({version: '1.0.0', gitHead: 'missing'}, 'master');
const result = await getCommits({version: '1.0.0', gitHead: 'missing'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.gitTag, 'v1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
@@ -227,18 +302,29 @@ test.serial('Get all commits since gitHead, when gitHead are mising from the sha
await gitShallowClone(repo);
// Retrieve the commits with the commits module, since commit 'First'
const result = await t.context.getCommits({version: '1.0.0', gitHead: commits[commits.length - 1].hash}, 'master');
const result = await getCommits(
{version: '1.0.0', gitHead: commits[commits.length - 1].hash},
'master',
t.context.logger
);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.version, '1.0.0');
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits since gitHead from tag, when tags are mising from the shallow clone', async t => {
@@ -257,17 +343,24 @@ test.serial('Get all commits since gitHead from tag, when tags are mising from t
t.is((await gitTags()).length, 0);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
const result = await t.context.getCommits({version: '1.0.0'}, 'master');
const result = await getCommits({version: '1.0.0'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
t.is(result.commits[0].hash.substring(0, 7), commits[0].hash);
t.is(result.commits[0].message, commits[0].message);
t.truthy(result.commits[0].committerDate);
t.truthy(result.commits[0].author.name);
t.truthy(result.commits[0].committer.name);
t.is(result.commits[1].hash.substring(0, 7), commits[1].hash);
t.is(result.commits[1].message, commits[1].message);
t.truthy(result.commits[1].committerDate);
t.truthy(result.commits[1].author.name);
t.truthy(result.commits[1].committer.name);
// Verify the last release is returned and updated
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[commits.length - 1].hash);
t.is(result.lastRelease.gitTag, 'v1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
@@ -278,7 +371,7 @@ test.serial('Return empty array if lastRelease.gitHead is the last commit', asyn
const commits = await gitCommits(['First', 'Second']);
// Retrieve the commits with the commits module, since commit 'Second' (therefore none)
const result = await t.context.getCommits({gitHead: commits[0].hash, version: '1.0.0'}, 'master');
const result = await getCommits({gitHead: commits[0].hash, version: '1.0.0'}, 'master', t.context.logger);
// Verify no commit is retrieved
t.deepEqual(result.commits, []);
@@ -286,6 +379,7 @@ test.serial('Return empty array if lastRelease.gitHead is the last commit', asyn
t.truthy(result.lastRelease);
t.is(result.lastRelease.gitHead.substring(0, 7), commits[0].hash);
t.is(result.lastRelease.version, '1.0.0');
t.falsy(result.lastRelease.gitTag);
});
test.serial('Return empty array if there is no commits', async t => {
@@ -293,7 +387,7 @@ test.serial('Return empty array if there is no commits', async t => {
await gitRepo();
// Retrieve the commits with the commits module
const result = await t.context.getCommits({}, 'master');
const result = await getCommits({}, 'master', t.context.logger);
// Verify no commit is retrieved
t.deepEqual(result.commits, []);
@@ -310,11 +404,11 @@ test.serial('Throws ENOGITHEAD error if the gitHead of the last release cannot b
await gitCommits(['First', 'Second']);
// Retrieve the commits with the commits module
const error = await t.throws(t.context.getCommits({version: '1.0.0'}, 'master'));
const error = await t.throws(getCommits({version: '1.0.0'}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOGITHEAD');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message explaining the error
t.regex(
t.context.error.firstCall.args[0],
@@ -329,11 +423,11 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in history', async t =
await gitCommits(['First', 'Second']);
// Retrieve the commits with the commits module
const error = await t.throws(t.context.getCommits({gitHead: 'notinhistory'}, 'master'));
const error = await t.throws(getCommits({gitHead: 'notinhistory'}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -352,11 +446,13 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in branch history but
await gitCheckout('master', false);
// Retrieve the commits with the commits module
const error = await t.throws(t.context.getCommits({version: '1.0.1', gitHead: commitsBranch[0].hash}, 'master'));
const error = await t.throws(
getCommits({version: '1.0.1', gitHead: commitsBranch[0].hash}, 'master', t.context.logger)
);
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -379,11 +475,13 @@ test.serial('Throws ENOTINHISTORY error if gitHead is not in detached head but p
await gitDetachedHead(repo, commitsMaster[0].hash);
// Retrieve the commits with the commits module, since commit 'Second'
const error = await t.throws(t.context.getCommits({version: '1.0.1', gitHead: commitsBranch[0].hash}, 'master'));
const error = await t.throws(
getCommits({version: '1.0.1', gitHead: commitsBranch[0].hash}, 'master', t.context.logger)
);
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
@@ -406,10 +504,10 @@ test.serial('Throws ENOTINHISTORY error when a tag is not in branch history but
await gitCommits(['Forth']);
// Retrieve the commits with the commits module
const error = await t.throws(t.context.getCommits({version: '1.0.0'}, 'master'));
const error = await t.throws(getCommits({version: '1.0.0'}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.true(error instanceof SemanticReleaseError);
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message mentionning the branch
t.regex(t.context.error.firstCall.args[0], /history of the "master" branch/);
// Verify the log function has been called with a message mentionning the missing gitHead
+137 -96
View File
@@ -1,10 +1,9 @@
import url from 'url';
import test from 'ava';
import {writeJson, writeFile} from 'fs-extra';
import {writeFile, writeJson} from 'fs-extra';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import normalizeData from 'normalize-package-data';
import {gitRepo} from './helpers/git-utils';
import yaml from 'js-yaml';
import {gitRepo, gitCommits, gitShallowClone, gitAddConfig} from './helpers/git-utils';
test.beforeEach(t => {
// Save the current process.env
@@ -12,7 +11,7 @@ test.beforeEach(t => {
// Save the current working diretory
t.context.cwd = process.cwd();
t.context.plugins = stub().returns({});
t.context.getConfig = proxyquire('../src/lib/get-config', {'./plugins': t.context.plugins});
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
});
test.afterEach.always(t => {
@@ -22,133 +21,175 @@ test.afterEach.always(t => {
process.env = Object.assign({}, t.context.env);
});
test.serial('Default values', async t => {
const pkg = {name: 'package_name', release: {}};
test.serial('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {repository: 'git@package.com:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
// Create package.json in repository root
await writeJson('./package.json', pkg);
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
});
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
});
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
const pkg = {repository: 'git+https://hostname.com/owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', pkg);
process.env.GH_TOKEN = 'GH_TOKEN';
const result = await t.context.getConfig();
const {options} = await t.context.getConfig();
// Verify the normalized package is returned
normalizeData(pkg);
t.deepEqual(result.pkg, pkg);
// Verify the default options are set
t.is(result.options.branch, 'master');
t.is(result.options.githubToken, process.env.GH_TOKEN);
// Verify the default npm options are set
t.is(result.npm.tag, 'latest');
// Use the environment variable npm_config_registry as the default so the test works on both npm and yarn
t.is(result.npm.registry, url.format(url.parse(process.env.npm_config_registry || 'https://registry.npmjs.org/')));
t.is(options.branch, 'master');
t.is(options.repositoryUrl, pkg.repository);
});
test.serial('Read package.json configuration', async t => {
test.serial('Read options from package.json', async t => {
const release = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotes',
getLastRelease: {
path: 'getLastRelease',
param: 'getLastRelease_param',
},
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
const pkg = {name: 'package_name', release};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', pkg);
delete process.env.GH_TOKEN;
process.env.GITHUB_TOKEN = 'GITHUB_TOKEN';
await writeJson('./package.json', {release});
const result = await t.context.getConfig();
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.is(result.options.analyzeCommits, release.analyzeCommits);
t.is(result.options.generateNotes, release.generateNotes);
t.deepEqual(result.options.getLastRelease, release.getLastRelease);
t.is(result.options.branch, release.branch);
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.is(t.context.plugins.firstCall.args[0].analyzeCommits, release.analyzeCommits);
t.is(t.context.plugins.firstCall.args[0].generateNotes, release.generateNotes);
t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, release.getLastRelease);
t.is(t.context.plugins.firstCall.args[0].branch, release.branch);
t.is(result.options.githubToken, process.env.GITHUB_TOKEN);
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Priority cli parameters over package.json configuration', async t => {
test.serial('Read options from .releaserc.yml', async t => {
const release = {
analyzeCommits: 'analyzeCommits',
generateNotes: 'generateNotes',
getLastRelease: {
path: 'getLastRelease',
param: 'getLastRelease_pkg',
},
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('.releaserc.yml', yaml.safeDump(release));
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from .releaserc.json', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('.releaserc.json', release);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from .releaserc.js', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('.releaserc.js', `module.exports = ${JSON.stringify(release)}`);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Read options from release.config.js', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_param'},
branch: 'test_branch',
repositoryUrl: 'git+https://hostname.com/owner/module.git',
};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeFile('release.config.js', `module.exports = ${JSON.stringify(release)}`);
const {options} = await t.context.getConfig();
// Verify the options contains the plugin config from package.json
t.deepEqual(options, release);
// Verify the plugins module is called with the plugin options from package.json
t.deepEqual(t.context.plugins.firstCall.args[0], release);
});
test.serial('Prioritise cli parameters over file configuration and git repo', async t => {
const release = {
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_pkg'},
branch: 'branch_pkg',
};
const options = {
getLastRelease: {
path: 'getLastRelease',
param: 'getLastRelease_cli',
},
getLastRelease: {path: 'getLastRelease', param: 'getLastRelease_cli'},
branch: 'branch_cli',
repositoryUrl: 'http://cli-url.com/owner/package',
};
const pkg = {name: 'package_name', release};
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const repo = await gitRepo();
await gitCommits(['First']);
// Create a clone
await gitShallowClone(repo);
// Create package.json in repository root
await writeJson('./package.json', pkg);
delete process.env.GH_TOKEN;
process.env.GITHUB_TOKEN = 'GITHUB_TOKEN';
const result = await t.context.getConfig(options);
// Verify the options contains the plugin config from cli
t.deepEqual(result.options.getLastRelease, options.getLastRelease);
t.is(result.options.branch, options.branch);
t.deepEqual(result.options, options);
// Verify the plugins module is called with the plugin options from cli
t.deepEqual(t.context.plugins.firstCall.args[0].getLastRelease, options.getLastRelease);
t.is(t.context.plugins.firstCall.args[0].branch, options.branch);
});
test.serial('Get tag from .npmrc', async t => {
const pkg = {name: 'package_name'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', pkg);
// Create local .npmrc
await writeFile('.npmrc', 'tag=npmrc_tag');
// Make sure to not use the environment variable set by npm when running tests with npm run test
delete process.env.npm_config_tag;
const result = await t.context.getConfig();
// Verify the tag used in the one in .npmrc
t.is(result.npm.tag, 'npmrc_tag');
});
test.serial('Get tag from package.json, even if defined in .npmrc', async t => {
const publishConfig = {tag: 'pkg_tag'};
const pkg = {name: 'package_name', publishConfig};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', pkg);
// Create local .npmrc
await writeFile('.npmrc', 'tag=npmrc_tag');
const result = await t.context.getConfig();
// Verify the tag used in the one in package.json
t.is(result.npm.tag, 'pkg_tag');
t.deepEqual(t.context.plugins.firstCall.args[0], options);
});
+6 -13
View File
@@ -1,36 +1,29 @@
import test from 'ava';
import {stub} from 'sinon';
import proxyquire from 'proxyquire';
import SemanticReleaseError from '@semantic-release/error';
import getNextVersion from '../lib/get-next-version';
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.getNextVersion = proxyquire('../src/lib/get-next-version', {'./logger': {log: t.context.log}});
t.context.logger = {log: t.context.log};
});
test('Increase version for patch release', t => {
const version = t.context.getNextVersion('patch', {version: '1.0.0'});
const version = getNextVersion('patch', {version: '1.0.0'}, t.context.logger);
t.is(version, '1.0.1');
});
test('Increase version for minor release', t => {
const version = t.context.getNextVersion('minor', {version: '1.0.0'});
const version = getNextVersion('minor', {version: '1.0.0'}, t.context.logger);
t.is(version, '1.1.0');
});
test('Increase version for major release', t => {
const version = t.context.getNextVersion('major', {version: '1.0.0'});
const version = getNextVersion('major', {version: '1.0.0'}, t.context.logger);
t.is(version, '2.0.0');
});
test('Return 1.0.0 if there is no previous release', t => {
const version = t.context.getNextVersion('minor', {});
const version = getNextVersion('minor', {}, t.context.logger);
t.is(version, '1.0.0');
});
test('Return an error if the release type is invalid', t => {
const error = t.throws(() => t.context.getNextVersion('invalid', {version: '1.0.0'}));
t.is(error.code, 'EINVALIDTYPE');
t.true(error instanceof SemanticReleaseError);
});
-82
View File
@@ -1,82 +0,0 @@
import test from 'ava';
import {stub} from 'sinon';
const getRegistry = require('../src/lib/get-registry');
test('Get registry from package.json', t => {
// Retrieve the registry with the get-registry module and verify it returns the one from the package.json in parameter
t.is(getRegistry({name: 'publish-config', publishConfig: {registry: 'a'}}, {}), 'a');
});
test('Prioritize the package.json registry config', t => {
// Stub the npmconf object
const get = stub();
// Retrieve the registry with the get-registry module and verify it returns the one from the package.json in parameter
t.is(getRegistry({name: 'publish-config', publishConfig: {registry: 'b'}}, {get}), 'b');
// Verify the registry has been retrieved from the package.json without trying the stubbed npmconf
t.true(get.notCalled);
});
test('Get registry for regular package name', t => {
// Stub the npmconf object returns 'b' for 'registry' property
const get = stub()
.withArgs('registry')
.returns('b');
// Retrieve the registry with the get-registry module and verify it returns the one configured in the stubbed npmconf
t.is(getRegistry({name: 'normal'}, {get}), 'b');
// Verify the registry has been retrieved by calling the stubbed npmconf
t.true(get.calledWithExactly('registry'));
});
test('Get default registry', t => {
// Stub the npmconf object, returns 'null'
const get = stub().returns(null);
// Retrieve the registry with the get-registry module and verify it returns default one
t.is(getRegistry({name: 'normal'}, {get}), 'https://registry.npmjs.org/');
// Verify the module tried first to retrieve the registry by calling the stubbed npmconf
t.true(get.calledWithExactly('registry'));
});
test('Get registry for scoped package name', t => {
// Stub the npmconf object, returns 'c' for '@scoped/registry' property
const get = stub()
.withArgs('@scoped/registry')
.returns('c');
// Retrieve the registry with the get-registry module and verify it returns the one configured in the stubbed npmconf
t.is(getRegistry({name: '@scoped/foo'}, {get}), 'c');
// Verify the registry for the scope '@scoped' has been retrieved by calling the stubbed npmconf
t.true(get.calledWithExactly('@scoped/registry'));
});
test('Get regular registry for scoped package name', t => {
// Stub the npmconf object, returns 'd' for 'registry' property
const get = stub()
.withArgs('registry')
.returns('d');
// Retrieve the registry with the get-registry module and verify it returns the regular default one for `@scoped` packages
t.is(getRegistry({name: '@scoped/baz'}, {get}), 'd');
// Verify the module tried to retrieve the @scoped registry by calling the stubbed npmconf
t.true(get.calledWithExactly('@scoped/registry'));
});
test('Get default registry for scoped package name', t => {
// Stub the npmconf object, returns 'd' for 'registry' property
const get = stub().returns(null);
// Retrieve the registry with the get-registry module and verify it returns default one for `@scoped` packages
t.is(getRegistry({name: '@scoped/baz'}, {get}), 'https://registry.npmjs.org/');
// Verify the module tried to retrieve the @scoped registry by calling the stubbed npmconf
t.true(get.calledWithExactly('@scoped/registry'));
// Verify the module tried to retrieve the regular registry by calling the stubbed npmconf
t.true(get.calledWithExactly('registry'));
});
+130
View File
@@ -0,0 +1,130 @@
import test from 'ava';
import fileUrl from 'file-url';
import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead, repoUrl} from '../lib/git';
import {
gitRepo,
gitCommits,
gitCheckout,
gitTagVersion,
gitShallowClone,
gitLog,
gitAddConfig,
} from './helpers/git-utils';
test.beforeEach(t => {
// Save the current working diretory
t.context.cwd = process.cwd();
});
test.afterEach.always(t => {
// Restore the current working directory
process.chdir(t.context.cwd);
});
test.serial('Get the last commit sha', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
const result = await gitHead();
t.is(result.substring(0, 7), commits[0].hash);
});
test.serial('Throw error if the last commit sha cannot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await t.throws(gitHead(), Error);
});
test.serial('Unshallow 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
await gitCommits(['First', 'Second']);
// Create a shallow clone with only 1 commit
await gitShallowClone(repo);
// Verify the shallow clone contains only one commit
t.is((await gitLog()).length, 1);
await unshallow();
// Verify the shallow clone contains all the commits
t.is((await gitLog()).length, 2);
});
test.serial('Do not throw error when unshallow a complete repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
await t.notThrows(unshallow());
});
test.serial('Verify if the commit `sha` is in the direct history of the current branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
// Create the new branch 'other-branch' from master
await gitCheckout('other-branch');
// Add commits to the 'other-branch' branch
const otherCommits = await gitCommits(['Second']);
await gitCheckout('master', false);
t.true(await isCommitInHistory(commits[0].hash));
t.false(await isCommitInHistory(otherCommits[0].hash));
});
test.serial('Get the tag associated with a commit sha or "null" if the commit does not exists', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
t.is(await gitCommitTag(commits[0].hash), 'v1.0.0');
t.falsy(await gitCommitTag('missing_sha'));
});
test.serial('Get the commit sha for a given tag or "null" if the tag does not exists', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
const commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
t.is((await gitTagHead('v1.0.0')).substring(0, 7), commits[0].hash);
t.falsy(await gitTagHead('missing_tag'));
});
test.serial('Return git remote repository url from config', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@hostname.com:owner/package.git');
t.is(await repoUrl(), 'git@hostname.com:owner/package.git');
});
test.serial('Return git remote repository url set while cloning', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
await gitCommits(['First']);
// Create a clone
await gitShallowClone(repo);
t.is(await repoUrl(), fileUrl(repo));
});
test.serial('Return "null" if git repository url is not set', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
t.is(await repoUrl(), null);
});
-91
View File
@@ -1,91 +0,0 @@
import test from 'ava';
import {gitRepo, gitCommits, gitHead} from './helpers/git-utils';
import nock from 'nock';
import {authenticate} from './helpers/mock-github';
import githubRelease from '../src/lib/github-release';
test.beforeEach(t => {
// Save the current working diretory
t.context.cwd = process.cwd();
});
test.afterEach.always(t => {
// Restore the current working directory
process.chdir(t.context.cwd);
// Reset nock
nock.cleanAll();
});
test.serial('Github release with default url', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
await gitCommits(['fix: First fix', 'feat: Second feature']);
const sha = await gitHead();
const owner = 'test_user';
const repo = 'test_repo';
const githubToken = 'github_token';
const notes = 'Test release note body';
const version = '1.0.0';
const branch = 'master';
const tagName = `v${version}`;
const options = {branch, githubToken};
const pkg = {version, repository: {url: `git+https://othertesturl.com/${owner}/${repo}.git`}};
const releaseUrl = `https://othertesturl.com/${owner}/${repo}/releases/${version}`;
// Mock github API for releases and git/refs endpoints
const github = authenticate({githubToken})
.post(`/repos/${owner}/${repo}/releases`, {
tag_name: tagName,
target_commitish: branch,
name: tagName,
body: notes,
})
.reply(200, {html_url: releaseUrl})
.post(`/repos/${owner}/${repo}/git/refs`, {ref: `refs/tags/${tagName}`, sha})
.reply({});
// Call the post module
t.is(releaseUrl, await githubRelease(pkg, notes, version, options));
// Verify the releases and git/refs endpoint have been call with expected requests
t.true(github.isDone());
});
test.serial('Github release with custom url', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
await gitCommits(['fix: First fix', 'feat: Second feature']);
const sha = await gitHead();
const owner = 'test_user';
const repo = 'test_repo';
const githubUrl = 'https://testurl.com:443';
const githubToken = 'github_token';
const githubApiPathPrefix = 'prefix';
const notes = 'Test release note body';
const version = '1.0.0';
const branch = 'master';
const tagName = `v${version}`;
const options = {branch, githubUrl, githubToken, githubApiPathPrefix};
const pkg = {version, repository: {url: `git@othertesturl.com:${owner}/${repo}.git`}};
const releaseUrl = `https://othertesturl.com/${owner}/${repo}/releases/${version}`;
// Mock github API for releases and git/refs endpoints
const github = authenticate({githubUrl, githubToken, githubApiPathPrefix})
.post(`/repos/${owner}/${repo}/releases`, {
tag_name: tagName,
target_commitish: branch,
name: tagName,
body: notes,
})
.reply(200, {html_url: releaseUrl})
.post(`/repos/${owner}/${repo}/git/refs`, {ref: `refs/tags/${tagName}`, sha})
.reply({});
// Call the post module
t.is(releaseUrl, await githubRelease(pkg, notes, version, options));
// Verify the releases and git/refs endpoint have been call with expected requests
t.true(github.isDone());
});
+35 -24
View File
@@ -1,11 +1,11 @@
import {mkdir} from 'fs-extra';
import tempy from 'tempy';
import execa from 'execa';
import pMapSeries from 'p-map-series';
import fileUrl from 'file-url';
import pReduce from 'p-reduce';
/**
* Commit message informations.
*
*
* @typedef {Object} Commit
* @property {string} branch The commit branch.
* @property {string} hash The commit hash.
@@ -13,19 +13,15 @@ import pMapSeries from 'p-map-series';
*/
/**
* Create a temporary git repository and change the current working directory to the repository root.
*
* @method gitCommits
* @param {Array<Commit>} commits the created commits.
*
* @return {string} The path of the repository.
*/
* Create a temporary git repository and change the current working directory to the repository root.
*
* @return {string} The path of the repository.
*/
export async function gitRepo() {
const dir = tempy.directory();
process.chdir(dir);
await mkdir('git-templates');
await execa('git', ['init', '--template=./git-templates']);
await execa('git', ['init']);
await gitCheckout('master');
return dir;
}
@@ -34,22 +30,27 @@ export async function gitRepo() {
* Create commits on the current git repository.
*
* @param {Array<string>} messages commit messages.
*
*
* @returns {Array<Commit>} The created commits, in reverse order (to match `git log` order).
*/
export async function gitCommits(messages) {
return (await pMapSeries(messages, async msg => {
const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout);
return {branch, hash, message};
})).reverse();
return (await pReduce(
messages,
async (commits, msg) => {
const {stdout} = await execa('git', ['commit', '-m', msg, '--allow-empty', '--no-gpg-sign']);
const [, branch, hash, message] = /^\[(\w+)\(?.*?\)?(\w+)\] (.+)$/.exec(stdout);
commits.push({branch, hash, message});
return commits;
},
[]
)).reverse();
}
/**
* Amend a commit (rewriting the sha) on the current git repository.
*
* @param {string} messages commit message.
*
*
* @returns {Array<Commit>} the created commits.
*/
export async function gitAmmendCommit(msg) {
@@ -92,20 +93,20 @@ export async function gitTagVersion(tagName, sha) {
* @return {Array<string>} The list of tags from the current git repository.
*/
export async function gitTags() {
return (await execa('git', ['tag'])).stdout.split('\n').filter(tag => !!tag);
return (await execa('git', ['tag'])).stdout.split('\n').filter(tag => Boolean(tag));
}
/**
* @return {Array<string>} The list of commit sha from the current git repository.
*/
export async function gitLog() {
return (await execa('git', ['log', '--format=format:%H'])).stdout.split('\n').filter(sha => !!sha);
return (await execa('git', ['log', '--format=format:%H'])).stdout.split('\n').filter(sha => Boolean(sha));
}
/**
* Create a shallow clone of a git repository and change the current working directory to the cloned repository root.
* The shallow will contain a limited number of commit and no tags.
*
*
* @param {string} origin The path of the repository to clone.
* @param {number} [depth=1] The number of commit to clone.
* @return {string} The path of the cloned repository.
@@ -114,13 +115,13 @@ export async function gitShallowClone(origin, branch = 'master', depth = 1) {
const dir = tempy.directory();
process.chdir(dir);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, `file://${origin}`, dir]);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, fileUrl(origin), dir]);
return dir;
}
/**
* Create a git repo with a detached head from another git repository and change the current working directory to the new repository root.
*
*
* @param {string} origin The path of the repository to clone.
* @param {number} head A commit sha of the origin repo that will become the detached head of the new one.
* @return {string} The path of the new repository.
@@ -142,3 +143,13 @@ export async function gitDetachedHead(origin, head) {
export async function gitPackRefs() {
await execa('git', ['pack-refs', '--all']);
}
/**
* Add a new Git configuration.
*
* @param {string} name Config name.
* @param {string} value Config value.
*/
export async function gitAddConfig(name, value) {
await execa('git', ['config', '--add', name, value]);
}
+2 -2
View File
@@ -2,12 +2,12 @@ import nock from 'nock';
/**
* Retun a `nock` object setup to respond to a github authentication request. Other expectation and responses can be chained.
*
*
* @param {String} [githubToken='GH_TOKEN'] The github token to return in the authentication response.
* @param {String} [githubUrl='https://api.github.com'] The url on which to intercept http requests.
* @return {Object} A `nock` object ready to respond to a github authentication request.
*/
export function authenticate(
export default function authenticate(
{githubToken = 'GH_TOKEN', githubUrl = 'https://api.github.com', githubApiPathPrefix = ''} = {}
) {
return nock(`${githubUrl}/${githubApiPathPrefix}`, {reqheaders: {Authorization: `token ${githubToken}`}});
+28 -10
View File
@@ -1,7 +1,10 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import pRetry from 'p-retry';
import {mockServerClient} from 'mockserver-client';
const IMAGE = 'jamesdbloom/mockserver:latest';
const MOCK_SERVER_PORT = 1080;
const MOCK_SERVER_HOST = 'localhost';
const docker = new Docker();
@@ -13,22 +16,35 @@ let container;
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull('jamesdbloom/mockserver:latest'));
await getStream(await docker.pull(IMAGE));
container = await docker.createContainer({
Image: 'jamesdbloom/mockserver',
Tty: true,
Image: IMAGE,
PortBindings: {[`${MOCK_SERVER_PORT}/tcp`]: [{HostPort: `${MOCK_SERVER_PORT}`}]},
});
return container.start();
await container.start();
try {
// Wait for the mock server to be ready
await pRetry(() => got.put(`http://${MOCK_SERVER_HOST}:${MOCK_SERVER_PORT}/status`, {cache: false}), {
retries: 7,
minTimeout: 1000,
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start mock-server after 2 min`);
}
}
/**
* Stop the `mockserver` Docker container.
* Stop and remote the `mockserver` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
return container.stop();
await container.stop();
await container.remove();
}
/**
@@ -47,19 +63,19 @@ const url = `http://${MOCK_SERVER_HOST}:${MOCK_SERVER_PORT}`;
* @param {Object} request Request expectation. The http request made on `path` has to match those criteria in order to be valid.
* @param {Object} request.body The JSON body the expected request must match.
* @param {Object} request.headers The headers the expected request must match.
* @param {Object} response The http response to return when receiving a request on `path`.
* @param {Object} response The http response to return when receiving a request on `path`.
* @param {String} [response.method='POST'] The http method for which to respond.
* @param {number} [response.statusCode=200] The status code to respond.
* @param {Object} response.body The JSON object to respond in the response body.
* @return {Object} An object representation the expectation. Pass to the `verify` function to validate the `mockserver` has been called with a `request` matching the expectations.
*/
function mock(
async function mock(
path,
{body: requestBody, headers: requestHeaders},
{method = 'POST', statusCode = 200, body: responseBody}
) {
client.mockAnyResponse({
httpRequest: {path},
await client.mockAnyResponse({
httpRequest: {path, method},
httpResponse: {
statusCode,
headers: [{name: 'Content-Type', values: ['application/json; charset=utf-8']}],
@@ -72,7 +88,9 @@ function mock(
method,
path,
headers: requestHeaders,
body: {type: 'JSON', json: JSON.stringify(requestBody), matchType: 'ONLY_MATCHING_FIELDS'},
body: requestBody
? {type: 'JSON', json: JSON.stringify(requestBody), matchType: 'ONLY_MATCHING_FIELDS'}
: undefined,
};
}
+78
View File
@@ -0,0 +1,78 @@
import Docker from 'dockerode';
import getStream from 'get-stream';
import got from 'got';
import pRetry from 'p-retry';
const IMAGE = 'npmjs/npm-docker-couchdb:1.6.1';
const SERVER_PORT = 15986;
const COUCHDB_PORT = 5984;
const SERVER_HOST = 'localhost';
const NPM_USERNAME = 'integration';
const NPM_PASSWORD = 'suchsecure';
const NPM_EMAIL = 'integration@test.com';
const docker = new Docker();
let container;
/**
* Download the `npm-docker-couchdb` Docker image, create a new container and start it.
*
* @return {Promise} Promise that resolves when the container is started.
*/
async function start() {
await getStream(await docker.pull(IMAGE));
container = await docker.createContainer({
Tty: true,
Image: IMAGE,
PortBindings: {[`${COUCHDB_PORT}/tcp`]: [{HostPort: `${SERVER_PORT}`}]},
});
await container.start();
try {
// Wait for the registry to be ready
await pRetry(() => got(`http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app`, {cache: false}), {
retries: 7,
minTimeout: 1000,
factor: 2,
});
} catch (err) {
throw new Error(`Couldn't start npm-docker-couchdb after 2 min`);
}
// Create user
await got(`http://${SERVER_HOST}:${SERVER_PORT}/_users/org.couchdb.user:${NPM_USERNAME}`, {
json: true,
auth: 'admin:admin',
method: 'PUT',
body: {
_id: `org.couchdb.user:${NPM_USERNAME}`,
name: NPM_USERNAME,
roles: [],
type: 'user',
password: NPM_PASSWORD,
email: NPM_EMAIL,
},
});
}
const url = `http://${SERVER_HOST}:${SERVER_PORT}/registry/_design/app/_rewrite/`;
const authEnv = {
npm_config_registry: url, // eslint-disable-line camelcase
NPM_USERNAME,
NPM_PASSWORD,
NPM_EMAIL,
};
/**
* Stop and remote the `npm-docker-couchdb` Docker container.
*
* @return {Promise} Promise that resolves when the container is stopped.
*/
async function stop() {
await container.stop();
await container.remove();
}
export default {start, stop, authEnv, url};
-14
View File
@@ -1,14 +0,0 @@
import execa from 'execa';
const opts = {cwd: __dirname};
const uri = 'http://localhost:' + (process.env.TRAVIS === 'true' ? 5984 : 15986) + '/registry/_design/app/_rewrite/';
function start() {
return execa('./start.sh', opts);
}
function stop() {
return execa('./stop.sh', opts);
}
export default {start, stop, uri};
-20
View File
@@ -1,20 +0,0 @@
[couchdb]
database_dir = data
view_index_dir = data
delayed_commits = false
uuid = bf4ecd84a7c89d60b5b2540fdf8c322c
[couch_httpd_auth]
public_fields = appdotnet, avatar, avatarMedium, avatarLarge, date, email, fields, freenode, fullname, github, homepage, name, roles, twitter, type, _id, _rev
users_db_public = true
[httpd]
port = 15986
bind_address = 127.0.0.1
secure_rewrites = false
[log]
file = couch/couch.log
[admins]
admin = -pbkdf2-89582b49cd2e8443e29a841f5a76d367956a8e58,1afa2f1531a17ac97f2ac9e334293753,10
-41
View File
@@ -1,41 +0,0 @@
#!/bin/bash
# exit if an error occurs
set -e
cd $(dirname $0)
mkdir -p couch
if [[ $TRAVIS = true ]]
then
COUCH=http://admin:password@127.0.0.1:5984
curl -X PUT http://127.0.0.1:5984/_config/admins/admin -d '"password"'
curl -X PUT $COUCH/_config/couchdb/delayed_commits -d '"false"'
curl -X PUT $COUCH/_config/couch_httpd_auth/users_db_public -d '"true"'
curl -X PUT $COUCH/_config/couch_httpd_auth/public_fields -d '"appdotnet, avatar, avatarMedium, avatarLarge, date, email, fields, freenode, fullname, github, homepage, name, roles, twitter, type, _id, _rev"'
curl -X PUT $COUCH/_config/httpd/secure_rewrites -d '"false"'
else
COUCH=http://admin:password@127.0.0.1:15986
couchdb -b -a local.ini -p couch/pid -o couch/stdout.log -e couch/stderr.log
# wait for couch to start
sleep 1
fi
# create "registry" database
curl -X PUT $COUCH/registry
# create sample npm user
curl -X PUT $COUCH/_users/org.couchdb.user:integration -H Content-Type:application/json --data-binary '{"_id": "org.couchdb.user:integration", "name": "integration", "roles": [], "type": "user", "password": "suchsecure", "email": "integration@test.com"}'
# npm-registry-couchpp needs this variable set to run
export DEPLOY_VERSION=nope
# setup npm-registry-couchapp
npm explore npm-registry-couchapp -- npm start --npm-registry-couchapp:couch=$COUCH/registry
npm explore npm-registry-couchapp -- npm run load --npm-registry-couchapp:couch=$COUCH/registry
export NO_PROMPT=yes
npm explore npm-registry-couchapp -- npm run copy --npm-registry-couchapp:couch=$COUCH/registry
-13
View File
@@ -1,13 +0,0 @@
#!/bin/bash
# close couchdb background process
couchdb -d
# delete data and logs
cd $(dirname $0)
cat couch/{couch,stdout,stderr}.log
cat couch/pid | xargs kill
rm -rf couch
rm -rf data
+233 -220
View File
@@ -1,278 +1,291 @@
import test from 'ava';
import proxyquire from 'proxyquire';
import {stub} from 'sinon';
import SemanticReleaseError from '@semantic-release/error';
const consoleLog = stub(console, 'log');
import tempy from 'tempy';
import DEFINITIONS from '../lib/plugins/definitions';
import {gitHead as getGitHead} from '../lib/git';
import {gitRepo, gitCommits, gitTagVersion} from './helpers/git-utils';
test.beforeEach(t => {
// Save the current process.env
t.context.env = Object.assign({}, process.env);
// Save the current working diretory
t.context.cwd = process.cwd();
// Stub the logger functions
t.context.log = stub();
t.context.error = stub();
t.context.logger = {log: t.context.log, error: t.context.error};
t.context.semanticRelease = proxyquire('../index', {'./lib/logger': t.context.logger});
t.context.stdout = stub(process.stdout, 'write');
t.context.stderr = stub(process.stderr, 'write');
});
test.afterEach.always(t => {
// Restore the current working directory
process.chdir(t.context.cwd);
// Restore process.env
process.env = Object.assign({}, t.context.env);
// Restore the current working directory
process.chdir(t.context.cwd);
t.context.stdout.restore();
t.context.stderr.restore();
});
test.after.always(t => {
consoleLog.restore();
});
test.serial('Plugins are called with expected values', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
let commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
test('Plugins are called with expected values', async t => {
const env = {NPM_TOKEN: 'NPM_TOKEN'};
const pkgOptions = {branch: 'master'};
const cliOptions = {githubToken: 'GH_TOKEN'};
const options = Object.assign({}, pkgOptions, cliOptions);
const pkg = {name: 'available', release: options, repository: {url: 'http://github.com/whats/up.git'}};
const npm = {registry: 'http://test.registry.com'};
const lastRelease = {version: '1.0.0', gitHead: 'test_commit_head'};
const commitsLastRelease = {version: '1.0.0', gitHead: 'tag_head'};
const commits = [{hash: '1', message: 'fix: First fix'}, {hash: '2', message: 'feat: First feature'}];
const nextRelease = {type: 'major', version: '2.0.0'};
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';
// Stub modules
const log = stub();
const error = stub();
const logger = {log, error};
const verifyAuth = stub().returns();
const publishNpm = stub().resolves();
const githubRelease = stub().resolves();
const getCommits = stub().resolves({commits, lastRelease: commitsLastRelease});
const getNextVersion = stub().returns(nextRelease.version);
// Stub plugins
const verifyConditions = stub().resolves();
const verifyConditions1 = stub().resolves();
const verifyConditions2 = stub().resolves();
const getLastRelease = stub().resolves(lastRelease);
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const getConfig = stub().resolves({
plugins: {getLastRelease, analyzeCommits, verifyRelease, verifyConditions, generateNotes},
env,
options,
pkg,
npm,
});
const publish = stub().resolves();
const semanticRelease = proxyquire('../src/index', {
'./lib/logger': logger,
'./lib/verify-auth': verifyAuth,
'./lib/get-config': getConfig,
'./lib/get-commits': getCommits,
'./lib/publish-npm': publishNpm,
'./lib/github-release': githubRelease,
'./lib/get-next-version': getNextVersion,
});
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions1, verifyConditions2],
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
// Call the index module
await semanticRelease(cliOptions);
await t.context.semanticRelease(options);
t.true(verifyConditions1.calledOnce);
t.deepEqual(verifyConditions1.firstCall.args[1], {options, logger: t.context.logger});
t.true(verifyConditions2.calledOnce);
t.deepEqual(verifyConditions2.firstCall.args[1], {options, logger: t.context.logger});
// Verify the sub-modules have been called with expected parameters
t.true(getConfig.calledOnce);
t.true(getConfig.calledWithExactly(cliOptions));
t.true(verifyAuth.calledOnce);
t.true(verifyAuth.calledWithExactly(options, env));
t.true(publishNpm.calledOnce);
t.true(publishNpm.calledWithExactly(pkg, npm, nextRelease));
t.true(githubRelease.calledOnce);
t.true(githubRelease.calledWithExactly(pkg, notes, nextRelease.version, options));
// Verify plugins have been called with expected parameters
t.true(verifyConditions.calledOnce);
t.true(verifyConditions.calledWithExactly({env, options, pkg, npm, logger}));
t.true(getLastRelease.calledOnce);
t.true(getLastRelease.calledWithExactly({env, options, pkg, npm, logger}));
t.deepEqual(getLastRelease.firstCall.args[1], {options, logger: t.context.logger});
t.true(analyzeCommits.calledOnce);
t.true(analyzeCommits.calledWithExactly({env, options, pkg, npm, logger, lastRelease: commitsLastRelease, commits}));
t.deepEqual(analyzeCommits.firstCall.args[1].options, options);
t.deepEqual(analyzeCommits.firstCall.args[1].logger, t.context.logger);
t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].message, commits[0].message);
t.true(verifyRelease.calledOnce);
t.true(
verifyRelease.calledWithExactly({
env,
options,
pkg,
npm,
logger,
lastRelease: commitsLastRelease,
commits,
nextRelease,
})
);
t.deepEqual(verifyRelease.firstCall.args[1].options, options);
t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger);
t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(verifyRelease.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
t.deepEqual(verifyRelease.firstCall.args[1].commits[0].message, commits[0].message);
t.deepEqual(verifyRelease.firstCall.args[1].nextRelease, nextRelease);
t.true(generateNotes.calledOnce);
t.true(
generateNotes.calledWithExactly({
env,
options,
pkg,
npm,
logger,
lastRelease: commitsLastRelease,
commits,
nextRelease,
})
);
t.deepEqual(generateNotes.firstCall.args[1].options, options);
t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger);
t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(generateNotes.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
t.deepEqual(generateNotes.firstCall.args[1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes.firstCall.args[1].nextRelease, nextRelease);
t.true(publish.calledOnce);
t.deepEqual(publish.firstCall.args[1].options, options);
t.deepEqual(publish.firstCall.args[1].logger, t.context.logger);
t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
t.deepEqual(publish.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
t.deepEqual(publish.firstCall.args[1].commits[0].message, commits[0].message);
t.deepEqual(publish.firstCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
});
test('Dry-run skips verifyAuth, verifyConditions, publishNpm and githubRelease', async t => {
const env = {NPM_TOKEN: 'NPM_TOKEN'};
const pkgOptions = {branch: 'master'};
const cliOptions = {githubToken: 'GH_TOKEN', dryRun: true};
const options = Object.assign({}, pkgOptions, cliOptions);
const pkg = {name: 'available', release: options, repository: {url: 'http://github.com/whats/up.git'}};
const npm = {registry: 'http://test.registry.com'};
const lastRelease = {version: '1.0.0', gitHead: 'test_commit_head'};
const commitsLastRelease = {version: '1.0.0', gitHead: 'tag_head'};
const commits = [{hash: '1', message: 'fix: First fix'}, {hash: '2', message: 'feat: First feature'}];
const nextRelease = {type: 'major', version: '2.0.0'};
test.serial('Use new gitHead, and recreate release notes if a publish plugin create a commit', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
let commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
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 generateNotes = stub().resolves(notes);
const publish1 = stub().callsFake(async () => {
await gitCommits(['Third']);
});
const publish2 = stub().resolves();
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: stub().resolves(),
getLastRelease: stub().resolves(lastRelease),
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes,
publish: [publish1, publish2],
};
await t.context.semanticRelease(options);
t.true(generateNotes.calledTwice);
t.deepEqual(generateNotes.firstCall.args[1].nextRelease, nextRelease);
t.true(publish1.calledOnce);
t.deepEqual(publish1.firstCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
nextRelease.gitHead = await getGitHead();
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
t.true(publish2.calledOnce);
t.deepEqual(publish2.firstCall.args[1].nextRelease, Object.assign({}, nextRelease, {notes}));
});
test.serial('Dry-run skips verifyConditions and publish', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
let commits = await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
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';
// Stub modules
const log = stub();
const error = stub();
const logger = {log, error};
const verifyAuth = stub().returns();
const publishNpm = stub().resolves();
const githubRelease = stub().resolves();
const getCommits = stub().resolves({commits, lastRelease: commitsLastRelease});
const getNextVersion = stub().returns(nextRelease.version);
// Stub plugins
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves(lastRelease);
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const getConfig = stub().resolves({
plugins: {getLastRelease, analyzeCommits, verifyRelease, verifyConditions, generateNotes},
env,
options,
pkg,
npm,
});
const publish = stub().resolves();
const semanticRelease = proxyquire('../src/index', {
'./lib/logger': logger,
'./lib/verify-auth': verifyAuth,
'./lib/get-config': getConfig,
'./lib/get-commits': getCommits,
'./lib/publish-npm': publishNpm,
'./lib/github-release': githubRelease,
'./lib/get-next-version': getNextVersion,
});
const options = {
dryRun: true,
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions,
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
// Call the index module
await semanticRelease(cliOptions);
await t.context.semanticRelease(options);
// Verify that publishNpm, githubRelease, verifyAuth, verifyConditions have not been called in a dry run
t.true(publishNpm.notCalled);
t.true(githubRelease.notCalled);
t.true(verifyAuth.notCalled);
t.true(verifyConditions.notCalled);
// Verify the release notes are logged
t.true(consoleLog.calledWithMatch(notes));
// Verify the sub-modules have been called with expected parameters
t.true(getConfig.calledOnce);
t.true(getConfig.calledWithExactly(cliOptions));
// Verify plugins have been called with expected parameters
t.true(getLastRelease.calledOnce);
t.true(getLastRelease.calledWithExactly({env, options, pkg, npm, logger}));
t.true(analyzeCommits.calledOnce);
t.true(analyzeCommits.calledWithExactly({env, options, pkg, npm, logger, lastRelease: commitsLastRelease, commits}));
t.true(verifyRelease.calledOnce);
t.true(
verifyRelease.calledWithExactly({
env,
options,
pkg,
npm,
logger,
lastRelease: commitsLastRelease,
commits,
nextRelease,
})
);
t.true(generateNotes.calledOnce);
t.true(
generateNotes.calledWithExactly({
env,
options,
pkg,
npm,
logger,
lastRelease: commitsLastRelease,
commits,
nextRelease,
})
);
t.true(publish.notCalled);
});
test('Throw SemanticReleaseError if there is no release to be done', async t => {
const env = {NPM_TOKEN: 'NPM_TOKEN'};
const pkgOptions = {branch: 'master'};
const cliOptions = {githubToken: 'GH_TOKEN'};
const options = Object.assign({}, pkgOptions, cliOptions);
const pkg = {name: 'available', release: options, repository: {url: 'http://github.com/whats/up.git'}};
const npm = {registry: 'http://test.registry.com'};
const lastRelease = {version: '1.0.0', gitHead: 'test_commit_head'};
const commitsLastRelease = {version: '1.0.0', gitHead: 'tag_head'};
const commits = [{hash: '1', message: 'fix: First fix'}, {hash: '2', message: 'feat: First feature'}];
const nextRelease = {type: undefined};
test.serial('Accept "undefined" values for the "getLastRelease" and "generateNotes" plugins', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
// Stub modules
const log = stub();
const error = stub();
const logger = {log, error};
const verifyAuth = stub().returns();
const publishNpm = stub().resolves();
const githubRelease = stub().resolves();
const getCommits = stub().resolves({commits, lastRelease: commitsLastRelease});
const getNextVersion = stub().returns(null);
// Stub plugins
const lastRelease = {gitHead: undefined, gitTag: undefined, version: undefined};
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves(lastRelease);
const getLastRelease = stub().resolves();
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const getConfig = stub().resolves({
plugins: {getLastRelease, analyzeCommits, verifyRelease, verifyConditions, generateNotes},
env,
options,
pkg,
npm,
});
const publish = stub().resolves();
const semanticRelease = proxyquire('../src/index', {
'./lib/logger': logger,
'./lib/verify-auth': verifyAuth,
'./lib/get-config': getConfig,
'./lib/get-commits': getCommits,
'./lib/publish-npm': publishNpm,
'./lib/github-release': githubRelease,
'./lib/get-next-version': getNextVersion,
});
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
await t.context.semanticRelease(options);
// Call the index module
const err = await t.throws(semanticRelease(cliOptions));
// Verify error code and type
t.is(err.code, 'ENOCHANGE');
t.true(err instanceof SemanticReleaseError);
// Verify the sub-modules have been called with expected parameters
t.true(getConfig.calledOnce);
t.true(getConfig.calledWithExactly(cliOptions));
t.true(verifyAuth.calledOnce);
t.true(verifyAuth.calledWithExactly(options, env));
// Verify plugins have been called with expected parameters
t.true(verifyConditions.calledOnce);
t.true(verifyConditions.calledWithExactly({env, options, pkg, npm, logger}));
t.true(getLastRelease.calledOnce);
t.true(getLastRelease.calledWithExactly({env, options, pkg, npm, logger}));
t.true(analyzeCommits.calledOnce);
t.true(analyzeCommits.calledWithExactly({env, options, pkg, npm, logger, lastRelease: commitsLastRelease, commits}));
// Verify that verifyRelease, publishNpm, generateNotes, githubRelease have not been called when no release is done
t.true(verifyRelease.notCalled);
t.true(generateNotes.notCalled);
t.true(publishNpm.notCalled);
t.true(githubRelease.notCalled);
t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
t.true(verifyRelease.calledOnce);
t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
t.true(generateNotes.calledOnce);
t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
t.true(publish.calledOnce);
t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
t.falsy(publish.firstCall.args[1].nextRelease.notes);
});
test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
// Set the current working directory to a temp directory
process.chdir(tempy.directory());
const error = await t.throws(t.context.semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOGITREPO');
t.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const error = await t.throws(t.context.semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOREPOURL');
t.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw an Error if returns an unexpected value', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
// Create the tag corresponding to version 1.0.0
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves('string');
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
};
const error = await t.throws(t.context.semanticRelease(options), Error);
// Verify error message
t.regex(error.message, new RegExp(DEFINITIONS.getLastRelease.output.message));
t.regex(error.message, /Received: 'string'/);
});
+240 -183
View File
@@ -3,57 +3,88 @@ import {writeJson, readJson} from 'fs-extra';
import {stub} from 'sinon';
import execa from 'execa';
import {gitRepo, gitCommits, gitHead, gitTagVersion, gitPackRefs, gitAmmendCommit} from './helpers/git-utils';
import registry from './helpers/registry';
import mockServer from './helpers/mockserver';
import semanticRelease from '../src';
import npmRegistry from './helpers/npm-registry';
import semanticRelease from '..';
// Environment variables used with cli
/* eslint camelcase: ["error", {properties: "never"}] */
// Environment variables used with semantic-release cli (similar to what a user would setup)
const env = {
npm_config_registry: registry.uri,
GH_TOKEN: 'github_token',
NPM_OLD_TOKEN: 'aW50ZWdyYXRpb246c3VjaHNlY3VyZQ==',
GITHUB_URL: mockServer.url,
NPM_EMAIL: 'integration@test.com',
NPM_USERNAME: 'integration',
NPM_PASSWORD: 'suchsecure',
};
// Environment variables used only for the local npm command used to do verification
const testEnv = Object.assign({}, process.env, {
npm_config_registry: npmRegistry.url,
NPM_EMAIL: 'integration@test.com',
LEGACY_TOKEN: Buffer.from(`${process.env.NPM_USERNAME}:${process.env.NPM_PASSWORD}`, 'utf8').toString('base64'),
});
const cli = require.resolve('../bin/semantic-release');
const noop = require.resolve('../src/lib/plugin-noop');
const pluginError = require.resolve('./fixtures/plugin-error-a');
const pluginError = require.resolve('./fixtures/plugin-error');
const pluginInheritedError = require.resolve('./fixtures/plugin-error-inherited');
test.before(async t => {
await mockServer.start();
test.before(async () => {
// Start the local NPM registry
await registry.start();
await npmRegistry.start();
// Start Mock Server
await mockServer.start();
});
test.beforeEach(async t => {
test.beforeEach(t => {
// Save the current process.env
t.context.env = Object.assign({}, process.env);
// Save the current working diretory
t.context.cwd = process.cwd();
// Delete env paramaters that could have been set on the machine running the tests
delete process.env.NPM_TOKEN;
delete process.env.NPM_USERNAME;
delete process.env.NPM_PASSWORD;
delete process.env.NPM_EMAIL;
delete process.env.GH_TOKEN;
delete process.env.GITHUB_TOKEN;
delete process.env.GH_URL;
delete process.env.GITHUB_URL;
delete process.env.GH_PREFIX;
delete process.env.GITHUB_PREFIX;
// Delete all `npm_config` environment variable set by CI as they take precedence over the `.npmrc` because the process that runs the tests is started before the `.npmrc` is created
for (let i = 0, keys = Object.keys(process.env); i < keys.length; i++) {
if (keys[i].startsWith('npm_config')) {
delete process.env[keys[i]];
}
}
// Disable logs during tests
t.context.log = stub(console, 'log');
t.context.error = stub(console, 'error');
t.context.stdout = stub(process.stdout, 'write');
t.context.stderr = stub(process.stderr, 'write');
});
test.afterEach.always(async t => {
test.afterEach.always(t => {
// Restore process.env
process.env = Object.assign({}, t.context.env);
// Restore the current working directory
process.chdir(t.context.cwd);
// Restore the logs
t.context.log.restore();
t.context.error.restore();
t.context.stdout.restore();
t.context.stderr.restore();
});
test.after.always(async t => {
await mockServer.stop();
test.after.always(async () => {
// Stop the local NPM registry
await registry.stop();
await npmRegistry.stop();
// Stop Mock Server
await mockServer.stop();
});
test.serial('Release patch, minor and major versions', async t => {
const packageName = 'test-module';
const repo = 'test-repo';
const packageName = 'test-release';
const owner = 'test-owner';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
@@ -61,14 +92,20 @@ test.serial('Release patch, minor and major versions', async t => {
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {verifyConditions: noop, githubUrl: mockServer.url},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
// Create a npm-shrinkwrap.json file
await execa('npm', ['shrinkwrap'], {env});
await execa('npm', ['shrinkwrap'], {env: testEnv});
/** No release **/
/* No release */
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
t.log('Commit a chore');
await gitCommits(['chore: Init repository']);
t.log('$ semantic-release');
@@ -76,15 +113,20 @@ test.serial('Release patch, minor and major versions', async t => {
t.regex(stdout, /There are no relevant changes, so no new version is released/);
t.is(code, 0);
/** Initial release **/
/* Initial release */
let version = '1.0.0';
let createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
let createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -97,7 +139,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
@@ -106,24 +148,30 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Patch release **/
/* Patch release */
version = '1.0.1';
createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -136,7 +184,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
@@ -145,24 +193,30 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', 'test-module', 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Minor release **/
/* Minor release */
version = '1.1.0';
createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -175,7 +229,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
@@ -184,24 +238,30 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', 'test-module', 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
/** Major release **/
/* Major release */
version = '2.0.0';
createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -214,7 +274,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and npm-shrinkwrap.json have been updated
@@ -223,19 +283,20 @@ test.serial('Release patch, minor and major versions', async t => {
// Retrieve the published package from the registry and check version and gitHead
[, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', 'test-module', 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Release versions from a packed git repository, using tags to determine last release gitHead', async t => {
const packageName = 'test-module-2';
const repo = 'test-repo';
const packageName = 'test-git-packaed';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
@@ -244,19 +305,25 @@ test.serial('Release versions from a packed git repository, using tags to determ
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git@github.com:${repo}/${packageName}.git`},
release: {verifyConditions: noop, githubUrl: mockServer.url},
repository: {url: `git@github.com:${owner}/${packageName}.git`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
let version = '1.0.0';
let createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
let createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -270,29 +337,35 @@ test.serial('Release versions from a packed git repository, using tags to determ
t.log('$ semantic-release');
let {stdout, code} = await execa(cli, [], {env});
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version
let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
// Create a tag version so the tag can be used later to determine the commit associated with the version
await gitTagVersion(`v${version}`);
t.log(`Create git tag v${version}`);
/** Patch release **/
/* Patch release */
version = '1.0.1';
createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -304,30 +377,30 @@ test.serial('Release versions from a packed git repository, using tags to determ
t.log('$ semantic-release');
({stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Exit with 1 if a plugin is not found', async t => {
const packageName = 'test-module-3';
const repo = 'test-repo';
const packageName = 'test-plugin-not-found';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {analyzeCommits: 'non-existing-path'},
});
@@ -337,8 +410,8 @@ test.serial('Exit with 1 if a plugin is not found', async t => {
});
test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', async t => {
const packageName = 'test-module-4';
const repo = 'test-repo';
const packageName = 'test-recovery';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
await gitRepo();
@@ -347,19 +420,25 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {verifyConditions: noop, githubUrl: mockServer.url},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
let version = '1.0.0';
let createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
let verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
let createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -371,19 +450,20 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.log('$ semantic-release');
let {stderr, stdout, code} = await execa(cli, [], {env});
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
const head = await gitHead();
t.is(releasedGitHead, head);
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
@@ -391,13 +471,17 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
await gitTagVersion(`v${version}`);
t.log(`Create git tag v${version}`);
/** Rewrite sha of commit used for release **/
/* Rewrite sha of commit used for release */
t.log('Amend release commit');
const {hash} = await gitAmmendCommit('feat: Initial commit');
/** Patch release **/
/* Patch release */
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
t.log('Commit a fix');
await gitCommits(['fix: bar']);
t.log('$ semantic-release');
@@ -412,19 +496,24 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
)
);
/** Create a tag to recover and redo release **/
/* Create a tag to recover and redo release */
t.log(`Create git tag v${version} to recover`);
await gitTagVersion(`v${version}`, hash);
version = '1.0.1';
createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}],
@@ -435,22 +524,23 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.log('$ semantic-release');
({stderr, stdout, code} = await execa(cli, [], {env}));
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).stdout;
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env: testEnv})).stdout;
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Dry-run', async t => {
const packageName = 'test-module-5';
const repo = 'test-repo';
const packageName = 'test-dry-run';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
@@ -458,16 +548,16 @@ test.serial('Dry-run', async t => {
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
/* Initial release */
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release -d');
let {stdout, code} = await execa(cli, ['-d'], {env});
const {stdout, code} = await execa(cli, ['-d'], {env});
t.regex(stdout, new RegExp(`There is no previous release, the next release version is ${version}`));
t.regex(stdout, new RegExp(`Release note for version ${version}`));
t.regex(stdout, /Initial commit/);
@@ -478,9 +568,8 @@ test.serial('Dry-run', async t => {
});
test.serial('Pass options via CLI arguments', async t => {
const packageName = 'test-module-6';
const repo = 'test-repo';
const githubToken = 'OTHER_TOKEN';
const packageName = 'test-cli';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
@@ -488,56 +577,38 @@ test.serial('Pass options via CLI arguments', async t => {
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
let version = '1.0.0';
let createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${githubToken}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${githubToken}`]}],
},
{body: {html_url: `release-url/${version}`}}
);
/* Initial release */
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stdout, code} = await execa(
const {stdout, code} = await execa(
cli,
['--github-token', githubToken, '--verify-conditions', `${noop}, ${noop}`, '--debug'],
['--verify-conditions', '@semantic-release/npm', '--publish', '@semantic-release/npm', '--debug'],
{env}
);
t.regex(stdout, new RegExp(`Published Github release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry ${registry.uri}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Run via JS API', async t => {
const packageName = 'test-module-7';
const repo = 'test-repo';
const packageName = 'test-js-api';
const owner = 'test-repo';
const githubToken = 'OTHER_TOKEN';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
@@ -546,19 +617,24 @@ test.serial('Run via JS API', async t => {
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
publishConfig: {registry: npmRegistry.url},
});
/** Initial release **/
let version = '1.0.0';
let createRefMock = mockServer.mock(
`/repos/${repo}/${packageName}/git/refs`,
/* Initial release */
const version = '1.0.0';
const verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${githubToken}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
const createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${githubToken}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let createReleaseMock = mockServer.mock(
`/repos/${repo}/${packageName}/releases`,
const createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
body: {tag_name: `v${version}`, target_commitish: 'master', name: `v${version}`},
headers: [{name: 'Authorization', values: [`token ${githubToken}`]}],
@@ -571,53 +647,34 @@ test.serial('Run via JS API', async t => {
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ Call semantic-release via API');
await semanticRelease({githubToken, verifyConditions: [noop, noop], debug: true});
await semanticRelease({
verifyConditions: [{path: '@semantic-release/github', githubToken}, '@semantic-release/npm'],
publish: [{path: '@semantic-release/github', githubToken}, '@semantic-release/npm'],
debug: true,
});
t.true(t.context.log.calledWithMatch(/Published Github release: /, new RegExp(`release-url/${version}`)));
t.true(t.context.log.calledWithMatch(/Publishing version .* to npm registry/, version, registry.uri));
t.true(t.context.log.calledWithMatch(/Publishing version .* to npm registry/, version));
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
// Retrieve the published package from the registry and check version and gitHead
let [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env})).stdout
const [, releasedVersion, releasedGitHead] = /^version = '(.+)'\s+gitHead = '(.+)'$/.exec(
(await execa('npm', ['show', packageName, 'version', 'gitHead'], {env: testEnv})).stdout
);
t.is(releasedVersion, version);
t.is(releasedGitHead, await gitHead());
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
test.serial('Returns and error code if NPM token is invalid', async t => {
const env = {npm_config_registry: registry.uri, GH_TOKEN: 'github_token', NPM_TOKEN: 'wrong_token'};
const packageName = 'test-module-8';
const repo = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
// Create package.json in repository root
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {verifyConditions: noop, githubUrl: mockServer.url},
});
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stderr, code} = await execa(cli, [], {env, reject: false});
t.regex(stderr, /forbidden Please log in before writing to the db/);
t.is(code, 1);
});
test.serial('Log unexpected errors from plugins and exit with 1', async t => {
const packageName = 'test-module-9';
const repo = 'test-repo';
const packageName = 'test-unexpected-error';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
@@ -625,15 +682,15 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url, verifyConditions: pluginError},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: pluginError},
});
/** Initial release **/
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stderr, code} = await execa(cli, [], {env, reject: false});
const {stderr, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stderr, /Error: a/);
// Verify the the stacktrace is logged
@@ -644,8 +701,8 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
});
test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async t => {
const packageName = 'test-module-10';
const repo = 'test-repo';
const packageName = 'test-inherited-error';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository and package.json');
await gitRepo();
@@ -653,15 +710,15 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${repo}/${packageName}`},
release: {githubUrl: mockServer.url, verifyConditions: pluginInheritedError},
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: pluginInheritedError},
});
/** Initial release **/
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
let {stdout, code} = await execa(cli, [], {env, reject: false});
const {stdout, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.is(code, 0);
@@ -669,21 +726,21 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async
test.serial('CLI returns error code and prints help if called with a command', async t => {
t.log('$ semantic-release pre');
let {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
const {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 1);
});
test.serial('CLI prints help if called with --help', async t => {
t.log('$ semantic-release --help');
let {stdout, code} = await execa(cli, ['--help'], {env});
const {stdout, code} = await execa(cli, ['--help'], {env});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 0);
});
test.serial('CLI returns error code with invalid option', async t => {
t.log('$ semantic-release --unknown-option');
let {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
const {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
t.regex(stderr, /unknown option/);
t.is(code, 1);
});
+1 -1
View File
@@ -1,6 +1,6 @@
import test from 'ava';
import {stub, match} from 'sinon';
import logger from '../src/lib/logger';
import logger from '../lib/logger';
test.beforeEach(t => {
t.context.log = stub(console, 'log');
-123
View File
@@ -1,123 +0,0 @@
import {promisify} from 'util';
import test from 'ava';
import proxyquire from 'proxyquire';
import {stub, match} from 'sinon';
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.plugins = proxyquire('../src/lib/plugins', {'./logger': {log: t.context.log}});
});
test('Export plugins', t => {
// Call the plugin module
const defaultPlugins = t.context.plugins({});
// Verify the module returns a function for each plugin
t.is(typeof defaultPlugins.analyzeCommits, 'function');
t.is(typeof defaultPlugins.generateNotes, 'function');
t.is(typeof defaultPlugins.verifyConditions, 'function');
t.is(typeof defaultPlugins.verifyRelease, 'function');
t.is(typeof defaultPlugins.getLastRelease, 'function');
});
test('Pipeline - Get all results', async t => {
// Call the plugin module with a verifyRelease plugin pipeline
const pipelinePlugins = t.context.plugins({
verifyRelease: ['./src/lib/plugin-noop', './test/fixtures/plugin-result-a', './test/fixtures/plugin-result-b'],
});
// Call the verifyRelease pipeline
const results = await pipelinePlugins.verifyRelease({});
// Verify the pipeline return the expected result for each plugin, in order
t.deepEqual(results, [undefined, 'a', 'b']);
// Verify the logger has been called with the plugins path
t.true(t.context.log.calledWith(match.string, './src/lib/plugin-noop'));
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-result-a'));
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-result-b'));
});
test('Pipeline - Pass pluginConfig and options to each plugins', async t => {
// Plugin configuration with options (plugin-result-config is a mock plugin returning its pluginConfig and options parameters)
const pluginConfig = {path: './test/fixtures/plugin-result-config', pluginParam: 'param1'};
// Semantic-release global options
const options = {semanticReleaseParam: 'param2'};
// Call the plugin module with a verifyRelease plugin pipeline
const pipelinePlugins = t.context.plugins({
verifyRelease: [pluginConfig, './test/fixtures/plugin-result-config'],
});
// Call the verifyRelease pipeline
const results = await pipelinePlugins.verifyRelease(options);
// Verify the pipeline first result is the pluginConfig and options parameters (to verify the plugin was called with the defined pluginConfig and options parameters)
t.deepEqual(results, [{pluginConfig, options}, {pluginConfig: {}, options}]);
// Verify the logger has been called with the plugins path
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-result-config'));
});
test('Pipeline - Get first error', async t => {
// Call the plugin module with a verifyRelease plugin pipeline
const pipelinePlugins = t.context.plugins({
verifyRelease: ['./src/lib/plugin-noop', './test/fixtures/plugin-error-a', './test/fixtures/plugin-error-b'],
});
// Call the verifyRelease pipeline and verify it returns the error thrown by './test/fixtures/plugin-error-a'
await t.throws(pipelinePlugins.verifyRelease({}), 'a');
// Verify the logger has been called with the plugins path
t.true(t.context.log.calledWith(match.string, './src/lib/plugin-noop'));
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-error-a'));
});
test('Normalize and load plugin from string', t => {
// Call the normalize function with a path
const plugin = t.context.plugins.normalize('./src/lib/plugin-noop');
// Verify the plugin is loaded
t.is(typeof plugin, 'function');
// Verify the logger has been called with the plugins path
t.true(t.context.log.calledWith(match.string, './src/lib/plugin-noop'));
});
test('Normalize and load plugin from object', t => {
// Call the normalize function with an object (with path property)
const plugin = t.context.plugins.normalize({path: './src/lib/plugin-noop'});
// Verify the plugin is loaded
t.is(typeof plugin, 'function');
// Verify the logger has been called with the plugins path
t.true(t.context.log.calledWith(match.string, './src/lib/plugin-noop'));
});
test('Load from fallback', t => {
// Call the normalize function with a fallback
const plugin = t.context.plugins.normalize(null, '../lib/plugin-noop');
// Verify the fallback plugin is loaded
t.is(typeof plugin, 'function');
});
test('Always pass a defined "pluginConfig" for plugin defined with string', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = t.context.plugins.normalize('./test/fixtures/plugin-result-config');
const pluginResult = await promisify(plugin)({});
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Always pass a defined "pluginConfig" for plugin defined with path', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = t.context.plugins.normalize({path: './test/fixtures/plugin-result-config'});
const pluginResult = await promisify(plugin)({});
t.deepEqual(pluginResult.pluginConfig, {path: './test/fixtures/plugin-result-config'});
});
test('Always pass a defined "pluginConfig" for fallback plugin', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = t.context.plugins.normalize(null, '../../test/fixtures/plugin-result-config');
const pluginResult = await promisify(plugin)({});
t.deepEqual(pluginResult.pluginConfig, {});
});
+100
View File
@@ -0,0 +1,100 @@
import test from 'ava';
import definitions from '../../lib/plugins/definitions';
test('The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(definitions.verifyConditions.config.validator({}));
t.false(definitions.verifyConditions.config.validator({path: null}));
t.true(definitions.verifyConditions.config.validator({path: 'plugin-path.js'}));
t.true(definitions.verifyConditions.config.validator());
t.true(definitions.verifyConditions.config.validator('plugin-path.js'));
t.true(definitions.verifyConditions.config.validator(() => {}));
t.true(definitions.verifyConditions.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "getLastRelease" plugin is mandatory, and must be a single plugin definition', t => {
t.false(definitions.getLastRelease.config.validator({}));
t.false(definitions.getLastRelease.config.validator({path: null}));
t.false(definitions.getLastRelease.config.validator([]));
t.false(definitions.getLastRelease.config.validator());
t.true(definitions.getLastRelease.config.validator({path: 'plugin-path.js'}));
t.true(definitions.getLastRelease.config.validator('plugin-path.js'));
t.true(definitions.getLastRelease.config.validator(() => {}));
});
test('The "analyzeCommits" plugin is mandatory, and must be a single plugin definition', t => {
t.false(definitions.analyzeCommits.config.validator({}));
t.false(definitions.analyzeCommits.config.validator({path: null}));
t.false(definitions.analyzeCommits.config.validator([]));
t.false(definitions.analyzeCommits.config.validator());
t.true(definitions.analyzeCommits.config.validator({path: 'plugin-path.js'}));
t.true(definitions.analyzeCommits.config.validator('plugin-path.js'));
t.true(definitions.analyzeCommits.config.validator(() => {}));
});
test('The "verifyRelease" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(definitions.verifyRelease.config.validator({}));
t.false(definitions.verifyRelease.config.validator({path: null}));
t.true(definitions.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(definitions.verifyRelease.config.validator());
t.true(definitions.verifyRelease.config.validator('plugin-path.js'));
t.true(definitions.verifyRelease.config.validator(() => {}));
t.true(definitions.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "generateNotes" plugin, if defined, must be a single plugin definition', t => {
t.false(definitions.generateNotes.config.validator({}));
t.false(definitions.generateNotes.config.validator({path: null}));
t.false(definitions.generateNotes.config.validator([]));
t.true(definitions.generateNotes.config.validator());
t.true(definitions.generateNotes.config.validator({path: 'plugin-path.js'}));
t.true(definitions.generateNotes.config.validator('plugin-path.js'));
t.true(definitions.generateNotes.config.validator(() => {}));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(definitions.publish.config.validator({}));
t.false(definitions.publish.config.validator({path: null}));
t.false(definitions.publish.config.validator());
t.true(definitions.publish.config.validator({path: 'plugin-path.js'}));
t.true(definitions.publish.config.validator('plugin-path.js'));
t.true(definitions.publish.config.validator(() => {}));
t.true(definitions.publish.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "getLastRelease" plugin output if defined, must be an object with an optionnal valid semver version in the "version" property', t => {
t.false(definitions.getLastRelease.output.validator('string'));
t.false(definitions.getLastRelease.output.validator(1));
t.false(definitions.getLastRelease.output.validator({version: 'invalid'}));
t.true(definitions.getLastRelease.output.validator());
t.true(definitions.getLastRelease.output.validator({}));
t.true(definitions.getLastRelease.output.validator({version: 'v1.0.0'}));
t.true(definitions.getLastRelease.output.validator({version: '1.0.0'}));
t.true(definitions.getLastRelease.output.validator({version: null}));
});
test('The "analyzeCommits" plugin output must be either undefined or a valid semver release type', t => {
t.false(definitions.analyzeCommits.output.validator('invalid'));
t.false(definitions.analyzeCommits.output.validator(1));
t.false(definitions.analyzeCommits.output.validator({}));
t.true(definitions.analyzeCommits.output.validator());
t.true(definitions.analyzeCommits.output.validator(null));
t.true(definitions.analyzeCommits.output.validator('major'));
});
test('The "generateNotes" plugin output, if defined, must be a string', t => {
t.false(definitions.generateNotes.output.validator(1));
t.false(definitions.generateNotes.output.validator({}));
t.true(definitions.generateNotes.output.validator());
t.true(definitions.generateNotes.output.validator(null));
t.true(definitions.generateNotes.output.validator(''));
t.true(definitions.generateNotes.output.validator('string'));
});
+113
View File
@@ -0,0 +1,113 @@
import test from 'ava';
import {noop} from 'lodash';
import {stub, match} from 'sinon';
import normalize from '../../lib/plugins/normalize';
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test('Normalize and load plugin from string', t => {
const plugin = normalize('verifyConditions', './test/fixtures/plugin-noop', t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, 'verifyConditions', './test/fixtures/plugin-noop'));
});
test('Normalize and load plugin from object', t => {
const plugin = normalize('publish', {path: './test/fixtures/plugin-noop'}, t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, 'publish', './test/fixtures/plugin-noop'));
});
test('Normalize and load plugin from function', t => {
const plugin = normalize('', () => {}, t.context.logger);
t.is(typeof plugin, 'function');
});
test('Normalize and load plugin that retuns multiple functions', t => {
const plugin = normalize('verifyConditions', './test/fixtures/multi-plugin', t.context.logger);
t.is(typeof plugin, 'function');
t.true(t.context.log.calledWith(match.string, 'verifyConditions', './test/fixtures/multi-plugin'));
});
test('Wrap plugin in a function that validate the output of the plugin', async t => {
const pluginFunction = stub().resolves(1);
const plugin = normalize('', pluginFunction, t.context.logger, {
validator: output => output === 1,
message: 'The output must be 1.',
});
await t.notThrows(plugin());
pluginFunction.resolves(2);
const error = await t.throws(plugin(), Error);
t.is(error.message, 'The output must be 1. Received: 2');
});
test('Plugin is called with "pluginConfig" (omitting "path") and input', async t => {
const pluginFunction = stub().resolves();
const conf = {path: pluginFunction, conf: 'confValue'};
const plugin = normalize('', conf, t.context.logger);
await plugin('param');
t.true(pluginFunction.calledWith({conf: 'confValue'}, 'param'));
});
test('Prevent plugins to modify "pluginConfig"', async t => {
const pluginFunction = stub().callsFake(pluginConfig => {
pluginConfig.conf.subConf = 'otherConf';
});
const conf = {path: pluginFunction, conf: {subConf: 'originalConf'}};
const plugin = normalize('', conf, t.context.logger);
await plugin();
t.is(conf.conf.subConf, 'originalConf');
});
test('Prevent plugins to modify its input', async t => {
const pluginFunction = stub().callsFake((pluginConfig, options) => {
options.param.subParam = 'otherParam';
});
const input = {param: {subParam: 'originalSubParam'}};
const plugin = normalize('', pluginFunction, t.context.logger);
await plugin(input);
t.is(input.param.subParam, 'originalSubParam');
});
test('Return noop if the plugin is not defined', t => {
const plugin = normalize();
t.is(plugin, noop);
});
test('Always pass a defined "pluginConfig" for plugin defined with string', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', './test/fixtures/plugin-result-config', t.context.logger);
const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Always pass a defined "pluginConfig" for plugin defined with path', async t => {
// Call the normalize function with the path of a plugin that returns its config
const plugin = normalize('', {path: './test/fixtures/plugin-result-config'}, t.context.logger);
const pluginResult = await plugin();
t.deepEqual(pluginResult.pluginConfig, {});
});
test('Throws an error if the plugin return an object without the expected plugin function', t => {
const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger), Error);
t.is(
error.message,
'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.'
);
});
+40
View File
@@ -0,0 +1,40 @@
import test from 'ava';
import {stub} from 'sinon';
import pipeline from '../../lib/plugins/pipeline';
test('Execute each function in series passing the same input', async t => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
const result = await pipeline([step1, step2, step3])(0);
t.deepEqual(result, [1, 2, 3]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.calledWith(0));
});
test('Execute each function in series passing a transformed input', async t => {
const step1 = stub().resolves(1);
const step2 = stub().resolves(2);
const step3 = stub().resolves(3);
const result = await pipeline([step1, step2, step3])(0, (prevResult, result) => prevResult + result);
t.deepEqual(result, [1, 2, 3]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(1));
t.true(step3.calledWith(3));
});
test('Stop execution and throw error is a step rejects', async t => {
const step1 = stub().resolves(1);
const step2 = stub().throws(new Error('test error'));
const step3 = stub().resolves(3);
const error = await t.throws(pipeline([step1, step2, step3])(0), Error);
t.is(error.message, 'test error');
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.notCalled);
});
+70
View File
@@ -0,0 +1,70 @@
import test from 'ava';
import {stub} from 'sinon';
import getPlugins from '../../lib/plugins';
test.beforeEach(t => {
// Stub the logger functions
t.context.log = stub();
t.context.logger = {log: t.context.log};
});
test('Export default plugins', t => {
const plugins = getPlugins({}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.publish, 'function');
});
test('Export plugins based on config', t => {
const plugins = getPlugins(
{
verifyConditions: ['./test/fixtures/plugin-noop', {path: './test/fixtures/plugin-noop'}],
getLastRelease: './test/fixtures/plugin-noop',
analyzeCommits: {path: './test/fixtures/plugin-noop'},
verifyRelease: () => {},
},
t.context.logger
);
// Verify the module returns a function for each plugin
t.is(typeof plugins.verifyConditions, 'function');
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.publish, 'function');
});
test('Use default when only options are passed for a single plugin', t => {
const plugins = getPlugins({getLastRelease: {}, analyzeCommits: {}}, t.context.logger);
// Verify the module returns a function for each plugin
t.is(typeof plugins.getLastRelease, 'function');
t.is(typeof plugins.analyzeCommits, 'function');
});
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger), Error);
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
);
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const error = t.throws(
() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, t.context.logger),
Error
);
t.is(
error.message,
'The "verifyConditions" plugin, if defined, must be a single or an array of plugins definition. A plugin definition is either a string or an object with a path property.'
);
});
-45
View File
@@ -1,45 +0,0 @@
import test from 'ava';
import SemanticReleaseError from '@semantic-release/error';
import verify from '../src/lib/verify-auth';
test('Verify npm and github auth', t => {
// Call the verify module with options and env
t.notThrows(() => verify({githubToken: 'sup'}, {NPM_TOKEN: 'yo'}));
});
test('Verify npm (old token and mail) and github auth', t => {
// Call the verify module with options and env
t.notThrows(() => verify({githubToken: 'sup'}, {NPM_OLD_TOKEN: 'yo', NPM_EMAIL: 'test@email.com'}));
});
test('Return error for missing github token', t => {
// Call the verify module with options and env
const error = t.throws(() => verify({}, {NPM_TOKEN: 'yo'}));
// Verify error code and type
t.is(error.code, 'ENOGHTOKEN');
t.true(error instanceof SemanticReleaseError);
});
test('Return error for missing npm token', t => {
// Call the verify module with options and env
const error = t.throws(() => verify({githubToken: 'sup'}, {}));
// Verify error code and type
t.is(error.code, 'ENONPMTOKEN');
t.true(error instanceof SemanticReleaseError);
});
test('Return error for missing old npm token', t => {
// Call the verify module with options and env
const error = t.throws(() => verify({githubToken: 'sup'}, {NPM_EMAIL: 'test@email.com'}));
// Verify error code and type
t.is(error.code, 'ENONPMTOKEN');
t.true(error instanceof SemanticReleaseError);
});
test('Return error for missing npm email', t => {
// Call the verify module with options and env
const error = t.throws(() => verify({githubToken: 'sup'}, {NPM_OLD_TOKEN: 'yo'}));
// Verify error code and type
t.is(error.code, 'ENONPMTOKEN');
t.true(error instanceof SemanticReleaseError);
});
-32
View File
@@ -1,32 +0,0 @@
import test from 'ava';
import SemanticReleaseError from '@semantic-release/error';
import verify from '../src/lib/verify-pkg';
test('Verify name and repository', t => {
// Call the verify module with package
t.notThrows(() => verify({name: 'package', repository: {url: 'http://github.com/whats/up.git'}}));
});
test('Return error for missing package name', t => {
// Call the verify module with package
const error = t.throws(() => verify({repository: {url: 'http://github.com/whats/up.git'}}));
// Verify error code and type
t.is(error.code, 'ENOPKGNAME');
t.true(error instanceof SemanticReleaseError);
});
test('Return error for missing repository', t => {
// Call the verify module with package
const error = t.throws(() => verify({name: 'package'}));
// Verify error code and type
t.is(error.code, 'ENOPKGREPO');
t.true(error instanceof SemanticReleaseError);
});
test('Return error for missing repository url', t => {
// Call the verify module with package
const error = t.throws(() => verify({name: 'package', repository: {}}));
// Verify error code and type
t.is(error.code, 'ENOPKGREPO');
t.true(error instanceof SemanticReleaseError);
});