JS Library 3/7: Enforce Commit Message Conventions

JS Library 3/7: Enforce Commit Message Conventions

 · 6 min read

Note: Check out the entire supplemental source code for this blog series.

If you’re using conventional commit messages, it’s important to enforce them. We can do just that using a combination of three tools: commitlint, husky, and commitizen.

Step 1 - Lint Commit Messages with commitlint

We can use commitlint to lint our commit messages. We can install commitlint with npm install commitlint --save-dev. There are additional packages that we can install that give us predefined configurations, but we’ll go ahead and demonstrate how to make a custom configuration. To do that we’ll create a configuration file commitlint.config.js in our root. Here is a sample that I’ll be working with:

./commitlint.config.js

module.exports = {
  rules: {
    'body-empty': [0, 'never'],
    'body-leading-blank': [2, 'always'],
    'footer-leading-blank': [2, 'always'],
    'scope-enum': [
      2,
      'always',
      [
        'scope1',
        'scope2',
        'scope3'
      ]
    ],
    'subject-case': [2, 'always', 'lower-case'],
    'subject-empty': [2, 'never'],
    'subject-exclamation-mark': [2, 'never'],
    'subject-full-stop': [2, 'never', '.'],
    'subject-max-length': [2, 'always', 50],
    'type-empty': [2, 'never'],
    'type-enum': [
      2,
      'always',
      [
        'chore',
        'fix',
        'feat'
      ],
    ]
  }
};

This configuration will require you to choose from a set of types and scopes. It makes the type and subject required while allowing the scope, body, and footer to be optional. It also specifies some formatting and casing for the subject.

We can test this by running echo "fix(invalid): fix some things" | npx commitlint and we should see the following output:

⧗   input: fix(invalid): fix some things
✖   scope must be one of [scope1, scope2, scope3] [scope-enum]

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

Here it tells me that there is a problem with my commit message because I have an invalid scope.

Step 2 - Enforce Commit Message Conventions with husky

Note: husky was designed to work with linux-based systems and this step may need to be modified if you’re running on Windows.

Now we have a way to lint our commit messages, but it’s still not enforced. To ensure that every commit message is linted and conforms to our convention, we can use husky.

First, we’ll install husky with npm install husky --save-dev. Next we’ll add a prepare npm script:

./package.json

"scripts": {
  ...
  "prepare": "husky install",
  ...
},

prepare is a special npm script that will run after all packages are installed. This will ensure that husky is installed. Now let’s run npm install to trigger this.

You should notice a new .husky folder was created. The folder consists of a nested _ folder which contains a bash script and a .gitignore file so that the _ folder won’t get committed to source control.

Husky works by using git hooks, and we will tie into the commit-msg hook. We’ll add a commit-msg file (no file extension) to the root of the .husky folder.

./.husky/commit-msg

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx --no -- commitlint --edit "$1"

Also, we’ll need to make sure the commit-msg file is executable by running chmod ug+x .husky/commit-msg.

Now, when we try to commit using git commit, our commit message will be linted and our commit will be prevented if it doesn’t meet our defined conventions.

Step 3 - Prompt for Commit Messages with commitizen

We can go one step further and make the developer experience even better by helping to format commit messages as we write them with commitizen.

Run npm install commitlint --save-dev to install commitizen and npm install @commitlint/cz-commitlint --save-dev to install the commitizen commitlint adapter. Commitizen is the tool that will prompt us for commit messages and @commitlint/cz-commitlint is the adapter for commitizen to use our existing commitlint configuration.

Next we’ll configure our package.json. We need to add a configuration section to tell commitizen to use cz-commitlint:

./package.json

"config": {
  "commitizen": {
    "path": "@commitlint/cz-commitlint"
  }
}

We can now trigger commitizen with npx cz, but to make things a little easier to remember we can add a commit npm script:

./package.json

"scripts": {
  "commit": "cz",
  ...
},

Now we can trigger the prompt with npm run commit and we’ll get an interactive prompt that will help us neatly format our commit messages according to our commitlint configuration:

? <type> holds information about the goal of a change.: (Use arrow keys)
❯ chore 
  fix 
  feat 

You can also customize the messages the prompt gives by configuring adding a prompt key to your configuration.

Now that we are enforcing conventional commits, we can set up continuous integration.