Skip to main content
All posts

Gitmoji x Conventional Commits Workflow (2): Validating Commit Messages with Commitlint and Husky

Bosh9 min read

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:

  1. Validation without Gitmoji: Continue the previous setup and check messages with @commitlint/config-conventional.
  2. Validation with Gitmoji: Use commitlint-config-gitmoji or commitlint-config-cz to support messages with emoji.
  3. 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:

  1. Message format validation: Check that each message follows the chosen convention, such as Conventional Commits or a custom Gitmoji format.
  2. 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.
  3. 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:

  1. cz-conventional-changelog

    A simple setup that generates messages compatible with Conventional Changelog and passes @commitlint/config-conventional validation directly.

  2. @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​

commitlint-config-gitmoji

  • 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: bosh as 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.

commitlint-config-cz

  • 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 typo isn't among the custom types.
tip

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​

NeedSuggested configurationReason
Strict commit conventions with mandatory Gitmojicommitlint-config-gitmojiStricter validation when you don't need custom types.
Custom conventions with cz-customizablecommitlint-config-czReads 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:

  1. Receives the commit message file.
  2. 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:

  1. Commitlint: Keeps commit messages structured and consistent, so formatting problems don't get in the way of collaboration.
  2. 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.


Reference​