Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 34 — Pre-commit Hooks & Local Dev Loop

Đã đọc 0/14 mục · còn khoảng 13 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng một buổi chiều thứ Sáu ở một startup fintech tại Sài Gòn. Bạn vừa git push một tính năng mới lên nhánh của mình, mở tách cà phê ra thư giãn, thì mười phút sau nhận được thông báo đỏ chói từ CI: pipeline fail. Lý do? Một dấu chấm phẩy thừa, một biến chưa dùng, hoặc code chưa được format đúng chuẩn. Bạn phải sửa, commit lại, push lại, rồi chờ thêm mười phút nữa để CI chạy lại. Nhân con số này với hàng chục lần mỗi ngày, cho cả một team mười người, bạn sẽ thấy bao nhiêu thời gian (và tiền chạy runner) bị đốt vô ích.

Bài học hôm nay giải quyết chính vấn đề đó. Pre-commit hooks và một local dev loop (vòng lặp phát triển tại máy) chặt chẽ giúp bạn phát hiện lỗi ngay trên máy của mình, trước khi commit, thay vì để CI phát hiện giúp. Nguyên lý cốt lõi rất đơn giản nhưng cực kỳ mạnh mẽ: feedback càng gần với thời điểm bạn viết code thì càng rẻ để sửa. Một lỗi bắt được lúc gõ code tốn vài giây; bắt được ở CI tốn mười phút; bắt được ở review tốn nửa ngày; bắt được ở production tốn cả một đêm trực sự cố.

Đây không phải chuyện "làm cho vui" hay "cho có". Trong các bài trước bạn đã học về pipeline, về các stage lint/test/build/scan. Bài này là mảnh ghép giúp CI của bạn sạch hơn, nhanh hơn và rẻ hơn — bằng cách đẩy một phần công việc kiểm tra về phía developer, ngay tại local.

Khái niệm cốt lõi

Git hooks là gì?

Git có sẵn một cơ chế gọi là hooks: các script được Git tự động chạy tại những thời điểm nhất định trong vòng đời của một thao tác Git. Chúng nằm trong thư mục .git/hooks/ của mỗi repo. Có nhiều loại hook, nhưng với bài này ta quan tâm nhất đến vài loại "client-side" (chạy trên máy developer):

  • pre-commit: chạy ngay trước khi commit được tạo. Nếu script trả về mã lỗi khác 0, commit bị hủy. Đây là nơi lý tưởng để lint, format, kiểm tra nhanh.
  • commit-msg: chạy sau khi bạn nhập message commit, dùng để kiểm tra định dạng message (ví dụ ép theo chuẩn Conventional Commits).
  • pre-push: chạy trước khi push lên remote, phù hợp cho các kiểm tra nặng hơn một chút (chạy một phần test).
Vấn đề của Git hooks "thuần" là: thư mục .git/hooks/ không được commit vào repo. Nghĩa là mỗi developer phải tự cài đặt hook thủ công, và không có cách nào đảm bảo cả team dùng cùng một bộ hook. Đây chính là lý do các framework quản lý hook ra đời.

Local dev loop là gì?

"Local dev loop" là vòng lặp bạn lặp đi lặp lại khi phát triển: viết code → kiểm tra → sửa → viết tiếp. Một dev loop tốt có ba đặc điểm: nhanh (feedback tính bằng giây), nhất quán (mọi người trên team chạy cùng một bộ kiểm tra), và tự động (không phải nhớ gõ lệnh gì). Pre-commit hooks là công cụ chủ lực để đạt cả ba điều này — chúng biến những kiểm tra bạn "nên nhớ chạy" thành những kiểm tra "tự động chạy".

Framework pre-commit (Python-based, language-agnostic)

Công cụ phổ biến nhất hiện nay để quản lý pre-commit hooks là framework có tên đúng bằng... pre-commit (tại pre-commit.com). Đừng nhầm lẫn: đây là một công cụ, khác với loại hook pre-commit của Git. Framework này viết bằng Python nhưng language-agnostic — nghĩa là bạn dùng nó được cho dự án Node.js, Go, PHP, Java, Rust... bất kể ngôn ngữ nào.

Điểm mạnh của nó:

  • Cấu hình bằng một file duy nhất .pre-commit-config.yaml được commit vào repo → cả team dùng chung, không ai lệch.
  • Quản lý môi trường tự động: mỗi hook được cài trong môi trường cô lập riêng (virtualenv, node_modules ẩn, v.v.), nên bạn không phải lo cài đặt thủ công từng linter.
  • Kho hook khổng lồ: hàng nghìn hook có sẵn cho mọi tác vụ — format code, kiểm tra file lớn, phát hiện secret bị lộ, xóa trailing whitespace...
  • Chỉ chạy trên file thay đổi (staged files) → cực nhanh.
