Skip to main content

06 - Git Hooks and Automation: Automatic Quality Gates

Implement git hooks for automation: pre-commit, commit-msg, pre-push with Husky, lint-staged, commitlint and CI/CD integration.

What do you think about this article?

At a Glance

Implement git hooks for automation: pre-commit, commit-msg, pre-push with Husky, lint-staged, commitlint and CI/CD integration.

Git Hooks and Automation: Automatic Quality Gates

Git hooks are scripts that Git automatically executes in response to events (commit, push, merge). They enable automating checks, linting, testing, and validations, creating quality gates that prevent issues before they reach the remote repository. With tools like Husky and lint-staged, configuring hooks becomes simple and effective.

🎯 What You'll Learn

  • What Git hooks are and when they execute
  • Pre-commit hook for linting and formatting
  • Commit-msg hook for conventional commits
  • Husky and lint-staged for easy hook management

What Are Git Hooks

Git hooks are scripts saved in .git/hooks/ that Git automatically executes at specific moments in the workflow. They can be written in any executable language (bash, Python, Node.js, etc.).

📋 Main Hooks

  • pre-commit: Before creating a commit (linting, tests)
  • prepare-commit-msg: Prepare commit message (template)
  • commit-msg: Validate commit message (conventional commits)
  • post-commit: After commit created (notifications)
  • pre-push: Before push (complete test suite)
  • pre-rebase: Before rebase

Pre-Commit Hook: Linting and Formatting

The pre-commit hook is the most used. It verifies code before committing:

.git/hooks/pre-commit (Bash)
#!/bin/bash

# Run ESLint on staged files
npm run lint
if [ $? -ne 0 ]; then
  echo "❌ Linting failed. Fix errors before committing."
  exit 1
fi

# Run tests
npm test
if [ $? -ne 0 ]; then
  echo "❌ Tests failed. Fix tests before committing."
  exit 1
fi

echo "✅ All checks passed!"
exit 0
Make the script executable
chmod +x .git/hooks/pre-commit

Problem: Hooks Not Shared

Hooks in .git/hooks/ are not committed (`.git` is gitignored). Each developer must configure them manually. Solution: Husky.

Husky: Simplified Hook Management

Husky allows defining hooks in package.json and sharing them with the team via Git.

Install Husky
# Install Husky
npm install --save-dev husky

# Initialize Husky
npx husky init

# Creates .husky/ directory with shareable hooks
Create Pre-Commit Hook with Husky
# Create pre-commit hook
npx husky add .husky/pre-commit "npm run lint && npm test"

# File created: .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint && npm test
package.json with Scripts
{{ '{' }}
  "scripts": {{ '{' }}
    "lint": "eslint . --ext .ts,.tsx",
    "format": "prettier --write .",
    "test": "jest",
    "prepare": "husky install"
  {{ '}' }},
  "devDependencies": {{ '{' }}
    "husky": "^8.0.0"
  {{ '}' }}
{{ '}' }}

Lint-Staged: Lint Only Modified Files

lint-staged runs linting/formatting only on staged files, not the entire project. Much faster!

Install lint-staged
npm install --save-dev lint-staged
package.json with lint-staged
{{ '{' }}
  "lint-staged": {{ '{' }}
    "*.{{ '{' }}ts,tsx{{ '}' }}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{{ '{' }}js,jsx{{ '}' }}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.css": [
      "prettier --write"
    ]
  {{ '}' }},
  "scripts": {{ '{' }}
    "prepare": "husky install"
  {{ '}' }}
{{ '}' }}
.husky/pre-commit with lint-staged
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

Commit-Msg Hook: Conventional Commits

Enforce conventional commits (feat:, fix:, docs:, etc.) for automatic changelogs:

Install commitlint
npm install --save-dev @commitlint/cli @commitlint/config-conventional
commitlint.config.js
module.exports = {{ '{' }}
  extends: ['@commitlint/config-conventional'],
  rules: {{ '{' }}
    'type-enum': [2, 'always', [
      'feat',     // New feature
      'fix',      // Bug fix
      'docs',     // Documentation
      'style',    // Formatting
      'refactor', // Refactoring
      'test',     // Tests
      'chore'     // Maintenance
    ]]
  {{ '}' }}
{{ '}' }};
.husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install commitlint --edit "$1"
Test Commit-Msg Hook
# ✅ Valid
git commit -m "feat: add user authentication"
git commit -m "fix: resolve payment bug"

# ❌ Invalid (missing type)
git commit -m "add feature"
# ⧗   input: add feature
# ✖   subject may not be empty [subject-empty]
# ✖   type may not be empty [type-empty]

Pre-Push Hook: Complete Test Suite

Run complete tests before push to prevent broken builds on CI:

.husky/pre-push
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# Run complete test suite
npm test

# Verify production build
npm run build

# If one fails, push is blocked
exit $?

Complete Example: Professional Setup

Complete configuration with Husky, lint-staged, commitlint, and pre-push tests:

Complete package.json
{{ '{' }}
  "name": "my-project",
  "scripts": {{ '{' }}
    "lint": "eslint . --ext .ts,.tsx",
    "format": "prettier --write .",
    "test": "jest",
    "test:ci": "jest --coverage",
    "build": "tsc && vite build",
    "prepare": "husky install"
  {{ '}' }},
  "lint-staged": {{ '{' }}
    "*.{{ '{' }}ts,tsx{{ '}' }}": [
      "eslint --fix",
      "prettier --write",
      "jest --bail --findRelatedTests"
    ],
    "*.{{ '{' }}css,scss{{ '}' }}": [
      "prettier --write"
    ]
  {{ '}' }},
  "devDependencies": {{ '{' }}
    "husky": "^8.0.0",
    "lint-staged": "^13.0.0",
    "@commitlint/cli": "^17.0.0",
    "@commitlint/config-conventional": "^17.0.0",
    "eslint": "^8.0.0",
    "prettier": "^2.8.0",
    "jest": "^29.0.0"
  {{ '}' }}
{{ '}' }}
File Structure
my-project/
├── .husky/
│   ├── pre-commit        # Lint-staged
│   ├── commit-msg        # Commitlint
│   └── pre-push          # Full tests + build
├── commitlint.config.js
├── .eslintrc.js
├── .prettierrc
├── package.json
└── src/

Bypass Hooks (Use With Caution)

Sometimes you need to skip hooks (e.g., WIP commits):

Bypass Hooks
# Bypass all hooks
git commit --no-verify -m "WIP: incomplete feature"
git push --no-verify

# Abbreviation
git commit -n -m "WIP"

# ⚠️ Use only when absolutely necessary!

CI/CD Integration

Local hooks aren't enough (they can be bypassed). Reproduce the same checks on CI:

.github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 18

      # Same checks as local hooks
      - run: npm ci
      - run: npm run lint
      - run: npm run format -- --check
      - run: npm run test:ci
      - run: npm run build

      # Verify conventional commits
      - uses: wagoid/commitlint-github-action@v5

Best Practices

✅ Do's

  • Use Husky to share hooks with the team
  • Use lint-staged for speed (only staged files)
  • Verify commits with commitlint
  • Keep hooks fast (< 10 seconds)
  • Reproduce checks on CI/CD
  • Document hook setup in README

❌ Don'ts

  • Don't make hooks too slow (frustrates developers)
  • Don't rely only on hooks (they can be bypassed)
  • Don't run complete builds in pre-commit (use pre-push)
  • Don't forget "prepare": "husky install" in package.json

Advanced Hooks

prepare-commit-msg: Automatic Template
#!/usr/bin/env sh
# .husky/prepare-commit-msg

COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2

# If it's a new commit (not merge/amend)
if [ -z "$COMMIT_SOURCE" ]; then
  # Add template with branch name
  BRANCH_NAME=$(git symbolic-ref --short HEAD)
  echo "[$BRANCH_NAME] " > "$COMMIT_MSG_FILE"
fi
post-commit: Slack Notification
#!/usr/bin/env sh
# .husky/post-commit

COMMIT_MSG=$(git log -1 --pretty=%B)
AUTHOR=$(git log -1 --pretty=%an)

# Send notification to Slack
curl -X POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL \
  -H 'Content-Type: application/json' \
  -d "{{ '{' }}\"text\":\"New commit by $AUTHOR: $COMMIT_MSG\"{{ '}' }}"

Troubleshooting

Common Problems
# Hook not executable
chmod +x .husky/pre-commit

# Husky not installed
npm run prepare

# Hook executed twice
# Remove duplicate hooks in .git/hooks/

# Lint-staged doesn't find files
# Verify pattern in package.json

# Commitlint doesn't work
# Check commitlint.config.js exists

Conclusion

Git hooks automate quality gates, preventing broken code, failed tests, and poorly formatted commits. Husky makes hooks easily shareable, lint-staged makes them fast, and commitlint ensures consistent messages. Combined with CI/CD, they create a robust workflow that maintains high code quality.

🎯 Key Points

  • Git hooks automate pre-commit and pre-push checks
  • Husky shares hooks via Git (no manual configuration)
  • lint-staged runs linting only on modified files
  • commitlint enforces conventional commits for changelog
  • Reproduce checks on CI/CD for safety

Frequently asked questions

What are Git hooks and how do they work?

Git hooks are scripts that Git automatically executes in response to events (commit, push, merge) in the workflow.

Why is it a problem when hooks are not shared among team members?

When hooks are not shared, each developer must configure them manually, which can lead to inconsistencies and difficulties in maintaining a uniform workflow.

What is the main benefit of using lint-staged compared to other tools?

Lint-staged runs linting/formatting only on staged files, making it much faster than running these checks on the entire project.

Why are local hooks not enough for ensuring quality gates in a workflow?

Local hooks can be bypassed, so reproducing the same checks on CI (Continuous Integration) is necessary to ensure that quality gates are enforced consistently.

Sources and further reading

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects