Compare commits

...
7 Commits
Author SHA1 Message Date
Pierre Vanduynslager 5bc46a08cf feat: allow to release from local machine 2018-01-02 14:31:43 -05:00
Pierre Vanduynslager 5cc62e49ee test: add missing mockserver mocks 2018-01-01 20:20:19 -05:00
Pierre Vanduynslager 95de6a5c80 test: update test for @semantic-release/github@3.0.1 2018-01-01 20:20:19 -05:00
greenkeeper[bot]andPierre Vanduynslager 7eded2f018 chore(package): update @semantic-release/github to version 3.0.1 2018-01-01 20:20:19 -05:00
Pierre Vanduynslager 3c80fd2bf1 docs: update badges
Include npm version badges for both `@latest` and `@next` dist-tag
2017-12-31 00:39:27 -05:00
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
8 changed files with 357 additions and 61 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"
+11 -11
View File
@@ -1,21 +1,19 @@
# :package::rocket: semantic-release
**fully automated package publishing**
**Fully automated package publishing**
> **Trust us, this will change your workflow for the better.**
> [egghead.io](https://egghead.io/lessons/javascript-how-to-write-a-javascript-library-automating-releases-with-semantic-release)
[![Join the chat at https://gitter.im/semantic-release/semantic-release](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/semantic-release/semantic-release?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![npm](https://img.shields.io/npm/v/semantic-release.svg)](https://www.npmjs.com/package/semantic-release)
[![Greenkeeper badge](https://badges.greenkeeper.io/semantic-release/semantic-release.svg)](https://greenkeeper.io/)
[![license](https://img.shields.io/github/license/semantic-release/semantic-release.svg)](https://github.com/semantic-release/semantic-release/blob/master/LICENSE)
[![styled with prettier](https://img.shields.io/badge/styled_with-prettier-ff69b4.svg)](https://github.com/prettier/prettier)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
[![Gitter](https://badges.gitter.im/semantic-release/semantic-release.svg)](https://gitter.im/semantic-release/semantic-release?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Travis](https://img.shields.io/travis/semantic-release/semantic-release/caribou.svg)](https://travis-ci.org/semantic-release/semantic-release)
[![Codecov](https://img.shields.io/codecov/c/github/semantic-release/semantic-release/caribou.svg)](https://codecov.io/gh/semantic-release/semantic-release)
[![Greenkeeper badge](https://badges.greenkeeper.io/semantic-release/semantic-release.svg)](https://greenkeeper.io/)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![npm](https://img.shields.io/npm/v/semantic-release/latest.svg)](https://www.npmjs.com/package/semantic-release)
[![npm](https://img.shields.io/npm/v/semantic-release/next.svg)](https://www.npmjs.com/package/semantic-release)
Out of the box this is just about _commit-messages_, but you can do so much more.
@@ -170,7 +168,8 @@ 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
- `no-ci`: Skip Continuous Integration environment verifications, allowing to make releases from a local machine
- `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
@@ -271,7 +270,8 @@ If you run `npm run semantic-release` locally a dry run gets performed, which lo
### Can I run this on my own machine rather than on a CI server?
Of course you can, but this doesnt necessarily mean you should. Running your tests on an independent machine before releasing software is a crucial part of this workflow. Also it is a pain to set this up locally, with tokens lying around and everything. That said, you can run the scripts with `--debug=false` explicitly. You have to export `GH_TOKEN=<your_token>` and `NPM_TOKEN=<your_other_token>`.
Yes, you can by explicitly setting the [`--no-ci` CLI option](#options), but this doesnt necessarily mean you should. Running your tests on an independent machine before releasing software is a crucial part of this workflow.
You will need to set all necessary authentication token (like `GH_TOKEN` and `NPM_TOKEN`) on your local machine.
### Can I manually trigger the release of a specific version?
+5 -3
View File
@@ -27,6 +27,10 @@ module.exports = async () => {
)
.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(
'--no-ci',
'Skip Continuous Integration environment verifications, allowing to make releases from a local machine'
)
.option('--debug', 'Output debugging information')
.option(
'-d, --dry-run',
@@ -48,12 +52,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 && !opts.noCi) {
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
opts.dryRun = true;
}
if (isCi && isPr && !opts.noCi) {
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 wont 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;
};
+1 -1
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:
+2 -2
View File
@@ -16,15 +16,15 @@
},
"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/github": "^3.0.1",
"@semantic-release/npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^3.1.0",
"debug": "^3.1.0",
"env-ci": "^1.0.0",
"execa": "^0.8.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
+229 -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,14 @@ 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);
@@ -197,6 +209,106 @@ test.serial('Dry-run skips verifyConditions and publish', async t => {
t.is(publish.callCount, 0);
});
test.serial('Force a dry-run if not on a CI and ignore "noCi" is not explicitly set', 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);
t.is(generateNotes.callCount, 1);
t.is(publish.callCount, 0);
});
test.serial('Allow local releases with "noCi" option', async t => {
// Create a git repository, set the current working directory at the root of the repo
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 = {
noCi: true,
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', isPr: true}),
});
t.truthy(await semanticRelease(options));
t.not(t.context.log.args[0][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.not(
t.context.log.args[0][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
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, 1);
});
test.serial('Accept "undefined" values for the "getLastRelease" and "generateNotes" plugins', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
@@ -227,7 +339,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 +361,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 wont 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 +495,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));
+78 -19
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
@@ -113,6 +118,11 @@ test.serial('Release patch, minor and major versions', async t => {
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -131,7 +141,7 @@ test.serial('Release patch, minor and major versions', async t => {
await gitCommits(['feat: Initial commit']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
@@ -148,6 +158,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
@@ -158,6 +169,11 @@ test.serial('Release patch, minor and major versions', async t => {
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -176,7 +192,7 @@ test.serial('Release patch, minor and major versions', async t => {
await gitCommits(['fix: bar']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
@@ -193,6 +209,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
@@ -203,6 +220,11 @@ test.serial('Release patch, minor and major versions', async t => {
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -221,7 +243,7 @@ test.serial('Release patch, minor and major versions', async t => {
await gitCommits(['feat: baz']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
@@ -238,6 +260,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
@@ -248,6 +271,11 @@ test.serial('Release patch, minor and major versions', async t => {
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -266,7 +294,7 @@ test.serial('Release patch, minor and major versions', async t => {
await gitCommits(['feat: foo\n\n BREAKING CHANGE: bar']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
@@ -283,6 +311,7 @@ test.serial('Release patch, minor and major versions', async t => {
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
@@ -299,7 +328,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},
});
@@ -315,6 +343,11 @@ test.serial('Release versions from a packed git repository, using tags to determ
{body: {ref: `refs/tags/v${version}`}, headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {ref: `refs/tags/${version}`}}
);
let getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, method: 'GET'}
);
let createReleaseMock = await mockServer.mock(
`/repos/${owner}/${packageName}/releases`,
{
@@ -329,7 +362,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
await gitPackRefs();
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
@@ -339,6 +372,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
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
@@ -352,6 +386,11 @@ test.serial('Release versions from a packed git repository, using tags to determ
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -369,7 +408,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
await gitCommits(['fix: bar']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
@@ -380,6 +419,7 @@ test.serial('Release versions from a packed git repository, using tags to determ
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
@@ -453,7 +493,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},
});
@@ -464,6 +503,11 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
let getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -481,7 +525,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
await gitCommits(['feat: Initial commit']);
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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
@@ -496,6 +540,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
@@ -520,7 +565,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(
@@ -539,6 +584,11 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, 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}`]}]},
@@ -555,7 +605,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(`Published GitHub release: release-url/${version}`));
t.regex(stdout, new RegExp(`Publishing version ${version} to npm registry`));
t.is(code, 0);
// Verify package.json has been updated
@@ -566,6 +616,7 @@ test.serial('Create a tag as a recovery solution for "ENOTINHISTORY" error', asy
t.is(releasedVersion, version);
t.log(`+ released ${releasedVersion}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
@@ -585,6 +636,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 +653,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 => {
@@ -659,6 +716,11 @@ test.serial('Run via JS API', async t => {
{headers: [{name: 'Authorization', values: [`token ${env.GH_TOKEN}`]}]},
{body: {permissions: {push: true}}, method: 'GET'}
);
const getRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs/tags/v${version}`,
{},
{body: {}, statusCode: 404, method: 'GET'}
);
const createRefMock = await mockServer.mock(
`/repos/${owner}/${packageName}/git/refs`,
{
@@ -681,11 +743,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);
@@ -699,6 +757,7 @@ test.serial('Run via JS API', async t => {
t.log(`+ released ${releasedVersion} with gitHead ${releasedGitHead}`);
await mockServer.verify(verifyMock);
await mockServer.verify(getRefMock);
await mockServer.verify(createRefMock);
await mockServer.verify(createReleaseMock);
});
@@ -731,7 +790,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 +811,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 => {