Compare commits

...
61 Commits
Author SHA1 Message Date
pvdlgandGregor Martynus c93775cc38 feat: add support for Bitbucket token in environment variables 2018-05-01 12:02:29 -07:00
greenkeeper[bot]andGregor Martynus 22c5bbe06b chore(package): update fs-extra to version 6.0.0 2018-05-01 08:27:42 -07:00
pvdlg e88ac6300c fix: fix Bitbucket authenticated URL 2018-05-01 10:44:02 -04:00
pvdlg a52f258fa4 refactor: rename origin parameters to repositoryUrl 2018-05-01 09:59:57 -04:00
pvdlg 3f614530a0 test: prevent username/password prompt in get-git-auth-url tests 2018-05-01 09:59:57 -04:00
pvdlg d15905c0d5 fix: verify the local branch is up to date with the remote one 2018-05-01 09:59:57 -04:00
pvdlg a11da0d5e3 docs: fix jsdoc typos 2018-05-01 09:59:57 -04:00
pvdlg 65aaa77e95 refactor: remove unused test util function 2018-05-01 09:59:57 -04:00
Anshuk KumarandPierre Vanduynslager 45d7e6f25f fix: unshallow repository with credentials 2018-05-01 09:47:15 -04:00
greenkeeper[bot]andPierre Vanduynslager c02b5cac6b chore(package): update sinon to version 5.0.1 2018-04-30 10:30:37 -04:00
greenkeeper[bot]andPierre Vanduynslager b9bd650967 fix(package): update env-ci to version 2.0.0 2018-04-29 16:13:22 -04:00
greenkeeper[bot]andPierre Vanduynslager 6e15c18bea chore(package): update p-retry to version 2.0.0 2018-04-26 16:31:39 -04:00
Schalk NeethlingandPierre Vanduynslager 5af90319a1 docs: add details on NPM error 403 to troubleshooting doc 2018-04-26 11:49:32 -04:00
Gabriel DuarteandPierre Vanduynslager 218aa32f79 docs: add semantic-release-chrome to the list of community plugins 2018-04-23 12:04:26 -04:00
Pierre Vanduynslager cb1f80cb56 fix: add trailing .git to repositoryUrl only if it's present in the configured URL 2018-04-12 17:42:53 -04:00
greenkeeper[bot]andPierre Vanduynslager 7c9ec41dd7 fix(package): update git-url-parse to version 9.0.0 2018-04-12 17:42:53 -04:00
Kévin BerthommierandPierre Vanduynslager a1138a6a80 docs: update plugins package name 2018-04-11 18:40:19 -04:00
Pierre ColleandPierre Vanduynslager 6172eb807c docs: add plugins to the community list 2018-04-11 18:40:19 -04:00
Pierre Vanduynslager 31ad23125a fix: match tag to tagFormat from the begining of the string 2018-04-11 17:11:33 -04:00
Pierre Vanduynslager a8a07b7d51 docs: clarify FAQ to publish non-JavaScript packages 2018-04-10 12:11:24 -04:00
greenkeeper[bot]andPierre Vanduynslager 02746aa276 fix(package): update git-url-parse to version 8.3.1 2018-04-03 16:04:39 -04:00
William HosfordandGregor Martynus e41726cb96 docs: fix grammar and typos in CI configuration, configuration, and shareable configurations 2018-03-21 20:03:21 -07:00
Pierre Vanduynslager 51e340f44e fix: handle case with no last release in history 2018-03-21 21:41:50 -04:00
Pierre Vanduynslager 30ee231116 fix: prevent git prompt before permissions verification 2018-03-20 11:24:08 -04:00
Pierre Vanduynslager 3c46455929 fix: remove execa timeout 2018-03-20 11:24:08 -04:00
Pierre Vanduynslager dff0a34ab2 docs: add "Introduction to Semantic Release" article 2018-03-16 16:17:07 -04:00
Pierre Vanduynslager 7c48afad47 fix: prevent git CLI to prompt user/password on CI 2018-03-16 14:33:18 -04:00
Honza JavorekandPierre Vanduynslager c84ac15020 fix(package): Remove commander.js dependency (#704) 2018-03-16 14:18:49 -04:00
Pierre VanduynslagerandGregor Martynus 5f1d530e2a feat: allow to use shorthand for repositoryUrl
- `owner/repo` => `https://github.com/owner/repo.git`
- `gitlab:owner/repo` => `https://gitlab.com/owner/repo.git`
- `bitbucket:owner/repo` => `https://bitbucket.com/owner/repo.git`
2018-03-15 22:40:09 +00:00
greenkeeper[bot]andPierre Vanduynslager f13ec6a615 fix(package): update execa to version 0.10.0 2018-03-15 08:18:04 -04:00
Pierre VanduynslagerandGregor Martynus b0b4fc82de fix: convert git+https URL in package.json to https 2018-03-13 00:54:32 +00:00
Pierre VanduynslagerandGregor Martynus 6f74dcbd4a fix: use correct debug namespace 2018-03-13 00:54:32 +00:00
Gregor MartynusandPierre Vanduynslager 1129d47e1d docs(configuration): environment variable DEBUG must be set to semantic-release:*, not semantic-release 2018-03-12 16:39:46 -04:00
Pierre VanduynslagerandGregor Martynus e4618a2fbc fix: exclude prereleases from version retrived by getLastRelease 2018-03-08 17:25:40 +00:00
Pierre VanduynslagerandGregor Martynus e5a73d8628 fix: remove unecessary console.log 2018-03-07 23:01:08 +00:00
robertandPierre Vanduynslager 50f3c6e140 docs(README): correct pluralization 2018-03-07 09:39:20 -05:00
greenkeeper[bot]andPierre Vanduynslager 78f0937f24 chore(package): update proxyquire to version 2.0.0 2018-03-03 16:42:54 -05:00
William HosfordandGregor Martynus a7c187f31b docs: fix grammar and typos in README, CONTRIBUTING, installation guide, and plugin guide 2018-02-25 17:43:51 +00:00
Pierre Vanduynslager c2beb643fa feat: add the prepare plugin hook
BREAKING CHANGE: Committing or creating files in the `publish` plugin hook is not supported anymore and now must be done in the `prepare` hook

Plugins with a `publish` hook that makes a commit or create a file that can be committed must use the `prepare` hook.
2018-02-19 00:28:50 -05:00
Pierre Vanduynslager 20246c02b1 fix: exclude empty env var value from replacement 2018-02-19 00:01:06 -05:00
Pierre Vanduynslager 857d4180e9 fix: allow boolean option to be set in config file 2018-02-16 00:51:58 -05:00
Pierre Vanduynslager 4d0490122c style: lint 2018-02-15 23:19:57 -05:00
Pierre Vanduynslager aa724e80f7 chore(package): simplify xo configuration 2018-02-15 23:19:57 -05:00
greenkeeper[bot]andPierre Vanduynslager ce15b75edf chore(package): update xo to version 0.20.0 2018-02-15 23:19:57 -05:00
Pierre VanduynslagerandGregor Martynus 1966f0e3e2 fix: verify branch first 2018-02-15 18:35:37 +00:00
Pierre Vanduynslager 305f4ee8eb fix: do not transform repositoryUrl if it allow to push
Even the user set Git credentials via environment variable, use the configured URL (with authentication) if it works.
This allow users to push tags and commits via ssh while still using the GitHub/GitLab API.
2018-02-14 10:45:44 -05:00
Pierre Vanduynslager 9788fcad4e feat: use @semantic-release/github as default for success and fail hooks
BREAKING CHANGE: `success` and `fail` hooks are now enabled by default

In order to disable the `@semantic-release/github` plugin for the `success` and `fail` hook, the corresponding options have to be set to `false` in the **semantic-release** configuration:

```json
{
  "release": {
    "success": false,
    "fail": false
  }
}
```

Users who do not use the `@semantic-release/github` plugin, should disable it in the `success` and `fail` by setting the corresponding options to `false` or to alternative plugin providing `success` and `fail` hooks.
2018-02-13 16:33:37 -05:00
Pierre Vanduynslager 04f3061bed fix: remove the github plugin from default success and fail hooks 2018-02-13 16:06:30 -05:00
Pierre Vanduynslager 1b3c51d624 docs: add missing success and fail options 2018-02-13 16:06:30 -05:00
Pierre Vanduynslager 8a2ef48308 test: revert to mockserver:latest docker container 2018-02-12 21:40:47 -05:00
Pierre Vanduynslager ce1e74f611 fix: set repository authentication when repositoryUrl is set as an option 2018-02-12 16:17:09 -05:00
Pierre Vanduynslager b6837a20a8 test: use older version of mockserver
Due to https://github.com/jamesdbloom/mockserver/issues/435
2018-02-12 16:17:09 -05:00
Pierre VanduynslagerandGregor Martynus 97cb354fea feat: improve CLI
- Replace `commander.js` with `yargs`
- Add CLI unit tests
- Add a `--version` option
- Improve `--help` output
- Remove `commander.js` related workaround
- Allow to set list option with arg repetition or space separated list
- Maintain the list options defined as comma separated list
2018-02-12 11:01:09 -08:00
Pierre VanduynslagerandGregor Martynus f92677b092 fix: log current version of semantic-release 2018-02-12 11:01:09 -08:00
Pierre Vanduynslager 9dd127ba98 fix: fix EPLUGINCONF error details 2018-02-12 00:36:06 -05:00
Pierre Vanduynslager c9bd5de075 docs: simplify GitLAb-ci config
- remove Node 4
2018-02-11 22:17:12 -05:00
Pierre Vanduynslager 51f6e5fd60 docs: simplify travis config
- remove OSs
- remove Node 4
2018-02-11 22:17:12 -05:00
Pierre Vanduynslager c6e95b0885 docs: add CircleCI workflows recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager 7fbd2721c4 docs: add Travis Build Stages recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager fcb64e6fef docs: remove unnecessary script override in travis recipe 2018-02-11 22:17:12 -05:00
Pierre Vanduynslager d72cfc2546 docs: fix typo in travis recipe 2018-02-11 22:17:12 -05:00
39 changed files with 1170 additions and 448 deletions
+9 -9
View File
@@ -58,7 +58,7 @@ Good pull requests, whether patches, improvements, or new features, are a fantas
**Please ask first** before embarking on any significant pull requests (e.g. implementing features, refactoring code), otherwise you risk spending a lot of time working on something that the project's developers might not want to merge into the project.
If you never created a pull request before, welcome 🎉 😄. [Here is a great tutorial](https://opensource.guide/how-to-contribute/#opening-a-pull-request) on how to send one :)
If you have never created a pull request before, welcome 🎉 😄. [Here is a great tutorial](https://opensource.guide/how-to-contribute/#opening-a-pull-request) on how to send one :)
Here is a summary of the steps to follow:
@@ -83,7 +83,7 @@ $ git push origin <topic-branch-name>
**Tips**:
- For ambitious tasks, open a Pull Request as soon as possible with the `[WIP]` prefix in the title, in order to get feedback and help from the community.
- [Allow semantic-release maintainers to make changes to you Pull Request branch](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This way, we can rebase it and make some minor changes if necessary. All changes we make will be done in new commit and we'll ask for your approval before merging them.
- [Allow semantic-release maintainers to make changes to your Pull Request branch](https://help.github.com/articles/allowing-changes-to-a-pull-request-branch-created-from-a-fork). This way, we can rebase it and make some minor changes if necessary. All changes we make will be done in new commit and we'll ask for your approval before merging them.
## Coding rules
@@ -91,7 +91,7 @@ $ git push origin <topic-branch-name>
To ensure consistency and quality throughout the source code, all code modifications must have:
- No [linting](#lint) errors
- A [test](#tests) for every possible cases introduced by your code change
- A [test](#tests) for every possible case introduced by your code change
- **100%** test coverage
- [Valid commit message(s)](#commit-message-guidelines)
- Documentation for new features
@@ -102,7 +102,7 @@ To ensure consistency and quality throughout the source code, all code modificat
To ensure consistency and quality, all documentation modifications must:
- Refer to brand in [bold](https://help.github.com/articles/basic-writing-and-formatting-syntax/#styling-text) with proper capitalization, i.e. **GitHub**, **semantic-release**, **npm**
- Prefer [tables](https://help.github.com/articles/organizing-information-with-tables) over [lists](https://help.github.com/articles/basic-writing-and-formatting-syntax/#lists) when listing key values, i.e. List of options with their description
- Use [links](https://help.github.com/articles/basic-writing-and-formatting-syntax/#links) when, the first you are referring to:
- Use [links](https://help.github.com/articles/basic-writing-and-formatting-syntax/#links) when you are referring to:
- a **semantic-release** concept described somewhere else in the documentation, i.e. How to [contribute](CONTRIBUTING.md)
- a third-party product/brand/service, i.e. Integrate with [GitHub](https://github.com)
- an external concept or feature, i.e. Create a [GitHub release](https://help.github.com/articles/creating-releases)
@@ -125,7 +125,7 @@ If possible, make [atomic commits](https://en.wikipedia.org/wiki/Atomic_commit),
- a functional change should be contained in exactly one commit
- a commit should not create an inconsistent state (such as test errors, linting errors, partial fix, feature with documentation etc...)
A complex feature can be broken down into multiple commits as long as each one keep a consistent state and consist of a self-contained change.
A complex feature can be broken down into multiple commits as long as each one maintains a consistent state and consists of a self-contained change.
#### Commit message format
@@ -219,13 +219,13 @@ $ npm install
### Lint
All the [semantic-release](https://github.com/semantic-release) repository use [XO](https://github.com/sindresorhus/xo) for linting and [Prettier](https://prettier.io) for formatting. Prettier formatting will be automatically verified and fixed by XO.
All the [semantic-release](https://github.com/semantic-release) repositories use [XO](https://github.com/sindresorhus/xo) for linting and [Prettier](https://prettier.io) for formatting. Prettier formatting will be automatically verified and fixed by XO.
Before pushing your code changes make sure there is no linting errors with `npm run lint`.
Before pushing your code changes make sure there are no linting errors with `npm run lint`.
**Tips**:
- Most linting errors can be automatically fixed with `npm run lint --fix`.
- Install the [XO plugin](https://github.com/sindresorhus/xo#editor-plugins) for your editor to see linting error directly in your editor and automatically fix them on save.
- Install the [XO plugin](https://github.com/sindresorhus/xo#editor-plugins) for your editor to see linting errors directly in your editor and automatically fix them on save.
### Tests
@@ -246,6 +246,6 @@ $ npm run test
### Commits
All the [semantic-release](https://github.com/semantic-release) repository use [Commitizen](https://github.com/commitizen/cz-cli) to help you create [valid commit messages](#commit-message-guidelines).
All the [semantic-release](https://github.com/semantic-release) repositories use [Commitizen](https://github.com/commitizen/cz-cli) to help you create [valid commit messages](#commit-message-guidelines).
After staging your changes with `git add`, run `npm run cm` to start the interactive commit message CLI.
+7 -6
View File
@@ -48,11 +48,11 @@ This removes the immediate connection between human emotions and version numbers
### Commit message format
**semantic-release** uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publish the release.
**semantic-release** uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publishes the release.
By default **semantic-release** uses [Angular Commit Message Conventions](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines). The commit message format can be changed with the [`preset` or `config` options](docs/usage/configuration.md#options) of the [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer#options) and [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator#options) plugins.
Tools such as [commitizen](https://github.com/commitizen/cz-cli), [commitlint](https://github.com/marionebl/commitlint) or [semantic-git-commit-cli](https://github.com/JPeer264/node-semantic-git-commit-cli) can be used to help contributor and enforce valid commits message.
Tools such as [commitizen](https://github.com/commitizen/cz-cli), [commitlint](https://github.com/marionebl/commitlint) or [semantic-git-commit-cli](https://github.com/JPeer264/node-semantic-git-commit-cli) can be used to help contributors and enforce valid commit messages.
Here is an example of the release type that will be done based on a commit messages:
@@ -82,11 +82,12 @@ After running the tests the command `semantic-release` will execute the followin
|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Verify Conditions | Verify all the conditions to proceed with the release with the [verify conditions plugins](docs/usage/plugins.md#verifyconditions-plugin). |
| Get last release | Obtain the commit corresponding to the last release by analyzing [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). |
| Analyze commits | Determine the type of release to do with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Analyze commits | Determine the type of release with the [analyze commits plugin](docs/usage/plugins.md#analyzecommits-plugin) based on the commits added since the last release. |
| Verify release | Verify the release conformity with the [verify release plugins](docs/usage/plugins.md#verifyrelease-plugin). |
| Generate notes | Generate release notes with the [generate notes plugin](docs/usage/plugins.md#generatenotes-plugin) for the commits added since the last release. |
| Create Git tag | Create a Git tag corresponding the new release version |
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
| Create Git tag | Create a Git tag corresponding to the new release version |
| Prepare | Prepare the release with the [prepare plugins](docs/usage/plugins.md#prepare-plugin). |
| Publish | Publish the release with the [publish plugins](docs/usage/plugins.md#publish-plugin). |
| Notify | Notify of new releases or errors with the [success](docs/usage/plugins.md#success-plugin) and [fail](docs/usage/plugins.md#fail-plugin) plugins. |
## Documentation
+55 -53
View File
@@ -1,64 +1,66 @@
const program = require('commander');
const {pickBy, isUndefined} = require('lodash');
function list(values) {
return values
.split(',')
.map(value => value.trim())
.filter(value => value && value !== 'false');
}
const stringList = {
type: 'string',
array: true,
coerce: values =>
values.length === 1 && values[0].trim() === 'false'
? []
: values.reduce((values, value) => values.concat(value.split(',').map(value => value.trim())), []),
};
module.exports = async () => {
program
.name('semantic-release')
.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>',
'Comma separated list of paths or packages name for the verifyConditions plugin(s)',
list
)
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
.option(
'--verify-release <paths>',
'Comma separated list of paths or packages name for the verifyRelease plugin(s)',
list
)
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
.option('--publish <paths>', 'Comma separated list of paths or packages name for the publish plugin(s)', list)
.option('--success <paths>', 'Comma separated list of paths or packages name for the success plugin(s)', list)
.option('--fail <paths>', 'Comma separated list of paths or packages name for the fail 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',
'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes'
)
.parse(process.argv);
if (program.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
const cli = require('yargs')
.command('$0', 'Run automated package publishing', yargs => {
yargs.demandCommand(0, 0).usage(`Run automated package publishing
Usage:
semantic-release [options] [plugins]`);
})
.option('b', {alias: 'branch', describe: 'Git branch to release from', type: 'string', group: 'Options'})
.option('r', {alias: 'repository-url', describe: 'Git repository URL', type: 'string', group: 'Options'})
.option('t', {alias: 'tag-format', describe: 'Git tag format', type: 'string', group: 'Options'})
.option('e', {alias: 'extends', describe: 'Shareable configurations', ...stringList, group: 'Options'})
.option('ci', {describe: 'Toggle CI verifications', default: undefined, type: 'boolean', group: 'Options'})
.option('verify-conditions', {...stringList, group: 'Plugins'})
.option('analyze-commits', {type: 'string', group: 'Plugins'})
.option('verify-release', {...stringList, group: 'Plugins'})
.option('generate-notes', {type: 'string', group: 'Plugins'})
.option('prepare', {...stringList, group: 'Plugins'})
.option('publish', {...stringList, group: 'Plugins'})
.option('success', {...stringList, group: 'Plugins'})
.option('fail', {...stringList, group: 'Plugins'})
.option('debug', {describe: 'Output debugging information', default: undefined, type: 'boolean', group: 'Options'})
.option('d', {alias: 'dry-run', describe: 'Skip publishing', default: undefined, type: 'boolean', group: 'Options'})
.option('h', {alias: 'help', default: undefined, group: 'Options'})
.option('v', {alias: 'version', default: undefined, group: 'Options'})
.strict(false)
.exitProcess(false);
try {
if (program.args.length > 0) {
program.outputHelp();
process.exitCode = 1;
} else {
const opts = program.opts();
// Set the `noCi` options as commander.js sets the `ci` options instead (because args starts with `--no`)
opts.noCi = opts.ci === false ? true : undefined;
// Remove option with undefined values, as commander.js sets non defined options as `undefined`
await require('.')(pickBy(opts, value => !isUndefined(value)));
// Remove option with undefined values, as yargs sets non defined options as `undefined`
const {help, version, ...opts} = pickBy(cli.argv, value => !isUndefined(value));
if (Boolean(help) || Boolean(version)) {
process.exitCode = 0;
return;
}
// Set the `noCi` options as yargs sets the `ci` options instead (because arg starts with `--no`)
if (opts.ci === false) {
opts.noCi = true;
}
if (opts.debug) {
// Debug must be enabled before other requires in order to work
require('debug').enable('semantic-release:*');
}
await require('.')(opts);
process.exitCode = 0;
} catch (err) {
if (err.name !== 'YError') {
console.error(err);
}
process.exitCode = 1;
}
};
+16 -2
View File
@@ -9,6 +9,7 @@
- [fail](https://github.com/semantic-release/github#fail): Open a GitHub issue when a release fails
- [@semantic-release/npm](https://github.com/semantic-release/npm)
- [verifyConditions](https://github.com/semantic-release/npm#verifyconditions): Verify the presence and the validity of the npm authentication and release configuration
- [prepare](https://github.com/semantic-release/npm#prepare): Update the package.json version and create the npm package tarball
- [publish](https://github.com/semantic-release/npm#publish): Publish the package on the npm registry
## Official plugins
@@ -18,17 +19,30 @@
- [publish](https://github.com/semantic-release/gitlab#publish): Publish a [GitLab release](https://docs.gitlab.com/ce/workflow/releases.html)
- [@semantic-release/git](https://github.com/semantic-release/git)
- [verifyConditions](https://github.com/semantic-release/git#verifyconditions): Verify the presence and the validity of the Git authentication and release configuration
- [publish](https://github.com/semantic-release/git#publish): Push a release commit and tag, including configurable files
- [prepare](https://github.com/semantic-release/git#prepare): Push a release commit and tag, including configurable files
- [@semantic-release/changelog](https://github.com/semantic-release/changelog)
- [verifyConditions](https://github.com/semantic-release/changelog#verifyconditions): Verify the presence and the validity of the configuration
- [publish](https://github.com/semantic-release/changelog#publish): Create or update the changelog file in the local project repository
- [prepare](https://github.com/semantic-release/changelog#prepare): Create or update the changelog file in the local project repository
- [@semantic-release/exec](https://github.com/semantic-release/exec)
- [verifyConditions](https://github.com/semantic-release/exec#verifyconditions): Execute a shell command to verify if the release should happen
- [analyzeCommits](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to determine the type of release
- [verifyRelease](https://github.com/semantic-release/exec#verifyrelease): Execute a shell command to verifying a release that was determined before and is about to be published.
- [generateNotes](https://github.com/semantic-release/exec#analyzecommits): Execute a shell command to generate the release note
- [prepare](https://github.com/semantic-release/exec#prepare): Execute a shell command to prepare the release
- [publish](https://github.com/semantic-release/exec#publish): Execute a shell command to publish the release
- [success](https://github.com/semantic-release/exec#success): Execute a shell command to notify of a new release
- [fail](https://github.com/semantic-release/exec#fail): Execute a shell command to notify of a failed release
## Community plugins
[Open a Pull Request](https://github.com/semantic-release/semantic-release/blob/caribou/CONTRIBUTING.md#submitting-a-pull-request) to add your plugin to the list.
- [semantic-release-docker](https://github.com/felixfbecker/semantic-release-docker) Set of semantic-release plugins for publishing a docker image to Docker Hub (tested on semantic-release **^11.0.0**)
- [verifyConditions](https://github.com/felixfbecker/semantic-release-docker#verifyconditions) Verify that all needed configuration is present and login to the Docker registry.
- [publish](https://github.com/felixfbecker/semantic-release-docker#publish) Tag the image specified by `name` with the new version, push it to Docker Hub and update the latest tag.
- [semantic-release-verify-deps](https://github.com/piercus/semantic-release-verify-deps)
- [verifyConditions](https://github.com/piercus/semantic-release-verify-deps) Check the dependencies format against a regexp before a release
- [semantic-release-chrome](https://github.com/GabrielDuarteM/semantic-release-chrome) Set of semantic-release plugins for publishing a Chrome extension release.
- [verifyConditions](https://github.com/GabrielDuarteM/semantic-release-chrome#verifyconditions) Verify the presence of the authentication (set via environment variables).
- [prepare](https://github.com/GabrielDuarteM/semantic-release-chrome#prepare) Write the correct version to the manifest.json and creates a zip file of the whole dist folder.
- [publish](https://github.com/GabrielDuarteM/semantic-release-chrome#publish) Uploads the generated zip file to the webstore, and publish the item.
+65 -1
View File
@@ -1 +1,65 @@
# CircleCI 2.0 workflows
# Using semantic-release with [CircleCI 2.0 workflows](https://circleci.com/docs/2.0/workflows)
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [CircleCi Project Settings](https://circleci.com/docs/2.0/env-vars/#adding-environment-variables-in-the-app)..
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
## Multiple Node jobs configuration
### `.circleci/config.yml` configuration for multiple Node jobs
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [CircleCI documentation](https://circleci.com/docs/2.0) for additional configuration options.
This example create the workflows `test_node_4`, `test_node_6`, `test_node_8` and `release`. The release workflows will [run `semantic-release` only after the all the `test_node_*` are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
```yaml
version: 2
jobs:
test_node_6:
docker:
- image: circleci/node:6
steps:
# Configure your test steps here (checkout, npm install, cache management, tests etc...)
test_node_8:
docker:
- image: circleci/node:8
steps:
# Configure your test steps here (checkout, npm install, cache management, tests etc...)
release:
docker:
- image: circleci/node:8
steps:
- checkout
- run: npm install
# Run optional required steps before releasing
# - run: npm run build-script
- run: npx semantic-release
workflows:
version: 2
test_and_release:
# Run the test jobs first, then the release only when all the test jobs are successful
jobs:
- test_node_6
- test_node_8
- release:
requires:
- test_node_6
- test_node_8
```
### `package.json` configuration for multiple Node jobs
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
}
}
```
+2 -8
View File
@@ -8,11 +8,11 @@ The [Authentication](../usage/ci-configuration.md#authentication) environment va
GitLab CI supports [Pipelines](https://docs.gitlab.com/ee/ci/pipelines.html) allowing to test on multiple Node versions and publishing a release only when all test pass.
**Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
**Note**: The publish pipeline must run a [Node >= 8 version](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
### `.gitlab-ci.yml` configuration for Node projects
This example is a minimal configuration for **semantic-release** with a build running Node 4, 6 and 8 on Linux. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [GitLab CI - Configuration of your jobs with .gitlab-ci.yml](https://docs.gitlab.com/ee/ci/yaml/README.html) for additional configuration options.
**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.
@@ -25,12 +25,6 @@ stages:
before_script:
- npm install
node:4:
image: node:4
stage: test
script:
- npm test
node:6:
image: node:6
stage: test
+94
View File
@@ -1 +1,95 @@
# Using semantic-release with [Travis CI build stages](https://docs.travis-ci.com/user/build-stages)
## Environment variables
The [Authentication](../usage/ci-configuration.md#authentication) environment variables can be configured in [Travis Repository Settings](https://docs.travis-ci.com/user/environment-variables/#defining-variables-in-repository-Settings) or with the [travis env set CLI](https://github.com/travis-ci/travis.rb#env).
Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with semantic-release-cli](../usage/ci-configuration.md#automatic-setup-with-semantic-release-cli).
## Multiple Node jobs configuration
### `.travis.yml` configuration for multiple Node jobs
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**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).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: node_js
node_js:
- 8
- 6
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
node_js: lts/*
# Advanced: optionally overwrite your default `script` step to skip the tests
# script: skip
deploy:
provider: script
skip_cleanup: true
script:
- npx semantic-release
```
### `package.json` configuration for multiple Node jobs
A `package.json` is required only for [local](../usage/installation.md#local-installation) **semantic-release** installation.
```json
{
"devDependencies": {
"semantic-release": "^12.0.0"
}
}
```
## Non-JavaScript projects configuration
For projects that require to be tested with one or multiple version of a Non-JavaScript [language](https://docs.travis-ci.com/user/languages), optionally on multiple [Operating Systems](https://docs.travis-ci.com/user/multi-os).
This recipe cover the Travis specifics only. See [Non JavaScript projects recipe](../support/FAQ.md#can-i-use-semantic-release-to-publish-non-javascript-packages) for more information on the **semantic-release** configuration.
### `.travis.yml` configuration for non-JavaScript projects
This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go). See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example creates a `release` [build stage](https://docs.travis-ci.com/user/build-stages) that [runs `semantic-release` only after all test jobs are successful](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
**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).
**Advanced configuration**: Running the tests in the `script` step of the `release` stage is not necessary as the previous stage(s) already ran them. To increase speed, the `script` step of the `release` stage can be overwritten to skip the tests. Note that other commands such as build or compilation might still be required.
```yaml
language: go
go:
- 1.6
- 1.7
jobs:
include:
# Define the release stage that runs semantic-release
- stage: release
# Advanced: optionally overwrite your default `script` step to skip the tests
# script:
# - make
deploy:
provider: script
skip_cleanup: true
script:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
- npx semantic-release
```
+5 -22
View File
@@ -10,7 +10,7 @@ Alternatively, the default `NPM_TOKEN` and `GH_TOKEN` can be easily [setup with
For projects that require to be tested only with a single [Node version](https://docs.travis-ci.com/user/getting-started/#Selecting-a-different-programming-language) on [one Operating System](https://docs.travis-ci.com/user/getting-started/#Selecting-infrastructure-(optional)).
**Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--8).
**Note**: [Node 8 is the minimal version required](../support/FAQ.md#why-does-semantic-release-require-node-version--83).
### `.travis.yml` configuration for single Node job
@@ -25,10 +25,6 @@ language: node_js
node_js: 8
script:
# Run tests
- npm run test
deploy:
provider: script
skip_cleanup: true
@@ -56,9 +52,9 @@ For projects that require to be tested with multiple [Node versions](https://doc
### `.travis.yml` configuration for multiple Node jobs
This example is a minimal configuration for **semantic-release** with a build running Node 4, 6 and 8 on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example is a minimal configuration for **semantic-release** with a build running Node 6 and 8. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to command [Run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [Run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
It's recommended to run the `semantic-release` command in the [Travis `deploy` step](https://docs.travis-ci.com/user/customizing-the-build/#The-Build-Lifecycle) so if an error occurs the build will fail and Travis will send a notification.
@@ -70,15 +66,6 @@ language: node_js
node_js:
- 8
- 6
- 4
os:
- linux
- osx
script:
# Run tests
- npm run test
deploy:
provider: script
@@ -110,7 +97,7 @@ This recipe cover the Travis specifics only. See [Non JavaScript projects recipe
### `.travis.yml` configuration for non-JavaScript projects
This example is a minimal configuration for semantic-release with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go) on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example is a minimal configuration for **semantic-release** with a build running [Go 1.6 and 1.7](https://docs.travis-ci.com/user/languages/go) on Linux and OSX. See [Travis - Customizing the Build](https://docs.travis-ci.com/user/customizing-the-build) for additional configuration options.
This example uses [`travis-deploy-once`](https://github.com/semantic-release/travis-deploy-once) in order to [run `semantic-release` only after all tests succeeded](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded). Alternatively you can use [Travis CI Build Stages recipe](travis-build-stages.md).
@@ -129,17 +116,13 @@ os:
- linux
- osx
script:
# Run tests
- go test -v ./...
deploy:
provider: script
skip_cleanup: true
script:
# Use nvm to install and use the Node LTS version (nvm is installed on all Travis images)
- nvm install lts/*
# Run semantic-release only on job, after all other are successful
# Run semantic-release only on one job, after all other are successful
- npx travis-deploy-once "npx semantic-release"
```
+1
View File
@@ -13,6 +13,7 @@
## Articles
- [“Introduction to SemVer” - Irina Gebauer](https://blog.greenkeeper.io/introduction-to-semver-d272990c44f2)
- ["Introduction to Semantic Release" - liv](https://blog.greenkeeper.io/introduction-to-semantic-release-33f73b117c8)
## Tutorials
+31 -2
View File
@@ -57,7 +57,36 @@ To publish a non-Node package (without a `package.json`) you would need to:
- Set **semantic-release** [options](../usage/configuration.md#options) via [CLI arguments or rc file](../usage/configuration.md#configuration)
- Make sure your CI job executing the `semantic-release` command has access to [Node >= 8](#why-does-semantic-release-require-node-version--83) to execute the `semantic-release` command
See the [CI configuration recipes](../usage/ci-configuration.md) for more details on specific CI environments.
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
In addition you will need to configure the **semantic-release** [plugins](../usage/plugins.md#plugins) to:
- Disable the [`@semantic-release/npm`](https://github.com/semantic-release/npm) plugin
- Define plugins for the [verifyConditions](../usage/plugins.md#verifyconditions-plugin), [prepare](../usage/plugins.md#prepare-plugin) and [publish](../usage/plugins.md#publish-plugin) steps to release on your package registry. The [`@semantic-release/exec`](https://github.com/semantic-release/exec) plugin is recommended for situation where a release can be done with a shell command.
Here is a basic example to create [GitHub releases](https://help.github.com/articles/about-releases) and use shell command to publish:
```json
{
"verifyConditions": ["@semantic-release/github"],
"prepare": [
{
"path": "@semantic-release/exec",
"cmd": "set-version ${nextRelease.version}"
}
],
"publish": [
"@semantic-release/github",
{
"path": "@semantic-release/exec",
"cmd": "publish-package"
}
],
}
```
**Note**: This is a theoretical example where the command `set-version` update the project version with the value passed as its first argument and `publish-package` publishes the package to a registry.
See the [package managers and languages recipes](../recipes/README.md#package-managers-and-languages) for more details on specific project types.
## Can I use semantic-release with any CI service?
@@ -65,7 +94,7 @@ Yes, **semantic-release** can be used with any CI service, as long as it provide
- A way to set [authentication](../usage/ci-configuration.md#authentication) via environment variables
- A way to guarantee that the `semantic-release` command is [executed only after all the tests of all the jobs in the CI build pass](../usage/ci-configuration.md#run-semantic-release-only-after-all-tests-succeeded)
See the [CI configuration recipes](../usage/ci-configuration.md) for more details on specific CI environments.
See the [CI configuration recipes](../recipes/README.md#ci-configurations) for more details on specific CI environments.
## Can I run semantic-release on my local machine rather than on a CI server?
+15
View File
@@ -29,3 +29,18 @@ You can recover from that issue by identifying the commit in your branch history
$ git tag -f v<version of the last release> <commit sha1 corresponding to last release>
$ git push -f --tags origin <your release branch>
```
## You do not have permission to publish 'package-name'
When running semantic-release you may encounter the following error:
```
An error occurred while running semantic-release: { Error: Command failed: npm publish ./. --registry https://registry.npmjs.org/
npm ERR! publish Failed PUT 403
npm ERR! code E403
npm ERR! You do not have permission to publish "<package-name>". Are you logged in as the correct user? : <package-name>
```
This message is a little unclear, and might not have anything to with your `NPM_TOKEN` or authentication method. It might instead be related to the package name itself. If there is already a package with the same name as yours or, there is a very close match, it could trigger this error.
Best way to be sure, is to search [npmjs.org](https://www.npmjs.com/)) using your package name. If there is a name conflict, rename your package in your `package.json`
+9 -8
View File
@@ -10,17 +10,18 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
**semantic-release** requires push access to the project Git repository in order to create [Git tags](https://git-scm.com/book/en/v2/Git-Basics-Tagging). The Git authentication can be set with one of the following environment variables:
| Variable | Description |
|------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
| Variable | Description |
|---------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| `GH_TOKEN` or `GITHUB_TOKEN` | A GitHub [personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line). |
| `GL_TOKEN` or `GITLAB_TOKEN` | A GitLab [personal access token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html). |
| `BB_TOKEN` or `BITBUCKET_TOKEN` | A Bitbucket [personal access token](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html). |
| `GIT_CREDENTIALS` | [URL encoded basic HTTP Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication#URL_encoding) credentials). |
`GIT_CREDENTIALS` can be the Git username and password in the format `<username>:<password>` or a token for certain Git providers like [Bitbucket](https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html).
`GIT_CREDENTIALS` must be the Git username and password in the format `<username>:<password>`.
Alternatively the Git authentication can be set up via [SSH keys](../recipes/git-auth-ssh-keys.md).
Most **semantic-release** [plugins](plugins.md) require to set up authentication in order to publish to a package manager registry. The default [npm](https://github.com/semantic-release/npm#environment-variables) and [github](https://github.com/semantic-release/github#environment-variables) plugins require the following environment variables:
Most **semantic-release** [plugins](plugins.md) require setting up authentication in order to publish to a package manager registry. The default [@semantic-release/npm](https://github.com/semantic-release/npm#environment-variables) and [@semantic-release/github](https://github.com/semantic-release/github#environment-variables) plugins require the following environment variables:
| Variable | Description |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -35,7 +36,7 @@ See [CI configuration recipes](../recipes/README.md#ci-configurations) for more
## Automatic setup with `semantic-release-cli`
[`semantic-release-cli`](https://github.com/semantic-release/cli) allow to easily [install](installation.md) **semantic-release** in your Node project and set up the CI configuration:
[`semantic-release-cli`](https://github.com/semantic-release/cli) allows for easy [installation](installation.md) of **semantic-release** in your Node project as well as setting up the CI configuration:
```bash
npm install -g semantic-release-cli
+41 -7
View File
@@ -27,7 +27,7 @@ $ semantic-release
**Note**: CLI arguments take precedence over options configured in the configuration file.
**Note**: Plugins options cannot be defined via CLI arguments and must be defined in the configuration file.
**Note**: Plugin options cannot be defined via CLI arguments and must be defined in the configuration file.
## Options
@@ -37,7 +37,7 @@ Type: `Array`, `String`
CLI arguments: `-e`, `--extends`
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configuration are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
List of modules or file paths containing a [shareable configuration](shareable-configurations.md). If multiple shareable configurations are set, they will be imported in the order defined with each configuration option taking precedence over the options defined in a previous shareable configuration.
**Note**: Options defined via CLI arguments or in the configuration file will take precedence over the ones defined in any shareable configuration.
@@ -63,8 +63,6 @@ The git repository URL.
Any valid git url format is supported (See [Git protocols](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols)).
**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`
@@ -81,7 +79,7 @@ The [Git tag](https://git-scm.com/book/en/v2/Git-Basics-Tagging) format used by
Type: `Boolean`
Default: `false` if running in a CI environment, `false` otherwise
Default: `false` if running in a CI environment, `true` otherwise
CLI arguments: `-d`, `--dry-run`
@@ -95,7 +93,7 @@ Default: `false`
CLI arguments: `--no-ci`
Skip Continuous Integration environment verifications, allowing to make releases from a local machine.
Skip Continuous Integration environment verifications. This allows for making releases from a local machine.
### debug
@@ -105,7 +103,7 @@ Default: `false`
CLI argument: `--debug`
Output debugging information. It can also be enabled by set the `DEBUG` environment variable to `semantic-release`.
Output debugging information. It can also be enabled by setting the `DEBUG` environment variable to `semantic-release:*`.
### verifyConditions
@@ -155,6 +153,18 @@ Define the [generate notes plugin](plugins.md#generatenotes-plugin).
See [Plugins configuration](plugins.md#configuration) for more details.
### prepare
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/npm']`
CLI argument: `--prepare`
Define the list of [prepare plugins](plugins.md#prepare-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### publish
Type: `Array`, `String`, `Object`
@@ -166,3 +176,27 @@ CLI argument: `--publish`
Define the list of [publish plugins](plugins.md#publish-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### success
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--success`
Define the list of [success plugins](plugins.md#success-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
### fail
Type: `Array`, `String`, `Object`
Default: `['@semantic-release/github']`
CLI argument: `--fail`
Define the list of [fail plugins](plugins.md#fail-plugin). Plugins will run in series, in the order defined in the `Array`.
See [Plugins configuration](plugins.md#configuration) for more details.
+5 -5
View File
@@ -2,7 +2,7 @@
## 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 [npx](https://www.npmjs.com/package/npx):
For [Node modules projects](https://docs.npmjs.com/getting-started/creating-node-modules) we recommend installing **semantic-release** locally and running the `semantic-release` command with [npx](https://www.npmjs.com/package/npx):
```bash
$ npm install --save-dev semantic-release
@@ -14,16 +14,16 @@ Then in the CI environment:
$ 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.
**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** directly in the CI environment, also with [npx](https://www.npmjs.com/package/npx):
For other type of projects we recommend installing **semantic-release** directly in the CI environment, also with [npx](https://www.npmjs.com/package/npx):
```bash
$ npx semantic-release
```
**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:** 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.
**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.
+21 -13
View File
@@ -1,56 +1,64 @@
# Plugins
Each [release steps](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured, allowing to support different [commit message format](../../README.md#commit-message-format), release not generator and publishing platforms.
Each [release step](../../README.md#release-steps) is implemented within a plugin or a list of plugins that can be configured. This allows for support of different [commit message formats](../../README.md#commit-message-format), release note generators and publishing platforms.
## Plugin types
### verifyConditions plugin
Plugin responsible for verifying all the conditions to proceed with the release: configuration is correct, authentication token are valid, etc...
Responsible for verifying conditions necessary to proceed with the release: configuration is correct, authentication token are valid, etc...
Default implementation: [npm](https://github.com/semantic-release/npm#verifyconditions) and [github](https://github.com/semantic-release/github#verifyconditions).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#verifyconditions) and [@semantic-release/github](https://github.com/semantic-release/github#verifyconditions).
### analyzeCommits plugin
Plugin responsible for determining the type of the next release (`major`, `minor` or `patch`).
Responsible for determining the type of the next release (`major`, `minor` or `patch`).
Default implementation: [@semantic-release/commit-analyzer](https://github.com/semantic-release/commit-analyzer).
### verifyRelease plugin
Plugin responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) allows to verify that if a release contains breaking changes, its type must be `major`.
Responsible for verifying the parameters (version, type, dist-tag etc...) of the release that is about to be published match certain expectations. For example the [cracks plugin](https://github.com/semantic-release/cracks) is able to verify that if a release contains breaking changes, its type must be `major`.
Default implementation: none.
### generateNotes plugin
Plugin responsible for generating release notes.
Responsible for generating release notes.
Default implementation: [@semantic-release/release-notes-generator](https://github.com/semantic-release/release-notes-generator).
### prepare plugin
Responsible for preparing the release, including:
- Creating or updating files such as `package.json`, `CHANGELOG.md`, documentation or compiled assets.
- Create and push commits
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#prepare).
### publish plugin
Plugin responsible for publishing the release.
Responsible for publishing the release.
Default implementation: [npm](https://github.com/semantic-release/npm#publish) and [github](https://github.com/semantic-release/github#publish).
Default implementation: [@semantic-release/npm](https://github.com/semantic-release/npm#publish) and [@semantic-release/github](https://github.com/semantic-release/github#publish).
### success plugin
Plugin responsible for notifying of a new release.
Responsible for notifying of a new release.
Default implementation: [github](https://github.com/semantic-release/github#success).
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#success).
### fail plugin
Plugin responsible for notifying of a failed release.
Responsible for notifying of a failed release.
Default implementation: [github](https://github.com/semantic-release/github#fail).
Default implementation: [@semantic-release/github](https://github.com/semantic-release/github#fail).
## Configuration
Plugin can be configured by specifying the plugin's module name or file path directly as a `String` or within the `path` key of an `Object`.
Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along the other **semantic-release** [options](configuration.md#options) will apply to all plugins, while the one defined within the plugin `Object` will apply only to this specific plugin.
Plugins specific options can be set similarly to the other **semantic-release** [options](configuration.md#options) or within the plugin `Object`. Plugins options defined along with the other **semantic-release** [options](configuration.md#options) will apply to all plugins. Options defined within the plugin `Object` will apply to that specific plugin.
For example:
```json
+1 -1
View File
@@ -1,5 +1,5 @@
# Shareable configurations
A sharable configuration is an [npm](https://www.npmjs.com/) package that exports a **semantic-release** configuration object. It allows to easily use the same configuration across several projects.
A sharable configuration is an [npm](https://www.npmjs.com/) package that exports a **semantic-release** configuration object. It allows for use of the same configuration across several projects.
The shareable configurations to use can be set with the [extends](configuration.md#extends) option.
+48 -21
View File
@@ -3,6 +3,7 @@ const marked = require('marked');
const TerminalRenderer = require('marked-terminal');
const envCi = require('env-ci');
const hookStd = require('hook-std');
const pkg = require('./package.json');
const hideSensitive = require('./lib/hide-sensitive');
const getConfig = require('./lib/get-config');
const verify = require('./lib/verify');
@@ -10,8 +11,10 @@ const getNextVersion = require('./lib/get-next-version');
const getCommits = require('./lib/get-commits');
const getLastRelease = require('./lib/get-last-release');
const {extractErrors} = require('./lib/utils');
const getGitAuthUrl = require('./lib/get-git-auth-url');
const logger = require('./lib/logger');
const {unshallow, gitHead: getGitHead, tag, push, deleteTag} = require('./lib/git');
const {unshallow, verifyAuth, isBranchUpToDate, gitHead: getGitHead, tag, push} = require('./lib/git');
const getError = require('./lib/get-error');
marked.setOptions({renderer: new TerminalRenderer()});
@@ -21,6 +24,10 @@ async function run(options, plugins) {
if (!isCi && !options.dryRun && !options.noCi) {
logger.log('This run was not triggered in a known CI environment, running in dry-run mode.');
options.dryRun = true;
} else {
// When running on CI, prevent the `git` CLI to prompt for username/password. See #703.
process.env.GIT_ASKPASS = 'echo';
process.env.GIT_TERMINAL_PROMPT = 0;
}
if (isCi && isPr && !options.noCi) {
@@ -28,8 +35,29 @@ async function run(options, plugins) {
return;
}
if (!await verify(options, branch, logger)) {
return;
if (branch !== options.branch) {
logger.log(
`This test run was triggered on the branch ${branch}, while semantic-release is configured to only publish from ${
options.branch
}, therefore a new version won’t be published.`
);
return false;
}
await verify(options);
options.repositoryUrl = await getGitAuthUrl(options);
if (!await isBranchUpToDate(options.branch)) {
logger.log(
"The local branch %s is behind the remote one, therefore a new version won't be published.",
options.branch
);
return false;
}
if (!await verifyAuth(options.repositoryUrl, options.branch)) {
throw getError('EGITNOPERMISSION', {options});
}
logger.log('Run automated release from branch %s', options.branch);
@@ -38,7 +66,7 @@ async function run(options, plugins) {
await plugins.verifyConditions({options, logger}, {settleAll: true});
// Unshallow the repo in order to get all the tags
await unshallow();
await unshallow(options.repositoryUrl);
const lastRelease = await getLastRelease(options.tagFormat, logger);
const commits = await getCommits(lastRelease.gitHead, options.branch, logger);
@@ -71,26 +99,14 @@ async function run(options, plugins) {
logger.log('Call plugin %s', 'generateNotes');
nextRelease.notes = await plugins.generateNotes(generateNotesParam);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
logger.log('Call plugin %s', 'prepare');
await plugins.prepare(
{options, logger, lastRelease, commits, nextRelease},
{
getNextInput: async lastResult => {
const newGitHead = await getGitHead();
// If previous publish plugin has created a commit (gitHead changed)
// If previous prepare plugin has created a commit (gitHead changed)
if (lastResult.nextRelease.gitHead !== newGitHead) {
// Delete the previously created tag
await deleteTag(options.repositoryUrl, nextRelease.gitTag);
// Recreate the tag, referencing the new gitHead
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
nextRelease.gitHead = newGitHead;
// Regenerate the release notes
logger.log('Call plugin %s', 'generateNotes');
@@ -99,11 +115,21 @@ async function run(options, plugins) {
// Call the next publish plugin with the updated `nextRelease`
return {options, logger, lastRelease, commits, nextRelease};
},
// Add nextRelease and plugin properties to published release
transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step}),
}
);
// Create the tag before calling the publish plugins as some require the tag to exists
logger.log('Create tag %s', nextRelease.gitTag);
await tag(nextRelease.gitTag);
await push(options.repositoryUrl, branch);
logger.log('Call plugin %s', 'publish');
const releases = await plugins.publish(
{options, logger, lastRelease, commits, nextRelease},
// Add nextRelease and plugin properties to published release
{transform: (release, step) => ({...(isPlainObject(release) ? release : {}), ...nextRelease, ...step})}
);
await plugins.success(
{options, logger, lastRelease, commits, nextRelease, releases: castArray(releases)},
{settleAll: true}
@@ -140,6 +166,7 @@ async function callFail(plugins, options, error) {
}
module.exports = async opts => {
logger.log(`Running %s version %s`, pkg.name, pkg.version);
const unhook = hookStd({silent: false}, hideSensitive);
try {
const config = await getConfig(opts, logger);
+5 -5
View File
@@ -53,13 +53,13 @@ Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`
Your configuration for the \`tagFormat\` option is \`${stringify(tagFormat)}\`.`,
}),
EPLUGINCONF: ({pluginName, pluginConf}) => ({
message: `The \`${pluginName}\` plugin configuration is invalid.`,
details: `The [${pluginName} plugin configuration](${linkify(
`docs/usage/plugins.md#${toLower(pluginName)}-plugin`
EPLUGINCONF: ({pluginType, pluginConf}) => ({
message: `The \`${pluginType}\` plugin configuration is invalid.`,
details: `The [${pluginType} plugin configuration](${linkify(
`docs/usage/plugins.md#${toLower(pluginType)}-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.
Your configuration for the \`${pluginName}\` plugin is \`${stringify(pluginConf)}\`.`,
Your configuration for the \`${pluginType}\` plugin is \`${stringify(pluginConf)}\`.`,
}),
EPLUGIN: ({pluginName, pluginType}) => ({
message: `A plugin configured in the step ${pluginType} is not a valid semantic-release plugin.`,
+6
View File
@@ -36,6 +36,12 @@ module.exports = {
error: 'ERELEASENOTESOUTPUT',
},
},
prepare: {
default: ['@semantic-release/npm'],
config: {
validator: conf => !conf || (isArray(conf) ? conf : [conf]).every(conf => validatePluginConfig(conf)),
},
},
publish: {
default: ['@semantic-release/npm', '@semantic-release/github'],
config: {
+3 -6
View File
@@ -6,7 +6,6 @@ const debug = require('debug')('semantic-release:config');
const {repoUrl} = require('./git');
const PLUGINS_DEFINITIONS = require('./definitions/plugins');
const plugins = require('./plugins');
const getGitAuthUrl = require('./get-git-auth-url');
module.exports = async (opts, logger) => {
const {config} = (await cosmiconfig('release', {rcExtensions: true}).load(process.cwd())) || {};
@@ -42,12 +41,10 @@ module.exports = async (opts, logger) => {
};
}
const repositoryUrl = (await pkgRepoUrl()) || (await repoUrl());
// Set default options values if not defined yet
options = {
branch: 'master',
repositoryUrl: repositoryUrl ? getGitAuthUrl(repositoryUrl) : repositoryUrl,
repositoryUrl: (await pkgRepoUrl()) || (await repoUrl()),
tagFormat: `v\${version}`,
// Remove `null` and `undefined` options so they can be replaced with default ones
...pickBy(options, option => !isUndefined(option) && !isNull(option)),
@@ -59,6 +56,6 @@ module.exports = async (opts, logger) => {
};
async function pkgRepoUrl() {
const {pkg} = await readPkgUp();
return pkg && pkg.repository ? pkg.repository.url : undefined;
const {pkg} = await readPkgUp({normalize: false});
return pkg && (isPlainObject(pkg.repository) ? pkg.repository.url : pkg.repository);
}
+42 -16
View File
@@ -1,30 +1,56 @@
const {parse, format} = require('url');
const {isUndefined} = require('lodash');
const gitUrlParse = require('git-url-parse');
const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
const GIT_TOKENS = ['GIT_CREDENTIALS', 'GH_TOKEN', 'GITHUB_TOKEN', 'GL_TOKEN', 'GITLAB_TOKEN'];
const GIT_TOKENS = {
GIT_CREDENTIALS: undefined,
GH_TOKEN: undefined,
GITHUB_TOKEN: undefined,
GL_TOKEN: 'gitlab-ci-token:',
GITLAB_TOKEN: 'gitlab-ci-token:',
BB_TOKEN: 'x-token-auth:',
BITBUCKET_TOKEN: 'x-token-auth:',
};
/**
* Generate the git repository URL with creadentials.
* If the `gitCredentials` is defined, returns a http or https URL with Basic Authentication (`https://username:passowrd@hostname:port/path.git`).
* If the `gitCredentials` is undefined, returns the `repositoryUrl`. In that case it's expected for the user to have setup the Git authentication on the CI (for example via SSH keys).
* Determine the the git repository URL to use to push, either:
* - The `repositoryUrl` as is if allowed to push
* - The `repositoryUrl` converted to `https` or `http` with Basic Authentication
*
* @param {String} gitCredentials Basic HTTP Authentication credentials, can be `username:password` or a token for certain Git providers.
* @param {String} repositoryUrl The git repository URL.
* In addition, expand shortcut URLs (`owner/repo` => `https://github.com/owner/repo.git`) and transform `git+https` / `git+http` URLs to `https` / `http`.
*
* @param {String} repositoryUrl The user provided Git repository URL.
* @return {String} The formatted Git repository URL.
*/
module.exports = repositoryUrl => {
const envVar = GIT_TOKENS.find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = ['GL_TOKEN', 'GITLAB_TOKEN'].includes(envVar)
? `gitlab-ci-token:${process.env[envVar]}`
: process.env[envVar];
module.exports = async ({repositoryUrl, branch}) => {
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
const {protocols} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
if (info && info.getDefaultRepresentation() === 'shortcut') {
// Expand shorthand URLs (such as `owner/repo` or `gitlab:owner/repo`)
repositoryUrl = info.https();
} else {
const {protocols} = gitUrlParse(repositoryUrl);
if (!gitCredentials) {
return protocols.includes('https') ? `${gitUrlParse(repositoryUrl).toString(protocol)}.git` : repositoryUrl;
// Replace `git+https` and `git+http` with `https` or `http`
if (protocols.includes('http') || protocols.includes('https')) {
repositoryUrl = format({
...parse(repositoryUrl),
...{protocol: protocols.includes('https') ? 'https' : 'http'},
});
}
}
return format({...parse(`${gitUrlParse(repositoryUrl).toString(protocol)}.git`), ...{auth: gitCredentials}});
// Test if push is allowed without transforming the URL (e.g. is ssh keys are set up)
if (!await verifyAuth(repositoryUrl, branch)) {
const envVar = Object.keys(GIT_TOKENS).find(envVar => !isUndefined(process.env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${process.env[envVar] || ''}`;
const {protocols, ...parsed} = gitUrlParse(repositoryUrl);
const protocol = protocols.includes('https') ? 'https' : protocols.includes('http') ? 'http' : 'https';
// If credentials are set via anvironment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
return gitCredentials ? {...parsed, protocols: [protocol], user: gitCredentials}.toString(protocol) : repositoryUrl;
}
return repositoryUrl;
};
+10 -12
View File
@@ -28,24 +28,22 @@ module.exports = async (tagFormat, logger) => {
// 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 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)))
.map(tag => ({gitTag: tag, version: (tag.match(tagRegexp) || new Array(2))[1]}))
.filter(
tag => tag.version && semver.valid(semver.clean(tag.version)) && !semver.prerelease(semver.clean(tag.version))
)
.sort((a, b) => semver.rcompare(a.version, b.version));
debug('found tags: %o', tags);
if (tags.length > 0) {
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);
const tag = await pLocate(tags, tag => isRefInHistory(tag.gitTag), {concurrency: 1, preserveOrder: true});
return {gitHead: await gitTagHead(gitTag), gitTag, version};
if (tag) {
logger.log('Found git tag %s associated with version %s', tag.gitTag, tag.version);
return {gitHead: await gitTagHead(tag.gitTag), ...tag};
}
logger.log('No git tag version found');
+23 -27
View File
@@ -1,5 +1,5 @@
const execa = require('execa');
const debug = require('debug')('semantic-release:get-version-head');
const debug = require('debug')('semantic-release:git');
/**
* Get the commit sha for a given tag.
@@ -44,9 +44,11 @@ async function isRefInHistory(ref) {
/**
* Unshallow the git repository (retriving every commits and tags).
*
* @param {String} repositoryUrl The remote repository URL.
*/
async function unshallow() {
await execa('git', ['fetch', '--unshallow', '--tags'], {reject: false});
async function unshallow(repositoryUrl) {
await execa('git', ['fetch', '--unshallow', '--tags', repositoryUrl], {reject: false});
}
/**
@@ -81,14 +83,14 @@ async function isGitRepo() {
/**
* Verify the write access authorization to remote repository with push dry-run.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The repositoru branch for which to verify write access.
*
* @return {Boolean} `true` is authorized to push, falsy otherwise.
*/
async function verifyAuth(origin, branch) {
async function verifyAuth(repositoryUrl, branch) {
try {
return (await execa('git', ['push', '--dry-run', origin, `HEAD:${branch}`])).code === 0;
return (await execa('git', ['push', '--dry-run', repositoryUrl, `HEAD:${branch}`])).code === 0;
} catch (err) {
debug(err);
}
@@ -107,34 +109,17 @@ async function tag(tagName) {
/**
* Push to the remote repository.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
}
/**
* Delete a tag locally and remotely.
*
* @param {String} origin The remote repository URL.
* @param {String} tagName The tag name to delete.
*/
async function deleteTag(origin, tagName) {
// Delete the local tag
let shell = await execa('git', ['tag', '-d', tagName], {reject: false});
debug('delete local tag', shell);
// Delete the tag remotely
shell = await execa('git', ['push', '--delete', origin, tagName], {reject: false});
debug('delete remote tag', shell);
async function push(repositoryUrl, branch) {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* 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.
*/
@@ -146,6 +131,17 @@ async function verifyTagName(tagName) {
}
}
/**
* Verify the local branch is up to date with the remote one.
*
* @param {String} branch The repository branch for which to verify status.
*
* @return {Boolean} `true` is the HEAD of the current local branch is the same as the HEAD of the remote branch, `false` otherwise.
*/
async function isBranchUpToDate(branch) {
return isRefInHistory(await execa.stdout('git', ['rev-parse', `${branch}@{u}`]));
}
module.exports = {
gitTagHead,
gitTags,
@@ -157,6 +153,6 @@ module.exports = {
verifyAuth,
tag,
push,
deleteTag,
verifyTagName,
isBranchUpToDate,
};
+4 -1
View File
@@ -1,6 +1,9 @@
const {escapeRegExp} = require('lodash');
const toReplace = Object.keys(process.env).filter(envVar => /token|password|credential|secret|private/i.test(envVar));
const toReplace = Object.keys(process.env).filter(
envVar => /token|password|credential|secret|private/i.test(envVar) && process.env[envVar].trim()
);
const regexp = new RegExp(toReplace.map(envVar => escapeRegExp(process.env[envVar])).join('|'), 'g');
module.exports = output => {
+2 -15
View File
@@ -1,17 +1,15 @@
const {template} = require('lodash');
const AggregateError = require('aggregate-error');
const {isGitRepo, verifyAuth, verifyTagName} = require('./git');
const {isGitRepo, verifyTagName} = require('./git');
const getError = require('./get-error');
module.exports = async (options, branch, logger) => {
module.exports = async options => {
const errors = [];
if (!await isGitRepo()) {
errors.push(getError('ENOGITREPO'));
} else if (!options.repositoryUrl) {
errors.push(getError('ENOREPOURL'));
} else if (!await verifyAuth(options.repositoryUrl, options.branch)) {
errors.push(getError('EGITNOPERMISSION', {options}));
}
// Verify that compiling the `tagFormat` produce a valid Git tag
@@ -29,15 +27,4 @@ module.exports = async (options, branch, logger) => {
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 ${
options.branch
}, therefore a new version won’t be published.`
);
return false;
}
return true;
};
+15 -27
View File
@@ -22,19 +22,19 @@
"@semantic-release/commit-analyzer": "^5.0.0",
"@semantic-release/error": "^2.2.0",
"@semantic-release/github": "^4.1.0",
"@semantic-release/npm": "^3.1.0",
"@semantic-release/npm": "^3.2.0",
"@semantic-release/release-notes-generator": "^6.0.0",
"aggregate-error": "^1.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"cosmiconfig": "^4.0.0",
"debug": "^3.1.0",
"env-ci": "^1.0.0",
"execa": "^0.9.0",
"env-ci": "^2.0.0",
"execa": "^0.10.0",
"get-stream": "^3.0.0",
"git-log-parser": "^1.2.0",
"git-url-parse": "^8.1.0",
"git-url-parse": "^9.0.0",
"hook-std": "^0.4.0",
"hosted-git-info": "^2.6.0",
"lodash": "^4.17.4",
"marked": "^0.3.9",
"marked-terminal": "^2.0.0",
@@ -42,7 +42,8 @@
"p-reduce": "^1.0.0",
"read-pkg-up": "^3.0.0",
"resolve-from": "^4.0.0",
"semver": "^5.4.1"
"semver": "^5.4.1",
"yargs": "^11.0.0"
},
"devDependencies": {
"ava": "^0.25.0",
@@ -52,21 +53,18 @@
"cz-conventional-changelog": "^2.0.0",
"delay": "^2.0.0",
"dockerode": "^2.5.2",
"eslint-config-prettier": "^2.5.0",
"eslint-plugin-prettier": "^2.3.0",
"file-url": "^2.0.2",
"fs-extra": "^5.0.0",
"fs-extra": "^6.0.0",
"got": "^8.0.0",
"js-yaml": "^3.10.0",
"mockserver-client": "^5.1.1",
"nock": "^9.0.2",
"nyc": "^11.2.1",
"p-retry": "^1.0.0",
"prettier": "~1.10.0",
"proxyquire": "^1.8.0",
"sinon": "^4.0.0",
"p-retry": "^2.0.0",
"proxyquire": "^2.0.0",
"sinon": "^5.0.1",
"tempy": "^0.2.1",
"xo": "^0.18.2"
"xo": "^0.20.0"
},
"engines": {
"node": ">=8.3"
@@ -106,10 +104,7 @@
"all": true
},
"prettier": {
"printWidth": 120,
"singleQuote": true,
"bracketSpacing": false,
"trailingComma": "es5"
"printWidth": 120
},
"publishConfig": {
"tag": "next"
@@ -130,14 +125,7 @@
"test": "nyc ava -v"
},
"xo": {
"extends": [
"prettier"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": 2
}
"prettier": true,
"space": true
}
}
+237
View File
@@ -0,0 +1,237 @@
import test from 'ava';
import proxyquire from 'proxyquire';
import clearModule from 'clear-module';
import {stub} from 'sinon';
// Save the current process.env and process.argv
const envBackup = Object.assign({}, process.env);
const argvBackup = Object.assign({}, process.argv);
test.beforeEach(t => {
clearModule('yargs');
t.context.logs = '';
t.context.errors = '';
t.context.stdout = stub(process.stdout, 'write').callsFake(val => {
t.context.logs += val.toString();
});
t.context.stderr = stub(process.stderr, 'write').callsFake(val => {
t.context.errors += val.toString();
});
});
test.afterEach.always(t => {
process.env = envBackup;
process.argv = argvBackup;
t.context.stdout.restore();
t.context.stderr.restore();
delete process.exitCode;
});
test.serial('Pass options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'-b',
'master',
'-r',
'https://github/com/owner/repo.git',
'-t',
`v\${version}`,
'-e',
'config1',
'config2',
'--verify-conditions',
'condition1',
'condition2',
'--analyze-commits',
'analyze',
'--verify-release',
'verify1',
'verify2',
'--generate-notes',
'notes',
'--prepare',
'prepare1',
'prepare2',
'--publish',
'publish1',
'publish2',
'--success',
'success1',
'success2',
'--fail',
'fail1',
'fail2',
'--debug',
'-d',
];
await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.deepEqual(run.args[0][0].verifyConditions, ['condition1', 'condition2']);
t.is(run.args[0][0].analyzeCommits, 'analyze');
t.deepEqual(run.args[0][0].verifyRelease, ['verify1', 'verify2']);
t.is(run.args[0][0].generateNotes, 'notes');
t.deepEqual(run.args[0][0].prepare, ['prepare1', 'prepare2']);
t.deepEqual(run.args[0][0].publish, ['publish1', 'publish2']);
t.deepEqual(run.args[0][0].success, ['success1', 'success2']);
t.deepEqual(run.args[0][0].fail, ['fail1', 'fail2']);
t.is(run.args[0][0].debug, true);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
});
test.serial('Pass options to semantic-release API with alias arguments', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'--branch',
'master',
'--repository-url',
'https://github/com/owner/repo.git',
'--tag-format',
`v\${version}`,
'--extends',
'config1',
'config2',
'--dry-run',
];
await cli();
t.is(run.args[0][0].branch, 'master');
t.is(run.args[0][0].repositoryUrl, 'https://github/com/owner/repo.git');
t.is(run.args[0][0].tagFormat, `v\${version}`);
t.deepEqual(run.args[0][0].extends, ['config1', 'config2']);
t.is(run.args[0][0].dryRun, true);
t.is(process.exitCode, 0);
});
test.serial('Pass unknown options to semantic-release API', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = [
'',
'',
'--bool',
'--first-option',
'value1',
'--second-option',
'value2',
'--second-option',
'value3',
];
await cli();
t.is(run.args[0][0].bool, true);
t.is(run.args[0][0].firstOption, 'value1');
t.deepEqual(run.args[0][0].secondOption, ['value2', 'value3']);
t.is(process.exitCode, 0);
});
test.serial('Pass empty Array to semantic-release API for list option set to "false"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--publish', 'false'];
await cli();
t.deepEqual(run.args[0][0].publish, []);
t.is(process.exitCode, 0);
});
test.serial('Do not set properties in option for which arg is not in command line', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '-b', 'master'];
await cli();
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'ci'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'd'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'dry-run'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'debug'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 'r'));
t.false(Object.prototype.hasOwnProperty.call(run.args[0][0], 't'));
});
test.serial('Set "noCi" options to "true" with "--no-ci"', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--no-ci'];
await cli();
t.is(run.args[0][0].noCi, true);
t.is(process.exitCode, 0);
});
test.serial('Display help', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--help'];
await cli();
t.regex(t.context.logs, /Run automated package publishing/);
t.is(process.exitCode, 0);
});
test.serial('Returns error code and prints help if called with a command', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', 'pre'];
await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
});
test.serial('Return error code if multiple plugin are set for single plugin', async t => {
const run = stub().resolves(true);
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', '', '--analyze-commits', 'analyze1', 'analyze2'];
await cli();
t.regex(t.context.errors, /Run automated package publishing/);
t.regex(t.context.errors, /Too many non-option arguments/);
t.is(process.exitCode, 1);
});
test.serial('Return error code if semantic-release throw error', async t => {
const run = stub().rejects(new Error('semantic-release error'));
const cli = proxyquire('../cli', {'.': run});
process.argv = ['', ''];
await cli();
t.regex(t.context.errors, /semantic-release error/);
t.is(process.exitCode, 1);
});
+11
View File
@@ -46,6 +46,17 @@ test('The "generateNotes" plugin, if defined, must be a single plugin definition
t.true(plugins.generateNotes.config.validator(() => {}));
});
test('The "prepare" plugin, if defined, must be a single or an array of plugins definition', t => {
t.false(plugins.verifyRelease.config.validator({}));
t.false(plugins.verifyRelease.config.validator({path: null}));
t.true(plugins.verifyRelease.config.validator({path: 'plugin-path.js'}));
t.true(plugins.verifyRelease.config.validator());
t.true(plugins.verifyRelease.config.validator('plugin-path.js'));
t.true(plugins.verifyRelease.config.validator(() => {}));
t.true(plugins.verifyRelease.config.validator([{path: 'plugin-path.js'}, 'plugin-path.js', () => {}]));
});
test('The "publish" plugin is mandatory, and must be a single or an array of plugins definition', t => {
t.false(plugins.publish.config.validator({}));
t.false(plugins.publish.config.validator({path: null}));
+21 -21
View File
@@ -31,12 +31,12 @@ test.afterEach.always(() => {
});
test.serial('Default values, reading repositoryUrl from package.json', async t => {
const pkg = {repository: 'git@package.com:owner/module.git'};
const pkg = {repository: 'https://host.null/owner/package.git'};
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
await gitAddConfig('remote.origin.url', 'git@host.null:owner/repo.git');
// Create package.json in repository root
await outputJson('./package.json', pkg);
@@ -44,7 +44,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.repositoryUrl, 'https://host.null/owner/package.git');
t.is(options.tagFormat, `v\${version}`);
});
@@ -52,18 +52,18 @@ test.serial('Default values, reading repositoryUrl from repo if not set in packa
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Add remote.origin.url config
await gitAddConfig('remote.origin.url', 'git@repo.com:owner/module.git');
await gitAddConfig('remote.origin.url', 'https://host.null/owner/module.git');
const {options} = await t.context.getConfig();
// Verify the default options are set
t.is(options.branch, 'master');
t.is(options.repositoryUrl, 'git@repo.com:owner/module.git');
t.is(options.repositoryUrl, 'https://host.null/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: 'https://hostname.com/owner/module.git'};
const pkg = {repository: 'https://host.null/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
@@ -73,7 +73,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.repositoryUrl, 'https://host.null/owner/module.git');
t.is(options.tagFormat, `v\${version}`);
});
@@ -82,7 +82,7 @@ test.serial('Read options from package.json', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -103,7 +103,7 @@ test.serial('Read options from .releaserc.yml', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -124,7 +124,7 @@ test.serial('Read options from .releaserc.json', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -145,7 +145,7 @@ test.serial('Read options from .releaserc.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -166,7 +166,7 @@ test.serial('Read options from release.config.js', async t => {
const release = {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -194,7 +194,7 @@ test.serial('Prioritise CLI/API parameters over file configuration and git repo'
repositoryUrl: 'http://cli-url.com/owner/package',
tagFormat: `cli\${version}`,
};
const pkg = {release, repository: 'git@hostname.com:owner/module.git'};
const pkg = {release, repository: 'git@host.null:owner/module.git'};
// Create a git repository, set the current working directory at the root of the repo
const repo = await gitRepo();
await gitCommits(['First']);
@@ -217,7 +217,7 @@ test.serial('Read configuration from file path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -245,7 +245,7 @@ test.serial('Read configuration from module path in "extends"', async t => {
analyzeCommits: {path: 'analyzeCommits', param: 'analyzeCommits_param'},
generateNotes: 'generateNotes',
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -273,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: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
@@ -318,7 +318,7 @@ test.serial('Prioritize configuration from config file over "extends"', async t
generateNotes: 'generateNotesShareable',
publish: [{path: 'publishShareable', param: 'publishShareable_param'}],
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
tagFormat: `v\${version}`,
};
@@ -346,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: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const release = {
extends: './shareable1.json',
@@ -359,7 +359,7 @@ test.serial('Prioritize configuration from cli/API options over "extends"', asyn
generateNotes: 'generateNotesShareable1',
publish: [{path: 'publishShareable', param: 'publishShareable_param1'}],
branch: 'test_branch1',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable2 = {
analyzeCommits: 'analyzeCommits2',
@@ -388,7 +388,7 @@ test.serial('Allow to unset properties defined in shareable config with "null"',
extends: './shareable.json',
analyzeCommits: null,
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
@@ -422,7 +422,7 @@ test.serial('Allow to unset properties defined in shareable config with "undefin
extends: './shareable.json',
analyzeCommits: undefined,
branch: 'test_branch',
repositoryUrl: 'https://hostname.com/owner/module.git',
repositoryUrl: 'https://host.null/owner/module.git',
};
const shareable = {
generateNotes: 'generateNotes',
+134 -44
View File
@@ -1,105 +1,195 @@
import test from 'ava';
import getAuthUrl from '../lib/get-git-auth-url';
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(() => {
// Restore process.env
process.env = {};
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 process.env.BB_TOKEN;
delete process.env.BITBUCKET_TOKEN;
process.env.GIT_ASKPASS = 'echo';
process.env.GIT_TERMINAL_PROMPT = 0;
});
test.afterEach.always(() => {
// Restore process.env
process.env = envBackup;
// Restore the current working directory
process.chdir(cwd);
});
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 "git" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'git@host.null: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 same "https" formatted URL if "gitCredentials" is not defined', async t => {
t.is(await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}), 'https://host.null/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');
async t => {
t.is(await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}), 'https://host.null/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('Do not add trailing ".git" if not present in the origian URL', async t => {
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo'}), 'git@host.null:owner/repo');
});
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('Handle "https" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://host.null/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');
test.serial('Handle "git" URL with group and subgroup', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'git@host.null:group/subgroup/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
test.serial('Convert shorthand URL', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'semanitc-release/semanitc-release'}),
'https://github.com/semanitc-release/semanitc-release.git'
);
});
test.serial('Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
test.serial('Convert GitLab shorthand URL', async t => {
t.is(
await getAuthUrl({repositoryUrl: 'gitlab:semanitc-release/semanitc-release'}),
'https://gitlab.com/semanitc-release/semanitc-release.git'
);
});
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
t => {
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+https://host.com/owner/repo.git'), 'https://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "http" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "https" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+https" URL',
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git+https://host.null/owner/repo.git'}),
'https://user:pass@host.null/owner/repo.git'
);
}
);
test.serial(
'Return the "http" formatted URL if "gitCredentials" is defined and repositoryUrl is a "git+http" URL',
t => {
async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(getAuthUrl('git+http://host.com/owner/repo.git'), 'http://user:pass@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git+http://host.null/owner/repo.git'}),
'http://user:pass@host.null/owner/repo.git'
);
}
);
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GH_TOKEN"', async t => {
process.env.GH_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITHUB_TOKEN"', async t => {
process.env.GITHUB_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://token@host.com/owner/repo.git');
t.is(await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}), 'https://token@host.null/owner/repo.git');
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GL_TOKEN"', async t => {
process.env.GL_TOKEN = 'token';
t.is(getAuthUrl('git@host.com:owner/repo.git'), 'https://gitlab-ci-token:token@host.com/owner/repo.git');
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', t => {
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "GITLAB_TOKEN"', async t => {
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'
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://gitlab-ci-token:token@host.null/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', t => {
process.env.GIT_CREDENTIALS = 'user:pass';
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BB_TOKEN"', async t => {
process.env.BB_TOKEN = 'token';
t.is(
getAuthUrl('git@host.com:group/subgroup/owner/repo.git'),
'https://user:pass@host.com/group/subgroup/owner/repo.git'
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Return the "https" formatted URL if "gitCredentials" is defined with "BITBUCKET_TOKEN"', async t => {
process.env.BITBUCKET_TOKEN = 'token';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:owner/repo.git'}),
'https://x-token-auth:token@host.null/owner/repo.git'
);
});
test.serial('Handle "https" URL with group and subgroup, with "GIT_CREDENTIALS"', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'https://host.null/group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Handle "git" URL with group and subgroup, with "GIT_CREDENTIALS', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
t.is(
await getAuthUrl({repositoryUrl: 'git@host.null:group/subgroup/owner/repo.git'}),
'https://user:pass@host.null/group/subgroup/owner/repo.git'
);
});
test.serial('Do not add git credential to repositoryUrl if push is allowed', async t => {
process.env.GIT_CREDENTIALS = 'user:pass';
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
t.is(await getAuthUrl({repositoryUrl}), repositoryUrl);
});
+40 -1
View File
@@ -17,7 +17,7 @@ test.afterEach.always(() => {
process.chdir(cwd);
});
test.serial('Get the highest valid tag', async t => {
test.serial('Get the highest non-prerelease valid tag', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
// Create some commits and tags
@@ -29,6 +29,8 @@ test.serial('Get the highest valid tag', async t => {
await gitTagVersion('v1.0.0');
await gitCommits(['Fourth']);
await gitTagVersion('v3.0');
await gitCommits(['Fifth']);
await gitTagVersion('v3.0.0-beta.1');
const result = await getLastRelease(`v\${version}`, t.context.logger);
@@ -60,6 +62,19 @@ test.serial('Get the highest tag in the history of the current branch', async t
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'v2.0.0', version: '2.0.0'});
});
test.serial('Match the tag name from the begining of the string', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const commits = await gitCommits(['First']);
await gitTagVersion('prefix/v1.0.0');
await gitTagVersion('prefix/v2.0.0');
await gitTagVersion('other-prefix/v3.0.0');
const result = await getLastRelease(`prefix/v\${version}`, t.context.logger);
t.deepEqual(result, {gitHead: commits[0].hash, gitTag: 'prefix/v2.0.0', version: '2.0.0'});
});
test.serial('Return empty object if no valid tag is found', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
@@ -77,6 +92,23 @@ test.serial('Return empty object if no valid tag is found', async t => {
t.is(t.context.log.args[0][0], 'No git tag version found');
});
test.serial('Return empty object if no valid tag is found in history', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
await gitCommits(['First']);
await gitCheckout('other-branch');
await gitCommits(['Second']);
await gitTagVersion('v1.0.0');
await gitTagVersion('v2.0.0');
await gitTagVersion('v3.0.0');
await gitCheckout('master', false);
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();
@@ -117,4 +149,11 @@ test.serial('Get the highest valid tag corresponding to the "tagFormat"', async
gitTag: '2.0.0-1.0.0-bar.1',
version: '1.0.0',
});
await gitTagVersion('3.0.0-bar.1');
t.deepEqual(await getLastRelease(`\${version}-bar.1`, t.context.logger), {
gitHead,
gitTag: '3.0.0-bar.1',
version: '3.0.0',
});
});
+36 -16
View File
@@ -10,8 +10,8 @@ import {
push,
gitTags,
isGitRepo,
deleteTag,
verifyTagName,
isBranchUpToDate,
} from '../lib/git';
import {
gitRepo,
@@ -23,6 +23,8 @@ import {
gitAddConfig,
gitCommitTag,
gitRemoteTagHead,
push as pushUtil,
reset,
} from './helpers/git-utils';
// Save the current working diretory
@@ -62,7 +64,7 @@ test.serial('Unshallow repository', async t => {
// Verify the shallow clone contains only one commit
t.is((await gitGetCommits()).length, 1);
await unshallow();
await unshallow(repo);
// Verify the shallow clone contains all the commits
t.is((await gitGetCommits()).length, 2);
@@ -70,10 +72,10 @@ test.serial('Unshallow repository', async t => {
test.serial('Do not throw error when unshallow a complete repository', async t => {
// Create a git repository, set the current working directory at the root of the repo
await gitRepo();
const repo = await gitRepo();
// Add commits to the master branch
await gitCommits(['First']);
await t.notThrows(unshallow());
await t.notThrows(unshallow(repo));
});
test.serial('Verify if the commit `sha` is in the direct history of the current branch', async t => {
@@ -139,18 +141,6 @@ test.serial('Add tag on head commit', async t => {
await t.is(await gitCommitTag(commits[0].hash), 'tag_name');
});
test.serial('Delete a tag', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
await gitCommits(['Test commit']);
await tag('tag_name');
await push(repo, 'master');
await deleteTag(repo, 'tag_name');
t.falsy(await gitTagHead('tag_name'));
t.falsy(await gitRemoteTagHead(repo, 'tag_name'));
});
test.serial('Push tag and commit to remote repository', async t => {
// Create a git repository with a remote, set the current working directory at the root of the repo
const repo = await gitRepo(true);
@@ -196,3 +186,33 @@ test.serial('Throws error if obtaining the tags fails', async t => {
await t.throws(gitTags());
});
test.serial('Return "true" if repository is up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
t.true(await isBranchUpToDate('master'));
});
test.serial('Return falsy if repository is not up to date', async t => {
await gitRepo(true);
await gitCommits(['First']);
await gitCommits(['Second']);
await pushUtil();
t.true(await isBranchUpToDate('master'));
await reset();
t.falsy(await isBranchUpToDate('master'));
});
test.serial('Return "true" if local repository is ahead', async t => {
await gitRepo(true);
await gitCommits(['First']);
await pushUtil();
await gitCommits(['Second']);
t.true(await isBranchUpToDate('master'));
});
+28 -22
View File
@@ -46,16 +46,16 @@ export async function gitRepo(withRemote, branch = 'master') {
* - Create an initial commits
* - Push to origin
*
* @param {String} origin The URL of the bare repository.
* @param {String} repositoryUrl The URL of the bare repository.
* @param {String} [branch='master'] the branch to initialize.
*/
export async function initBareRepo(origin, branch = 'master') {
export async function initBareRepo(repositoryUrl, branch = 'master') {
const clone = tempy.directory();
await execa('git', ['clone', '--no-hardlinks', origin, clone]);
await execa('git', ['clone', '--no-hardlinks', repositoryUrl, clone]);
process.chdir(clone);
await gitCheckout(branch);
await gitCommits(['Initial commit']);
await execa('git', ['push', origin, branch]);
await execa('git', ['push', repositoryUrl, branch]);
}
/**
@@ -98,7 +98,7 @@ export async function gitGetCommits(from) {
* Checkout a branch on the current git repository.
*
* @param {String} branch Branch name.
* @param {boolean} create `true` to create the branche ans switch, `false` to only switch.
* @param {Boolean} create `true` to create the branche ans switch, `false` to only switch.
*/
export async function gitCheckout(branch, create = true) {
await execa('git', create ? ['checkout', '-b', branch] : ['checkout', branch]);
@@ -121,39 +121,36 @@ export async function gitTagVersion(tagName, sha) {
await execa('git', sha ? ['tag', '-f', tagName, sha] : ['tag', tagName]);
}
export async function gitRemoteTagVersion(origin, tagName, sha = 'HEAD') {
await execa('git', ['push', origin, `${sha}:refs/tags/${tagName}`]);
}
/**
* Create a shallow clone of a git repository and change the current working directory to the cloned repository root.
* The shallow will contain a limited number of commit and no tags.
*
* @param {String} origin The path of the repository to clone.
* @param {String} repositoryUrl The path of the repository to clone.
* @param {String} [branch='master'] the branch to clone.
* @param {Number} [depth=1] The number of commit to clone.
* @return {String} The path of the cloned repository.
*/
export async function gitShallowClone(origin, branch = 'master', depth = 1) {
export async function gitShallowClone(repositoryUrl, branch = 'master', depth = 1) {
const dir = tempy.directory();
process.chdir(dir);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, origin, dir]);
await execa('git', ['clone', '--no-hardlinks', '--no-tags', '-b', branch, '--depth', depth, repositoryUrl, dir]);
return dir;
}
/**
* Create a git repo with a detached head from another git repository and change the current working directory to the new repository root.
*
* @param {String} origin The path of the repository to clone.
* @param {Number} head A commit sha of the origin repo that will become the detached head of the new one.
* @param {String} repositoryUrl The path of the repository to clone.
* @param {Number} head A commit sha of the remote repo that will become the detached head of the new one.
* @return {String} The path of the new repository.
*/
export async function gitDetachedHead(origin, head) {
export async function gitDetachedHead(repositoryUrl, head) {
const dir = tempy.directory();
process.chdir(dir);
await execa('git', ['init']);
await execa('git', ['remote', 'add', 'origin', origin]);
await execa('git', ['remote', 'add', 'origin', repositoryUrl]);
await execa('git', ['fetch']);
await execa('git', ['checkout', head]);
return dir;
@@ -183,12 +180,12 @@ export async function gitTagHead(tagName) {
/**
* Get the first commit sha referenced by the tag `tagName` in the remote repository.
*
* @param {String} origin The repository remote URL.
* @param {String} repositoryUrl The repository remote URL.
* @param {String} tagName The tag name to seach for.
* @return {String} The sha of the commit associated with `tagName` on the remote repository.
*/
export async function gitRemoteTagHead(origin, tagName) {
return (await execa.stdout('git', ['ls-remote', '--tags', origin, tagName]))
export async function gitRemoteTagHead(repositoryUrl, tagName) {
return (await execa.stdout('git', ['ls-remote', '--tags', repositoryUrl, tagName]))
.split('\n')
.filter(tag => Boolean(tag))
.map(tag => tag.match(/^(\S+)/)[1])[0];
@@ -208,10 +205,19 @@ export async function gitCommitTag(gitHead) {
/**
* Push to the remote repository.
*
* @param {String} origin The remote repository URL.
* @param {String} repositoryUrl The remote repository URL.
* @param {String} branch The branch to push.
* @throws {Error} if the push failed.
*/
export async function push(origin, branch) {
await execa('git', ['push', '--tags', origin, `HEAD:${branch}`]);
export async function push(repositoryUrl = 'origin', branch = 'master') {
await execa('git', ['push', '--tags', repositoryUrl, `HEAD:${branch}`]);
}
/**
* Reset repository to a commit.
*
* @param {String} [commit='HEAD~1'] Commit reference to reset the repo to.
*/
export async function reset(commit = 'HEAD~1') {
await execa('git', ['reset', commit]);
}
+15
View File
@@ -40,3 +40,18 @@ test.serial('Accept "undefined" input', t => {
test.serial('Return same string if no environment variable has to be replaced', t => {
t.is(require('../lib/hide-sensitive')('test'), 'test');
});
test.serial('Exclude empty environment variables from the regexp', t => {
process.env.SOME_PASSWORD = 'password';
process.env.SOME_TOKEN = '';
t.is(
require('../lib/hide-sensitive')(`https://user:${process.env.SOME_PASSWORD}@host.com?token=`),
'https://user:[secure]@host.com?token='
);
});
test.serial('Exclude empty environment variables from the regexp if there is only empty ones', t => {
process.env.SOME_PASSWORD = '';
process.env.SOME_TOKEN = ' \n ';
t.is(require('../lib/hide-sensitive')(`https://host.com?token=`), 'https://host.com?token=');
});
+87 -22
View File
@@ -14,6 +14,7 @@ import {
gitRemoteTagHead,
push,
gitShallowClone,
reset,
} from './helpers/git-utils';
// Save the current process.env
@@ -57,6 +58,7 @@ test.serial('Plugins are called with expected values', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
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'};
@@ -67,6 +69,7 @@ test.serial('Plugins are called with expected values', async t => {
const verifyRelease = stub().resolves();
const generateNotes = stub().resolves(notes);
const release1 = {name: 'Release 1', url: 'https://release1.com'};
const prepare = stub().resolves();
const publish1 = stub().resolves(release1);
const success = stub().resolves();
@@ -77,6 +80,7 @@ test.serial('Plugins are called with expected values', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare,
publish: [publish1, pluginNoop],
success,
};
@@ -119,6 +123,15 @@ test.serial('Plugins are called with expected values', async t => {
t.deepEqual(generateNotes.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(prepare.callCount, 1);
t.deepEqual(prepare.args[0][0], config);
t.deepEqual(prepare.args[0][1].options, options);
t.deepEqual(prepare.args[0][1].logger, t.context.logger);
t.deepEqual(prepare.args[0][1].lastRelease, lastRelease);
t.deepEqual(prepare.args[0][1].commits[0].hash, commits[0].hash);
t.deepEqual(prepare.args[0][1].commits[0].message, commits[0].message);
t.deepEqual(prepare.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][0], config);
t.deepEqual(publish1.args[0][1].options, options);
@@ -151,6 +164,7 @@ test.serial('Use custom tag format', async t => {
await gitCommits(['First']);
await gitTagVersion('test-1.0.0');
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'test-2.0.0'};
const notes = 'Release notes';
@@ -161,6 +175,7 @@ test.serial('Use custom tag format', async t => {
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -177,7 +192,7 @@ test.serial('Use custom tag format', async t => {
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 => {
test.serial('Use new gitHead, and recreate release notes if a prepare 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);
// Add commits to the master branch
@@ -186,15 +201,17 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
const generateNotes = stub().resolves(notes);
const publish1 = stub().callsFake(async () => {
const prepare1 = stub().callsFake(async () => {
commits = (await gitCommits(['Third'])).concat(commits);
});
const publish2 = stub().resolves();
const prepare2 = stub().resolves();
const publish = stub().resolves();
const options = {
branch: 'master',
@@ -203,7 +220,8 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
analyzeCommits: stub().resolves(nextRelease.type),
verifyRelease: stub().resolves(),
generateNotes,
publish: [publish1, publish2],
prepare: [prepare1, prepare2],
publish,
success: stub().resolves(),
fail: stub().resolves(),
};
@@ -217,14 +235,17 @@ test.serial('Use new gitHead, and recreate release notes if a publish plugin cre
t.is(generateNotes.callCount, 2);
t.deepEqual(generateNotes.args[0][1].nextRelease, nextRelease);
t.is(publish1.callCount, 1);
t.deepEqual(publish1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(prepare1.callCount, 1);
t.deepEqual(prepare1.args[0][1].nextRelease, {...nextRelease, ...{notes}});
nextRelease.gitHead = await getGitHead();
t.deepEqual(generateNotes.secondCall.args[1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish2.callCount, 1);
t.deepEqual(publish2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.deepEqual(generateNotes.args[1][1].nextRelease, {...nextRelease, ...{notes}});
t.is(prepare2.callCount, 1);
t.deepEqual(prepare2.args[0][1].nextRelease, {...nextRelease, ...{notes}});
t.is(publish.callCount, 1);
t.deepEqual(publish.args[0][1].nextRelease, {...nextRelease, ...{notes}});
// Verify the tag has been created on the local and remote repo and reference the last gitHead
t.is(await gitTagHead(nextRelease.gitTag), commits[0].hash);
@@ -240,6 +261,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -258,6 +280,7 @@ test.serial('Call all "success" plugins even if one errors out', async t => {
verifyConditions: [verifyConditions1, verifyConditions2],
analyzeCommits,
generateNotes,
prepare: stub().resolves(),
publish,
success: [success1, success2],
};
@@ -286,6 +309,7 @@ test.serial('Log all "verifyConditions" errors', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
const error1 = new Error('error 1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -302,9 +326,9 @@ test.serial('Log all "verifyConditions" errors', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2, error3]);
t.deepEqual(errors, [error1, error2, error3]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 2', 'ERR2']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 3', 'ERR3']);
t.deepEqual(t.context.error.args[t.context.error.args.length - 1], [
@@ -328,6 +352,7 @@ test.serial('Log all "verifyRelease" errors', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -345,9 +370,9 @@ test.serial('Log all "verifyRelease" errors', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual(errors, [error1, error2]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
t.is(fail.callCount, 1);
@@ -364,6 +389,7 @@ test.serial('Dry-run skips publish and success', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -383,6 +409,7 @@ test.serial('Dry-run skips publish and success', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
};
@@ -411,6 +438,7 @@ test.serial('Dry-run skips fail', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const error1 = new SemanticReleaseError('error 1', 'ERR1');
const error2 = new SemanticReleaseError('error 2', 'ERR2');
@@ -428,9 +456,9 @@ test.serial('Dry-run skips fail', async t => {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
const errors = await t.throws(semanticRelease(options));
const errors = [...(await t.throws(semanticRelease(options)))];
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual(errors, [error1, error2]);
t.deepEqual(t.context.log.args[t.context.log.args.length - 2], ['%s error 1', 'ERR1']);
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], ['%s error 2', 'ERR2']);
t.is(fail.callCount, 0);
@@ -445,6 +473,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -464,6 +493,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
@@ -475,7 +505,7 @@ test.serial('Force a dry-run if not on a CI and "noCi" is not explicitly set', a
});
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(t.context.log.args[1][0], 'This run was not triggered in a known CI environment, running in dry-run mode.');
t.is(verifyConditions.callCount, 1);
t.is(analyzeCommits.callCount, 1);
t.is(verifyRelease.callCount, 1);
@@ -493,6 +523,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const nextRelease = {type: 'major', version: '2.0.0', gitHead: await getGitHead(), gitTag: 'v2.0.0'};
const notes = 'Release notes';
@@ -512,6 +543,7 @@ test.serial('Allow local releases with "noCi" option', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success,
fail: stub().resolves(),
@@ -545,6 +577,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
commits = (await gitCommits(['Second'])).concat(commits);
await push();
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'};
@@ -560,6 +593,7 @@ test.serial('Accept "undefined" value returned by the "generateNotes" plugins',
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
@@ -596,11 +630,32 @@ test.serial('Returns falsy value if triggered by a PR', async t => {
t.falsy(await semanticRelease({repositoryUrl}));
t.is(
t.context.log.args[8][0],
t.context.log.args[t.context.log.args.length - 1][0],
"This run was triggered by a pull request and therefore a new version won't be published."
);
});
test.serial('Returns falsy value if triggered on an outdated clone', async t => {
// Create a git repository, set the current working directory at the root of the repo
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await gitCommits(['Second']);
await push();
await reset();
const semanticRelease = proxyquire('..', {
'./lib/logger': t.context.logger,
'env-ci': () => ({isCi: true, branch: 'master', isPr: false}),
});
t.falsy(await semanticRelease({repositoryUrl}));
t.deepEqual(t.context.log.args[t.context.log.args.length - 1], [
"The local branch %s is behind the remote one, therefore a new version won't be published.",
'master',
]);
});
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
const repositoryUrl = await gitRepo(true);
@@ -611,6 +666,7 @@ test.serial('Returns falsy value if not running from the configured branch', asy
analyzeCommits: stub().resolves(),
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -623,7 +679,7 @@ test.serial('Returns falsy value if not running from the configured branch', asy
t.falsy(await semanticRelease(options));
t.is(
t.context.log.args[0][0],
t.context.log.args[1][0],
'This test run was triggered on the branch other-branch, while semantic-release is configured to only publish from master, therefore a new version won’t be published.'
);
});
@@ -633,6 +689,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
const repositoryUrl = await gitRepo(true);
// Add commits to the master branch
await gitCommits(['First']);
await push();
const analyzeCommits = stub().resolves();
const verifyRelease = stub().resolves();
@@ -646,6 +703,7 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
analyzeCommits,
verifyRelease,
generateNotes,
prepare: stub().resolves(),
publish,
success: stub().resolves(),
fail: stub().resolves(),
@@ -661,7 +719,10 @@ test.serial('Returns falsy value if there is no relevant changes', async t => {
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.');
t.is(
t.context.log.args[t.context.log.args.length - 1][0],
'There are no relevant changes, so no new version is released.'
);
});
test.serial('Exclude commits with [skip release] or [release skip] from analysis', async t => {
@@ -678,6 +739,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
'Test commit\n\n commit body\n[skip release]',
'Test commit\n\n commit body\n[release skip]',
]);
await push();
const analyzeCommits = stub().resolves();
const config = {branch: 'master', repositoryUrl, globalOpt: 'global'};
const options = {
@@ -686,6 +748,7 @@ test.serial('Exclude commits with [skip release] or [release skip] from analysis
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
@@ -723,8 +786,8 @@ test.serial('Hide sensitive environment variable values from the logs', async t
await t.throws(semanticRelease(options));
t.regex(t.context.stdout.args[8][0], /Console: The token \[secure\] is invalid/);
t.regex(t.context.stdout.args[9][0], /Log: The token \[secure\] is invalid/);
t.regex(t.context.stdout.args[t.context.stdout.args.length - 2][0], /Console: The token \[secure\] is invalid/);
t.regex(t.context.stdout.args[t.context.stdout.args.length - 1][0], /Log: The token \[secure\] is invalid/);
t.regex(t.context.stderr.args[0][0], /Error: The token \[secure\] is invalid/);
t.regex(t.context.stderr.args[1][0], /Invalid token \[secure\]/);
});
@@ -785,7 +848,7 @@ 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 errors = Array.from(await t.throws(semanticRelease()));
const errors = [...(await t.throws(semanticRelease()))];
// Verify error code and type
t.is(errors[0].code, 'ENOREPOURL');
@@ -801,6 +864,7 @@ test.serial('Throw an Error if plugin returns an unexpected value', async t => {
await gitTagVersion('v1.0.0');
// Add new commits to the master branch
await gitCommits(['Second']);
await push();
const verifyConditions = stub().resolves();
const analyzeCommits = stub().resolves('string');
@@ -844,6 +908,7 @@ test.serial('Get all commits including the ones not in the shallow clone', async
analyzeCommits,
verifyRelease: stub().resolves(),
generateNotes: stub().resolves(notes),
prepare: stub().resolves(),
publish: stub().resolves(),
success: stub().resolves(),
fail: stub().resolves(),
+7 -28
View File
@@ -603,40 +603,19 @@ test.serial('Exit with 1 if missing permission to push to the remote repository'
// Create a git repository, set the current working directory at the root of the repo
t.log('Create git repository');
const {repositoryUrl} = await gitbox.createRepo(packageName);
await writeJson('./package.json', {
name: packageName,
version: '0.0.0-dev',
repository: {url: repositoryUrl},
});
await gitbox.createRepo(packageName);
await writeJson('./package.json', {name: packageName, version: '0.0.0-dev'});
/* Initial release */
t.log('Commit a feature');
await gitCommits(['feat: Initial commit']);
t.log('$ semantic-release');
const {stdout, code} = await execa(cli, [], {env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false});
const {stdout, code} = await execa(
cli,
['--repository-url', 'http://user:wrong_pass@localhost:2080/git/unauthorized.git'],
{env: {...env, ...{GH_TOKEN: 'user:wrong_pass'}}, reject: false}
);
// Verify the type and message are logged
t.regex(stdout, /EGITNOPERMISSION/);
t.is(code, 1);
});
test.serial('CLI returns error code and prints help if called with a command', async t => {
t.log('$ semantic-release pre');
const {stdout, code} = await execa(cli, ['pre'], {env, reject: false});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 1);
});
test.serial('CLI prints help if called with --help', async t => {
t.log('$ semantic-release --help');
const {stdout, code} = await execa(cli, ['--help'], {env});
t.regex(stdout, /Usage: semantic-release/);
t.is(code, 0);
});
test.serial('CLI returns error code with invalid option', async t => {
t.log('$ semantic-release --unknown-option');
const {stderr, code} = await execa(cli, ['--unknown-option'], {env, reject: false});
t.regex(stderr, /unknown option/);
t.is(code, 1);
});
+4 -4
View File
@@ -118,7 +118,7 @@ test('Throw all errors from the first step throwing an AggregateError', async t
const errors = await t.throws(pipeline([step1, step2, step3])(0));
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.notCalled);
@@ -133,7 +133,7 @@ test('Execute all even if a Promise rejects', async t => {
const errors = await t.throws(pipeline([step1, step2, step3])(0, {settleAll: true}));
t.deepEqual(Array.from(errors), [error1, error2]);
t.deepEqual([...errors], [error1, error2]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
t.true(step3.calledWith(0));
@@ -149,7 +149,7 @@ test('Throw all errors from all steps throwing an AggregateError', async t => {
const errors = await t.throws(pipeline([step1, step2])(0, {settleAll: true}));
t.deepEqual(Array.from(errors), [error1, error2, error3, error4]);
t.deepEqual([...errors], [error1, error2, error3, error4]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0));
});
@@ -165,7 +165,7 @@ test('Execute each function in series passing a transformed input even if a step
const errors = await t.throws(pipeline([step1, step2, step3, step4])(0, {settleAll: true, getNextInput}));
t.deepEqual(Array.from(errors), [error2, error3]);
t.deepEqual([...errors], [error2, error3]);
t.true(step1.calledWith(0));
t.true(step2.calledWith(0 + 1));
t.true(step3.calledWith(0 + 1 + error2));
+8 -4
View File
@@ -27,6 +27,7 @@ test('Export default plugins', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -49,6 +50,7 @@ test('Export plugins based on config', t => {
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -79,6 +81,7 @@ test.serial('Export plugins loaded from the dependency of a shareable config mod
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -106,6 +109,7 @@ test.serial('Export plugins loaded from the dependency of a shareable config fil
t.is(typeof plugins.analyzeCommits, 'function');
t.is(typeof plugins.verifyRelease, 'function');
t.is(typeof plugins.generateNotes, 'function');
t.is(typeof plugins.prepare, 'function');
t.is(typeof plugins.publish, 'function');
t.is(typeof plugins.success, 'function');
t.is(typeof plugins.fail, 'function');
@@ -136,16 +140,16 @@ test('Merge global options with plugin options', async t => {
});
test('Throw an error if plugins configuration are missing a path for plugin pipeline', t => {
const errors = Array.from(t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger)));
const errors = [...t.throws(() => getPlugins({verifyConditions: {}}, {}, t.context.logger))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
});
test('Throw an error if an array of plugin configuration is missing a path for plugin pipeline', t => {
const errors = Array.from(
t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger))
);
const errors = [
...t.throws(() => getPlugins({verifyConditions: [{path: '@semantic-release/npm'}, {}]}, {}, t.context.logger)),
];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EPLUGINCONF');
+7 -19
View File
@@ -1,5 +1,4 @@
import test from 'ava';
import {stub} from 'sinon';
import tempy from 'tempy';
import verify from '../lib/verify';
import {gitRepo} from './helpers/git-utils';
@@ -9,17 +8,13 @@ const envBackup = Object.assign({}, process.env);
// Save the current working diretory
const cwd = process.cwd();
test.beforeEach(t => {
test.beforeEach(() => {
// 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(() => {
@@ -32,7 +27,7 @@ test.afterEach.always(() => {
test.serial('Throw a AggregateError', async t => {
await gitRepo();
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
const errors = [...(await t.throws(verify({})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOREPOURL');
@@ -46,7 +41,7 @@ test.serial('Throw a SemanticReleaseError if does not run on a git repository',
const dir = tempy.directory();
process.chdir(dir);
const errors = Array.from(await t.throws(verify({}, 'master', t.context.logger)));
const errors = [...(await t.throws(verify({})))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ENOGITREPO');
@@ -56,7 +51,7 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" is not valid', asyn
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: `?\${version}`};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'EINVALIDTAGFORMAT');
@@ -66,7 +61,7 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" does not contains t
const repositoryUrl = await gitRepo(true);
const options = {repositoryUrl, tagFormat: 'test'};
const errors = Array.from(await t.throws(verify(options, 'master', t.context.logger)));
const errors = [...(await t.throws(verify(options, 'master', t.context.logger)))];
t.is(errors[0].name, 'SemanticReleaseError');
t.is(errors[0].code, 'ETAGNOVERSION');
@@ -76,22 +71,15 @@ test.serial('Throw a SemanticReleaseError if the "tagFormat" contains multiple "
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)));
const errors = [...(await t.throws(verify(options)))];
t.is(errors[0].name, 'SemanticReleaseError');
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));
await t.notThrows(verify(options));
});