Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
687435b9a2 | ||
|
|
7e785fa757 | ||
|
|
053c9edf9b | ||
|
|
2edd9da598 | ||
|
|
cbf57851fb | ||
|
|
af8c830335 | ||
|
|
d8208237f2 | ||
|
|
39536fa34e | ||
|
|
faabffb208 | ||
|
|
2f75dff3fc | ||
|
|
dc416b2ce9 | ||
|
|
e3c9bb8bd4 |
@@ -11,6 +11,7 @@ module.exports = async () => {
|
||||
.description('Run automated package publishing')
|
||||
.option('-b, --branch <branch>', 'Branch to release from')
|
||||
.option('-r, --repository-url <repositoryUrl>', 'Git repository URL')
|
||||
.option('-t, --tag-format <tagFormat>', `Git tag format`)
|
||||
.option('-e, --extends <paths>', 'Comma separated list of shareable config paths or packages name', list)
|
||||
.option(
|
||||
'--verify-conditions <paths>',
|
||||
|
||||
@@ -47,12 +47,7 @@ publish:
|
||||
image: node:8
|
||||
stage: release
|
||||
script:
|
||||
# Only for a local semantic-release installation
|
||||
- npm run semantic-release
|
||||
|
||||
# Only for a global semantic-release installation
|
||||
- npm install -g semantic-release
|
||||
- semantic-release
|
||||
- npx semantic-release
|
||||
```
|
||||
|
||||
### `package.json` configuration
|
||||
@@ -63,9 +58,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
|
||||
{
|
||||
"devDependencies": {
|
||||
"semantic-release": "^12.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"semantic-release": "semantic-release"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+3
-22
@@ -20,8 +20,6 @@ It's recommended to run the `semantic-release` command in the [Travis `deploy` s
|
||||
|
||||
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
|
||||
|
||||
**Note**: The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) semantic-release installation.
|
||||
|
||||
```yaml
|
||||
language: node_js
|
||||
|
||||
@@ -35,10 +33,7 @@ deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script:
|
||||
# Only for a local semantic-release installation
|
||||
- npm run semantic-release
|
||||
# Only for a global semantic-release installation
|
||||
- npm install -g semantic-release && semantic-release
|
||||
- npx semantic-release
|
||||
```
|
||||
|
||||
### `package.json` configuration for single Node job
|
||||
@@ -49,9 +44,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
|
||||
{
|
||||
"devDependencies": {
|
||||
"semantic-release": "^11.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"semantic-release": "semantic-release"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -72,8 +64,6 @@ It's recommended to run the `semantic-release` command in the [Travis `deploy` s
|
||||
|
||||
**Note**: It's not recommended to run the `semantic-release` command in the Travis `script` step as each script in this step will be executed regardless of the outcome of the previous one. See [travis-ci/travis-ci#1066](https://github.com/travis-ci/travis-ci/issues/1066).
|
||||
|
||||
**Note**: The`semantic-release` execution command varies depending if you are using a [local](../usage/installation.md#local-installation) or [global](../usage/installation.md#global-installation) **semantic-release** installation.
|
||||
|
||||
```yaml
|
||||
language: node_js
|
||||
|
||||
@@ -94,10 +84,7 @@ deploy:
|
||||
provider: script
|
||||
skip_cleanup: true
|
||||
script:
|
||||
# Only for a local semantic-release installation
|
||||
- npm run travis-deploy-once "npm run semantic-release"
|
||||
# Only for a global semantic-release installation
|
||||
- npm install -g travis-deploy-once semantic-release && travis-deploy-once "semantic-release"
|
||||
- npx travis-deploy-once "npx semantic-release"
|
||||
```
|
||||
|
||||
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
|
||||
@@ -111,10 +98,6 @@ A `package.json` is required only for [local](../usage/installation.md#local-ins
|
||||
"devDependencies": {
|
||||
"semantic-release": "^12.0.0",
|
||||
"travis-deploy-once": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"semantic-release": "semantic-release",
|
||||
"travis-deploy-once": "travis-deploy-once"
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -156,10 +139,8 @@ deploy:
|
||||
script:
|
||||
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
|
||||
- nvm install lts/*
|
||||
# Install travis-deploy-once and semantic-release
|
||||
- npm install -g travis-deploy-once semantic-release
|
||||
# Run semantic-release only on job, after all other are successful
|
||||
- travis-deploy-once "semantic-release"
|
||||
- npx travis-deploy-once "npx semantic-release"
|
||||
```
|
||||
|
||||
**Note**: See the `travis-deploy-once` [`pro`](https://github.com/semantic-release/travis-deploy-once#-p---pro) and [`travis-url`](https://github.com/semantic-release/travis-deploy-once#-u---travis-url) options for using with [Travis Pro](https://docs.travis-ci.com/user/travis-pro) and [Travis Enterprise](https://enterprise.travis-ci.com).
|
||||
|
||||
+16
-2
@@ -35,11 +35,19 @@ Alternatively you can use a [global](../usage/installation.md#global-installatio
|
||||
If your CI environment provides [nvm](https://github.com/creationix/nvm) you can switch to Node 8 before installing and running the `semantic-release` command:
|
||||
|
||||
```bash
|
||||
$ nvm install 8 && yarn install -g semantic-release && semantic-release
|
||||
$ nvm install 8 && yarn global add semantic-release && semantic-release
|
||||
```
|
||||
|
||||
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
|
||||
|
||||
As `semantic-release` is recommended to be executed with [`npx`](https://www.npmjs.com/package/npx) an alternative is required for usage with Yarn. Even though it is possible to install npx with Yarn, it's not recommended. Yarn and npx would be using different cache locations.
|
||||
|
||||
For [local installation](../usage/installation.md#local-installation) replace
|
||||
`npx semantic-release` with `yarn run semantic-release`.
|
||||
|
||||
For [global installation](../usage/installation.md#global-installation) replace
|
||||
`npx semantic-release` with `yarn global add semantic-release && semantic-release`.
|
||||
|
||||
## Can I use semantic-release to publish non-JavaScript packages?
|
||||
|
||||
Yes, **semantic-release** is a Node CLI application but it can be used to publish any type of packages.
|
||||
@@ -64,7 +72,7 @@ See the [CI configuration recipes](../usage/ci-configuration.md) for more detail
|
||||
Yes, you can by explicitly setting the [`--no-ci` CLI option](../usage/configuration.md#options) option. You will also have to set the required [authentication](../usage/ci-configuration.md#authentication) via environment variables on your local machine, for example:
|
||||
|
||||
```bash
|
||||
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npm run semantic-release --no-ci
|
||||
$ NPM_TOKEN=<your_npm_token> GH_TOKEN=<your_github_token> npx semantic-release --no-ci
|
||||
```
|
||||
|
||||
However this is not the recommended approach, as running unit and integration tests on an independent machine before publishing software is a crucial part of the release workflow.
|
||||
@@ -156,3 +164,9 @@ In addition the [verify conditions step](../../README.md#release-steps) verifies
|
||||
**semantic-release** is written using the latest [ECMAScript 2017](https://www.ecma-international.org/publications/standards/Ecma-262.htm) features, without transpilation which **requires Node version 8.3 or higher**.
|
||||
|
||||
See [Node version requirement](../support/node-version.md#node-version-requirement) for more details and solutions.
|
||||
|
||||
# What is npx?
|
||||
|
||||
[`npx`](https://www.npmjs.com/package/npx) – short for "npm exec" – is a CLI to find and execute npm binaries within the local `node_modules` folder or in the $PATH. If a binary can't be located npx will download the required package and execute it from its cache location.
|
||||
The tool is bundled with [npm](https://www.npmjs.com/package/npm) >= 5.2, or can be installed via `npm install -g npx`.
|
||||
For more details and motivation read the [introductory blog post](https://medium.com/@maybekatz/introducing-npx-an-npm-package-runner-55f7d4bd282b) by [@zkat](https://github.com/zkat).
|
||||
|
||||
@@ -18,35 +18,18 @@ See [CI configuration](../usage/ci-configuration.md) and [CI configuration recip
|
||||
|
||||
### Use `npx`
|
||||
|
||||
[`npx`](https://github.com/zkat/npx) is a CLI to execute npm binaries. It is bundled with [npm](https://www.npmjs.com/package/npm) >= 5.4, or can be installed via `npm install -g npx`.
|
||||
|
||||
`npx` can be used to download the latest [Node 8 package published on npm](https://www.npmjs.com/package/node) and use it to execute the `semantic-release` command.
|
||||
|
||||
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation:
|
||||
`npx` is included with npm >= 5.2 and can be used to download the latest [Node 8 package published on npm](https://www.npmjs.com/package/node). Use it to execute the `semantic-release` command.
|
||||
|
||||
```bash
|
||||
$ npm install -g npx && npx -p node@8 -c "npm run semantic-release"
|
||||
$ npx -p node@8 -c "npx semantic-release"
|
||||
```
|
||||
|
||||
If you are using a [global](../usage/installation.md#global-installation) **semantic-release** installation:
|
||||
|
||||
```bash
|
||||
# For global semantic-release install
|
||||
$ npm install -g semantic-release npx && npx -p node@8 -c "semantic-release"
|
||||
```
|
||||
**Note:**: See [What is npx](./FAQ.md#what-is-npx) for more details.
|
||||
|
||||
### Use `nvm`
|
||||
|
||||
If your CI environment provides [nvm](https://github.com/creationix/nvm) you can use it to switch to Node 8 before running the `semantic-release` command.
|
||||
|
||||
If you are using a [local](../usage/installation.md#local-installation) **semantic-release** installation:
|
||||
|
||||
```bash
|
||||
$ nvm install 8 && npm run semantic-release
|
||||
```
|
||||
|
||||
If you are using a [global](../usage/installation.md#global-installation) **semantic-release** installation:
|
||||
|
||||
```bash
|
||||
$ nvm install 8 && npm install -g semantic-release && semantic-release
|
||||
$ nvm install 8 && npx semantic-release
|
||||
```
|
||||
|
||||
@@ -44,6 +44,6 @@ cd your-module
|
||||
semantic-release-cli setup
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
See the [semantic-release-cli](https://github.com/semantic-release/cli#what-it-does) documentation for more details.
|
||||
|
||||
@@ -65,6 +65,18 @@ Any valid git url format is supported (See [Git protocols](https://git-scm.com/b
|
||||
|
||||
**Note**: 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.**
|
||||
|
||||
### tagFormat
|
||||
|
||||
Type: `String`
|
||||
|
||||
Default: `v${version}`
|
||||
|
||||
CLI arguments: `-t`, `--tag-format`
|
||||
|
||||
The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by **semantic-release** to identify releases. The tag name is generated with [Lodash template](https://lodash.com/docs#template) and will be compiled with the `version` variable.
|
||||
|
||||
**Note**: The `tagFormat` must contain the `version` variable and compile to a [valid Git reference](https://git-scm.com/docs/git-check-ref-format#_description).
|
||||
|
||||
### dryRun
|
||||
|
||||
Type: `Boolean`
|
||||
|
||||
+10
-14
@@ -2,32 +2,28 @@
|
||||
|
||||
## Local installation
|
||||
|
||||
For [Node modules projects](https://docs.npmjs.com/getting-started/creating-node-modules) we recommend to install **semantic-release** locally and to run the `semantic-release` command with a [npm script](https://docs.npmjs.com/misc/scripts):
|
||||
For [Node modules projects](https://docs.npmjs.com/getting-started/creating-node-modules) we recommend to install **semantic-release** locally and to run the `semantic-release` command with [npx](https://www.npmjs.com/package/npx):
|
||||
|
||||
```bash
|
||||
$ npm install --save-dev semantic-release
|
||||
```
|
||||
|
||||
In your `package.json`:
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"semantic-release": "semantic-release"
|
||||
}
|
||||
```
|
||||
|
||||
Then in the CI environment:
|
||||
|
||||
```bash
|
||||
$ npm run semantic-release
|
||||
$ npx semantic-release
|
||||
```
|
||||
|
||||
**Note:**: `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently find the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
|
||||
|
||||
## Global installation
|
||||
|
||||
For other type of projects we recommend to install **semantic-release** globally directly in the CI environment:
|
||||
For other type of projects we recommend to install **semantic-release** directly in the CI environment, also with [npx](https://www.npmjs.com/package/npx):
|
||||
|
||||
```bash
|
||||
$ npm install -g semantic-release
|
||||
$ semantic-release
|
||||
$ npx semantic-release
|
||||
```
|
||||
**Note:**: For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
|
||||
|
||||
**Note:**: For a global installation, it's recommended to specify the major **semantic-release** version to install (for example with with `npx semantic-release@12`, or `npm install -g semantic-release@12`). This way your build will not automatically use the next major **semantic-release** release that could possibly break your build. You will have to upgrade manually when a new major version is released.
|
||||
|
||||
**Note:**: `npx` is a tool bundled with `npm@>=5.2.0`. It is used to conveniently install the semantic-release binary and to execute it. See [What is npx](../support/FAQ.md#what-is-npx) for more details.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
const {template} = require('lodash');
|
||||
const marked = require('marked');
|
||||
const TerminalRenderer = require('marked-terminal');
|
||||
const envCi = require('env-ci');
|
||||
@@ -38,7 +39,7 @@ async function run(opts) {
|
||||
// Unshallow the repo in order to get all the tags
|
||||
await unshallow();
|
||||
|
||||
const lastRelease = await getLastRelease(logger);
|
||||
const lastRelease = await getLastRelease(options.tagFormat, logger);
|
||||
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
|
||||
|
||||
logger.log('Call plugin %s', 'analyze-commits');
|
||||
@@ -53,7 +54,7 @@ async function run(opts) {
|
||||
return;
|
||||
}
|
||||
const version = getNextVersion(type, lastRelease, logger);
|
||||
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: `v${version}`};
|
||||
const nextRelease = {type, version, gitHead: await getGitHead(), gitTag: template(options.tagFormat)({version})};
|
||||
|
||||
logger.log('Call plugin %s', 'verify-release');
|
||||
await plugins.verifyRelease({options, logger, lastRelease, commits, nextRelease}, true);
|
||||
|
||||
+6
-18
@@ -1,8 +1,7 @@
|
||||
const readPkgUp = require('read-pkg-up');
|
||||
const {castArray, pickBy, isUndefined, isNull, isString, isPlainObject} = require('lodash');
|
||||
const readPkgUp = require('read-pkg-up');
|
||||
const cosmiconfig = require('cosmiconfig');
|
||||
const resolveFrom = require('resolve-from');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const debug = require('debug')('semantic-release:config');
|
||||
const {repoUrl} = require('./git');
|
||||
const PLUGINS_DEFINITION = require('./plugins/definitions');
|
||||
@@ -43,29 +42,18 @@ module.exports = async (opts, logger) => {
|
||||
};
|
||||
}
|
||||
|
||||
const repositoryUrl = (await pkgRepoUrl()) || (await repoUrl());
|
||||
|
||||
// Set default options values if not defined yet
|
||||
options = {
|
||||
branch: 'master',
|
||||
repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
|
||||
repositoryUrl: repositoryUrl ? getGitAuthUrl(repositoryUrl) : repositoryUrl,
|
||||
tagFormat: `v\${version}`,
|
||||
// Remove `null` and `undefined` options so they can be replaced with default ones
|
||||
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
|
||||
};
|
||||
|
||||
debug('options values: %O', Object.keys(options));
|
||||
debug('name: %O', options.name);
|
||||
debug('branch: %O', options.branch);
|
||||
debug('repositoryUrl: %O', options.repositoryUrl);
|
||||
debug('analyzeCommits: %O', options.analyzeCommits);
|
||||
debug('generateNotes: %O', options.generateNotes);
|
||||
debug('verifyConditions: %O', options.verifyConditions);
|
||||
debug('verifyRelease: %O', options.verifyRelease);
|
||||
debug('publish: %O', options.publish);
|
||||
|
||||
if (!options.repositoryUrl) {
|
||||
throw new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL');
|
||||
}
|
||||
|
||||
options.repositoryUrl = getGitAuthUrl(options.repositoryUrl);
|
||||
debug('options values: %O', options);
|
||||
|
||||
return {options, plugins: await plugins(options, pluginsPath, logger)};
|
||||
};
|
||||
|
||||
@@ -19,11 +19,12 @@ module.exports = repositoryUrl => {
|
||||
? `gitlab-ci-token:${process.env[envVar]}`
|
||||
: process.env[envVar];
|
||||
|
||||
if (!gitCredentials) {
|
||||
return repositoryUrl;
|
||||
}
|
||||
|
||||
const {protocols} = gitUrlParse(repositoryUrl);
|
||||
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
|
||||
|
||||
if (!gitCredentials) {
|
||||
return protocols.includes('https') ? `${gitUrlParse(repositoryUrl).toString(protocol)}.git` : repositoryUrl;
|
||||
}
|
||||
|
||||
return format({...parse(`${gitUrlParse(repositoryUrl).toString(protocol)}.git`), ...{auth: gitCredentials}});
|
||||
};
|
||||
|
||||
+24
-8
@@ -1,3 +1,4 @@
|
||||
const {escapeRegExp, template} = require('lodash');
|
||||
const semver = require('semver');
|
||||
const pLocate = require('p-locate');
|
||||
const debug = require('debug')('semantic-release:get-last-release');
|
||||
@@ -15,21 +16,36 @@ const {gitTags, isRefInHistory, gitTagHead} = require('./git');
|
||||
* Determine the Git tag and version of the last tagged release.
|
||||
*
|
||||
* - Obtain all the tags referencing commits in the current branch history
|
||||
* - Filter out the ones that are not valid semantic version
|
||||
* - Sort the tags
|
||||
* - Retrive the highest tag
|
||||
* - Filter out the ones that are not valid semantic version or doesn't match the `tagFormat`
|
||||
* - Sort the versions
|
||||
* - Retrive the highest version
|
||||
*
|
||||
* @param {Object} logger Global logger.
|
||||
* @return {Promise<LastRelease>} The last tagged release or `undefined` if none is found.
|
||||
*/
|
||||
module.exports = async logger => {
|
||||
const tags = (await gitTags()).filter(tag => semver.valid(semver.clean(tag))).sort(semver.rcompare);
|
||||
module.exports = async (tagFormat, logger) => {
|
||||
// Generate a regex to parse tags formatted with `tagFormat`
|
||||
// by replacing the `version` variable in the template by `(.+)`.
|
||||
// The `tagFormat` is compiled with space as the `version` as it's an invalid tag character,
|
||||
// so it's guaranteed to no be present in the `tagFormat`.
|
||||
const tagRegexp = escapeRegExp(template(tagFormat)({version: ' '})).replace(' ', '(.+)');
|
||||
const tags = (await gitTags())
|
||||
.map(tag => {
|
||||
return {gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]};
|
||||
})
|
||||
.filter(tag => tag.version && semver.valid(semver.clean(tag.version)))
|
||||
.sort((a, b) => semver.rcompare(a.version, b.version));
|
||||
|
||||
debug('found tags: %o', tags);
|
||||
|
||||
if (tags.length > 0) {
|
||||
const gitTag = await pLocate(tags, tag => isRefInHistory(tag), {concurrency: 1, preserveOrder: true});
|
||||
logger.log('Found git tag version %s', gitTag);
|
||||
return {gitTag, gitHead: await gitTagHead(gitTag), version: semver.valid(semver.clean(gitTag))};
|
||||
const {gitTag, version} = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {
|
||||
concurrency: 1,
|
||||
preserveOrder: true,
|
||||
});
|
||||
logger.log('Found git tag %s associated with version %s', gitTag, version);
|
||||
|
||||
return {gitHead: await gitTagHead(gitTag), gitTag, version};
|
||||
}
|
||||
|
||||
logger.log('No git tag version found');
|
||||
|
||||
+45
-9
@@ -9,7 +9,11 @@ const debug = require('debug')('semantic-release:get-version-head');
|
||||
* @return {string} The commit sha of the tag in parameter or `null`.
|
||||
*/
|
||||
async function gitTagHead(tagName) {
|
||||
return execa.stdout('git', ['rev-list', '-1', tagName], {reject: false});
|
||||
try {
|
||||
return await execa.stdout('git', ['rev-list', '-1', tagName]);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,10 +32,14 @@ async function gitTags() {
|
||||
*
|
||||
* @param {string} ref The reference to look for.
|
||||
*
|
||||
* @return {boolean} `true` if the reference is in the history of the current branch, `false` otherwise.
|
||||
* @return {boolean} `true` if the reference is in the history of the current branch, falsy otherwise.
|
||||
*/
|
||||
async function isRefInHistory(ref) {
|
||||
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'], {reject: false})).code === 0;
|
||||
try {
|
||||
return (await execa('git', ['merge-base', '--is-ancestor', ref, 'HEAD'])).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,14 +60,22 @@ async function gitHead() {
|
||||
* @return {string} The value of the remote git URL.
|
||||
*/
|
||||
async function repoUrl() {
|
||||
return execa.stdout('git', ['remote', 'get-url', 'origin'], {reject: false});
|
||||
try {
|
||||
return await execa.stdout('git', ['remote', 'get-url', 'origin']);
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {Boolean} `true` if the current working directory is in a git repository, `false` otherwise.
|
||||
* @return {Boolean} `true` if the current working directory is in a git repository, falsy otherwise.
|
||||
*/
|
||||
async function isGitRepo() {
|
||||
return (await execa('git', ['rev-parse', '--git-dir'], {reject: false})).code === 0;
|
||||
try {
|
||||
return (await execa('git', ['rev-parse', '--git-dir'])).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,10 +84,14 @@ async function isGitRepo() {
|
||||
* @param {String} origin The remote repository URL.
|
||||
* @param {String} branch The repositoru branch for which to verify write access.
|
||||
*
|
||||
* @return {Boolean} `true` is authorized to push, `false` otherwise.
|
||||
* @return {Boolean} `true` is authorized to push, falsy otherwise.
|
||||
*/
|
||||
async function verifyAuth(origin, branch) {
|
||||
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`], {reject: false})).code === 0;
|
||||
try {
|
||||
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`])).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,10 +128,25 @@ async function deleteTag(origin, tagName) {
|
||||
debug('delete local tag', shell);
|
||||
|
||||
// Delete the tag remotely
|
||||
shell = await execa('git', ['push', '-d', origin, tagName], {reject: false});
|
||||
shell = await execa('git', ['push', '--delete', origin, tagName], {reject: false});
|
||||
debug('delete remote tag', shell);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a tag name is a valid Git reference.
|
||||
*
|
||||
* @method verifyTagName
|
||||
* @param {string} tagName the tag name to verify.
|
||||
* @return {boolean} `true` if valid, falsy otherwise.
|
||||
*/
|
||||
async function verifyTagName(tagName) {
|
||||
try {
|
||||
return (await execa('git', ['check-ref-format', `refs/tags/${tagName}`])).code === 0;
|
||||
} catch (err) {
|
||||
debug(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
gitTagHead,
|
||||
gitTags,
|
||||
@@ -124,4 +159,5 @@ module.exports = {
|
||||
tag,
|
||||
push,
|
||||
deleteTag,
|
||||
verifyTagName,
|
||||
};
|
||||
|
||||
+11
-3
@@ -1,11 +1,13 @@
|
||||
const {isArray, isObject, omit} = require('lodash');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const PLUGINS_DEFINITION = require('./definitions');
|
||||
const pipeline = require('./pipeline');
|
||||
const normalize = require('./normalize');
|
||||
|
||||
module.exports = (options, pluginsPath, logger) =>
|
||||
Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => {
|
||||
module.exports = (options, pluginsPath, logger) => {
|
||||
const errors = [];
|
||||
const plugins = Object.keys(PLUGINS_DEFINITION).reduce((plugins, pluginType) => {
|
||||
const {config, output, default: def} = PLUGINS_DEFINITION[pluginType];
|
||||
let pluginConfs;
|
||||
if (options[pluginType]) {
|
||||
@@ -14,7 +16,8 @@ module.exports = (options, pluginsPath, logger) =>
|
||||
options[pluginType].path = def;
|
||||
}
|
||||
if (config && !config.validator(options[pluginType])) {
|
||||
throw new SemanticReleaseError(config.message, 'EPLUGINCONF');
|
||||
errors.push(new SemanticReleaseError(config.message, 'EPLUGINCONF'));
|
||||
return plugins;
|
||||
}
|
||||
pluginConfs = options[pluginType];
|
||||
} else {
|
||||
@@ -29,3 +32,8 @@ module.exports = (options, pluginsPath, logger) =>
|
||||
|
||||
return plugins;
|
||||
}, {});
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
return plugins;
|
||||
};
|
||||
|
||||
+36
-5
@@ -1,19 +1,50 @@
|
||||
const {template} = require('lodash');
|
||||
const SemanticReleaseError = require('@semantic-release/error');
|
||||
const {isGitRepo, verifyAuth} = require('./git');
|
||||
const AggregateError = require('aggregate-error');
|
||||
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
|
||||
|
||||
module.exports = async (options, branch, logger) => {
|
||||
const errors = [];
|
||||
|
||||
if (!await isGitRepo()) {
|
||||
logger.error('Semantic-release must run from a git repository.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await verifyAuth(options.repositoryUrl, options.branch)) {
|
||||
throw new SemanticReleaseError(
|
||||
`The git credentials doesn't allow to push on the branch ${options.branch}.`,
|
||||
'EGITNOPERMISSION'
|
||||
if (!options.repositoryUrl) {
|
||||
errors.push(new SemanticReleaseError('The repositoryUrl option is required', 'ENOREPOURL'));
|
||||
} else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
|
||||
errors.push(
|
||||
new SemanticReleaseError(
|
||||
`The git credentials doesn't allow to push on the branch ${options.branch}.`,
|
||||
'EGITNOPERMISSION'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Verify that compiling the `tagFormat` produce a valid Git tag
|
||||
if (!await verifyTagName(template(options.tagFormat)({version: '0.0.0'}))) {
|
||||
errors.push(
|
||||
new SemanticReleaseError('The tagFormat template must compile to a valid Git tag format', 'EINVALIDTAGFORMAT')
|
||||
);
|
||||
}
|
||||
|
||||
// Verify the `tagFormat` contains the variable `version` by compiling the `tagFormat` template
|
||||
// with a space as the `version` value and verify the result contains the space.
|
||||
// The space is used as it's an invalid tag character, so it's guaranteed to no be present in the `tagFormat`.
|
||||
if ((template(options.tagFormat)({version: ' '}).match(/ /g) || []).length !== 1) {
|
||||
errors.push(
|
||||
new SemanticReleaseError(
|
||||
`The tagFormat template must contain the variable "\${version}" exactly once`,
|
||||
'ETAGNOVERSION'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new AggregateError(errors);
|
||||
}
|
||||
|
||||
if (branch !== options.branch) {
|
||||
logger.log(
|
||||
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^5.0.0",
|
||||
"@semantic-release/error": "^2.1.0",
|
||||
"@semantic-release/github": "^4.0.0",
|
||||
"@semantic-release/github": "^4.0.2",
|
||||
"@semantic-release/npm": "^3.0.0",
|
||||
"@semantic-release/release-notes-generator": "^6.0.0",
|
||||
"aggregate-error": "^1.0.0",
|
||||
@@ -33,7 +33,7 @@
|
||||
"execa": "^0.9.0",
|
||||
"get-stream": "^3.0.0",
|
||||
"git-log-parser": "^1.2.0",
|
||||
"git-url-parse": "^8.0.0",
|
||||
"git-url-parse": "^8.1.0",
|
||||
"hook-std": "^0.4.0",
|
||||
"lodash": "^4.17.4",
|
||||
"marked": "^0.3.9",
|
||||
|
||||
+31
-15
@@ -13,12 +13,12 @@ const envBackup = Object.assign({}, process.env);
|
||||
const cwd = process.cwd();
|
||||
|
||||
test.beforeEach(t => {
|
||||
// Delete environment variables that could have been set on the machine running the tests
|
||||
delete process.env.GIT_CREDENTIALS;
|
||||
delete process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.GL_TOKEN;
|
||||
delete process.env.GITLAB_TOKEN;
|
||||
// Delete environment variables that could have been set on the machine running the tests
|
||||
t.context.plugins = stub().returns({});
|
||||
t.context.getConfig = proxyquire('../lib/get-config', {'./plugins': t.context.plugins});
|
||||
});
|
||||
@@ -45,6 +45,7 @@ test.serial('Default values, reading repositoryUrl from package.json', async t =
|
||||
// Verify the default options are set
|
||||
t.is(options.branch, 'master');
|
||||
t.is(options.repositoryUrl, 'git@package.com:owner/module.git');
|
||||
t.is(options.tagFormat, `v\${version}`);
|
||||
});
|
||||
|
||||
test.serial('Default values, reading repositoryUrl from repo if not set in package.json', async t => {
|
||||
@@ -58,10 +59,11 @@ test.serial('Default values, reading repositoryUrl from repo if not set in packa
|
||||
// Verify the default options are set
|
||||
t.is(options.branch, 'master');
|
||||
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
|
||||
t.is(options.tagFormat, `v\${version}`);
|
||||
});
|
||||
|
||||
test.serial('Default values, reading repositoryUrl (http url) from package.json if not set in repo', async t => {
|
||||
const pkg = {repository: 'git+https://hostname.com/owner/module.git'};
|
||||
const pkg = {repository: 'https://hostname.com/owner/module.git'};
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Create package.json in repository root
|
||||
@@ -72,6 +74,7 @@ test.serial('Default values, reading repositoryUrl (http url) from package.json
|
||||
// Verify the default options are set
|
||||
t.is(options.branch, 'master');
|
||||
t.is(options.repositoryUrl, pkg.repository);
|
||||
t.is(options.tagFormat, `v\${version}`);
|
||||
});
|
||||
|
||||
test.serial('Read options from package.json', async t => {
|
||||
@@ -79,7 +82,8 @@ test.serial('Read options from package.json', async t => {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
generateNotes: 'generateNotes',
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -99,7 +103,8 @@ test.serial('Read options from .releaserc.yml', async t => {
|
||||
const release = {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -119,7 +124,8 @@ test.serial('Read options from .releaserc.json', async t => {
|
||||
const release = {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -139,7 +145,8 @@ test.serial('Read options from .releaserc.js', async t => {
|
||||
const release = {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -159,7 +166,8 @@ test.serial('Read options from release.config.js', async t => {
|
||||
const release = {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -184,6 +192,7 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_cli'},
|
||||
branch: 'branch_cli',
|
||||
repositoryUrl: 'http://cli-url.com/owner/package',
|
||||
tagFormat: `cli\${version}`,
|
||||
};
|
||||
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -208,7 +217,8 @@ test.serial('Read configuration from file path in "extends"', async t => {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
generateNotes: 'generateNotes',
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -235,7 +245,8 @@ test.serial('Read configuration from module path in "extends"', async t => {
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
generateNotes: 'generateNotes',
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -262,7 +273,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
|
||||
verifyRelease: 'verifyRelease1',
|
||||
analyzeCommits: {path: 'analyzeCommits1', param: 'analyzeCommits_param1'},
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
};
|
||||
|
||||
const shareable2 = {
|
||||
@@ -270,6 +281,7 @@ test.serial('Read configuration from an array of paths in "extends"', async t =>
|
||||
generateNotes: 'generateNotes2',
|
||||
analyzeCommits: {path: 'analyzeCommits2', param: 'analyzeCommits_param2'},
|
||||
branch: 'test_branch',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -306,7 +318,8 @@ test.serial('Prioritize configuration from config file over "extends"', async t
|
||||
generateNotes: 'generateNotesShareable',
|
||||
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -333,7 +346,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
|
||||
extends: './shareable2.json',
|
||||
branch: 'branch_opts',
|
||||
publish: [{path: 'publishOpts', param: 'publishOpts_param'}],
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
};
|
||||
const release = {
|
||||
extends: './shareable1.json',
|
||||
@@ -346,12 +359,13 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
|
||||
generateNotes: 'generateNotesShareable1',
|
||||
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
|
||||
branch: 'test_branch1',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
};
|
||||
const shareable2 = {
|
||||
analyzeCommits: 'analyzeCommits2',
|
||||
publish: [{path: 'publishShareable', param: 'publishShareable_param2'}],
|
||||
branch: 'test_branch2',
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -374,11 +388,12 @@ test.serial('Allow to unset properties defined in shareable config with "null"',
|
||||
extends: './shareable.json',
|
||||
analyzeCommits: null,
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
};
|
||||
const shareable = {
|
||||
generateNotes: 'generateNotes',
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
@@ -407,11 +422,12 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
|
||||
extends: './shareable.json',
|
||||
analyzeCommits: undefined,
|
||||
branch: 'test_branch',
|
||||
repositoryUrl: 'git+https://hostname.com/owner/module.git',
|
||||
repositoryUrl: 'https://hostname.com/owner/module.git',
|
||||
};
|
||||
const shareable = {
|
||||
generateNotes: 'generateNotes',
|
||||
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
|
||||
tagFormat: `v\${version}`,
|
||||
};
|
||||
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
|
||||
@@ -14,10 +14,29 @@ test.afterEach.always(() => {
|
||||
process.env = envBackup;
|
||||
});
|
||||
|
||||
test.serial('Return the same "repositoryUrl" is no "gitCredentials" is defined', t => {
|
||||
test.serial('Return the same "git" formatted URL if "gitCredentials" is not defined', t => {
|
||||
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'git@host.com:owner/repo.git');
|
||||
});
|
||||
|
||||
test.serial('Return the same "https" formatted URL if "gitCredentials" is not defined', t => {
|
||||
t.is(getAuthUrl('https://host.com/owner/repo.git'), 'https://host.com/owner/repo.git');
|
||||
});
|
||||
|
||||
test.serial(
|
||||
'Return the "https" formatted URL if "gitCredentials" is not defined and repositoryUrl is a "git+https" URL',
|
||||
t => {
|
||||
t.is(getAuthUrl('git+https://host.com/owner/repo.git'), 'https://host.com/owner/repo.git');
|
||||
}
|
||||
);
|
||||
|
||||
test.serial('Handle "https" URL with group and subgroup', t => {
|
||||
t.is(getAuthUrl('https://host.com/group/subgroup/owner/repo.git'), 'https://host.com/group/subgroup/owner/repo.git');
|
||||
});
|
||||
|
||||
test.serial('Handle "git" URL with group and subgroup', t => {
|
||||
t.is(getAuthUrl('git@host.com:group/subgroup/owner/repo.git'), 'git@host.com:group/subgroup/owner/repo.git');
|
||||
});
|
||||
|
||||
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL', t => {
|
||||
process.env.GIT_CREDENTIALS = 'user:pass';
|
||||
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
|
||||
@@ -68,3 +87,19 @@ test.serial('Return the "https" formatted URL if "gitCredentials" is defined wit
|
||||
process.env.GITLAB_TOKEN = 'token';
|
||||
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
|
||||
});
|
||||
|
||||
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', t => {
|
||||
process.env.GIT_CREDENTIALS = 'user:pass';
|
||||
t.is(
|
||||
getAuthUrl('https://host.com/group/subgroup/owner/repo.git'),
|
||||
'https://user:pass@host.com/group/subgroup/owner/repo.git'
|
||||
);
|
||||
});
|
||||
|
||||
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', t => {
|
||||
process.env.GIT_CREDENTIALS = 'user:pass';
|
||||
t.is(
|
||||
getAuthUrl('git@host.com:group/subgroup/owner/repo.git'),
|
||||
'https://user:pass@host.com/group/subgroup/owner/repo.git'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -30,10 +30,10 @@ test.serial('Get the highest valid tag', async t => {
|
||||
await gitCommits(['Fourth']);
|
||||
await gitTagVersion('v3.0');
|
||||
|
||||
const result = await getLastRelease(t.context.logger);
|
||||
const result = await getLastRelease(`v\${version}`, t.context.logger);
|
||||
|
||||
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
|
||||
t.deepEqual(t.context.log.args[0], ['Found git tag version %s', 'v2.0.0']);
|
||||
t.deepEqual(t.context.log.args[0], ['Found git tag %s associated with version %s', 'v2.0.0', '2.0.0']);
|
||||
});
|
||||
|
||||
test.serial('Get the highest tag in the history of the current branch', async t => {
|
||||
@@ -55,7 +55,7 @@ test.serial('Get the highest tag in the history of the current branch', async t
|
||||
// Create the tag corresponding to version 2.0.0
|
||||
await gitTagVersion('v2.0.0');
|
||||
|
||||
const result = await getLastRelease(t.context.logger);
|
||||
const result = await getLastRelease(`v\${version}`, t.context.logger);
|
||||
|
||||
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
|
||||
});
|
||||
@@ -71,8 +71,50 @@ test.serial('Return empty object if no valid tag is found', async t => {
|
||||
await gitCommits(['Third']);
|
||||
await gitTagVersion('v3.0');
|
||||
|
||||
const result = await getLastRelease(t.context.logger);
|
||||
const result = await getLastRelease(`v\${version}`, t.context.logger);
|
||||
|
||||
t.deepEqual(result, {});
|
||||
t.is(t.context.log.args[0][0], 'No git tag version found');
|
||||
});
|
||||
|
||||
test.serial('Get the highest valid tag corresponding to the "tagFormat"', async t => {
|
||||
// Create a git repository, set the current working directory at the root of the repo
|
||||
await gitRepo();
|
||||
// Create some commits and tags
|
||||
const [{hash: gitHead}] = await gitCommits(['First']);
|
||||
|
||||
await gitTagVersion('1.0.0');
|
||||
t.deepEqual(await getLastRelease(`\${version}`, t.context.logger), {
|
||||
gitHead,
|
||||
gitTag: '1.0.0',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await gitTagVersion('foo-1.0.0-bar');
|
||||
t.deepEqual(await getLastRelease(`foo-\${version}-bar`, t.context.logger), {
|
||||
gitHead,
|
||||
gitTag: 'foo-1.0.0-bar',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await gitTagVersion('foo-v1.0.0-bar');
|
||||
t.deepEqual(await getLastRelease(`foo-v\${version}-bar`, t.context.logger), {
|
||||
gitHead,
|
||||
gitTag: 'foo-v1.0.0-bar',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await gitTagVersion('(.+)/1.0.0/(a-z)');
|
||||
t.deepEqual(await getLastRelease(`(.+)/\${version}/(a-z)`, t.context.logger), {
|
||||
gitHead,
|
||||
gitTag: '(.+)/1.0.0/(a-z)',
|
||||
version: '1.0.0',
|
||||
});
|
||||
|
||||
await gitTagVersion('2.0.0-1.0.0-bar.1');
|
||||
t.deepEqual(await getLastRelease(`2.0.0-\${version}-bar.1`, t.context.logger), {
|
||||
gitHead,
|
||||
gitTag: '2.0.0-1.0.0-bar.1',
|
||||
version: '1.0.0',
|
||||
});
|
||||
});
|
||||
|
||||
+18
-3
@@ -11,6 +11,7 @@ import {
|
||||
gitTags,
|
||||
isGitRepo,
|
||||
deleteTag,
|
||||
verifyTagName,
|
||||
} from '../lib/git';
|
||||
import {
|
||||
gitRepo,
|
||||
@@ -87,7 +88,7 @@ test.serial('Verify if the commit `sha` is in the direct history of the current
|
||||
await gitCheckout('master', false);
|
||||
|
||||
t.true(await isRefInHistory(commits[0].hash));
|
||||
t.false(await isRefInHistory(otherCommits[0].hash));
|
||||
t.falsy(await isRefInHistory(otherCommits[0].hash));
|
||||
});
|
||||
|
||||
test.serial('Get the commit sha for a given tag or falsy if the tag does not exists', async t => {
|
||||
@@ -168,11 +169,25 @@ test.serial('Return "true" if in a Git repository', async t => {
|
||||
t.true(await isGitRepo());
|
||||
});
|
||||
|
||||
test.serial('Return "false" if not in a Git repository', async t => {
|
||||
test.serial('Return falsy if not in a Git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
t.false(await isGitRepo());
|
||||
t.falsy(await isGitRepo());
|
||||
});
|
||||
|
||||
test.serial('Return "true" for valid tag names', async t => {
|
||||
t.true(await verifyTagName('1.0.0'));
|
||||
t.true(await verifyTagName('v1.0.0'));
|
||||
t.true(await verifyTagName('tag_name'));
|
||||
t.true(await verifyTagName('tag/name'));
|
||||
});
|
||||
|
||||
test.serial('Return falsy for invalid tag names', async t => {
|
||||
t.falsy(await verifyTagName('?1.0.0'));
|
||||
t.falsy(await verifyTagName('*1.0.0'));
|
||||
t.falsy(await verifyTagName('[1.0.0]'));
|
||||
t.falsy(await verifyTagName('1.0.0..'));
|
||||
});
|
||||
|
||||
test.serial('Throws error if obtaining the tags fails', async t => {
|
||||
|
||||
+39
-4
@@ -68,7 +68,7 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `v\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions: [verifyConditions1, verifyConditions2],
|
||||
@@ -130,6 +130,41 @@ test.serial('Plugins are called with expected values', async t => {
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
|
||||
});
|
||||
|
||||
test.serial('Use custom tag format', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
await gitCommits(['First']);
|
||||
await gitTagVersion('test-1.0.0');
|
||||
await gitCommits(['Second']);
|
||||
|
||||
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
|
||||
const notes = 'Release notes';
|
||||
const verifyConditions = stub().resolves();
|
||||
const analyzeCommits = stub().resolves(nextRelease.type);
|
||||
const verifyRelease = stub().resolves();
|
||||
const generateNotes = stub().resolves(notes);
|
||||
const publish = stub().resolves();
|
||||
|
||||
const config = {branch: 'master', repositoryUrl, globalOpt: 'global', tagFormat: `test-\${version}`};
|
||||
const options = {
|
||||
...config,
|
||||
verifyConditions,
|
||||
analyzeCommits,
|
||||
verifyRelease,
|
||||
generateNotes,
|
||||
publish,
|
||||
};
|
||||
|
||||
const semanticRelease = proxyquire('..', {
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
t.truthy(await semanticRelease(options));
|
||||
|
||||
// Verify the tag has been created on the local and remote repo and reference the gitHead
|
||||
t.is(await gitTagHead(nextRelease.gitTag), nextRelease.gitHead);
|
||||
t.is(await gitRemoteTagHead(repositoryUrl, nextRelease.gitTag), nextRelease.gitHead);
|
||||
});
|
||||
|
||||
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
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
@@ -601,11 +636,11 @@ test.serial('Throw SemanticReleaseError if repositoryUrl is not set and cannot b
|
||||
'./lib/logger': t.context.logger,
|
||||
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
|
||||
});
|
||||
const error = await t.throws(semanticRelease());
|
||||
const errors = Array.from(await t.throws(semanticRelease()));
|
||||
|
||||
// Verify error code and type
|
||||
t.is(error.code, 'ENOREPOURL');
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
});
|
||||
|
||||
test.serial('Throw an Error if plugin returns an unexpected value', async t => {
|
||||
|
||||
@@ -127,26 +127,34 @@ test('Merge global options with plugin options', async t => {
|
||||
t.deepEqual(result.pluginConfig, {localOpt: 'local', globalOpt: 'global', otherOpt: 'locally-defined'});
|
||||
});
|
||||
|
||||
test('Throw an error if plugin configuration is missing a path for plugin pipeline', t => {
|
||||
const error = t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger));
|
||||
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
|
||||
const errors = Array.from(
|
||||
t.throws(() => getPlugins({verifyConditions: {}, verifyRelease: {}}, {}, t.context.logger))
|
||||
);
|
||||
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.is(error.code, 'EPLUGINCONF');
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
error.message,
|
||||
errors[0].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.'
|
||||
);
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
errors[1].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.'
|
||||
);
|
||||
});
|
||||
|
||||
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
|
||||
const error = t.throws(() =>
|
||||
getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)
|
||||
const errors = Array.from(
|
||||
t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger))
|
||||
);
|
||||
|
||||
t.is(error.name, 'SemanticReleaseError');
|
||||
t.is(error.code, 'EPLUGINCONF');
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].code, 'EPLUGINCONF');
|
||||
t.is(
|
||||
error.message,
|
||||
errors[0].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.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import test from 'ava';
|
||||
import {stub} from 'sinon';
|
||||
import tempy from 'tempy';
|
||||
import verify from '../lib/verify';
|
||||
import {gitRepo} from './helpers/git-utils';
|
||||
|
||||
// Save the current process.env
|
||||
const envBackup = Object.assign({}, process.env);
|
||||
// Save the current working diretory
|
||||
const cwd = process.cwd();
|
||||
|
||||
test.beforeEach(t => {
|
||||
// Delete environment variables that could have been set on the machine running the tests
|
||||
delete process.env.GIT_CREDENTIALS;
|
||||
delete process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.GL_TOKEN;
|
||||
delete process.env.GITLAB_TOKEN;
|
||||
// Stub the logger functions
|
||||
t.context.log = stub();
|
||||
t.context.error = stub();
|
||||
t.context.logger = {log: t.context.log, error: t.context.error};
|
||||
});
|
||||
|
||||
test.afterEach.always(() => {
|
||||
// Restore process.env
|
||||
process.env = envBackup;
|
||||
// Restore the current working directory
|
||||
process.chdir(cwd);
|
||||
});
|
||||
|
||||
test.serial('Return "false" if does not run on a git repository', async t => {
|
||||
const dir = tempy.directory();
|
||||
process.chdir(dir);
|
||||
|
||||
t.false(await verify({}, 'master', t.context.logger));
|
||||
});
|
||||
|
||||
test.serial('Throw a AggregateError', async t => {
|
||||
await gitRepo();
|
||||
|
||||
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The repositoryUrl option is required');
|
||||
t.is(errors[0].code, 'ENOREPOURL');
|
||||
t.is(errors[1].name, 'SemanticReleaseError');
|
||||
t.is(errors[1].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[1].code, 'EINVALIDTAGFORMAT');
|
||||
t.is(errors[2].name, 'SemanticReleaseError');
|
||||
t.is(errors[2].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[2].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `?\${version}`};
|
||||
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, 'The tagFormat template must compile to a valid Git tag format');
|
||||
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains the "version" variable', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: 'test'};
|
||||
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "version" variables', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `\${version}v\${version}`};
|
||||
|
||||
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
|
||||
|
||||
t.is(errors[0].name, 'SemanticReleaseError');
|
||||
t.is(errors[0].message, `The tagFormat template must contain the variable "\${version}" exactly once`);
|
||||
t.is(errors[0].code, 'ETAGNOVERSION');
|
||||
});
|
||||
|
||||
test.serial('Return "false" if the current branch is not the once configured', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
|
||||
|
||||
t.false(await verify(options, 'other', t.context.logger));
|
||||
});
|
||||
|
||||
test.serial('Return "true" if all verification pass', async t => {
|
||||
const repositoryUrl = await gitRepo(true);
|
||||
const options = {repositoryUrl, tagFormat: `v\${version}`, branch: 'master'};
|
||||
|
||||
t.true(await verify(options, 'master', t.context.logger));
|
||||
});
|
||||
Reference in New Issue
Block a user