Writing changelogs for a project can be tedious. Usually, this lengthy process would start with your project manager, organizing your tickets in the sprint (depending on how your project is organized), and taking time out of the day to write the changelog itself. This process becomes even more complicated when working on developer-centric projects. Remembering what is and isn't a breaking change (to keep a sensible SEMVER), what technical changes were made, and what you should do to migrate to newer versions might be a challenge in itself, on top of the typical release patterns.

This versioning complexity birthed a set of tools that allows you to generate changelogs automatically. Now, this may sound too good to be true: "How can it generate something without any metadata?" Well, dear reader, that's the trick of it: You do provide the metadata in the form of commit messages.

If you enforce a standardized set of commit messages (both header and body), then a tool can automatically run through each commit since your last release and generate the changelog. Furthermore, because the commit message standards you'll follow outline when a new feature, bug fix, or breaking change is introduced, this tooling can assume what portion of SEMVER (major, minor, or patch) to bump. It can change the version numbers in your files as well!

Step 0: Commit Rules

Before we start setting up tooling (to generate the changelogs, commit message verification, and more), we need first to understand what the rules are that we're signing up for. As mentioned before, we'll need to standardize the way we write our commit messages for our tooling to work effectively. The standardized commit message template we'll be following in this article is called Conventional Commits. Conventional Commits generally follow an outline as such:

  • First, start with the type of change you're making
  • Then, have an (optional) scope, indicating what section of your app you're changing
  • A description of your changes
  • Then, an optional body that outlines further information that you might want to preserve in your changelog
type(scope): description

body

"Now, by 'type', what exactly do you mean?"

I'm glad you've asked! In Conventional Commits setups, there is an allowed array of terms that can be used for your type. For example, when following the Angular Style of commit messages, you'll have these options at your disposal:

[
  'build',
  'ci',
  'docs',
  'feat',
  'fix',
  'perf',
  'refactor',
  'revert',
  'style',
  'test'
]

This means that your commit message might be something along the lines of:

test(pagination): added pagination edgecase to test suite

We had an error thrown as a result of a miscalculation when changing pages on an odd number of items in the collection. This test should ensure this bug doesn't regress

In this case, your type is test, whereas your scope is pagination. This way, when you're generating your public changelog, it will likely not include this commit message, as your users don't often care about the implementation or tests within. While this isn't a great example, let's take the next two examples:

fix(pagination): fixed pagination throwing errors when an odd number of items in collection
feat(pagination): added new "first" and "last" events when pagination is moved to first or the last page 

Your tooling knows only to bump the patch release because your first example is listed as a type of fix. However, in the second example, you have a type of feat that tells your tooling to bump your release version by a minor number.

Likewise, to tell your tooling that a commit introduces a breaking change, you'll do something along the lines of this:

refactor(pagination): consolidates "first" and "last" events into a "pageTo" event that includes the number in the event payload

BREAKING CHANGE: If you're using the `first` or `last` events in the paginator, you'll need to migrate your logic to use `pageTo` event and getting the page from the event payload (using `$event`). By doing so, you can add back conditional logic based on the number of page jumps 

The BREAKING CHANGE: at the start of your commit body tells your tooling that this should indicate a package bump of a MAJOR version, and will highlight this change at the top of your changelog as such.

Commit Scope

An immediate question that might be asked is, "why would I put the scope of changes? How could this realistically help me?" One use-case where adding a commit scope is hugely advantageous is when using a monorepo for multiple packages in a single repo. When using Lerna to help manage a monorepo, there are even addons that enable restricting your scope to match one of the project's packages names. By doing so, you're able to generate individual CHANGELOG.md files for each package, enabling your tooling to scope with your project's scale.

Step 1: Commit Message Enforcement

Any suitable set of tooling should have guide-rails that help you follow the rules you set for yourself (and your team). Like a linter helps keeps your codebase syntactically consistent, Conventional Commit setups often have a linter setup of their own. This linter isn't concerned about your code syntax, but rather your commit message syntax.

Just as you have many options regarding what linting ruleset you'd like to enforce on your codebase, you have a few options provided to you for your commit messages. You can utilize the default linting rules out-of-the-box, follow the Angular Team's guidelines, or even utilize the format that Jira has set out.

Another similarity to their code syntax contemporaries is that your commit linter has a myriad of configuration options available. These options allow you to overwrite the existing configuration you're utilizing or even create your configuration from scratch.

Setup

While you can go as in-depth as creating your own configuration, let's assume that we want to stick with the out-of-box settings. Let's assume that you already have a package.json configured. First thing's first, let's install the dependencies we need:

npm install --save-dev @commitlint/cli @commitlint/config-conventional

The commitlint CLI is what will actually do the linting on the commit message while the @commitlint/config-conventional is the ruleset that the linter will follow. Now, we'll create the configuration file that will tell the CLI what rules to use. Create a file called commitlint.config.js at the root of your project and place the following code inside:

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

Now, you can test that your setup works properly by linting the last commit in your branch:

npx commitlint --from=HEAD~1

It should either validate or fail, depending on whether the last commit message followed the ruleset.

Husky Setup

