5 cool (and surprising) ways to configure Lefthook for automation joy
Topics
Translations
- ChineseLefthook的五种武器
Lefthook has been under active development for more than 4 years. While at first it was just a wrapper for calling custom scripts on Git hooks, as time has gone on, Lefthook has evolved into a tool that provides a clean and flexible way to automate your development routine, both in obvious and quite surprising ways. Want to know more? In this post, you’ll see examples of Lefthook configurations used in the real projects.
Linting, formatting, and fixing typos
Running linters and formatters over files that have been touched is the most common way to use Lefthook.
For example, I have a Go CLI project where I store documentation in Markdown format right in the repo. Whenever I fix bugs or implement features, I need to both test and lint the code, and perform a spellcheck in order to not confuse the reader. Further, all of the links within the documentation must point to existing pages. And regardless of the tools I choose, I want them to run every time I commit something. Let’s take a look at a Lefthook configuration that covers all of the above quickly, and in just 20 lines of YAML.
# lefthook.yml
pre-commit:
parallel: true
commands:
lint:
glob: "*.go"
run: golangci-lint run --fix
stage_fixed: true
fix-typos:
run: typos --write-changes {staged_files}
stage_fixed: true
test:
glob: "*.go"
run: go test -cpu 24 -race -count=1 -timeout=30s ./...
links:
glob: "*.md"
run: lychee {staged_files}
Every time you commit something, the linter, typo checker, and link checker will be run in parallel. Note that stage_fixed: true
automatically performs git add
for the changed files in a pre-commit hook. It’s also important to mention that Lefthook respects only staged changes, so unstaged changes will remain untouched after performing git commit
.
Monorepo
Here’s another example: there are two folders in my repo: client
and server
. client
is an SPA written in TypeScript, and server
is a Ruby on Rails application. I want to perform linting for both, and run client tests before I push changes, because this can be done sufficiently fast and I also don’t want to waste time on careless CI errors.
To that end, I can make Lefthook run specific commands in subfolders using the root
option:
# lefthook.yml
pre-commit:
parallel: true
commands:
server-lint:
root: "server/"
glob: "*.rb"
run: bundle exec rubocop -A --force-exclusion {staged_files}
stage_fixed: true
client-lint:
root: "client/"
glob: "*.{ts,tsx}"
run: yarn format {staged_files}
stage_fixed: true
pre-push:
commands:
client-tests:
root: "client/"
glob: "*.{ts,tsx}"
run: yarn test
The root
and glob
options work as filters, so if you commit something in the server part, client-lint
will be just skipped, and vice-versa.
Do make sure that your Gemfile
is in the server/
folder and package.json
is in the client/
.
Interactive hooks
There’s no need to waste extra time thinking about commit message format or enforce proper style within your team because there are well-known tools for doing that: Commitzen and commitlint. And we can save even more time with Lefthook; here’s how to use it to configure those:
# lefthook.yml
# Build commit messages interactively.
prepare-commit-msg:
commands:
commitzen:
interactive: true
run: yarn run cz
env:
LEFTHOOK: 0
# Validate commit messages; can be used with or without cz.
commit-msg:
commands:
"lint commit message":
run: yarn run commitlint --edit {1}
The interactive: true
option binds the command’s STDIN
with your TTY and allows you to interact with the cz
command with every git commit
. (When using this configuration, the developer must always commit from the terminal, which might be inconvenient.) Nevertheless, you can use commitlint
to validate that the commit message is valid.
prepare-commit-msg
can also be suggested as an optional hook for a lefthook-local.yml
configuration.
Implementing a CI-like pipeline
It can be a good idea to automatically run new migrations and install dependencies when pulling new commits from Git, and it makes sense to do this in a post-merge
hook.
But there are two things to consider:
- I have to install the backend dependencies before running the migrations
- I want to install client dependencies in parallel with other steps
To implement this, let’s use the helper hook:
# lefthook.yml
post-merge:
parallel: true
files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD"
only:
- ref: 'main'
skip:
- rebase
commands:
migrations:
run: lefthook run migrations
env:
LEFTHOOK_QUIET: execution_info,meta,skips,summary
client-dependencies:
glob: "{yarn.lock,package.json}"
run: yarn install
migrations:
piped: true
files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD"
commands:
bundle:
priority: 1
glob: "Gemfile*"
run: bundle install
migrate:
priority: 2
glob: "db/migrations/*"
run: bundle exec rails db:migrate
Globs allow us to skip unneeded steps, while the migrations
helper allows us to pipe 2 commands; they’ll run one after another, and migrate
won’t start if bundle
fails. The common files
gets the new files and applies them to every command for implicit filtering.
Shared configs
What if you’re tired of configuring the same boilerplate lefthook.yml
in every repo? I have hundreds of Ruby projects, numerous JavaScript projects, and a few Go microservices, and I want to control all the hooks in one place, while committing the change only once.
And sure enough, Lefthook has the remote
option for that!
To start, I have 3 configurations for each project type that I’ll store in one repo. Let’s name it github.com/organization/lefthook-configs
, and tere will be 3 files in it:
├── lefthook-golang.yml
├── lefthook-js.yml
└── lefthook-ruby.yml
Each config file has the pre-commit
hook for linting, formatting, tests, and so on:
# lefthook-golang.yml
pre-commit:
commands:
lint:
glob: "*.go"
run: golangci-lint run --fix
stage_fixed: true
Now, in every Go repo you need to add the following config:
# lefthook.yml
remote:
git: https://github.com/organization/lefthook-configs
config: lefthook-golang.yml
And that’s it! When you change something in the lefthook-configs
repo, you need to make sure other developers will run lefthook install
locally. The latest changes will be fetched to their hooks. While this requires strongly recommending everyone in your team chat run lefthook install
, in the end, this is much easier than commiting one change to ten repos!
At Evil Martians, we transform growth-stage startups into unicorns, build developer tools, and create open source products. If you’re ready to engage warp drive, give us a shout!