Một hệ sinh thái tương tự trong thế giới JavaScript là Husky (kết hợp với lint-staged). Cả hai triết lý giống nhau; ta sẽ nói kỹ về pre-commit framework vì tính đa ngôn ngữ, và nhắc Husky ở phần ví dụ.

Nguyên tắc vàng: nhanh ở local, kỹ ở CI

Một hiểu lầm chết người là nhồi mọi thứ vào pre-commit hook: chạy toàn bộ test suite, chạy scan bảo mật đầy đủ, build cả dự án. Kết quả là mỗi lần commit mất 3 phút, developer bực bội và bắt đầu dùng git commit --no-verify để bỏ qua hook — và thế là công sức của bạn tan thành mây khói.

Nguyên tắc là: pre-commit chỉ giữ những kiểm tra nhanh (dưới ~5 giây), mang tính "vệ sinh" và tất định (format, lint, kiểm tra syntax, chặn secret). Những kiểm tra nặng và tốn thời gian — test tích hợp, build đầy đủ, DAST — vẫn thuộc về CI. Local dev loop và CI bổ sung cho nhau chứ không thay thế.

Tình huống thực tế

Ví dụ 1 — Tiki: cắt 30% số lần CI fail vì lỗi format

Một team backend giả định tại một sàn thương mại điện tử lớn kiểu Tiki, gồm 12 kỹ sư làm dịch vụ đặt hàng bằng Node.js. Trước khi áp dụng pre-commit hook, họ theo dõi số liệu và thấy: khoảng 35% số lần pipeline fail không phải vì logic sai, mà vì những lỗi vụn vặt — ESLint báo lỗi, Prettier chưa format, hoặc console.log để quên. Mỗi lần fail như vậy tốn trung bình 8 phút runner cộng thời gian developer bị gián đoạn.

Team quyết định cài Husky + lint-staged, chạy ESLint và Prettier chỉ trên các file được stage. Sau một tháng, tỷ lệ CI fail do lỗi "vệ sinh" giảm từ 35% xuống dưới 5%. Quan trọng hơn con số: các buổi code review trở nên tập trung vào logic và kiến trúc, thay vì tranh cãi về dấu cách và cách xuống dòng — vì máy đã lo hết phần định dạng.

Bài học: giá trị lớn nhất của pre-commit hook không chỉ là tiết kiệm phút runner, mà là giải phóng năng lượng con người khỏi những việc máy làm tốt hơn.

Ví dụ 2 — Startup fintech: một secret suýt lên GitHub

Một startup fintech ở TP.HCM có khoảng 8 developer. Một bạn junior, trong lúc debug tích hợp cổng thanh toán, đã hardcode tạm một API key thật vào file config để test cho nhanh, rồi... quên xóa. Bạn ấy git add . rồi commit.

May mắn thay, team đã cài hook detect-secrets (một hook có sẵn trong framework pre-commit) từ trước. Ngay khoảnh khắc commit, hook quét diff, phát hiện một chuỗi có entropy cao trông giống secret, và chặn commit lại với thông báo đỏ. Bạn junior giật mình, xóa key ra khỏi code, chuyển nó vào biến môi trường, rồi commit lại thành công.

Hãy hình dung kịch bản không có hook: secret đó lên GitHub, dù có xóa ở commit sau thì nó vẫn nằm trong lịch sử Git mãi mãi, buộc phải revoke key và điều tra xem đã bị lộ chưa. Với một công ty fintech, đó là sự cố bảo mật phải báo cáo. Một hook chạy trong 0,5 giây đã ngăn được nhiều giờ xử lý khủng hoảng.

Bài học: pre-commit hook là tuyến phòng thủ đầu tiên cho bảo mật — bắt lỗi ở nơi rẻ nhất (chưa rời khỏi máy developer). Đây là ứng dụng "shift-left security" ở mức cực đại.

Ví dụ 3 — Team đa ngôn ngữ và bài học "đừng lạm dụng"

Một công ty phần mềm gia công ở Đà Nẵng có một monorepo chứa cả code Go (backend), TypeScript (frontend) và Python (data script). Ban đầu, đội DevOps hào hứng nhồi tất cả vào pre-commit: format cả ba ngôn ngữ, chạy go vet, chạy mypy, và cả... một phần unit test. Mỗi commit mất trung bình 40 giây.

Kết quả rất tệ: developer bắt đầu commit dồn cục lớn (để đỡ phải chờ hook nhiều lần) và ngầm bảo nhau dùng --no-verify. Chỉ số commit "sạch" thực tế còn tệ hơn trước khi có hook.

