Semantic versioning in JavaScript projects made easy
Tags: tutorial, productivity, javascript
Published: 2020-05-23 16:16:58

If you've used a package manager like npm or yarn before, you're probably familiar with a versioning format like X.Y.Z, where X, Y, and Z each represent a number, separated by dots. But what do those numbers mean?

This versioning format is called [Semantic Versioning](https://semver.org/) (or SemVer for short). Those three numbers correspond to: `<MAJOR>.<MINOR>.<PATCH>`. Updating the major version means introducing a breaking change, the minor version is incremented when adding a new feature and the patch version is increased when including backward-compatible bug fixes. Increasing the version number (often called "bumping") also requires an update of the project's changelog. However, managing this manually for every release seems like a tedious task. After all, a developer most likely prefers writing code over documentation. Luckily, there are some tools to help automate this!

## 🛠 Tools

[Standard version](https://github.com/conventional-changelog/standard-version) is a utility that takes care of all these versioning steps. It bumps the version, writes the changes to the changelog, and creates a git tag with the new version. It requires [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) messages when committing, meaning all commit messages should follow a specific pattern:

```
<type>[optional scope]: <description>

[optional body]

[optional footer]
```

The `fix:` and `feat:` types correlate to the `PATCH` and `MINOR` version respectively. Adding a `BREAKING CHANGE:` prefix to the body or footer of the commit message indicates a bump of the `MAJOR` version.

But how can you make sure contributors stick to this format, to prevent standard version from breaking?
Similar to how a linter like [eslint](https://eslint.org/) can be used to analyze your code, a tool like [commitlint](https://github.com/conventional-changelog/commitlint) can be used to analyze your commit messages. By adding commitlint as a commit-msg git hook, all commit messages can be evaluated against a predefined config, ahead of the actual commit. So if the linter throws an error, the commit fails. An easy way to create those git hooks, is by using a helper like [husky](https://github.com/typicode/husky), which allows you to define your hooks directly inside the `package.json`.

Additionally, using an interactive CLI like [commitizen](https://github.com/commitizen/cz-cli), simplifies writing the commit messages in the conventional commit format by asking questions about your changes and using your answers to structure the message.

## 💿 Setup

Install all the necessary tools.

```bash
npm install --save-dev standard-version commitizen @commitlint/{cli,config-conventional} husky
```

Create a `commitlint.config.js` file in the root of the project. This file defines the rules that all commit messages should follow. By extending the conventional commit config, created by the commitlint team, all conventional commit rules will be added to the config.

```js
module.exports = {extends: ['@commitlint/config-conventional']};
```

Configure the hook in the `package.json`.

```json
{
  ...
  "husky": {
    "hooks": {
      "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
  }
}
```

A commit not following the conventional commit pattern, will now fail and give appropriate feedback regarding what caused the error:

```bash
$git commit -m "non-conventional commit"
husky > commit-msg (node v10.15.3)
⧗   input: non-conventional commit
✖   subject may not be empty [subject-empty]
✖   type may not be empty [type-empty]

✖   found 2 problems, 0 warnings
ⓘ   Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint

husky > commit-msg hook failed (add --no-verify to bypass)
```

Next, initialize the conventional changelog adapter to make the repo commitizen-friendly:

```bash
npx commitizen init cz-conventional-changelog --save-dev --save-exact
```

Add 2 scripts to the `package.json`: one to run the commitizen cli and one for standard-version:

```json
{
  ...
  "scripts": {
    "cm": "git-cz",
    "release": "standard-version"
  }
}
```

## 💻 Usage

Now, when using `npm run cm` to commit, commitizen's cli will be shown.  It asks a series of questions about the changes you're committing and builds the message based on the provided answers. For example, committing a new feature looks like this:

![commitizen cli example](https://dev-to-uploads.s3.amazonaws.com/i/phgbekfey5wft5m2r9jq.gif)

When everything is ready for a new release, use standard-version to update the version number, changelog and create the git tag:

```bash
npm run release
```

Standard version's output shows the bumping of the minor version to 1.1.0, as expected when committing a feature, and that a correct git tag was created.

```bash
✔ bumping version in package.json from 1.0.0 to 1.1.0
✔ outputting changes to CHANGELOG.md
✔ committing package.json and CHANGELOG.md
husky > commit-msg (node v10.15.3)

✔ tagging release v1.1.0
ℹ Run `git push --follow-tags origin master && npm publish` to publish
```

The outputted changes to the `CHANGELOG.md` look like this:

```md
# Changelog

All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.

## 1.1.0 (2020-04-13)


### Features

* short desc, added to changelog ([cd9dbc9](https://github.com/Hzunax/semantic-versioning-example/commit/cd9dbc9627b7fc64ba0490e495fd71686a604e57))
```

Each `fix`, `feat`, or `BREAKING CHANGE` commit will show up in the changelog with its short description and a link to the commit on the remote.

Standard version also takes care of committing these changes (with a conventional commit message), so all that's left to do is push the changes to the remote and we're done!

## 📖 Further reading

I made an [example setup](https://github.com/Hzunax/semantic-versioning-example) where I use the tools described in this post. Feel free to check out the commit messages and how they are represented in the changelog.

For more complex configurations and more detailed information on the tools and concepts used in this post, check out the links below.

- [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
- [SemVer](https://semver.org/)
- [Commitizen](https://github.com/commitizen/cz-cli)
- [Standard version](https://github.com/conventional-changelog/standard-version)
- [Commitlint](https://commitlint.js.org/#/)
- [Husky](https://github.com/typicode/husky)