Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d548edcf37 | ||
|
|
991a7b5f97 | ||
|
|
332608378a | ||
|
|
018835f56d | ||
|
|
c8748ad0e2 | ||
|
|
a4951b7c30 | ||
|
|
17a600672f | ||
|
|
470697c01d | ||
|
|
94b00e21c7 | ||
|
|
d0180c4348 | ||
|
|
186950acc0 |
@@ -0,0 +1 @@
|
||||
base: app0ZOxG0FnHmOiuU
|
||||
@@ -54,7 +54,7 @@ This removes the immediate connection between human emotions and version numbers
|
||||
“We fail to follow SemVer – and why it needn’t matter”
|
||||
</th>
|
||||
<th>
|
||||
“semantic-release Q&A with Kent C. Dodds”
|
||||
“semantic-release Q&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
|
||||
|
||||
@@ -172,9 +169,6 @@ These options are currently available:
|
||||
- `branch`: The branch on which releases should happen. Default: `'master'`
|
||||
- `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 that’s where `npm` itself is going to read it from.
|
||||
@@ -183,8 +177,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`:
|
||||
@@ -215,7 +207,6 @@ module.exports = function (pluginConfig, config, callback) {}
|
||||
- `pluginConfig`: If the user of your plugin specifies additional plugin config in the `package.json` (see the `verifyConditions` example above) then it’s this object.
|
||||
- `config`: A config object containing a lot of information to act upon.
|
||||
- `env`: All environment variables
|
||||
- `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.
|
||||
@@ -230,14 +221,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 it’s 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 it’s 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 +241,17 @@ Passing an array of plugins will run them in series.
|
||||
|
||||
This plugin is responsible for determining a package’s 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
|
||||
|
||||
|
||||
@@ -23,6 +23,6 @@ npx is bundled with npm >= 5.4, or available via npm. More info: npm.im/npx`
|
||||
}
|
||||
|
||||
// node 8+ from this point on
|
||||
require('../src/cli')().catch(() => {
|
||||
require('../cli')().catch(() => {
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
+4
-6
@@ -10,22 +10,20 @@ 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(
|
||||
'--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 +41,7 @@ module.exports = async () => {
|
||||
program.outputHelp();
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
await require('./index')(program);
|
||||
await require('./index')(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
|
||||
@@ -0,0 +1,66 @@
|
||||
const marked = require('marked');
|
||||
const TerminalRenderer = require('marked-terminal');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const {gitHead: getGitHead} = require('./lib/git');
|
||||
const getConfig = require('./lib/get-config');
|
||||
const getNextVersion = require('./lib/get-next-version');
|
||||
const getCommits = require('./lib/get-commits');
|
||||
const logger = require('./lib/logger');
|
||||
|
||||
module.exports = async opts => {
|
||||
const config = await getConfig(opts, logger);
|
||||
const {plugins, env, options, pkg} = config;
|
||||
|
||||
logger.log('Run automated release for branch %s', options.branch);
|
||||
|
||||
if (!options.dryRun) {
|
||||
logger.log('Call plugin %s', 'verify-conditions');
|
||||
await plugins.verifyConditions({env, options, pkg, logger});
|
||||
}
|
||||
|
||||
logger.log('Call plugin %s', 'get-last-release');
|
||||
const {commits, lastRelease} = await getCommits(
|
||||
await plugins.getLastRelease({env, options, pkg, logger}),
|
||||
options.branch,
|
||||
logger
|
||||
);
|
||||
|
||||
logger.log('Call plugin %s', 'analyze-commits');
|
||||
const type = await plugins.analyzeCommits({env, options, pkg, 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({env, options, pkg, logger, lastRelease, commits, nextRelease});
|
||||
|
||||
const generateNotesParam = {env, options, pkg, 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, pkg, 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, pkg, logger, lastRelease, commits, nextRelease};
|
||||
});
|
||||
logger.log('Published release: %s', nextRelease.version);
|
||||
}
|
||||
};
|
||||
@@ -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,7 +14,7 @@ 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.
|
||||
@@ -22,7 +22,7 @@ const logger = require('./logger');
|
||||
|
||||
/**
|
||||
* 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 +30,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 +39,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 +62,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) {
|
||||
@@ -94,18 +83,24 @@ function noGitHeadMessage(branch, version) {
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by releasing from outside the repository directory or with innaccessible git metadata.
|
||||
|
||||
You can recover from this error by creating a tag for the version "${version}" on the commit corresponding to this release:
|
||||
You can recover from this error by creating a tag for the version "${
|
||||
version
|
||||
}" on the commit corresponding to this release:
|
||||
$ git tag -f v${version} <commit sha1 corresponding to last release>
|
||||
$ git push -f --tags origin ${branch}
|
||||
`;
|
||||
}
|
||||
|
||||
function notInHistoryMessage(gitHead, branch, version) {
|
||||
return `The commit the last release of this package was derived from is not in the direct history of the "${branch}" branch.
|
||||
return `The commit the last release of this package was derived from is not in the direct history of the "${
|
||||
branch
|
||||
}" branch.
|
||||
This means semantic-release can not extract the commits between now and then.
|
||||
This is usually caused by force pushing, releasing from an unrelated branch, or using an already existing package name.
|
||||
|
||||
You can recover from this error by restoring the commit "${gitHead}" or by creating a tag for the version "${version}" on the commit corresponding to this release:
|
||||
You can recover from this error by restoring the commit "${gitHead}" or by creating a tag for the version "${
|
||||
version
|
||||
}" on the commit corresponding to this release:
|
||||
$ git tag -f v${version || '<version>'} <commit sha1 corresponding to last release>
|
||||
$ git push -f --tags origin ${branch}
|
||||
`;
|
||||
@@ -0,0 +1,19 @@
|
||||
const {readJson} = require('fs-extra');
|
||||
const {defaults} = require('lodash');
|
||||
const normalizeData = require('normalize-package-data');
|
||||
const debug = require('debug')('semantic-release:config');
|
||||
const plugins = require('./plugins');
|
||||
|
||||
module.exports = async (opts, logger) => {
|
||||
const pkg = await readJson('./package.json');
|
||||
normalizeData(pkg);
|
||||
const options = defaults(opts, pkg.release, {branch: 'master'});
|
||||
debug('branch: %O', options.branch);
|
||||
debug('analyzeCommits: %O', options.analyzeCommits);
|
||||
debug('generateNotes: %O', options.generateNotes);
|
||||
debug('verifyConditions: %O', options.verifyConditions);
|
||||
debug('verifyRelease: %O', options.verifyRelease);
|
||||
debug('publish: %O', options.publish);
|
||||
|
||||
return {env: process.env, pkg, options, plugins: await plugins(options, logger), logger};
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
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) {
|
||||
version = '1.0.0';
|
||||
@@ -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)};
|
||||
}
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead};
|
||||
@@ -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)
|
||||
);
|
||||
},
|
||||
@@ -0,0 +1,78 @@
|
||||
const {isString, isObject, isFunction, isArray} = require('lodash');
|
||||
const semver = require('semver');
|
||||
const conditionTravis = require('@semantic-release/condition-travis');
|
||||
const commitAnalyzer = require('@semantic-release/commit-analyzer');
|
||||
const releaseNotesGenerator = require('@semantic-release/release-notes-generator');
|
||||
const npm = require('@semantic-release/npm');
|
||||
const github = require('@semantic-release/github');
|
||||
|
||||
const RELEASE_TYPE = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'];
|
||||
|
||||
module.exports = {
|
||||
verifyConditions: {
|
||||
default: [npm.verifyConditions, github.verifyConditions, conditionTravis],
|
||||
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: npm.getLastRelease,
|
||||
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: commitAnalyzer,
|
||||
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 must be either undefined or 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: releaseNotesGenerator,
|
||||
config: {
|
||||
validator: conf => !conf || validatePluginConfig(conf),
|
||||
message:
|
||||
'The "generateNotes" plugin, if defined, must be a single plugin definition. A plugin definition is either a string or an object with a path property.',
|
||||
},
|
||||
output: {
|
||||
validator: output => isString(output),
|
||||
message: 'The "generateNotes" plugin output must be a string.',
|
||||
},
|
||||
},
|
||||
publish: {
|
||||
default: [npm.publish, github.publish],
|
||||
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.',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const validatePluginConfig = conf => isString(conf) || isString(conf.path) || isFunction(conf);
|
||||
@@ -0,0 +1,24 @@
|
||||
const {isArray} = 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 (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;
|
||||
}, {});
|
||||
@@ -0,0 +1,34 @@
|
||||
const {promisify, 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', path);
|
||||
}
|
||||
const plugin = isFunction(path) ? path : importFrom.silent(__dirname, path) || importFrom(process.cwd(), path);
|
||||
|
||||
let func;
|
||||
if (isFunction(plugin)) {
|
||||
func = promisify(plugin.bind(null, cloneDeep(config)));
|
||||
} else if (isObject(plugin) && plugin[pluginType] && isFunction(plugin[pluginType])) {
|
||||
func = promisify(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;
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+16
-15
@@ -15,27 +15,25 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^3.0.1",
|
||||
"@semantic-release/commit-analyzer": "^4.0.0",
|
||||
"@semantic-release/condition-travis": "^6.0.0",
|
||||
"@semantic-release/error": "^2.1.0",
|
||||
"@semantic-release/last-release-npm": "^2.0.0",
|
||||
"@semantic-release/github": "^1.0.0",
|
||||
"@semantic-release/npm": "^1.0.0",
|
||||
"@semantic-release/release-notes-generator": "^5.0.0",
|
||||
"chalk": "^2.3.0",
|
||||
"commander": "^2.11.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",
|
||||
"semver": "^5.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -52,13 +50,12 @@
|
||||
"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",
|
||||
"mockserver-client": "^2.0.0",
|
||||
"nock": "^9.0.2",
|
||||
"npm-registry-couchapp": "^2.6.12",
|
||||
"nyc": "^11.2.1",
|
||||
"p-map-series": "^1.0.0",
|
||||
"prettier": "^1.7.0",
|
||||
"prettier": "~1.8.0",
|
||||
"proxyquire": "^1.8.0",
|
||||
"rimraf": "^2.5.0",
|
||||
"sinon": "^4.0.0",
|
||||
@@ -82,7 +79,9 @@
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"src"
|
||||
"lib",
|
||||
"index.js",
|
||||
"cli.js"
|
||||
],
|
||||
"homepage": "https://github.com/semantic-release/semantic-release#readme",
|
||||
"keywords": [
|
||||
@@ -100,7 +99,9 @@
|
||||
"main": "index.js",
|
||||
"nyc": {
|
||||
"include": [
|
||||
"src/**/*.js"
|
||||
"lib/**/*.js",
|
||||
"index.js",
|
||||
"cli.js"
|
||||
],
|
||||
"reporter": [
|
||||
"json",
|
||||
@@ -129,7 +130,7 @@
|
||||
"clean": "rimraf coverage && rimraf .nyc_output",
|
||||
"cm": "git-cz",
|
||||
"codecov": "codecov -f coverage/coverage-final.json",
|
||||
"lint": "eslint .",
|
||||
"lint": "eslint index.js cli.js lib test",
|
||||
"pretest": "npm run clean && npm run lint",
|
||||
"semantic-release": "./bin/semantic-release.js",
|
||||
"test": "nyc ava -v"
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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};
|
||||
};
|
||||
@@ -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/';
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
verifyConditions(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
|
||||
getLastRelease(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
analyzeCommits(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
verifyRelease(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
generateNotes(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
publish(config, options, cb) {
|
||||
cb();
|
||||
},
|
||||
};
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
module.exports = function(config, options, cb) {
|
||||
cb(new Error('b'));
|
||||
};
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
module.exports = function(config, options, cb) {
|
||||
cb(null, 'a');
|
||||
};
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
module.exports = function(config, options, cb) {
|
||||
cb(null, 'b');
|
||||
};
|
||||
+120
-21
@@ -1,5 +1,4 @@
|
||||
import test from 'ava';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
gitLog,
|
||||
gitDetachedHead,
|
||||
} from './helpers/git-utils';
|
||||
import getCommits from '../lib/get-commits';
|
||||
|
||||
test.beforeEach(t => {
|
||||
// Save the current working diretory
|
||||
@@ -19,9 +19,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 +34,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 +95,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 +123,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 +153,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 +180,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 +214,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 +241,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 +273,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 +303,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 +344,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 +372,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 +380,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 +388,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,7 +405,7 @@ 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');
|
||||
@@ -329,7 +424,7 @@ 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');
|
||||
@@ -352,7 +447,9 @@ 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');
|
||||
@@ -379,7 +476,9 @@ 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');
|
||||
@@ -406,7 +505,7 @@ 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);
|
||||
|
||||
+3
-52
@@ -1,6 +1,5 @@
|
||||
import url from 'url';
|
||||
import test from 'ava';
|
||||
import {writeJson, writeFile} from 'fs-extra';
|
||||
import {writeJson} from 'fs-extra';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import normalizeData from 'normalize-package-data';
|
||||
@@ -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 => {
|
||||
@@ -28,7 +27,6 @@ test.serial('Default values', async t => {
|
||||
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();
|
||||
|
||||
@@ -37,12 +35,6 @@ test.serial('Default values', async t => {
|
||||
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/')));
|
||||
});
|
||||
|
||||
test.serial('Read package.json configuration', async t => {
|
||||
@@ -61,8 +53,6 @@ test.serial('Read package.json configuration', async t => {
|
||||
await gitRepo();
|
||||
// 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();
|
||||
|
||||
@@ -77,11 +67,9 @@ test.serial('Read package.json configuration', async t => {
|
||||
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);
|
||||
});
|
||||
|
||||
test.serial('Priority cli parameters over package.json configuration', async t => {
|
||||
test.serial('Prioritise cli parameters over package.json configuration', async t => {
|
||||
const release = {
|
||||
analyzeCommits: 'analyzeCommits',
|
||||
generateNotes: 'generateNotes',
|
||||
@@ -104,8 +92,6 @@ test.serial('Priority cli parameters over package.json configuration', async t =
|
||||
await gitRepo();
|
||||
// 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);
|
||||
|
||||
@@ -117,38 +103,3 @@ test.serial('Priority cli parameters over package.json configuration', async t =
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
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'}));
|
||||
const error = t.throws(() => getNextVersion('invalid', {version: '1.0.0'}, t.context.logger));
|
||||
t.is(error.code, 'EINVALIDTYPE');
|
||||
t.true(error instanceof SemanticReleaseError);
|
||||
});
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import test from 'ava';
|
||||
import {gitRepo, gitCommits, gitCheckout, gitTagVersion, gitShallowClone, gitLog} from './helpers/git-utils';
|
||||
import {gitTagHead, gitCommitTag, isCommitInHistory, unshallow, gitHead} from '../lib/git';
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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
|
||||
let 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
|
||||
let 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'));
|
||||
});
|
||||
@@ -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());
|
||||
});
|
||||
+10
-15
@@ -1,11 +1,10 @@
|
||||
import {mkdir} from 'fs-extra';
|
||||
import tempy from 'tempy';
|
||||
import execa from 'execa';
|
||||
import pMapSeries from 'p-map-series';
|
||||
|
||||
/**
|
||||
* Commit message informations.
|
||||
*
|
||||
*
|
||||
* @typedef {Object} Commit
|
||||
* @property {string} branch The commit branch.
|
||||
* @property {string} hash The commit hash.
|
||||
@@ -13,19 +12,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,7 +29,7 @@ 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) {
|
||||
@@ -49,7 +44,7 @@ export async function gitCommits(messages) {
|
||||
* 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) {
|
||||
@@ -105,7 +100,7 @@ export async function gitLog() {
|
||||
/**
|
||||
* 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.
|
||||
@@ -120,7 +115,7 @@ export async function gitShallowClone(origin, branch = 'master', depth = 1) {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
||||
@@ -2,7 +2,7 @@ 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.
|
||||
|
||||
@@ -47,19 +47,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 +72,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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+163
-230
@@ -1,278 +1,211 @@
|
||||
import {callbackify} from 'util';
|
||||
import test from 'ava';
|
||||
import {writeJson} from 'fs-extra';
|
||||
import proxyquire from 'proxyquire';
|
||||
import {stub} from 'sinon';
|
||||
import SemanticReleaseError from '@semantic-release/error';
|
||||
|
||||
const consoleLog = stub(console, 'log');
|
||||
import normalizeData from 'normalize-package-data';
|
||||
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 name = 'package-name';
|
||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
|
||||
// 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',
|
||||
verifyConditions: [callbackify(verifyConditions1), callbackify(verifyConditions2)],
|
||||
getLastRelease: callbackify(getLastRelease),
|
||||
analyzeCommits: callbackify(analyzeCommits),
|
||||
verifyRelease: callbackify(verifyRelease),
|
||||
generateNotes: callbackify(generateNotes),
|
||||
publish: callbackify(publish),
|
||||
};
|
||||
const pkg = {name, version: '0.0.0-dev'};
|
||||
normalizeData(pkg);
|
||||
|
||||
// Call the index module
|
||||
await semanticRelease(cliOptions);
|
||||
await writeJson('./package.json', pkg);
|
||||
|
||||
await t.context.semanticRelease(options);
|
||||
|
||||
t.true(verifyConditions1.calledOnce);
|
||||
t.deepEqual(verifyConditions1.firstCall.args[1], {env: process.env, options, pkg, logger: t.context.logger});
|
||||
t.true(verifyConditions2.calledOnce);
|
||||
t.deepEqual(verifyConditions2.firstCall.args[1], {env: process.env, options, pkg, 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], {env: process.env, options, pkg, 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].env, process.env);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].options, options);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].pkg, pkg);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].logger, t.context.logger);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].lastRelease, lastRelease);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
|
||||
t.deepEqual(analyzeCommits.firstCall.args[1].commits[0].message, commits[0].message);
|
||||
|
||||
t.true(verifyRelease.calledOnce);
|
||||
t.true(
|
||||
verifyRelease.calledWithExactly({
|
||||
env,
|
||||
options,
|
||||
pkg,
|
||||
npm,
|
||||
logger,
|
||||
lastRelease: commitsLastRelease,
|
||||
commits,
|
||||
nextRelease,
|
||||
})
|
||||
);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].env, process.env);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].options, options);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].pkg, pkg);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].logger, t.context.logger);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].lastRelease, lastRelease);
|
||||
t.deepEqual(verifyRelease.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
|
||||
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].env, process.env);
|
||||
t.deepEqual(generateNotes.firstCall.args[1].options, options);
|
||||
t.deepEqual(generateNotes.firstCall.args[1].pkg, pkg);
|
||||
t.deepEqual(generateNotes.firstCall.args[1].logger, t.context.logger);
|
||||
t.deepEqual(generateNotes.firstCall.args[1].lastRelease, lastRelease);
|
||||
t.deepEqual(generateNotes.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
|
||||
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].pkg, pkg);
|
||||
t.deepEqual(publish.firstCall.args[1].logger, t.context.logger);
|
||||
t.deepEqual(publish.firstCall.args[1].lastRelease, lastRelease);
|
||||
t.deepEqual(publish.firstCall.args[1].commits[0].hash.substring(0, 7), commits[0].hash);
|
||||
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',
|
||||
verifyConditions: callbackify(stub().resolves()),
|
||||
getLastRelease: callbackify(stub().resolves(lastRelease)),
|
||||
analyzeCommits: callbackify(stub().resolves(nextRelease.type)),
|
||||
verifyRelease: callbackify(stub().resolves()),
|
||||
generateNotes: callbackify(generateNotes),
|
||||
publish: [callbackify(publish1), callbackify(publish2)],
|
||||
};
|
||||
|
||||
await writeJson('./package.json', {});
|
||||
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 name = 'package-name';
|
||||
const lastRelease = {version: '1.0.0', gitHead: commits[commits.length - 1].hash, gitTag: 'v1.0.0'};
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
|
||||
// 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',
|
||||
verifyConditions: callbackify(verifyConditions),
|
||||
getLastRelease: callbackify(getLastRelease),
|
||||
analyzeCommits: callbackify(analyzeCommits),
|
||||
verifyRelease: callbackify(verifyRelease),
|
||||
generateNotes: callbackify(generateNotes),
|
||||
publish: callbackify(publish),
|
||||
};
|
||||
const pkg = {name, version: '0.0.0-dev'};
|
||||
normalizeData(pkg);
|
||||
|
||||
// Call the index module
|
||||
await semanticRelease(cliOptions);
|
||||
await writeJson('./package.json', pkg);
|
||||
|
||||
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,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
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};
|
||||
|
||||
// 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 verifyConditions = stub().resolves();
|
||||
const getLastRelease = stub().resolves(lastRelease);
|
||||
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 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,
|
||||
});
|
||||
|
||||
// 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.true(publish.notCalled);
|
||||
});
|
||||
|
||||
+169
-136
@@ -5,21 +5,23 @@ 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 semanticRelease from '..';
|
||||
|
||||
// Environment variables used with cli
|
||||
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',
|
||||
};
|
||||
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 => {
|
||||
// Start Mock Server
|
||||
await mockServer.start();
|
||||
// Start the local NPM registry
|
||||
await registry.start();
|
||||
@@ -33,6 +35,8 @@ test.beforeEach(async t => {
|
||||
|
||||
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 => {
|
||||
@@ -43,6 +47,8 @@ test.afterEach.always(async t => {
|
||||
|
||||
t.context.log.restore();
|
||||
t.context.error.restore();
|
||||
t.context.stdout.restore();
|
||||
t.context.stderr.restore();
|
||||
});
|
||||
|
||||
test.after.always(async t => {
|
||||
@@ -52,8 +58,8 @@ test.after.always(async t => {
|
||||
});
|
||||
|
||||
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 +67,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: registry.uri},
|
||||
});
|
||||
// Create a npm-shrinkwrap.json file
|
||||
await execa('npm', ['shrinkwrap'], {env});
|
||||
|
||||
/** 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');
|
||||
@@ -78,13 +90,18 @@ test.serial('Release patch, minor and major versions', async t => {
|
||||
|
||||
/** 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 +114,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
|
||||
@@ -112,18 +129,24 @@ test.serial('Release patch, minor and major versions', async t => {
|
||||
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 **/
|
||||
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 +159,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 +168,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})).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 **/
|
||||
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 +204,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 +213,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})).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 **/
|
||||
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 +249,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 +258,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})).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 +280,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: registry.uri},
|
||||
});
|
||||
|
||||
/** 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,7 +312,7 @@ 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);
|
||||
@@ -278,6 +320,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
|
||||
let releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).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
|
||||
@@ -286,13 +329,18 @@ test.serial('Release versions from a packed git repository, using tags to determ
|
||||
|
||||
/** 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,7 +352,7 @@ 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);
|
||||
@@ -313,21 +361,21 @@ test.serial('Release versions from a packed git repository, using tags to determ
|
||||
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).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 +385,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 +395,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: registry.uri},
|
||||
});
|
||||
|
||||
/** 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,7 +425,7 @@ 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);
|
||||
@@ -384,6 +438,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
|
||||
t.is(releasedGitHead, head);
|
||||
t.is(releasedVersion, version);
|
||||
t.log(`+ released ${releasedVersion}`);
|
||||
await mockServer.verify(verifyMock);
|
||||
await mockServer.verify(createRefMock);
|
||||
await mockServer.verify(createReleaseMock);
|
||||
|
||||
@@ -397,7 +452,11 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
|
||||
const {hash} = await gitAmmendCommit('feat: Initial commit');
|
||||
|
||||
/** 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');
|
||||
@@ -408,7 +467,9 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
|
||||
t.regex(
|
||||
stderr,
|
||||
new RegExp(
|
||||
`You can recover from this error by restoring the commit "${head}" or by creating a tag for the version "${version}" on the commit corresponding to this release`
|
||||
`You can recover from this error by restoring the commit "${head}" or by creating a tag for the version "${
|
||||
version
|
||||
}" on the commit corresponding to this release`
|
||||
)
|
||||
);
|
||||
|
||||
@@ -418,13 +479,18 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
|
||||
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,7 +501,7 @@ 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);
|
||||
@@ -444,13 +510,14 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
|
||||
releasedVersion = (await execa('npm', ['show', packageName, 'version'], {env})).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,8 +525,7 @@ 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}`},
|
||||
});
|
||||
|
||||
/** Initial release **/
|
||||
@@ -478,9 +544,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,36 +553,21 @@ 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: registry.uri},
|
||||
});
|
||||
|
||||
/** 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}`}}
|
||||
);
|
||||
|
||||
t.log('Commit a feature');
|
||||
await gitCommits(['feat: Initial commit']);
|
||||
t.log('$ semantic-release');
|
||||
let {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
|
||||
@@ -530,14 +580,11 @@ test.serial('Pass options via CLI arguments', async t => {
|
||||
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 +593,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: registry.uri},
|
||||
});
|
||||
|
||||
/** 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 ${githubToken}`]}]},
|
||||
{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 ${githubToken}`]}]},
|
||||
{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 ${githubToken}`]}],
|
||||
@@ -571,10 +623,14 @@ 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);
|
||||
@@ -587,37 +643,14 @@ test.serial('Run via JS API', async t => {
|
||||
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,8 +658,8 @@ 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 **/
|
||||
@@ -644,8 +677,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,8 +686,8 @@ 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 **/
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
@@ -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, {});
|
||||
});
|
||||
@@ -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 must be a string', t => {
|
||||
t.false(definitions.generateNotes.output.validator());
|
||||
t.false(definitions.generateNotes.output.validator(null));
|
||||
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('string'));
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
import {callbackify} from 'util';
|
||||
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('', './test/fixtures/plugin-noop', t.context.logger);
|
||||
|
||||
t.is(typeof plugin, 'function');
|
||||
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-noop'));
|
||||
});
|
||||
|
||||
test('Normalize and load plugin from object', t => {
|
||||
const plugin = normalize('', {path: './test/fixtures/plugin-noop'}, t.context.logger);
|
||||
|
||||
t.is(typeof plugin, 'function');
|
||||
t.true(t.context.log.calledWith(match.string, './test/fixtures/plugin-noop'));
|
||||
});
|
||||
|
||||
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, './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('', callbackify(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());
|
||||
t.is(error.message, 'The output must be 1. Received: 2');
|
||||
});
|
||||
|
||||
test('Plugin is called with "pluginConfig" (omitting "path") and input', async t => {
|
||||
const pluginFunction = stub().resolves();
|
||||
const conf = {path: callbackify(pluginFunction), conf: 'confValue'};
|
||||
const 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, options, cb) => {
|
||||
pluginConfig.conf.subConf = 'otherConf';
|
||||
cb();
|
||||
});
|
||||
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, cb) => {
|
||||
options.param.subParam = 'otherParam';
|
||||
cb();
|
||||
});
|
||||
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', async 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', async t => {
|
||||
const error = t.throws(() => normalize('inexistantPlugin', './test/fixtures/multi-plugin', t.context.logger));
|
||||
|
||||
t.is(
|
||||
error.message,
|
||||
'The inexistantPlugin plugin must be a function, or an object with a function in the property inexistantPlugin.'
|
||||
);
|
||||
});
|
||||
@@ -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));
|
||||
t.is(error.message, 'test error');
|
||||
t.true(step1.calledWith(0));
|
||||
t.true(step2.calledWith(0));
|
||||
t.true(step3.notCalled);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
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 => {
|
||||
// Call the plugin module
|
||||
const plugins = getPlugins({}, t.context.logger);
|
||||
// Verify the module returns a function for each plugin
|
||||
t.is(typeof plugins.verifyConditions, 'function');
|
||||
t.is(typeof plugins.getLastRelease, 'function');
|
||||
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 => {
|
||||
// Call the plugin module
|
||||
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('Throw an error if plugin configuration is invalid', t => {
|
||||
const error = t.throws(() => getPlugins({verifyConditions: {}}, t.context.logger));
|
||||
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.'
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user