Compare commits

...
3 Commits
Author SHA1 Message Date
Pierre Vanduynslager e145134a62 ci: fix travis-deploy-once install cmd 2017-12-31 00:08:21 -05:00
Pierre Vanduynslager 8d575654c2 feat: make semantic-release CI agnostic
- Remove `@semantic-release/condition-travis` from the default plugins
- Verify the current branch in the core
- Verify the build is not triggered by a PR in the core
- Run in dry-run mode if not triggered on CI
- Dry-run mode runs the `verifyConditions` plugins, allowing to detect configuration error locally
- Return without error when no version has to be released due to no changes
- Return without error if the build is triggered from a PR
- Return without error if the current branch is not the configured branch
- CLI return with exit code 1 if there is a `semanticReleaseError`, allowing to fail builds in case of config error, missing token etc...

BREAKING CHANGE: `semantic-release` doesn't make sure it runs only on one Travis job anymore.
The CI configuration has to be done such that `semantic-release`
- runs only once per build
- runs only after all tests are successful on every jobs of the build
- runs on Node >=8

This can easily be done with [travis-deploy-once](https://github.com/semantic-release/travis-deploy-once).

Migration Guide

Modify your `.travis.yml` to use `travis-deploy-once`.
Replace:
```yaml
after_success:
  - npm run semantic-release
```
by:
Replace
```yaml
after_success:
  - npm install -g travis-deploy-once@4
  - travis-deploy-once "npm run semantic-release"
```
2017-12-30 23:15:25 -05:00
Pierre Vanduynslager 996305d69c feat: move npm workaround for missing gitHead to the npm plugin 2017-12-30 21:17:04 -05:00
13 changed files with 266 additions and 188 deletions
+2 -1
View File
@@ -25,4 +25,5 @@ script:
after_success:
- npm run codecov
- npm run semantic-release
- npm install -g travis-deploy-once@4
- travis-deploy-once "npm run semantic-release"
+1 -1
View File
@@ -170,7 +170,7 @@ semantic-release
These options are currently available:
- `branch`: The branch on which releases should happen. Default: `'master'`
- `repositoryUrl`: The git repository URL. Default: `repository` property in `package.json` or git origin url. Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)). If the [Github plugin](https://github.com/semantic-release/github) is used the URL must be a valid Github URL that include the `owner`, the `repository` name and the `host`. The Github shorthand URL is not supported.
- `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes
- `dry-run`: Dry-run mode, skip publishing, print next version and release notes
- `extends`: Array of module or files path containing a shareable configuration. Options defined via CLI or in the `release` property will take precedence over the one defined in a shareable configuration.
- `debug`: Output debugging information
+1 -3
View File
@@ -48,12 +48,10 @@ module.exports = async () => {
await require('.')(pickBy(program.opts(), value => !isUndefined(value)));
}
} 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
// Otherwise it's an unexpected error (configuration issue, code issue, plugin issue etc...) and the cli will return 1
process.exitCode = 1;
if (err.semanticRelease) {
logger.log(`%s ${err.message}`, err.code);
} else {
process.exitCode = 1;
logger.error('An error occurred while running semantic-release: %O', err);
}
}
+29 -7
View File
@@ -1,6 +1,6 @@
const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const SemanticReleaseError = require('@semantic-release/error');
const envCi = require('env-ci');
const getConfig = require('./lib/get-config');
const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
@@ -8,19 +8,39 @@ const logger = require('./lib/logger');
const {gitHead: getGitHead, isGitRepo} = require('./lib/git');
module.exports = async opts => {
const {isCi, branch, isPr} = envCi();
if (!isCi && !opts.dryRun) {
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
opts.dryRun = true;
}
if (isCi && isPr) {
logger.log('This run was triggered by a pull request and therefore a new version won’t be published.');
return;
}
if (!await isGitRepo()) {
throw new SemanticReleaseError('Semantic-release must run from a git repository', 'ENOGITREPO');
logger.error('Semantic-release must run from a git repository.');
return;
}
const config = await getConfig(opts, logger);
const {plugins, options} = config;
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version won’t be published.`
);
return;
}
logger.log('Run automated release from branch %s', options.branch);
if (!options.dryRun) {
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger});
}
logger.log('Call plugin %s', 'verify-conditions');
await plugins.verifyConditions({options, logger});
logger.log('Call plugin %s', 'get-last-release');
const {commits, lastRelease} = await getCommits(
@@ -32,7 +52,8 @@ module.exports = async opts => {
logger.log('Call plugin %s', 'analyze-commits');
const type = await plugins.analyzeCommits({options, logger, lastRelease, commits});
if (!type) {
throw new SemanticReleaseError('There are no relevant changes, so no new version is released.', 'ENOCHANGE');
logger.log('There are no relevant changes, so no new version is released.');
return;
}
const version = getNextVersion(type, lastRelease, logger);
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
@@ -67,4 +88,5 @@ module.exports = async opts => {
});
logger.log('Published release: %s', nextRelease.version);
}
return true;
};
+22 -27
View File
@@ -1,8 +1,8 @@
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 SemanticReleaseError = require('@semantic-release/error');
const {unshallow, gitCommitTag, gitTagHead, isCommitInHistory} = require('./git');
/**
* Commit message.
@@ -44,22 +44,28 @@ const getVersionHead = require('./get-version-head');
* @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, logger) => {
let gitTag;
if (gitHead || version) {
try {
({gitHead, gitTag} = await getVersionHead(gitHead, version, branch));
} catch (err) {
if (err.code === 'ENOTINHISTORY') {
logger.error(notInHistoryMessage(err.gitHead, branch, version));
} else {
logger.error(noGitHeadMessage(branch, version));
}
throw err;
if (gitHead) {
// If gitHead doesn't exists in release branch
if (!await isCommitInHistory(gitHead)) {
// Unshallow the repository
await unshallow();
}
logger.log('Retrieving commits since %s, corresponding to version %s', gitHead, version);
// If gitHead still doesn't exists in release branch
if (!await isCommitInHistory(gitHead)) {
// Try to find the commit corresponding to the version, using got tags
const tagHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
// If tagHead doesn't exists in release branch
if (!tagHead || !await isCommitInHistory(tagHead)) {
// Then the commit corresponding to the version cannot be found in the bracnh hsitory
logger.error(notInHistoryMessage(gitHead, branch, version));
throw new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
}
gitHead = tagHead;
}
debug('Use gitHead: %s', gitHead);
} 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
@@ -76,20 +82,9 @@ module.exports = async ({version, gitHead} = {}, branch, logger) => {
);
logger.log('Found %s commits since last release', commits.length);
debug('Parsed commits: %o', commits);
return {commits, lastRelease: {version, gitHead, gitTag}};
return {commits, lastRelease: {version, gitHead, gitTag: await gitCommitTag(gitHead)}};
};
function noGitHeadMessage(branch, version) {
return `The commit the last release of this package was derived from cannot be determined from the release metadata nor from the repository tags.
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:
$ 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.
This means semantic-release can not extract the commits between now and then.
-52
View File
@@ -1,52 +0,0 @@
const debug = require('debug')('semantic-release:get-version-head');
const SemanticReleaseError = require('@semantic-release/error');
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<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`.
*/
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, gitTag: await gitCommitTag(gitHead)};
}
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, gitTag: await gitCommitTag(gitHead)};
}
let tagHead;
if (version) {
// If a version is defined search a corresponding tag
tagHead = (await gitTagHead(`v${version}`)) || (await gitTagHead(version));
// Check if tagHead is found and exists in release branch again
if (tagHead && (await isCommitInHistory(tagHead))) {
debug('Use tagHead: %s', tagHead);
return {gitHead: tagHead, gitTag: await gitCommitTag(tagHead)};
}
}
// Either gitHead is defined or a tagHead has been found but none is in the branch history
if (gitHead || tagHead) {
const error = new SemanticReleaseError('Commit not in history', 'ENOTINHISTORY');
error.gitHead = gitHead || tagHead;
throw error;
}
// There is no gitHead in the last release and there is no tags correponsing to the last release version
throw new SemanticReleaseError('There is no commit associated with last release', 'ENOGITHEAD');
};
+2 -2
View File
@@ -25,7 +25,7 @@ async function gitTagHead(tagName) {
*
* @param {string} gitHead The commit sha for which to retrieve the associated tag.
*
* @return {string} The tag associatedwith the sha in parameter or `null`.
* @return {string} The tag associatedwith the sha in parameter or `undefined`.
*/
async function gitCommitTag(gitHead) {
try {
@@ -34,7 +34,7 @@ async function gitCommitTag(gitHead) {
return shell.stdout;
} catch (err) {
debug(err);
return null;
return undefined;
}
}
+3 -3
View File
@@ -6,7 +6,7 @@ const validatePluginConfig = conf => isString(conf) || isString(conf.path) || is
module.exports = {
verifyConditions: {
default: ['@semantic-release/npm', '@semantic-release/github', '@semantic-release/condition-travis'],
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
message:
@@ -24,9 +24,9 @@ module.exports = {
validator: output =>
!output ||
(isObject(output) && !output.version) ||
(isString(output.version) && Boolean(semver.valid(semver.clean(output.version)))),
(isString(output.version) && Boolean(semver.valid(semver.clean(output.version))) && Boolean(output.gitHead)),
message:
'The "getLastRelease" plugin output if defined, must be an object with an optionnal valid semver version in the "version" property.',
'The "getLastRelease" plugin output if defined, must be an object with a valid semver version in the "version" property and the corresponding git reference in "gitHead" property.',
},
},
analyzeCommits: {
+1 -1
View File
@@ -16,7 +16,6 @@
},
"dependencies": {
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/condition-travis": "^7.0.0",
"@semantic-release/error": "^2.1.0",
"@semantic-release/github": "^3.0.0",
"@semantic-release/npm": "^2.0.0",
@@ -25,6 +24,7 @@
"commander": "^2.11.0",
"cosmiconfig": "^3.1.0",
"debug": "^3.1.0",
"env-ci": "^1.0.0",
"execa": "^0.8.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
+7 -59
View File
@@ -180,7 +180,7 @@ 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 getCommits({version: '1.0.0'}, 'master', t.context.logger);
const result = await getCommits({version: '1.0.0', gitHead: 'missing_ref'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
@@ -214,7 +214,7 @@ 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 getCommits({version: '1.0.0'}, 'master', t.context.logger);
const result = await getCommits({version: '1.0.0', gitHead: 'missing_ref'}, '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);
@@ -241,7 +241,7 @@ 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 getCommits({version: '1.0.0'}, 'master', t.context.logger);
const result = await getCommits({version: '1.0.0', gitHead: 'missing_ref'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
@@ -261,40 +261,7 @@ test.serial('Get all commits since gitHead (from tag formatted like v<version>)
t.is(result.lastRelease.gitTag, 'v1.0.0');
t.is(result.lastRelease.version, '1.0.0');
});
test.serial('Get commits when last release gitHead is missing but a tag match the version', 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', 'Third'])).concat(commits);
// Retrieve the commits with the commits module, since commit 'First' (associated with tag v1.0.0)
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');
});
test.serial('Get all commits since gitHead, when gitHead are mising from the shallow clone', async t => {
test.serial('Get all commits since gitHead, when gitHead is missing from the shallow clone', 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
@@ -328,7 +295,7 @@ test.serial('Get all commits since gitHead, when gitHead are mising from the sha
t.falsy(result.lastRelease.gitTag);
});
test.serial('Get all commits since gitHead from tag, when tags are mising from the shallow clone', async t => {
test.serial('Get all commits since gitHead from tag, when tags is missing from the shallow clone', 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
@@ -344,7 +311,7 @@ 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 getCommits({version: '1.0.0'}, 'master', t.context.logger);
const result = await getCommits({version: '1.0.0', gitHead: 'missing_ref'}, 'master', t.context.logger);
// Verify the commits created and retrieved by the module are identical
t.is(result.commits.length, 2);
@@ -398,25 +365,6 @@ test.serial('Return empty array if there is no commits', async t => {
t.falsy(result.lastRelease.version);
});
test.serial('Throws ENOGITHEAD error if the gitHead of the last release cannot be found', 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', 'Second']);
// Retrieve the commits with the commits module
const error = await t.throws(getCommits({version: '1.0.0'}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOGITHEAD');
t.is(error.name, 'SemanticReleaseError');
// Verify the log function has been called with a message explaining the error
t.regex(
t.context.error.args[0][0],
/The commit the last release of this package was derived from cannot be determined from the release metadata nor from the repository tags/
);
});
test.serial('Throws ENOTINHISTORY error if gitHead is not in history', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
@@ -505,7 +453,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(getCommits({version: '1.0.0'}, 'master', t.context.logger));
const error = await t.throws(getCommits({version: '1.0.0', gitHead: shaTag}, 'master', t.context.logger));
// Verify error code and type
t.is(error.code, 'ENOTINHISTORY');
t.is(error.name, 'SemanticReleaseError');
+177 -17
View File
@@ -19,7 +19,6 @@ test.beforeEach(t => {
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});
});
test.afterEach.always(() => {
@@ -61,7 +60,11 @@ test.serial('Plugins are called with expected values', async t => {
publish,
};
await t.context.semanticRelease(options);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
t.is(verifyConditions1.callCount, 1);
t.deepEqual(verifyConditions1.args[0][0], config);
@@ -140,7 +143,11 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
publish: [publish1, publish2],
};
await t.context.semanticRelease(options);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
@@ -154,7 +161,7 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
t.deepEqual(publish2.args[0][1].nextRelease, Object.assign({}, nextRelease, {notes}));
});
test.serial('Dry-run skips verifyConditions and publish', async t => {
test.serial('Dry-run skips 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
@@ -187,9 +194,62 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
publish,
};
await t.context.semanticRelease(options);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
t.is(verifyConditions.callCount, 0);
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
t.is(getLastRelease.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
t.is(generateNotes.callCount, 1);
t.is(publish.callCount, 0);
});
test.serial('Force a dry-run if not on a CI', 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 verifyConditions = stub().resolves();
const getLastRelease = stub().resolves(lastRelease);
const analyzeCommits = stub().resolves(nextRelease.type);
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const publish = stub().resolves();
const options = {
dryRun: false,
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions,
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: false, branch: 'master'}),
});
t.truthy(await semanticRelease(options));
t.is(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
t.is(getLastRelease.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
@@ -227,7 +287,11 @@ test.serial('Accept "undefined" values for the "getLastRelease" and "generateNot
publish,
};
await t.context.semanticRelease(options);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.truthy(await semanticRelease(options));
t.is(getLastRelease.callCount, 1);
@@ -245,29 +309,121 @@ test.serial('Accept "undefined" values for the "getLastRelease" and "generateNot
t.falsy(publish.args[0][1].nextRelease.notes);
});
test.serial('Throw SemanticReleaseError if not running from a git repository', async t => {
test.serial('Returns falsy value if not running from a git repository', async t => {
// Set the current working directory to a temp directory
process.chdir(tempy.directory());
const error = await t.throws(t.context.semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOGITREPO');
t.is(error.name, 'SemanticReleaseError');
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease());
t.is(t.context.error.args[0][0], 'Semantic-release must run from a git repository.');
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found', async t => {
test.serial('Returns falsy value if triggered by a PR', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const error = await t.throws(t.context.semanticRelease());
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: true}),
});
t.falsy(await semanticRelease({repositoryUrl: 'git@hostname.com:owner/module.git'}));
t.is(
t.context.log.args[0][0],
'This run was triggered by a pull request and therefore a new version won’t be published.'
);
});
test.serial('Returns falsy value if not running from the configured branch', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'other-branch', isPr: false}),
});
t.falsy(await semanticRelease(options));
t.is(
t.context.log.args[0][0],
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.'
);
});
test.serial('Returns falsy value if there is no relevant changes', 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']);
const verifyConditions = stub().resolves();
const getLastRelease = stub().resolves();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
repositoryUrl: 'git@hostname.com:owner/module.git',
verifyConditions: [verifyConditions],
getLastRelease,
analyzeCommits,
verifyRelease,
generateNotes,
publish,
};
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease(options));
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 0);
t.is(generateNotes.callCount, 0);
t.is(publish.callCount, 0);
t.is(t.context.log.args[6][0], 'There are no relevant changes, so no new version is released.');
});
test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot be found from repo config', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const error = await t.throws(semanticRelease());
// Verify error code and type
t.is(error.code, 'ENOREPOURL');
t.is(error.name, 'SemanticReleaseError');
});
test.serial('Throw an Error if returns an unexpected value', async t => {
test.serial('Throw an Error if plugin returns an unexpected value', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add commits to the master branch
@@ -287,7 +443,11 @@ test.serial('Throw an Error if returns an unexpected value', async t => {
getLastRelease,
};
const error = await t.throws(t.context.semanticRelease(options), Error);
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const error = await t.throws(semanticRelease(options), Error);
// Verify error message
t.regex(error.message, new RegExp(DEFINITIONS.getLastRelease.output.message));
+17 -12
View File
@@ -53,6 +53,12 @@ test.beforeEach(() => {
delete process.env.GITHUB_URL;
delete process.env.GH_PREFIX;
delete process.env.GITHUB_PREFIX;
process.env.TRAVIS = 'true';
process.env.CI = 'true';
process.env.TRAVIS_BRANCH = 'master';
process.env.TRAVIS_PULL_REQUEST = 'false';
// Delete all `npm_config` environment variable set by CI as they take precedence over the `.npmrc` because the process that runs the tests is started before the `.npmrc` is created
for (let i = 0, keys = Object.keys(process.env); i < keys.length; i++) {
if (keys[i].startsWith('npm_config')) {
@@ -86,7 +92,6 @@ test.serial('Release patch, minor and major versions', async t => {
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
// Create a npm-shrinkwrap.json file
@@ -288,7 +293,7 @@ test.serial('Release patch, minor and major versions', async t => {
});
test.serial('Release versions from a packed git repository, using tags to determine last release gitHead', async t => {
const packageName = 'test-git-packaed';
const packageName = 'test-git-packed';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
@@ -299,7 +304,6 @@ test.serial('Release versions from a packed git repository, using tags to determ
name: packageName,
version: '0.0.0-dev',
repository: {url: `git@github.com:${owner}/${packageName}.git`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
@@ -453,7 +457,6 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
name: packageName,
version: '0.0.0-dev',
repository: {url: `git+https://github.com/${owner}/${packageName}`},
release: {verifyConditions: ['@semantic-release/github', '@semantic-release/npm']},
publishConfig: {registry: npmRegistry.url},
});
@@ -520,7 +523,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
({stderr, stdout, code} = await execa(cli, [], {env, reject: false}));
t.log('Log "ENOTINHISTORY" message');
t.is(code, 0);
t.is(code, 1);
t.regex(
stderr,
new RegExp(
@@ -585,6 +588,11 @@ test.serial('Dry-run', async t => {
});
/* Initial release */
const verifyMock = await mockServer.mock(
`/repos/${owner}/${packageName}`,
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
const version = '1.0.0';
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
@@ -597,6 +605,7 @@ test.serial('Dry-run', async t => {
// Verify package.json and has not been modified
t.is((await readJson('./package.json')).version, '0.0.0-dev');
await mockServer.verify(verifyMock);
});
test.serial('Pass options via CLI arguments', async t => {
@@ -681,11 +690,7 @@ 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({
verifyConditions: [{path: '@semantic-release/github'}, '@semantic-release/npm'],
publish: [{path: '@semantic-release/github'}, '@semantic-release/npm'],
debug: true,
});
await semanticRelease();
// Verify package.json and has been updated
t.is((await readJson('./package.json')).version, version);
@@ -731,7 +736,7 @@ test.serial('Log unexpected errors from plugins and exit with 1', async t => {
t.is(code, 1);
});
test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async t => {
test.serial('Log errors inheriting SemanticReleaseError and exit with 1', async t => {
const packageName = 'test-inherited-error';
const owner = 'test-repo';
// Create a git repository, set the current working directory at the root of the repo
@@ -752,7 +757,7 @@ test.serial('Log errors inheriting SemanticReleaseError and exit with 0', async
const {stdout, code} = await execa(cli, [], {env, reject: false});
// Verify the type and message are logged
t.regex(stdout, /EINHERITED Inherited error/);
t.is(code, 0);
t.is(code, 1);
});
test.serial('CLI returns error code and prints help if called with a command', async t => {
+4 -3
View File
@@ -67,15 +67,16 @@ test('The "publish" plugin is mandatory, and must be a single or an array of plu
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 => {
test('The "getLastRelease" plugin output if defined, must be an object with a valid semver version in the "version" property and the corresponding git reference in "gitHead" property', t => {
t.false(definitions.getLastRelease.output.validator('string'));
t.false(definitions.getLastRelease.output.validator(1));
t.false(definitions.getLastRelease.output.validator({version: 'v1.0.0'}));
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: 'v1.0.0', gitHead: '123'}));
t.true(definitions.getLastRelease.output.validator({version: '1.0.0', gitHead: '123'}));
t.true(definitions.getLastRelease.output.validator({version: null}));
});