TLDR: skip to the Implementation Runbook to get right to the step-by-step instructions.
Introduction
In a previous article, Continuous Delivery for Gems, I detailed how to set up an automated release process using the googleapis/release-please-action
and the rubygems/release-gem
GitHub actions.
A critical aspect of that setup is the consistent use of Conventional Commits. In the article Enforce Conventional Commits with GitHub, I showed you how to make sure that all commits on PRs to your release branch follow this standard.
In this article, I’ll show you how to make sure the commits you create in your development environment follow the Conventional Commit standard. Using a commit-msg git hook and the commitlint command line tool, commits whose commit message do not follow this standard will be blocked.
What to know about Conventional Commits
In the article Enforce Conventional Commits with GitHub, I laid out the rational for using Conventional Commits in the section Why use Conventional Commits?. I also summarized what you needed to know about Conventional Commits in the section What to know about Conventional Commits.
Rather than repeat that text here, please read those sections if you are interested.
Why enforce Conventional Commits with Git?
If you implemented a GitHub action to block non-conventional commits from being merged into your deployment branch, you may ask why you should use a pre-commit hook that does essentially the same thing.
The reason is to provide the fastest feedback possible to the developer making the commit. Rather than let one or more non-conforming commits get flagged after the commits have been push and a PR created, blocking them from being created in the first place allows the developer to correct them one at a time while the offending commit is at the top of their mind.
You may also wonder why the GitHub workflow is needed if a pre-commit hook can block non-conforming commits. The reason is that there is no way to force developers to run the pre-commit script locally. The pre-commit script MUST be installed locally by the developer in their development environment.
The best you can do it make it easy for the developer to setup the pre-commit hook and easy to use.
Implementation Runbook
These instructions will guide you through setting up husky to manage git hooks, the commitlint command line, and a git commit-msg hook which will call commitlint to validate that commit messages conform to the Conventional Commit standard.
As a pre-requisite, you must have NodeJS installed and the npx and npm commands available in your path. Check that before continuing.
1. Install husky and commitlint
Add the file package.json
to the root directory of your project with the following content.
If you already have a package.json
file, just make sure the devDependencies
contains the packages listed below and the prepare
script runs husky
.
{
"devDependencies": {
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"husky": "^9.1.7"
},
"scripts": {
"prepare": "husky"
}
}
Run npm to install these packages:
npm install
To verify that husky and commitlint tools are installed, open a bash shell in the root directory of your project, define the following function:
function is_installed() {
printf "$1..."
if [ -x "./node_modules/.bin/$1" ]; then
echo "INSTALLED"
else
echo "NOT INSTALLED"
fi
}
and then run:
is_installed husky
is_installed commitlint
2. Create the git commit-msg hook script
Add the file .husky/commit-msg
to your project with the following content.
npx --no-install commitlint --edit "$1"
git will pass the name of the file containing the commit message as “$1”. If this command returns a non-zero exit code, git will not let the commit be created.
3. Configure your Conventional Commit format
To configure commitlint, add the file .commitlintrc.yml
to the root directory of your project with the following content:
---
extends: '@commitlint/config-conventional'
rules:
# See: https://commitlint.js.org/reference/rules.html
#
# Rules are made up by a name and a configuration array. The configuration
# array contains:
#
# * Severity [0..2]: 0 disable rule, 1 warning if violated, or 2 error if
# violated
# * Applicability [always|never]: never inverts the rule
# * Value: value to use for this rule (if applicable)
#
# Run `npx commitlint --print-config` to see the current setting for all
# rules.
#
header-max-length: [2, always, 100] # Header can not exceed 100 chars
type-case: [2, always, lower-case] # Type must be lower case
type-empty: [2, never] # Type must not be empty
# Supported conventional commit types
type-enum: [2, always, [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test]]
scope-case: [2, always, lower-case] # Scope must be lower case
# Error if subject is one of these cases (encourages lower-case)
subject-case: [2, never, [sentence-case, start-case, pascal-case, upper-case]]
subject-empty: [2, never] # Subject must not be empty
subject-full-stop: [2, never, "."] # Subject must not end with a period
body-leading-blank: [2, always] # Body must have a blank line before it
body-max-line-length: [2, always, 100] # Body lines can not exceed 100 chars
footer-leading-blank: [2, always] # Footer must have a blank line before it
footer-max-line-length: [2, always, 100] # Footer lines can not exceed 100 chars
To test that commits are being validated correctly open a bash shell in the root directory of your project and define the following function:
function test_commit_message() {
tmpfile=$(mktemp)
echo "$1" > "$tmpfile"
if npx --no-install commitlint --edit "$tmpfile"; then
echo "PASS"
fi
rm "$tmpfile"
}
Run some tests. For example, these tests should PASS:
test_commit_message "feat: added a new feature"
test_commit_message "fix: corrected processing with an empty feed"
and these tests should FAIL:
test_commit_message "added tests to increase code coverage"
test_commit_message "invalid: Message is bad."
4. Verify that non-conventional commits are blocked
Try to add an empty commit with an invalid commit message, it should FAIL with an error message saying what was wrong (the type is not a recognized type):
git commit --allow-empty -m "fox: a non-conventional commit message"
If the commit succeeded (which is NOT what want), then you will need to go back through these instructions to figure out what went wrong. First you should remove the empty commit:
git reset HEAD~1
If the commit was blocked, then you have configured your environment to block non-conforming commits. There are just a couple of more things to do.
5. Provide a developer setup script
Make it easy for contributors to validate commit messages by installing husky and commitlint from your bin/setup
script. Add the following snippet to the end of the bin/setup
file:
if [ -x "$(command -v npm)" ]; then
npm install
else
echo "npm is not installed"
echo "Install npm then re-run this script to enable conventional commit message validation."
fi
Now all a contributor needs to do to enable commit message validation is run bin/setup
.
6. Configure git to ignore NodeJS artifacts
The last step is to make sure that locally installed node modules and the package-lock.json
file are not committed to git. Add the following to the .gitignore
file in the project’s root directory.
node_modules
package-lock.json
7. Commit these changes using a conventional commit message
If everything is working, commit these changes to your project and push the changes to GitHub. Remember to use a conventional commit message!
git commit -m "chore: enforce conventional commits with a git commit-message hook"
Conclusion
By implementing a commit-msg
Git hook using husky
and commitlint
as detailed in this article, you've now brought the enforcement of Conventional Commits directly into your local development workflow. This setup provides the fastest possible feedback to developers, ensuring that commit messages are validated before they are even created. This proactive approach saves time, reduces the frustration of discovering non-conformant commits later in the development cycle (such as during pull request review or CI checks), and helps maintain a clean, actionable Git history from the very start.
This local validation is the crucial final piece in the strategy outlined in this series for robust Continuous Delivery for your Ruby Gem projects.