Creating custom rules for Commit Messages using commitlint

Artur Moreira
ProFUSION Engineering
5 min readFeb 22, 2023

It is nice to have good practices standardized into our codebase but it is even better to have tools to enforce those good practices for us. I had a recent experience with new members of the team lacking the knowledge of our commit message writing standards. In this team, we always reference the issue in the footer of the commit message, labeling it as Part of #<issueNumber> or Closes #<issueNumber> which is something quite specific on our squad. Even though they are experienced developers, their previous projects referenced issues differently and that's okay since this is not the only way to do it. After some code reviews, I realized our commitlint tool was being underutilized: pull requests with commit messages out of our standards were being created.

Introductory section

This article is not about how to install commitlint, you can find it in the official documentation, but rather about my experience on how I created a custom plugin for solving this specific problem. The idea is to commitlint to test the commit message and ensure that its footer has Part of #<issueNumber> or Closes #<issueNumber>. I'll share my thoughts, what I've learned, and how I went from a basic implementation to one that was better suited to our team, and maybe it could be better suited to your project as well.

Starting point

Husky + commitlint

We had a basic implementation for commitlint with husky. There were no custom rules, as we were only using the ruleset from conventional commits. So our configuration file for commitlint looked like this:

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

And that was all.

TypeScript

This project is using TypeScript, so why not take advantage of it? Since we are going to tinker with the commitlint.config.js it is a nice opportunity to introduce types to ease our development turning it into commitlint.config.ts. This is completely optional, I'll add the .js option at the end as well. If you are using TS, make sure to add commitlint.config.ts to tsconfig.json:

// tsconfig.json
{
(...)
"include": [
(...)
"src",
"commitlint.config.ts"
]
}

The commitlint

Now let’s start tinkering with our config file for commit lint.

Get to the Types!

Firstly, let’s import some types and try adding some pre-made rules:

// commitlint.config.ts

/* eslint-disable import/no-import-module-exports */
/* eslint-disable filenames/match-exported */
import type { UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
rules: {
'references-empty': [2, 'never'],
'body-empty': [1, 'never'],
},
};
module.exports = Configuration;

The first couple of lines that have comments starting with /* eslint-disable (...) are just a workaround for the es-lint rules we are using on this project, you might not need them.

Note that the typing helps us a lot with the available pre-made rules in commitlint. Here is the full list of those rules. We will follow that standard for creating our own rules. Here I am using the references-empty and body-empty as part of our commit messages standards. Note that removing these will not affect the custom rule.

A custom rule

Now for a brand new, custom rule. We will add it as an inline plugin, which means we can declare and use it right away! It would look something like this:

// commitlint.config.ts

/* eslint-disable import/no-import-module-exports */
/* eslint-disable filenames/match-exported */
import type { Commit, RuleOutcome, UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
plugins: [
{
rules: {
'footer-references-pattern': (
commitMessage: Commit,
when,
(...)
): RuleOutcome => {
(...)
},
},
},
],
rules: {
'references-empty': [2, 'never'],
'body-empty': [1, 'never'],
'footer-references-pattern': (...),
},
};
module.exports = Configuration;

Note that the callback passed to our custom rule named footer-references-pattern has the first argument as a Commit, an object related to the content of the commit message; the second argument when is a string literal for 'always' | 'never' as in the second position on the tuples from the rules; and as many other arguments as needed.

Putting everything together

With that example, we already have a clear way to create a custom rule. But how did I do it? Recalling the intro for this article, I needed to make sure that the commit’s footer had either Closes or Part of followed by #<IssueNumber>. To make it more generic, I made the third argument of the callback (using the third position of the tuple as input) a RegEx to ensure that pattern. Here's the final result:

// commitlint.config.ts

/* eslint-disable import/no-import-module-exports */
/* eslint-disable filenames/match-exported */
import type { Commit, RuleOutcome, UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
plugins: [
{
rules: {
'footer-references-pattern': (
commitMessage: Commit,
when,
pattern: string | undefined,
): RuleOutcome => {
const { footer } = commitMessage;
if (pattern == null) {
return [false, 'missing RegEx pattern'];
}
if (footer == null) {
return [false, 'missing footer reference'];
}
const regex = new RegExp(pattern);
let result = regex.test(footer);
if (when === 'never') {
result = !result;
}
return [result, `your footer should follow the pattern '${pattern}'`];
},
},
},
],
rules: {
'references-empty': [2, 'never'],
'body-empty': [1, 'never'],
'footer-references-pattern': [2, 'always', /(Part of|Closes) #[0-9]{1,}/i],
},
};
module.exports = Configuration;

Now my custom rule named 'footer-references-pattern' is being declared and used right away. For JavaScript you can just ignore the types and do:

module.exports = {(...)}

Final considerations

We were able to create a new rule by using an inline plugin for commitlint. In my case, I’ve destructured the type Commit and only used its footer property but it has so much more than that! Here's its complete type definition:

export interface Commit {
raw: string;
header: string;
type: string | null;
scope: string | null;
subject: string | null;
body: string | null;
footer: string | null;
mentions: string[];
notes: CommitNote[];
references: CommitReference[];
revert: any;
merge: any;
}
export interface CommitNote {
title: string;
text: string;
}
export interface CommitReference {
raw: string;
prefix: string;
action: string | null;
owner: string | null;
repository: string | null;
issue: string | null;
}

With it, you can create your custom rules for testing any part of the commit message you want!

I have one last tip, for testing it we could just run a git commit -m <CommitMessageHere> but there is an easier way to test it: On the shell terminal run the commands:

echo <CommitMessageHere> | yarn commitlint

Note that I am using yarn to run the commitlint test but you can also run it with npm run commitlint.

What do you think about these custom rules? Does it solve your problem? Let me know your thoughts.

Sources

--

--