Browse files

feat: Refactor CLI to run with one command, improve logs, modularize,…

… add tests

- Run with one command and do not rely on error exit codes to stop the process when a release is not necessary
- Break `index.js` in smaller modules in order to improve testability and simplify the code
- Add several missing unit and integration tests to reach 100% coverage
- Integration tests now test end to end, including publishing to Github (with on Docker)
- Use `tj/commander.js` to print an help message, verify and parse CLI arguments
- Semantic-release can now be called via Javascript API: `require('semantic-release')(options)`
- Remove npmlog dependency and add more log messages
- Logger is now passed to plugins
- Add debug logs with `visionmedia/debug`. `debug` is enabled for both semantic-release and plugins with `--debug`
- Use `kevva/npm-conf` in place of the deprecated `npm/npmconf`
- Pass lastRelease, nextRelease and commits to generate-notes plugin
- In dry-run mode, print the release note instead of publishing it to Github as draft, and skip the CI verifications
- The dry-run mode does not require npm and Github TOKEN to be set anymore and can be run locally

BREAKING CHANGE: Semantic-Release must now be executed with `semantic-release` instead of `semantic-release pre && npm publish && semantic-release post`.
BREAKING CHANGE: The `semantic-release` command now returns with exit code 0 on expected exception (no release has to be done, running on a PR, gitHead not found, other CI job failed etc...). It only returns with 1 when there is an unexpected error (code error in a plugin, plugin not found, git command cannot be run etc..).
BREAKING CHANGE: Calling the `semantic-release` command with unexpected argument(s) now exit with 1 and print an help message.
BREAKING CHANGE: Semantic-Release does not rely on `npmlog` anymore and the log level cannot be configured. Debug logs can be activated with CLI option `--debug` or with environment variable `DEBUG=semantic-release:*`
BREAKING CHANGE: The CLI options `--debug` doesn't enable the dry-run mode anymore but activate the debugs. The dry run mode is now set with the CLI command `--dry-run` or `-d`.
  • Loading branch information...
