Leveraging Lefthook to enforce commit guidelines at exile.watch
15 min read
Last updated
15 min read
Last updated
When it comes to collaboration with others (whether from the same organization, team, or in open source), having clear rules is essential. No one really enjoys chaos, and if you do, then godspeed.
Focusing on architecture from the start may bring some pain and frustration, but as time flies, a well-set architecture will save you far more headaches and money.
One of the key rules and architectural aspects are git hooks.
Git has a way to fire off custom scripts when certain important actions occur.
There are two groups of these hooks: client-side and server-side.
Client-side hooks are triggered by operations such as committing and merging, while server-side hooks run on network operations like receiving pushed commits.
In this post, we will solely focus on client-side hooks, setting the stage for an efficient management system.
Understanding git hooks sets the stage, but managing them efficiently is where git hooks managers come into play.
The first thing you'll likely find about git hooks is the official Git Hooks documentation.
However, there are already git hooks managers available that are fast, well-maintained, and battle-tested, so I didn't see the need to create a custom git hook manager.
To name a few: Husky, pre-commit, overcommit, and more!
While there are several git hooks managers out there, my googling led me to a particular one that stood out—Lefthook.
I've always worked with Husky, but ever since the v5 fiasco, it became clear that, at the time, this was an unstable project that breached the trust.
When I gave Husky v9 a shot a few weeks ago, I'll be honest: I had no clue how to create a shareable configuration and was somewhat wary.
Add lint-staged, commonly used with Husky, to the mix, and you get a sense that things could have been simpler in 2024.
If you visit commit message guidelines at https://docs.exile.watch/ you will notice following banner at the very top:
Your contributions will be rejected if you won't follow this guideline.
With a clear framework for commit messages established, integrating Lefthook to enforce these standards was our next step, particularly within our Lerna environment.
Commitizen is a CLI tool designed to help developers create more consistent and structured commit messages following conventional commit guidelines.
commitlint checks if commit messages meet specified guidelines, playing a crucial role in our commit process.
If this is your first encounter with Lerna and how it's applied at exile.watch, I suggest giving Lerna - the hidden powerhouse of exile.watch a read.
If you're short on time, here's the gist: exile.watch relies heavily on dependencies structured as boilerplates for consumer applications to use.
And guess what? Lefthook is one of these crucial "boilerplates."
Lefthook finds its place within the @exile-watch/splinters build tools monorepo, specifically under the @exile-watch/lefthook-config package.
To give you an idea, here's how the @exile-watch/splinters
repo is organized, focusing just on the Lefthook part:
Right off the bat, 4 key components stand out:
.lefthook/commit-msg
withcommitlint.sh
bash script at the root of the repository
lefthook-config
having scripts
directory with commitlint.sh
lefthook.yml
in both - the root of the repository and lefthook-config
commitlint.config.js
Let's delve into the roles each plays.
Diving into the specifics, let's tackle an essential caveat first:
This situation could mess up our setup.
To avoid this problem, we make sure the root .lefthook/commit-msg/commitlint.sh
script calls the script inside the package directly:
This setup essentially ensures that our root .lefthook
's commitlint.sh
script activates the script defined in the package itself, thereby enforcing our commit guidelines directly from the package.
By doing so, we maintain control and clarity, using the root script to trigger the package-defined script, thereby unifying our commit linting process under one predictable mechanism.
One more note: The /scripts
directory is also required to make remotes work—more on that later.
Our primary aim is to uphold commit standards, necessitated by commitlint
, which avails the commitlint
command.
The question then becomes: how best to activate our commitlint
package?
Through the commitlint.sh
bash script, of course.
This crucial detail explains why having the @exile-watch/lefthook-config
as a devDependency is vital—it empowers us to execute Lefthook within the same repository where the package resides:
And by invoking run_commitlint
, we kickstart the linting process as defined within our package's commitlint.sh
script.
The naming conventions here are flexible; I opted for simplicity and consistency across the board, hence the recurrent use of "commitlint."
Let's break down this script bit by bit:
$1 and head -n1 $1: Fetches and echoes the first line of the commit message, preparing it for linting.
npx commitlint: Executes commitlint, applying our project's rules to the commit message in question.
--edit, --color, --help-url: Options enhancing usability, like colored output and providing a URL for guideline assistance.
But how do we ensure Lefthook autonomously triggers this script at the pre-commit stage? That's where lefthook.yml
within lefthook-config
comes into play
Notice this part:
This configuration informs Lefthook about the commitlint.sh
script, ensuring it's triggered appropriately.
The brilliance and simplicity of extending this setup to other repositories lie in the lefthook.yml
file at the root, pointing to our configured lefthook.yml
within the lefthook-config
package:
You're essentially instructing Lefthook to fetch and merge the specified configurations, making project-wide guideline enforcement a breeze.
Check how nucleus and crucible repos are consuming our lefthook-config.
commitlint.config.js
Last component in our highlighted setup is commitlint.config.js
file.
This config sets our commit message rules, ensuring every message meets our project standards. It's key for keeping our git history clean and meaningful.
Before we evaluate our setup's complexity, there is one last step that we have to follow up with.
After installing dependencies, we run Lefthook script:
By post installing Lefthook, we are synchronizing our git hooks with latest changes that are provided by our lefthook-config package.
However, there's a bit of uncertainty on my end about whether this setup is the way it's supposed to work.
The idea of a consumer package pulling its configuration from the lefthook-config
package, which then executes a script from the root of its own repository, works like a charm in practice.
Yet, it leaves me wondering if we've taken the simplicity of Lefthook's intended use and stretched it a bit too far.
I've sparked a conversation on the Lefthook repository to clarify this very point.
Ultimately, our approach works seamlessly for now, but it raises the question:
Is this how it was meant to be used?
Only time will tell if adjustments or a nod of approval await us down the line.
Author: Sebastian Krzyżanowski About exile.watch: https://docs.exile.watch/ Github: https://github.com/exile-watch Visit https://exile.watch/ to experience it first hand