Mở đầu — vì sao bài này quan trọng
Hãy tưởng tượng một buổi sáng thứ Hai điển hình của một Tech Lead. Bạn mở tab pull request và thấy 12 comment. Đọc kỹ lại, bạn phát hiện ra một sự thật đau lòng: 9 trong số 12 comment là những thứ như "thiếu dấu chấm phẩy", "chỗ này nên dùng 2 space chứ không phải tab", "tên biến nên viết camelCase", "import này thừa". Chỉ có 3 comment thực sự nói về logic, về thiết kế, về một edge case mà tác giả bỏ sót.
Đây chính là nỗi đau mà bài học hôm nay muốn giải quyết. Con người — nguồn lực đắt đỏ nhất trong đội kỹ thuật — đang dành phần lớn năng lượng của một buổi review để làm công việc mà một cỗ máy có thể làm chính xác hơn, nhanh hơn, và không bao giờ mệt mỏi hay cảm tính.
Ở bài trước, chúng ta đã bàn về việc xây dựng văn hóa code review — làm sao để review trở thành một hoạt động hợp tác thay vì áp đặt. Bài này đi sâu vào một trụ cột kỹ thuật cụ thể để văn hóa đó bền vững: tự động hóa mọi quy tắc có thể kiểm tra được bằng máy, để reviewer con người chỉ tập trung vào những gì máy không làm được — logic nghiệp vụ, thiết kế, khả năng bảo trì, và các trường hợp biên (edge case).
Nếu bạn đang dẫn dắt một đội kỹ thuật ở Việt Nam — nơi đội ngũ thường phát triển nhanh, tuyển thêm junior liên tục, và thời gian review là tài nguyên khan hiếm — thì đây là một trong những khoản đầu tư có tỷ suất hoàn vốn cao nhất bạn có thể làm cho quy trình kỹ thuật của mình.
Khái niệm cốt lõi
Triết lý nền tảng: "Mọi thứ máy kiểm tra được, đừng để con người làm"
Nguyên tắc dẫn đường cho toàn bộ bài học này rất đơn giản nhưng có sức nặng lớn:
> Nếu một quy tắc có thể được diễn đạt thành một điều kiện đúng/sai mà máy tính kiểm tra được một cách khách quan, thì việc đó phải do máy làm — không bao giờ do con người trong review.
Lý do không chỉ là để tiết kiệm thời gian. Có ba lý do sâu hơn:
Thứ nhất, con người kiểm tra quy tắc máy móc một cách kém. Reviewer có thể bỏ sót một biến không dùng đến ở dòng 340, trong khi linter thì không bao giờ. Con người mệt mỏi, đọc lướt, và có ngày tốt ngày xấu. Máy thì nhất quán tuyệt đối.
Thứ hai, comment về style tạo ra ma sát cảm xúc. Khi một senior comment "chỗ này format sai" trên PR của một junior, junior đó cảm thấy bị soi mói, bị đánh giá. Nhưng khi một công cụ tự động báo lỗi format, đó chỉ là "máy nói vậy" — không có cái tôi, không có sự phán xét. Ma sát biến mất.
Thứ ba, tranh luận về style làm loãng review. Khi PR có 12 comment và 9 comment là về style, tác giả rất dễ bỏ qua hoặc trả lời vội cả 3 comment quan trọng còn lại. Tín hiệu quan trọng bị nhiễu bởi tiếng ồn.
Phân tầng: cái gì máy làm, cái gì người làm
Để áp dụng triết lý này, bạn cần phân loại rõ ràng. Hãy hình dung một cái phễu, mọi PR đi qua các tầng từ dưới lên:
Tầng máy (tự động hóa hoàn toàn):
- Formatting — thụt lề, khoảng trắng, dấu ngoặc, độ dài dòng. Đây là thứ không đáng để bất kỳ ai tranh luận. Công cụ: Prettier (JS/TS), Black (Python), gofmt (Go), spotless (Java).
- Linting — biến không dùng, import thừa, so sánh nguy hiểm (
==thay vì===), pattern dễ gây bug. Công cụ: ESLint, Ruff, golangci-lint, RuboCop. - Type checking — kiểu dữ liệu sai, gọi hàm thiếu tham số. Công cụ: TypeScript, mypy, các trình biên dịch có kiểu tĩnh.
- Test và coverage — test có chạy qua không, độ phủ có tụt xuống dưới ngưỡng không.
- Security scanning — lộ secret trong code, thư viện có lỗ hổng đã biết. Công cụ: gitleaks, Dependabot, Snyk, Trivy.
- Quy ước dự án — tên file, cấu trúc thư mục, độ dài commit message. Có thể viết custom lint rule.
- Logic có đúng không — code này có thực sự giải quyết đúng bài toán không?
- Thiết kế — cách phân chia module, đặt tên khái niệm, ranh giới trách nhiệm có hợp lý không?
- Edge case — điều gì xảy ra khi input rỗng, khi mạng lỗi, khi hai request chạy song song?
- Khả năng bảo trì — 6 tháng nữa người khác đọc có hiểu không?
- Sự đánh đổi (trade-off) — giải pháp này có phù hợp với bối cảnh và ưu tiên hiện tại không?
Ba trạng thái của một kiểm tra tự động
Không phải mọi kiểm tra tự động đều nên chặn merge. Hãy phân biệt:
- Blocking (chặn merge): format sai, lint lỗi nghiêm trọng, test fail, lộ secret. Không đạt thì không merge — không thương lượng.
- Warning (cảnh báo, không chặn): coverage giảm nhẹ, một quy ước "nên có" bị vi phạm. Hiển thị để tác giả biết nhưng vẫn cho merge.
- Informational (thông tin): phân tích kích thước bundle, số dòng thay đổi, gợi ý reviewer phù hợp.
Tình huống thực tế
Ví dụ 1: Startup fintech ở TP.HCM — từ "review chiến trường" đến "review chất lượng"
Một startup fintech tại Quận 1, TP.HCM (gọi tên là PayViet) có đội kỹ thuật 14 người, tăng từ 6 người trong vòng 8 tháng. Khi đội còn nhỏ, mọi người "ngầm hiểu" nhau về style. Nhưng khi tuyển thêm 8 kỹ sư mới, mọi PR bỗng biến thành chiến trường: mỗi người mang một thói quen format khác nhau từ công ty cũ.
Tech Lead của PayViet đo lại và phát hiện: trung bình mỗi PR có 7 comment về style trên tổng 10 comment, thời gian một PR từ lúc mở đến lúc merge là 2,3 ngày. Các junior nản lòng vì cảm thấy bị "bắt lỗi vặt" liên tục.
Anh quyết định làm ba việc trong một tuần: cài Prettier với một file cấu hình duy nhất commit vào repo, thêm ESLint với bộ luật thống nhất, và thiết lập một pre-commit hook cùng một CI check chặn merge nếu format hoặc lint không đạt. Điểm mấu chốt: anh chạy prettier --write một lần trên toàn bộ codebase để đưa mọi thứ về chuẩn ngay từ đầu, tránh việc "PR nào đụng file nào thì format lại file đó" gây xung đột.
Kết quả sau một tháng: comment về style giảm xuống gần như bằng 0. Số comment trung bình mỗi PR còn 4, nhưng toàn bộ là về logic và thiết kế. Thời gian merge giảm còn 0,9 ngày. Quan trọng nhất, một junior chia sẻ trong retro: "Giờ em không sợ mở PR nữa, vì em biết máy đã kiểm hết mấy thứ vặt, mấy anh chỉ góp ý về cách nghĩ của em thôi."
Bài học: Tự động hóa không chỉ giúp nhanh hơn — nó thay đổi cảm xúc của quá trình review, biến nó từ "bị soi" thành "được cố vấn".
Ví dụ 2: Sự cố lộ API key — khi thiếu automation phải trả giá bằng tiền thật
Một công ty thương mại điện tử ở Hà Nội có quy trình review khá tốt về mặt con người — reviewer đọc kỹ, góp ý sâu sắc. Nhưng họ không có công cụ quét secret tự động. Một kỹ sư, trong lúc debug, đã hardcode một cặp AWS access key vào file config để test nhanh, rồi quên xóa. PR có 3 reviewer, tất cả đều tập trung vào logic nghiệp vụ và... không ai để ý dòng key nằm lẫn giữa 400 dòng thay đổi.
Key bị đẩy lên GitHub. Trong vòng khoảng 40 phút, một bot dò quét đã tìm thấy nó và bắt đầu spin up hàng loạt EC2 instance để đào coin. Hóa đơn AWS tháng đó phát sinh thêm gần 3.000 USD trước khi đội phát hiện và thu hồi key.
Đây là ví dụ kinh điển cho thấy: con người, dù giỏi đến đâu, cũng không phải là lớp phòng thủ tin cậy cho những thứ máy quét được. Sau sự cố, đội cài gitleaks chạy ở cả pre-commit hook lẫn CI, chặn merge nếu phát hiện pattern giống secret. Chi phí cài đặt: nửa ngày công. Chi phí của việc không cài: 3.000 USD và một buổi tối khủng hoảng.
Bài học: Có những loại kiểm tra không chỉ nên tự động hóa vì tiện, mà bắt buộc phải tự động hóa vì con người về bản chất không đủ khả năng làm nó đáng tin cậy ở quy mô.
Ví dụ 3: Grab và "tường thành" custom lint
Ở quy mô lớn hơn, các công ty như Grab (với đội kỹ thuật hàng nghìn người khắp Đông Nam Á) không thể dựa vào việc mọi reviewer nhớ hết mọi quy ước nội bộ. Họ viết custom lint rule để mã hóa các quy ước riêng thành luật máy: ví dụ, cấm import trực tiếp giữa một số module nhất định để bảo vệ ranh giới kiến trúc, hoặc bắt buộc mọi hàm gọi database phải đi qua một lớp wrapper có logging.
Điểm hay ở đây là tư duy: mỗi khi một quy ước được nhắc lại lần thứ ba trong review, nó là ứng viên để trở thành một luật tự động. Thay vì 50 reviewer phải nhớ "đừng import chéo module", một luật lint sẽ nhắc mọi người một cách nhất quán, ngay tại thời điểm gõ code.
Bài học: Automation không chỉ dành cho style phổ quát. Những quy ước riêng của tổ chức bạn — vốn là nơi kiến thức dễ thất thoát nhất khi đội phình to — cũng là mảnh đất màu mỡ để tự động hóa.
Hướng dẫn từng bước
Đây là lộ trình thực tế để đưa một đội từ "review thủ công mọi thứ" sang "automation gánh phần máy móc":
Bước 1 — Audit các comment review hiện tại. Dành một tuần, ghi lại (hoặc dùng script quét lịch sử PR) để phân loại comment: bao nhiêu % là style/format, bao nhiêu % là logic/thiết kế. Con số này chính là "bằng chứng" để thuyết phục đội và cũng là baseline để đo cải thiện.
Bước 2 — Bắt đầu với formatter, vì nó gây ít tranh cãi nhất. Chọn một formatter chuẩn cho ngôn ngữ (Prettier, Black, gofmt). Commit một file cấu hình duy nhất. Chạy format một lần trên toàn repo trong một commit riêng biệt, đặt tên rõ ràng như "chore: format toàn bộ codebase" để không lẫn với thay đổi logic.
Bước 3 — Thêm linter với bộ luật khởi đầu vừa phải. Đừng bật tất cả luật ngay — sẽ có hàng nghìn cảnh báo và đội sẽ bỏ cuộc. Bắt đầu với preset khuyến nghị (recommended), rồi siết dần theo tháng.
Bước 4 — Đặt các kiểm tra vào pipeline hai lớp. Lớp gần: pre-commit hook (dùng husky, lefthook, hoặc pre-commit) để lỗi được bắt trước khi đẩy lên. Lớp xa: CI check chặn merge, để không ai lách được dù có bỏ qua hook cục bộ. Hai lớp bổ trợ nhau: hook cho phản hồi nhanh, CI cho sự đảm bảo tuyệt đối.
Bước 5 — Thêm type checking, test, coverage và security scan. Lần lượt đưa từng lớp vào. Với coverage, hãy đặt ngưỡng ở mức hiện tại của đội chứ đừng đặt một con số lý tưởng xa vời — mục tiêu là "không tụt", không phải "phải đạt 90% ngay".
Bước 6 — Ghi rõ các kiểm tra vào tài liệu và làm chúng chạy tự động cho người mới. Một kỹ sư mới clone repo về, chạy một lệnh setup, và mọi hook đã sẵn sàng. Đừng để việc cài automation phụ thuộc vào trí nhớ.
Bước 7 — Thiết lập vòng lặp: quy ước lặp lại → luật tự động. Trong retro định kỳ, hỏi: "Comment nào chúng ta lặp đi lặp lại trong tháng qua?" Mỗi quy ước lặp lại là một ứng viên để viết thành custom lint rule.
Lỗi thường gặp & mẹo
Lỗi 1: Format toàn repo lẫn trong PR logic. Khi bạn vừa sửa logic vừa reformat 200 file trong cùng một PR, reviewer không thể phân biệt đâu là thay đổi thật. Mẹo: luôn tách commit format thành một commit/PR riêng, và thêm nó vào file .git-blame-ignore-revs để git blame không bị nhiễu.
Lỗi 2: Bật quá nhiều luật cùng lúc. Đội đối mặt với 3.000 cảnh báo, cảm thấy ngợp, và bắt đầu bỏ qua toàn bộ output của linter — kể cả những cảnh báo quan trọng. Mẹo: bật từ từ, mỗi sprint thêm một nhóm luật, xử lý sạch trước khi thêm nhóm mới.
Lỗi 3: Chỉ có CI check mà không có pre-commit hook. Kỹ sư đẩy code lên, chờ 5 phút CI chạy, rồi mới biết format sai, sửa, đẩy lại, chờ tiếp. Vòng lặp phản hồi quá chậm gây bực bội. Mẹo: luôn có lớp hook cục bộ để bắt lỗi trong vài giây.
Lỗi 4: Reviewer vẫn comment về những thứ máy đã lo. Automation chỉ hiệu quả khi con người thực sự ngừng làm phần của máy. Mẹo: đặt ra một thỏa thuận đội rõ ràng — "Nếu linter không bắt nó, thì nó không phải là vấn đề style đáng comment. Nếu bạn thấy một style đáng comment mà máy bỏ sót, hãy đi thêm luật thay vì comment."
Lỗi 5: Đối xử với warning như noise mãi mãi. Cảnh báo tích tụ đến hàng trăm rồi không ai nhìn nữa. Mẹo: hoặc nâng warning quan trọng lên blocking, hoặc dọn sạch, đừng để trạng thái lấp lửng lâu dài.
Mẹo vàng: Hãy coi cấu hình automation (file Prettier, ESLint, CI) là code sản phẩm — nó cần được review, version, và cải thiện liên tục, không phải cài một lần rồi quên.
Bài tập thực hành
- Audit thực tế: Lấy 10 PR gần nhất trong dự án của bạn. Phân loại từng comment thành hai nhóm — "máy làm được" và "chỉ người làm được". Tính tỷ lệ phần trăm. Nếu nhóm "máy làm được" chiếm trên 30%, bạn có một cơ hội tự động hóa rõ ràng.
- Cài lớp đầu tiên: Chọn một repo bạn đang làm, cài formatter và linter với pre-commit hook. Đo thời gian cài đặt và ghi lại. Chạy format một lần cho toàn repo trong một commit riêng.
- Viết một custom rule: Tìm một quy ước riêng của đội bạn (ví dụ "mọi API endpoint phải có handler xử lý lỗi") mà hiện đang được nhắc bằng miệng trong review. Phác thảo cách bạn sẽ biến nó thành một kiểm tra tự động — dù chỉ là một grep script đơn giản trong CI.
- Thiết kế bảng phân tầng: Liệt kê mọi kiểm tra tự động bạn muốn có, và gán mỗi cái vào một trong ba trạng thái: blocking / warning / informational. Giải thích lý do cho từng lựa chọn.
Tóm tắt
Bài học cốt lõi của Bài 14 nằm gọn trong một câu: mọi quy tắc có thể kiểm tra bằng máy thì đừng để con người làm. Reviewer là nguồn lực quý giá nhất của đội kỹ thuật — hãy để họ tập trung vào logic, thiết kế, và edge case, những thứ máy không thể thay thế.
Các điểm cần nhớ:
- Phân tầng rõ ràng giữa tầng máy (format, lint, type, test, security) và tầng người (logic, thiết kế, khả năng bảo trì, trade-off).
- Triển khai theo hai lớp: pre-commit hook cho phản hồi nhanh, CI check cho đảm bảo tuyệt đối.
- Bắt đầu nhẹ, siết dần — đừng bật mọi luật cùng lúc.
- Automation không chỉ tiết kiệm thời gian; nó khử ma sát cảm xúc và làm review trở thành hoạt động cố vấn thay vì bắt lỗi.
- Có những kiểm tra (như quét secret) mà con người về bản chất không thể làm đáng tin — với chúng, automation là bắt buộc, không phải tùy chọn.
- Mỗi quy ước lặp lại ba lần trong review là một ứng viên trở thành luật tự động.