pvdlg authored and gr2m committed Oct 5, 2017
1 parent 1bd095d commit e2a8a5cd32b6e33f5b3b938c817a562f48080b03
@@ -131,7 +131,5 @@ package-lock.json
# Registry tests
@@ -1,6 +1,7 @@
language: node_js
- couchdb
- docker
email: false
@@ -83,11 +83,17 @@ When `semantic-release` is set up it will do that after every successful continu
If you fear the loss of control over timing and marketing implications of software releases you should know that `semantic-release` supports [release channels]( using `npm`’s [dist-tags]( This way you can keep control over what your users end up using by default, you can decide when to promote an automatically released version to the stable channel, and you can choose which versions to write blogposts and tweets about. You can use the same mechanism to [support older versions of your software](, for example with important security fixes.
This is what happens in series:
| 1. `git push` | 2. `semantic-release pre` | 3. `npm publish` | 4. `semantic-release post` |
| :--- | :--- | :--- | :---- |
| New code is pushed and triggers a CI build. | Based on all commits that happened since the last release, the new version number gets written to the `package.json`. | The new version gets published to `npm`. | A changelog gets generated and a [release]( (including a git tag) on GitHub gets created. |
When pushing new commits with `git push` a CI build is triggered. After running the tests the command `semantic-release` will execute the following tasks in series:
| Step | Description |
| ------------------ | ------------------------------------------------------------------------------------------- |
| Verify Conditions` | Run the [verifyConditions](#verifyConditions) plugin) |
| Get last release` | Obtain last release with the [getLastRelease](#getLastRelease) plugin |
| Analyze commits | Determine the type of release to do with the [analyzeCommits](#analyzeCommits) plugin |
| Verify release | Call the [verifyRelease](#verifyRelease) plugin |
| npm publish | Update the version in `package.json` and call `npm publish` |
| Generate notes | Generate release notes with plugin [generateNotes](#generateNotes) |
| Github release | A git tag and [Github release]( is created |
_Note:_ The current release/tag implementation is tied to GitHub, but could be opened up to Bitbucket, GitLab, et al. Feel free to send PRs for these services.
@@ -147,14 +153,25 @@ _[This is what happens under the hood.](
You can pass options either via command line (in [kebab-case]( or in the `release` field of your `package.json` (in [camelCase]( The following two examples are the same, but CLI arguments take precedence.
| CLI | package.json |
| --- | --- |
| <pre><code>semantic-release pre --no-debug</code><pre> | <pre><code><div>//package.json</div><div>"release": {</div><div> "debug": false</div><div>}</div></code></pre><pre><code>semantic-release pre</code></pre> |
##### CLI
semantic-release --branch next
##### package.json
"release": {
"branch": "next"
These options are currently available:
- `branch`: The branch on which releases should happen. Default: `'master'`
- `debug`: If true doesn’t actually publish to npm or write things to file. Default: `!process.env.CI`
- `dry-run`: Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes
- `debug`: Output debugging information
- `githubToken`: The token used to authenticate with GitHub. Default: `process.env.GH_TOKEN`
- `githubUrl`: Optional. Pass your GitHub Enterprise endpoint.
- `githubApiPathPrefix`: Optional. The path prefix for your GitHub Enterprise API.
@@ -181,11 +198,12 @@ There are numerous steps where you can customize `semantic-release`’s behaviou
"path": "./path/to/a/module",
"additional": "config"
semantic-release pre --analyze-commits="npm-module-name"
semantic-release --analyze-commits="npm-module-name"
A plugin itself is an async function that always receives three arguments.
@@ -215,6 +233,7 @@ Have a look at the [default implementation](
### `generateNotes`
This plugin is responsible for generating release notes. Call the callback with the notes as a string. Have a look at the [default implementation](
It receives a `commits` array, the `lastRelease` and `nextRelease` inside `config`.
### `verifyConditions`
@@ -23,8 +23,6 @@ npx is bundled with npm >= 5.4, or available via npm. More info:`
// node 8+ from this point on
require('../src')().catch(err => {
console.error('An error occurred while running semantic-release');
require('../src/cli')().catch(() => {
process.exitCode = 1;
@@ -1,7 +1,7 @@
"name": "semantic-release",
"description": "Automated semver compliant package publishing",
"version": "0.0.0-placeholder",
"version": "0.0.0-development",
"author": "Stephan Bönnemann <> (",
"bin": {
"semantic-release": "bin/semantic-release.js"
@@ -20,16 +20,19 @@
"@semantic-release/error": "^2.0.0",
"@semantic-release/last-release-npm": "^2.0.0",
"@semantic-release/release-notes-generator": "^4.0.0",
"chalk": "^2.3.0",
"commander": "^2.11.0",
"debug": "^3.1.0",
"execa": "^0.8.0",
"fs-extra": "^4.0.2",
"git-head": "^1.2.1",
"github": "^12.0.0",
"lodash": "^4.0.0",
"marked": "^0.3.6",
"marked-terminal": "^2.0.0",
"nerf-dart": "^1.0.0",
"nopt": "^4.0.0",
"normalize-package-data": "^2.3.4",
"npmconf": "^2.1.2",
"npmlog": "^4.0.0",
"npm-conf": "^1.1.2",
"p-series": "^1.0.0",
"parse-github-repo-url": "^1.3.0",
"require-relative": "^0.8.7",
@@ -40,6 +43,7 @@
"codecov": "^3.0.0",
"commitizen": "^2.9.6",
"cz-conventional-changelog": "^2.0.0",
"dockerode": "^2.5.2",
"eslint": "^4.7.0",
"eslint-config-prettier": "^2.5.0",
"eslint-config-standard": "^10.2.1",
@@ -48,12 +52,14 @@
"eslint-plugin-prettier": "^2.3.0",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-standard": "^3.0.1",
"get-stream": "^3.0.0",
"mockserver-client": "^1.0.16",
"nock": "^9.0.2",
"npm-registry-couchapp": "^2.6.12",
"nyc": "^11.2.1",
"p-map-series": "^1.0.0",
"prettier": "^1.7.0",
"proxyquire": "^1.7.3",
"proxyquire": "^1.8.0",
"rimraf": "^2.5.0",
"sinon": "^4.0.0",
"tempy": "^0.2.1"
@@ -91,7 +97,7 @@
"license": "MIT",
"main": "bin/semantic-release.js",
"main": "index.js",
"nyc": {
"include": [
@@ -125,7 +131,7 @@
"codecov": "codecov -f coverage/coverage-final.json",
"lint": "eslint .",
"pretest": "npm run clean && npm run lint",
"semantic-release": "./bin/semantic-release.js pre && npm publish && ./bin/semantic-release.js post",
"semantic-release": "./bin/semantic-release.js",
"test": "nyc ava -v"
@@ -0,0 +1,59 @@
const program = require('commander');
const SemanticReleaseError = require('@semantic-release/error');
const logger = require('./lib/logger');
function list(values) {
return values.split(',').map(value => value.trim());
module.exports = async () => {
.description('Run automated package publishing')
.option('-b, --branch <branch>', 'Branch to release from')
.option('--github-token <token>', 'Token to authenticate with Github API')
.option('--github-url <url>', 'GitHub Enterprise endpoint')
.option('--github-api-path-prefix <prefix>', 'Prefix of the GitHub Enterprise endpoint')
'--verify-conditions <paths>',
'Comma separated list of paths or packages name for the verifyConditions plugin',
.option('--get-last-release <path>', 'Path or package name for the getLastRelease plugin')
.option('--analyze-commits <path>', 'Path or package name for the analyzeCommits plugin')
'--verify-release <paths>',
'Comma separated list of paths or packages name for the verifyRelease plugin',
.option('--generate-notes <path>', 'Path or package name for the generateNotes plugin')
.option('--debug', 'Output debugging information')
'-d, --dry-run',
'Dry-run mode, skipping verifyConditions, publishing and release, printing next version and release notes'
if (program.debug) {
// Debug must be enabled before other requires in order to work
try {
if (program.args.length > 0) {
process.exitCode = 1;
} else {
await require('./index')(program);
} catch (err) {
// If error is a SemanticReleaseError then it's an expected exception case (no release to be done, running on a PR etc..) and the cli will return with 0
// Otherwise it's an unexpected error (configuration issue, code issue, plugin issue etc...) and the cli will return 1
if (err instanceof SemanticReleaseError) {
logger.log(`%s ${err.message}`, err.code);
} else {
process.exitCode = 1;
Oops, something went wrong.

0 comments on commit e2a8a5c

Please sign in to comment.