Đội DevOps họp lại, cắt gọn: giữ lại format + lint (chạy dưới 4 giây nhờ chỉ quét file staged), chuyển toàn bộ test và mypy sang stage của CI. Thời gian commit rơi về ~3 giây. Developer ngừng dùng --no-verify, và tỷ lệ tuân thủ hook đạt gần 100%.

Bài học: một pre-commit hook chỉ có giá trị khi developer thực sự để nó chạy. Tốc độ và sự tôn trọng thời gian của developer quan trọng hơn độ "phủ" kiểm tra. Cái gì nặng, đẩy sang CI.

Hướng dẫn từng bước

Ta sẽ dựng một bộ pre-commit hook thực tế bằng framework pre-commit. Ví dụ dùng cho một dự án bất kỳ.

Bước 1 — Cài công cụ. Framework này là một package Python:

pip install pre-commit
# hoặc trên macOS:
brew install pre-commit
# kiểm tra:
pre-commit --version

Bước 2 — Tạo file cấu hình .pre-commit-config.yaml ở gốc repo:

repos:
  # Bộ hook "vệ sinh" cơ bản, cực nhanh
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace      # xóa khoảng trắng thừa cuối dòng
      - id: end-of-file-fixer        # đảm bảo file kết thúc bằng 1 dòng trống
      - id: check-added-large-files  # chặn file lớn (mặc định >500KB)
      - id: check-merge-conflict     # chặn dấu <<<<<< còn sót
      - id: check-yaml               # kiểm tra YAML hợp lệ

  # Chặn secret bị lộ
  - repo: https://github.com/gitleaks/gitleaks
    rev: v8.18.4
    hooks:
      - id: gitleaks

  # Ví dụ format cho JS/TS bằng Prettier
  - repo: https://github.com/pre-commit/mirrors-prettier
    rev: v3.1.0
    hooks:
      - id: prettier

Bước 3 — Cài hook vào Git. Lệnh này ghi script vào .git/hooks/pre-commit, kết nối Git với framework:

pre-commit install

Từ giờ, mỗi lần bạn git commit, framework sẽ tự động chạy các hook trên các file bạn đã stage.

Bước 4 — Chạy thử trên toàn bộ repo (lần đầu nên làm để "dọn" code cũ):

pre-commit run --all-files

Lần đầu chạy hơi lâu vì framework phải cài môi trường cho từng hook; các lần sau đã được cache nên rất nhanh.

Bước 5 — Ghim phiên bản và cập nhật. Chú ý trường rev: — luôn ghim phiên bản cụ thể để cả team dùng đúng một version, tránh "máy tôi chạy được máy bạn thì không". Khi muốn nâng cấp:

pre-commit autoupdate

Bước 6 (tùy chọn) — Kiểm tra message commit. Nếu team dùng Conventional Commits, thêm hook commitlint hoặc conventional-pre-commit và cài vào stage commit-msg:

pre-commit install --hook-type commit-msg

Bước 7 — Đưa vào CI như một "lưới an toàn". Đây là mẹo quan trọng: đừng chỉ tin vào local. Hãy chạy lại chính bộ hook đó trong CI, để bắt trường hợp ai đó dùng --no-verify:

# .github/workflows/lint.yml (rút gọn)
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- run: pip install pre-commit
- run: pre-commit run --all-files

Như vậy cùng một cấu hình phục vụ cả hai mục đích: feedback nhanh ở local và cưỡng chế bắt buộc ở CI.

Lỗi thường gặp & mẹo

Lỗi 1 — Nhồi quá nhiều thứ nặng vào pre-commit. Như ví dụ Đà Nẵng ở trên: chạy full test hoặc build trong pre-commit khiến commit chậm, dẫn tới developer bỏ qua hook. Mẹo: đặt "ngân sách thời gian" tối đa ~5 giây cho pre-commit. Cái gì vượt ngưỡng, đẩy sang pre-push hoặc CI.

Lỗi 2 — Quên rằng hook không tự cài cho người mới. Khi có thành viên mới clone repo, họ phải tự chạy pre-commit install — nếu quên thì hook không chạy. Mẹo: ghi rõ trong README, hoặc dùng một script setup chung, hoặc thêm bước pre-commit install vào script postinstall của dự án Node.

Lỗi 3 — Coi pre-commit hook là "hàng rào bảo mật tuyệt đối". Nó chạy trên máy developer nên hoàn toàn có thể bị bỏ qua bằng --no-verify. Mẹo: luôn có tầng cưỡng chế thật sự ở CI (chạy lại hook, hoặc dùng branch protection rule bắt buộc check pass mới merge). Local để thuận tiện, CI để bắt buộc.

