Introduction
In the previous post, we used Commitizen and Gitmoji to bring some consistency to commit messages. But having a convention doesn't mean everyone will follow it. Plenty of teams still run into this:
“The format looks simple enough, but I keep forgetting the type or getting the syntax wrong.”
Without a check, the convention is just a suggestion. To make sure commit messages consistently follow it, we need an automated check that catches invalid messages.
That's where Commitlint comes in. It validates commit messages against a convention. Whether you're using Conventional Commits or Gitmoji, you can configure checks to keep invalid messages out of the history. Of course, the tool also needs to run at the right moment. With Husky, we can use Git hooks to run Commitlint automatically before a commit is completed, making validation part of everyday development.
In this post, we'll set up:
- Validation without Gitmoji: Continue the previous setup and check messages with @commitlint/config-conventional.
- Validation with Gitmoji: Use commitlint-config-gitmoji or commitlint-config-cz to support messages with emoji.
- Husky integration: Run Commitlint through Git hooks to block commits that don't follow the convention.
Validating Commit Messages with Commitlint
Commitlint checks commit message formatting against a preset convention or custom rules. Its main capabilities are:
- Message format validation: Check that each message follows the chosen convention, such as Conventional Commits or a custom Gitmoji format.
- Blocking invalid commits as they happen: Integrate with Git hooks through a tool such as Husky to run checks during a commit and keep malformed messages out of the history.
- Extensibility: Use existing configurations such as @commitlint/config-conventional and commitlint-config-gitmoji, or define custom rules.
For example, this message follows Conventional Commits:
feat(api): add new endpoint for user authentication
This one doesn't:
updated something
Commitlint reports an error and, when integrated into the commit workflow, blocks the commit so the invalid message doesn't enter the history.
Setup 1: Commitlint Without Gitmoji
The previous post covered two options for working without Gitmoji:
-
cz-conventional-changelog
A simple setup that generates messages compatible with Conventional Changelog and passes
@commitlint/config-conventionalvalidation directly. -
@commitlint/cz-commitlint + @commitlint/config-conventional
Generates messages from the Commitlint rules in
commitlint.config.js. This suits projects that need custom validation rules.
We'll build on that setup with Commitlint and @commitlint/config-conventional to validate messages against Conventional Commits.
Installing and Configuring Commitlint
Step 1: Install Commitlint
Install the Commitlint CLI and the preset for Conventional Commits:
npm install --save-dev @commitlint/{cli,config-conventional}
Step 2: Add a configuration file
Create commitlint.config.js in the project root:
module.exports = {
extends: ['@commitlint/config-conventional'],
};
This configures Commitlint to use the Conventional Commits rules.
Connecting Commitizen and Commitlint
To check that Commitizen's output matches our validation rules, we'll test messages generated by either cz-conventional-changelog or @commitlint/cz-commitlint + @commitlint/config-conventional against @commitlint/config-conventional.
Step 1: Configure Commitizen in package.json
{
"scripts": {
"commit": "cz"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog" // or @commitlint/cz-commitlint
}
}
}
Step 2: Start an interactive commit with Commitizen
Run:
npm run commit
Suppose we enter the following in the prompts:
type: feat
scope: api
subject: add authentication endpoint
Commitizen generates this message:
feat(api): add authentication endpoint
Step 3: Validate the message generated by Commitizen
With Commitlint installed, use this command to test the previous message:
npx commitlint --from=HEAD~1 --to=HEAD
Commitlint reports no errors, which means the message passes validation.
Step 4: Test an invalid message
Try entering a message that doesn't follow the convention:
update something
Commitlint reports these errors; with the Git hook configured, they block the commit:
⧗ input: update something
✖ type must not be empty [type-empty]
✖ found 2 problems, 0 warnings
Setup 2: Commitlint with Gitmoji
Now let's handle messages that include Gitmoji. We'll look at two configuration packages, commitlint-config-gitmoji and commitlint-config-cz, and how they differ so you can choose one for your workflow.
How commitlint-config-gitmoji and commitlint-config-cz Differ
- Purpose: Validate messages against the Gitmoji convention, requiring an appropriate emoji in each message.
- Limitation: It can't read custom types from
.cz-config.js. If you add a custom type in cz-customizable, validation rejects it. For example:-
Define
:construction_worker: boshas a custom type in.cz-config.js. -
Commit with this message:
👷 bosh: Hi, I'm bosh- Result: Commitlint reports an error because that type isn't in commitlint-config-gitmoji's built-in list.
-
- Purpose: Validate the format defined by cz-customizable, including custom types.
- Limitation: Its validation is more permissive than commitlint-config-gitmoji. A message with no type at all, such as
First commit, won't produce an error. - When it reports an error:
-
A message has a type, but that type isn't defined in
.cz-config.js. For example:typo: fix spelling error- Result: Commitlint reports an error because
typoisn't among the custom types.
- Result: Commitlint reports an error because
-
If you configured cz-customizable in the first post, choose one of these packages according to your team's needs. You don't need both.
Installing and Configuring Commitlint
Install one of the following configurations:
# Install commitlint-config-gitmoji
npm install --save-dev commitlint-config-gitmoji
# Or install commitlint-config-cz
npm install --save-dev commitlint-config-cz
Extend the corresponding preset in commitlint.config.js at the project root:
-
For commitlint-config-gitmoji:
module.exports = {extends: ['gitmoji'],}; -
For commitlint-config-cz:
module.exports = {extends: ['cz'],};
This selects the rules from commitlint-config-gitmoji or commitlint-config-cz, respectively.
Connecting Commitizen and Commitlint
As in the previous section, we need to tell Commitizen which adapter to use. For interactive prompts with Gitmoji, select cz-customizable.
Step 1: Configure Commitizen in package.json
{
"scripts": {
"commit": "cz"
},
"config": {
"commitizen": {
"path": "cz-customizable" // or @commitlint/cz-commitlint
}
}
}
Then follow the same steps as before: generate an interactive commit with Commitizen and test its message with Commitlint.
Choosing the Right Configuration
| Need | Suggested configuration | Reason |
|---|---|---|
| Strict commit conventions with mandatory Gitmoji | commitlint-config-gitmoji | Stricter validation when you don't need custom types. |
| Custom conventions with cz-customizable | commitlint-config-cz | Reads custom types from .cz-config.js. |
Integrating Husky: Run Commitlint Automatically with Git Hooks
We've seen how to validate messages with Commitlint, but manually running npx commitlint won't ensure that every commit gets checked. Let's use Husky and Git hooks to run it automatically during a commit.
Installing and Configuring Husky
Step 1: Install Husky
npm install --save-dev husky
Step 2: Enable Husky
Initialize Husky and enable Git hooks:
npx husky init
This creates a .husky/ directory in the project root for hook scripts and adds a prepare script to package.json:
{
"scripts": {
"prepare": "husky"
}
}
Step 4: Add a commit-msg hook
To run Commitlint on each commit message, add a commit-msg hook:
echo "npx --no-install commitlint --edit $1" > .husky/commit-msg
This creates .husky/commit-msg with the following contents:
npx --no-install commitlint --edit $1
The script:
- Receives the commit message file.
- Runs Commitlint against it and blocks the commit if validation fails.
Seeing It in Action
For this test, I'm using the configuration from Setup 2: Commitlint with Gitmoji.
Test a valid commit message:
Use npm run commit to generate:
> conventional-commit-workflow@1.0.0 commit
> cz
cz-cli@4.3.1, cz-customizable@7.2.1
All lines except first will be wrapped after 100 characters.
? Does this commit include a breaking change? If so, describe it (optional; press Enter to skip):
? Select the type of change you are committing: 🔧 chore: Other changes that do not modify src or test files
? Select the scope of this change (optional; press Enter to skip): empty
? Write a short description of the change (required): add .husky
? Provide a longer description (optional; press Enter to skip):
? List related issues, e.g. #31, #34 (optional; press Enter to skip):
###--------------------------------------------------------###
:wrench: chore: add .husky
###--------------------------------------------------------###
? Are you sure you want to commit the above? Yes
[main d9c48dd] :wrench: chore: add .husky
2 files changed, 1 insertion(+)
create mode 100644 .husky/commit-msg
create mode 100644 .husky/pre-commit
Commitlint passes, and the commit succeeds.
Test an invalid commit message:
git commit -m "add new features"
Commitlint immediately reports an error and blocks the commit:
⧗ input: add new features
✖ Your commit should start with gitmoji code. Please check the emoji code on https://gitmoji.dev/. [start-with-gitmoji]
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 3 problems, 0 warnings
ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
husky - commit-msg script failed (code 1)
Wrapping Up: Consistency Without Extra Work
We've added two tools to the workflow:
- Commitlint: Keeps commit messages structured and consistent, so formatting problems don't get in the way of collaboration.
- Husky: Runs the checks through Git hooks, making commit message validation part of the normal development process.
Together with the previous post, this gives us a consistent, efficient commit workflow. Next, I'll use that structured commit history to generate changelogs and manage semantic versions automatically, making version control and project maintenance a little easier.