While you could set up a CI system with something like the commitlint command from above, it wouldn't be very effective at making sure you and your team remain vigilant with your commit schema. You're able to enforce your commit messages directly from your development machine at the time of commit. To do so, we'll hookup git hooks to validate our commit messages before they finalize (and prevent a commit when they don't pass the linting rules). While there are ways to do this manually, the easiest (and most sharable) method to do so using package.json is by installing a dependency called husky.

npm install --save-dev husky

By installing husky, we can now add the following to our package.json to tell git to run our commitlint:

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

Test The Hook

Now that we have husky configured properly, we're able to ensure that the linting is working as expected. Now, if you run git commit it will give the following behavior pattern:

git commit -m "foo: this will fail"
husky > commit-msg (node v10.1.0)
No staged files match any of provided globs.
⧗   input: foo: this will fail
✖   type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]

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

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

Step 2: Manage Your Releases

While contiguous commit consistency is cool (what a mouthful), our end goal is to have easier management of our releases. To this end, we have the standard-version . This tool allows you to generate git tags, changelogs, and bump your package.json files. To start, we'll install the package as a developer dependency:

npm i --save-dev standard-version

Afterward, we can add a release script in our package.json:

{
  "scripts": {
    "release": "standard-version"
  }
}

Finally, standard-version needs to have a starting point to append the CHANGELOG and other versions to. Simply run:

npm run release -- --first-release

To generate your initial CHANGELOG.md file. This will also create a tag of the current state so that every subsequent release can change your version numbers.

Usage

Having an initial starting point for releases is cool but ultimately useless without understanding how to cut a new release. Once you've made a series of commits, you'll want to re-run npm run release. This will do all of the standard release actions. As mentioned before, the type of commits will dictate what number (patch, minor, major) is bumped. As all of your changes will make it into your CHANGELOG.md, you may want to consider squashing PRs before merging them, so that your changelog is clean and reflective of your public changes (not just the implementation detail).

One thing to note is that you'll want to run npm run release before running your build or release. This is because it bumps your package version, and as-such won't change the package version in your deployed updates.

Changelog Customization

From here, your CHANGELOG.md file should look like the following:

# 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.

### 0.0.1-alpha.1 (2020-01-01)

Initial release

Let's say we introduce a new version that has a set of features and bug fixes:

### [0.0.2](https://github.com/unicorn-utterances/batteries-not-included/compare/v0.0.1...v0.0.2) (2020-02-25)

### Features

* added overflow property to keyboard handler ([3f85fdc](https://github.com/unicorn-utterances/batteries-not-included/commit/3f85fdcc9ff2bf2e765585c500b0d2f3421c92dc))
* added wrap number util ([762f1cd](https://github.com/unicorn-utterances/batteries-not-included/commit/762f1cd5ff60274b221eccf6da829b72fac97d7b))

### Bug Fixes

* parameter in name in doc in wrap-number.ts ([249b63b](https://github.com/unicorn-utterances/batteries-not-included/commit/249b63bebe1816655dd64cc1acf7f57875b0613e))
* updated overflow to work on keyboard handler ([eb50de0](https://github.com/unicorn-utterances/batteries-not-included/commit/eb50de0c401d98f84a5c9628c6d34c6cef311eb1))

You might think "Well, this file is auto-generated. I shouldn't modify it, least it stop working!" Luckily for us, this is not the case! So long as we leave the headers as-is, we're able to customize the CHANGELOG.md file with further details. We can even include images using the standard markdown ![]() syntax! Using this knowledge, we can create extremely robust and explanative changelogs for our consumers.

Bump Version Files

While working in a monorepo, I often find myself needing to change the version number in more than a single file at a time. I've also found myself in need of multi-file version bumping when using a different package.json for release than the one I use for development.

Regardless of the reason behind needing to change multiple files' package number, standard-version's got you covered!

You'll want to create a .versionrc file and put the following in it:

{
  "bumpFiles": [
    {
      "filename": "MY_VERSION_TRACKER.txt",
      // The `plain-text` updater assumes the file contents represents the version.
      "type": "plain-text"
    },
    {
      "filename": "a/deep/package/dot/json/file/package.json",
      // The `json` updater assumes the version is available under a `version` key in the provided JSON document.
      "type": "json"
    },
    {
      "filename": "package.json",
      "type": "json"
    },
  ]
}

Multiple different kinds of files that can be updated, and you can even write your own updater method to update any file you'd so like.

Conclusion

Keep in mind, simply because you have a new tool to manage releases doesn't mean that you have a free pass on ignoring your branching strategy. If you're developing a developer tool that has breaking changes every week, you're certainly going to alienate anyone that's not a staunch consumer. You'll want to keep following best practices for your use-cases to ensure that this tool isn't squandered by other project issues.

While the outline we've provided should suffice for most usage, each of these tools includes many options that you're able to utilize customize the process to your liking.

Find options you think we should cover in this article? Have questions about how to get conventional-commit and standard-version working? Let us know! We've got a comments section down below as well as a Discord Community that we use to chat.

Creative Commons License

Newsletter

Subscribe to our newsletter to get updates on new content we create, events we have coming up, and more! We'll make sure not to spam you and provide good insights to the content we have.