Lỗi 4 — Hook tự sửa file nhưng developer không để ý. Nhiều hook format (như Prettier, black) sẽ sửa file rồi báo fail để buộc bạn stage lại. Người mới thường bối rối "sao commit fail mà không thấy lỗi". Mẹo: giải thích rõ với team: nếu hook sửa file, chỉ cần git add lại các file đã sửa rồi commit lần nữa.

Lỗi 5 — Không ghim phiên bản (rev). Để rev trỏ tới nhánh động sẽ khiến mỗi người tải version khác nhau, gây kết quả không nhất quán. Mẹo: luôn ghim tag cụ thể, nâng cấp có kiểm soát bằng pre-commit autoupdate.

Mẹo hiệu năng — hiểu "staged files". Framework chỉ chạy trên file đã git add. Nếu bạn sửa file sau khi đã stage, hook chạy trên bản đã stage chứ không phải bản trên đĩa — thỉnh thoảng gây nhầm lẫn. Cứ nhớ: stage rồi mới commit.

Mẹo văn hóa — bán ý tưởng, đừng ép. Cách tốt nhất để cả team đón nhận pre-commit hook là cho họ thấy nó tiết kiệm thời gian như thế nào (ít CI fail hơn, review nhẹ nhàng hơn), thay vì áp đặt từ trên xuống. Bắt đầu với vài hook nhẹ nhàng, ai cũng thấy hữu ích, rồi mở rộng dần.

Bài tập thực hành

  • Dựng bộ hook cơ bản. Trong một repo bất kỳ của bạn, cài framework pre-commit, tạo .pre-commit-config.yaml với ít nhất bốn hook: trailing-whitespace, end-of-file-fixer, check-added-large-files, và một hook chặn secret (gitleaks hoặc detect-secrets). Chạy pre-commit run --all-files và sửa hết các lỗi nó báo.
  • Kiểm chứng lớp bảo mật. Cố tình thêm một chuỗi giống API key (ví dụ AKIA... hoặc một token giả) vào một file, rồi thử commit. Xác nhận hook chặn commit lại. Sau đó xóa chuỗi đó và commit thành công. Ghi lại thông báo hook hiển thị.
  • Đo tốc độ. Thêm một hook format cho ngôn ngữ dự án của bạn (Prettier cho JS/TS, black cho Python, gofmt cho Go). Đo thời gian commit trước và sau. Nếu vượt 5 giây, hãy suy nghĩ: hook nào nên chuyển sang pre-push hoặc CI?
  • Đóng vòng với CI. Viết một job CI (GitHub Actions hoặc GitLab CI) chạy pre-commit run --all-files. Thử push một commit bằng git commit --no-verify với lỗi format cố ý, và xác nhận CI bắt được lỗi đó — chứng minh vì sao CI là tầng cưỡng chế cần thiết.
  • Suy ngẫm. Viết ra 3 kiểm tra bạn cho là nên nằm ở pre-commit và 3 kiểm tra nên nằm ở CI trong dự án thực tế của bạn, kèm lý do dựa trên tiêu chí tốc độ và tính tất định.

Tóm tắt

  • Pre-commit hooks dịch chuyển việc phát hiện lỗi về sát thời điểm viết code — nơi lỗi rẻ nhất để sửa. Feedback càng sớm càng rẻ.
  • Git có sẵn cơ chế hook, nhưng thư mục .git/hooks/ không commit được; vì thế ta dùng framework pre-commit (language-agnostic, cấu hình qua .pre-commit-config.yaml) hoặc Husky + lint-staged cho hệ JS.
  • Nguyên tắc vàng: local nhanh, CI kỹ. Pre-commit chỉ giữ kiểm tra nhanh, tất định (format, lint, chặn secret, chặn file lớn); test nặng và scan đầy đủ vẫn thuộc CI.
  • Hook local có thể bị bỏ qua bằng --no-verify, nên luôn chạy lại bộ hook trong CI như tầng cưỡng chế thật sự.
  • Bài học từ thực tế: giá trị lớn nhất không chỉ là tiết kiệm phút runner, mà là bảo vệ bảo mật (chặn secret), giải phóng review khỏi tranh cãi vụn vặt, và giữ một dev loop mà cả team tự nguyện tuân theo vì nó thực sự nhanh và hữu ích.
Một local dev loop tốt là món quà bạn tặng cho chính mình và đồng đội mỗi ngày: ít gián đoạn hơn, ít sự cố hơn, và nhiều thời gian hơn cho những việc thực sự đáng giá.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi