問題:CI 失敗的等待循環

你:push → CI 排隊 5min → lint fail → 切回來改 → push → CI 排隊...
                                ↑
                         這是 CI 失敗修 lint 的平均次數:2-3 次

遠端 CI 是為了保護共享分支,不是為了讓你知道漏了分號。漏分號這種事在 push 之前就能檢查。


git hook 的層次

Hook時機適合做什麼
pre-commitgit commit 執行前Lint、Format 檢查、Secret 掃描
commit-msgcommit message 寫完後Conventional Commits 格式驗證
pre-pushgit push 執行前跑測試、Typecheck(比 pre-commit 重,適合非每次 commit 都跑的任務)

工具選擇

lint-staged + husky(JS 生態系最常見)

lint-staged 的核心優勢:只對 staged 的檔案跑 lint,不跑整個 repo——大 repo 每次 commit 跑全量 lint 會很慢。

npm install --save-dev husky lint-staged
npx husky init

package.json

{
  "lint-staged": {
    "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{js,jsx}": ["eslint --fix"],
    "*.{md,json,yaml}": ["prettier --write"]
  }
}

.husky/pre-commit

npx lint-staged

lefthook(多語言、更快)

lefthook 用 YAML 設定,支援並行執行多個 hook:

# lefthook.yml
pre-commit:
  parallel: true
  commands:
    lint:
      glob: "*.{ts,tsx,js}"
      run: npx eslint {staged_files}
    format:
      glob: "*.{ts,tsx,js,json,md}"
      run: npx prettier --check {staged_files}
    typecheck:
      run: npx tsc --noEmit
 
commit-msg:
  commands:
    conventional:
      run: npx commitlint --edit {1}

pre-commit.com(Python 生態系,跨語言)

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.3.0
    hooks:
      - id: ruff
      - id: ruff-format
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: detect-private-key      # 防止 commit private key
      - id: check-added-large-files  # 防止 commit 大檔案
      - id: end-of-file-fixer
pip install pre-commit
pre-commit install  # 安裝 hook 到 .git/hooks/

Pre-push:跑測試

Pre-commit 跑 lint 夠快(< 5 秒),但跑完整測試可能要 30 秒以上,每次 commit 都跑會很煩。

建議把測試移到 pre-push,只在 push 前跑一次:

# .husky/pre-push
npm run test -- --passWithNoTests
npm run typecheck

或用 lefthook:

pre-push:
  commands:
    tests:
      run: npm test
    typecheck:
      run: npx tsc --noEmit

Secret 掃描

這是最重要的 pre-commit check——防止把 API key、password、private key 推上去:

# 用 detect-secrets(Python)
pip install detect-secrets
detect-secrets scan > .secrets.baseline

或在 pre-commit-config.yamldetect-private-key hook。

重要:如果 secret 已經 commit 了,即使刪掉也在 git history 裡。發現後要立刻 revoke key,不能只刪檔案了事。


速度原則

Pre-commit 如果太慢,開發者會加 --no-verify 繞過——徹底失去守門的效果。

保持 pre-commit < 5 秒的做法

  • 只對 staged 檔案跑(lint-staged / lefthook 的 {staged_files}
  • Lint 和 format 並行執行
  • 把慢的 typecheck 移到 pre-push
  • 測試只跑受影響的範圍(Jest 的 --findRelatedTests