Mở đầu — vì sao bài này quan trọng
Nếu bạn từng làm việc trong một đội ngũ phát triển phần mềm, bạn sẽ nhận ra một sự thật thú vị: phần lớn thời gian một dòng code "sống" không phải nằm ở lúc lập trình viên gõ nó ra, mà nằm ở khoảng giữa lúc code được viết xong và lúc nó thực sự được merge vào nhánh chính. Khoảng thời gian đó chính là vòng đời của một Pull Request (PR) — hay còn gọi là Merge Request (MR) trong thế giới GitLab.
Pull Request Workflow là điểm giao thoa quan trọng nhất giữa con người và pipeline tự động. Đây là nơi CI chạy để kiểm tra chất lượng, là nơi đồng nghiệp đọc và góp ý code của bạn, là nơi kiến thức về hệ thống được lan truyền trong đội. Một quy trình PR tốt giúp đội đi nhanh mà vẫn an toàn. Một quy trình PR tồi khiến developer nản lòng, code review trở thành hình thức, và bug lọt xuống production.
Trong bài này, chúng ta tập trung vào luồng làm việc của PR và văn hóa code review — cách một PR đi từ lúc mở đến lúc merge, vai trò của người review, và làm sao để review vừa nghiêm túc vừa không trở thành nút thắt cổ chai. Chúng ta sẽ không đi sâu vào cách viết workflow YAML của GitHub Actions (đó là Bài 6, 7) hay chiến lược phân nhánh chi tiết (Bài 3, 56) — mà nhìn PR như một "sản phẩm cộng tác" giữa người viết code, người review, và pipeline.
Khái niệm cốt lõi
Pull Request là gì và tại sao nó tồn tại
Pull Request là một lời đề nghị: "Tôi muốn đưa những thay đổi từ nhánh của tôi vào nhánh đích (thường là main hoặc develop). Xin hãy xem xét và phê duyệt." Nó không chỉ là thao tác merge — nó là một đơn vị công việc có thể thảo luận được. PR gói gọn ba thứ: tập hợp các commit (diff), một phần mô tả bằng ngôn ngữ tự nhiên, và một cuộc hội thoại (review, comment, phê duyệt).
Vì sao không merge thẳng vào main? Vì main là nhánh mà mọi người deploy từ đó. Nếu ai cũng push thẳng, một dòng code lỗi có thể làm sập build của cả đội, hoặc tệ hơn là lọt ra production. PR tạo ra một "cổng kiểm soát" (quality gate) mà mọi thay đổi phải đi qua.
Luồng PR chuẩn (Standard PR flow)
Một PR điển hình đi qua các bước sau:
- Tạo nhánh từ
main— Bạn checkout một nhánh mới, thường đặt tên theo quy ước nhưfeature/gio-hang-moihoặcfix/loi-thanh-toan-momo. Nhánh này là không gian làm việc riêng, cô lập với code của người khác. - Commit công việc — Bạn viết code, chia thành các commit có ý nghĩa. Commit tốt là commit nhỏ, thông điệp rõ ràng, mô tả "vì sao" chứ không chỉ "cái gì".
- Push và mở PR — Đẩy nhánh lên remote, mở PR trỏ vào nhánh đích. Đây là lúc bạn viết phần mô tả PR — phần quan trọng mà nhiều người bỏ qua.
- CI chạy tự động — Pipeline khởi động: build, test, lint, security scan. Trạng thái các check này hiển thị ngay trên PR (dấu tích xanh hoặc chữ X đỏ).
- Gán reviewer — Một hoặc nhiều đồng nghiệp được chỉ định (hoặc tự động gán qua
CODEOWNERS) để đọc và đánh giá. - Vòng lặp review — Reviewer để lại comment, bạn sửa, push thêm commit, CI chạy lại. Lặp cho đến khi được approve.
- Merge — Khi đủ số approve và tất cả check xanh, PR được merge. Nhánh feature thường được xóa sau đó.
Vai trò của CI trong PR
CI không thay thế con người, mà giải phóng con người khỏi những việc máy làm tốt hơn. Máy kiểm tra: code có build được không, test có pass không, có lỗi lint không, có lỗ hổng bảo mật đã biết không. Nhờ vậy reviewer không phải mất công soi những thứ máy đã soi, mà tập trung vào thứ máy không làm được: logic có đúng không, thiết kế có hợp lý không, code có dễ bảo trì không.
Một nguyên tắc vàng: PR không nên merge được khi CI còn đỏ. Trên GitHub, đây là "required status checks" — bạn cấu hình branch protection để chặn merge nếu check chưa xanh.
Branch protection và merge gate
Branch protection rules là "hàng rào" bảo vệ nhánh chính. Các cấu hình phổ biến:
- Yêu cầu tối thiểu N approve (thường 1–2).
- Yêu cầu tất cả status check phải xanh.
- Yêu cầu nhánh phải up-to-date với
maintrước khi merge. - Cấm push trực tiếp, buộc mọi thay đổi qua PR.
- Yêu cầu review lại nếu có commit mới sau khi đã approve (dismiss stale approvals).
Ba kiểu merge
Khi merge, bạn thường có ba lựa chọn, mỗi kiểu để lại lịch sử git khác nhau:
- Merge commit: giữ nguyên mọi commit của nhánh, thêm một commit merge. Lịch sử đầy đủ nhưng rối.
- Squash and merge: gộp tất cả commit thành một. Lịch sử
mainsạch, mỗi PR = một commit. Rất phổ biến ở các đội theo trunk-based. - Rebase and merge: đưa từng commit lên đầu
main, không có commit merge. Lịch sử tuyến tính nhưng đòi hỏi kỷ luật commit.
Tình huống thực tế
Ví dụ 1 — Tiki và bài học "PR khổng lồ"
Một đội backend giả định tại một sàn thương mại điện tử lớn ở Việt Nam (lấy bối cảnh tương tự Tiki) gặp vấn đề kinh điển: một developer mở một PR sửa lại toàn bộ module thanh toán với hơn 3.200 dòng thay đổi trên 47 file. Reviewer nhìn vào PR, thở dài, rồi để đó ba ngày vì "chưa có thời gian đọc kỹ". Cuối cùng, dưới áp lực deadline, reviewer approve qua loa với một comment duy nhất: "LGTM" (Looks Good To Me).
Hai tuần sau, một bug về làm tròn số tiền (lỗi 1 đồng do dùng float thay vì số nguyên đơn vị VNĐ) lọt ra production, gây lệch sổ sách. Khi truy vết, bug nằm ngay trong PR khổng lồ đó, ở dòng thứ 2.100 — nơi không ai thực sự đọc.
Bài học rút ra: PR càng lớn, chất lượng review càng giảm theo cấp số nhân. Nghiên cứu thực nghiệm (SmartBear, Cisco) cho thấy khả năng phát hiện lỗi của reviewer sụt mạnh khi PR vượt quá 400 dòng. Đội này sau đó áp dụng quy tắc "PR dưới 400 dòng", chia công việc lớn thành nhiều PR nhỏ nối tiếp (stacked PR). Kết quả: thời gian review trung bình giảm từ 2,5 ngày xuống còn 4 giờ, và số bug lọt production giảm rõ rệt.
Ví dụ 2 — Startup fintech Đông Nam Á và CODEOWNERS
Một startup fintech ở Singapore phục vụ thị trường Đông Nam Á có một vấn đề: mọi PR đều bị gán cho tech lead review, và anh này trở thành nút thắt cổ chai. Trung bình PR chờ 18 giờ mới được nhìn tới, có PR chờ cả cuối tuần. Developer bực bội vì code viết xong thứ Sáu phải chờ tới thứ Ba mới merge được.
Đội giải quyết bằng file CODEOWNERS. Họ chia codebase theo domain: thư mục /payments/ thuộc về đội thanh toán, /kyc/ thuộc về đội định danh, /frontend/ thuộc về đội web. Khi một PR động vào file nào, GitHub tự động gán đúng người sở hữu domain đó làm reviewer. Tech lead chỉ còn được gán cho các thay đổi hạ tầng lõi.
Họ cũng đặt ra một cam kết văn hóa: "review trong vòng 4 giờ làm việc" — reviewer không cần review ngay lập tức, nhưng phải phản hồi trong nửa ngày. Họ dùng bot ChatOps nhắc trong Slack khi có PR chờ quá lâu.
Bài học rút ra: Bottleneck trong review thường không phải vấn đề kỹ thuật mà là vấn đề quy trình và phân bổ. CODEOWNERS phân tán trách nhiệm về đúng người hiểu nhất, và một SLA (cam kết thời gian) rõ ràng giúp PR không bị bỏ quên. Sau ba tháng, thời gian chờ review trung bình giảm còn 3 giờ.
Ví dụ 3 — Đội của tôi và cuộc chiến "nitpick"
Ở một đội tôi từng cố vấn, code review từng là chiến trường. Một reviewer khó tính có thói quen bắt bẻ từng dấu cách, từng cách đặt tên biến, viết cả chục comment kiểu "biến này nên tên là userList chứ không phải users". Developer trẻ cảm thấy bị soi mói, và không khí đội căng thẳng.
Chúng tôi giải quyết bằng hai thay đổi. Thứ nhất, tự động hóa mọi thứ về style — đưa Prettier và ESLint vào pipeline và pre-commit, để máy lo phần định dạng, con người không cần cãi nhau về dấu cách nữa. Thứ hai, chúng tôi áp dụng quy ước gắn nhãn comment: một reviewer khi góp ý phải phân loại rõ — nit: (chuyện nhỏ, không bắt buộc sửa), question: (tôi chưa hiểu, giải thích giúp), hoặc blocking: (phải sửa mới merge được).
Bài học rút ra: Phần lớn xung đột trong review đến từ việc trộn lẫn "ý kiến cá nhân" với "yêu cầu bắt buộc". Khi tách bạch rõ ràng, developer biết cái gì cần sửa và cái gì chỉ là gợi ý. Không khí đội dịu đi, và tốc độ merge tăng lên. Code review chuyển từ "kiểm tra bắt lỗi" sang "cộng tác nâng chất".
Hướng dẫn từng bước
Dưới đây là cách thiết lập và vận hành một PR workflow lành mạnh cho đội của bạn.
Bước 1 — Chuẩn hóa việc tạo nhánh và commit. Thống nhất quy ước đặt tên nhánh (feature/, fix/, hotfix/) và định dạng commit message. Nhiều đội dùng Conventional Commits (feat:, fix:, docs:) vì nó giúp tự động sinh changelog sau này.
Bước 2 — Tạo PR template. Thêm file .github/pull_request_template.md để mỗi PR mới đều có sẵn khung mô tả. Một template tốt gồm: mục đích thay đổi, cách kiểm thử, ảnh chụp màn hình (nếu là UI), và checklist tự kiểm.
## Mô tả
Thay đổi này làm gì và vì sao?
## Cách kiểm thử
- [ ] Đã test local
- [ ] Đã thêm unit test
## Ghi chú cho reviewer
Có phần nào cần chú ý đặc biệt không?
Bước 3 — Bật branch protection. Vào Settings → Branches, thêm rule cho main: yêu cầu PR, yêu cầu ít nhất 1 approve, yêu cầu status check xanh, cấm push trực tiếp.
Bước 4 — Cấu hình required CI checks. Đảm bảo build, test, lint, security scan chạy trên mỗi PR và được đánh dấu "required". PR không thể merge nếu bất kỳ check nào đỏ.
Bước 5 — Thiết lập CODEOWNERS. Tạo file .github/CODEOWNERS để tự động gán reviewer theo đường dẫn:
/payments/ @doi-thanh-toan
/frontend/ @doi-web
*.tf @doi-devops
Bước 6 — Quy ước review comment. Thống nhất trong đội cách gắn nhãn (nit:, question:, blocking:) và cam kết SLA thời gian phản hồi.
Bước 7 — Chọn chiến lược merge. Thống nhất dùng squash, merge commit hay rebase. Với đa số đội, "Squash and merge" cho lịch sử main sạch sẽ, mỗi PR là một dòng lịch sử.
Bước 8 — Tự động dọn dẹp. Bật tự động xóa nhánh sau merge để repo không bị ngập nhánh cũ.
Lỗi thường gặp & mẹo
Lỗi: PR quá lớn. Đây là lỗi số một. PR trên 400 dòng khiến review hời hợt. Mẹo: chia nhỏ theo từng thay đổi logic độc lập; nếu buộc phải làm việc lớn, dùng stacked PR (nhiều PR nhỏ chồng lên nhau).
Lỗi: Mô tả PR trống rỗng. Reviewer phải tự đoán ý định của bạn. Mẹo: luôn viết mô tả trả lời câu hỏi "vì sao thay đổi này cần thiết", không chỉ "cái gì thay đổi" — vì cái gì thì diff đã nói rồi.
Lỗi: "LGTM" mù quáng. Approve mà không thực sự đọc là phản bội mục đích của review. Mẹo: nếu bạn không đủ thời gian review kỹ, hãy nói thẳng thay vì approve cho xong.
Lỗi: Cãi nhau về style thủ công. Tốn thời gian và gây căng thẳng. Mẹo: giao toàn bộ định dạng cho formatter/linter tự động, con người chỉ bàn logic và thiết kế.
Lỗi: Review chỉ trích cá nhân. Comment kiểu "code này tệ" làm tổn thương và giảm động lực. Mẹo: nhận xét về code chứ không về người; đặt câu hỏi thay vì ra lệnh ("Liệu ta có thể dùng map ở đây không?" thay vì "Sai rồi, dùng map đi").
Mẹo về draft PR. Nếu công việc chưa xong nhưng muốn xin ý kiến sớm, mở PR ở chế độ "Draft" — CI vẫn chạy nhưng chưa xin ai approve, tránh làm phiền reviewer khi chưa sẵn sàng.
Mẹo về self-review. Trước khi xin người khác, hãy tự đọc diff của chính mình một lượt. Bạn sẽ bất ngờ vì bắt được nhiều lỗi ngớ ngẩn ngay tại đó.
Bài tập thực hành
- Thiết lập cổng bảo vệ: Trên một repo cá nhân, bật branch protection cho
mainyêu cầu ít nhất 1 approve và một status check (ví dụ một workflow chạynpm test). Thử push thẳng vàomainvà quan sát bị chặn thế nào.
- Viết PR template: Tạo file
.github/pull_request_template.mdvới các mục mô tả, cách test, và checklist. Mở một PR thử để xác nhận template hiện ra.
- Chia nhỏ PR khổng lồ: Lấy một thay đổi giả định gồm ba việc (thêm API, cập nhật UI, viết docs). Thay vì gộp thành một PR, hãy tách thành ba PR riêng và mô tả sự phụ thuộc giữa chúng.
- Thực hành review: Tìm một PR mã nguồn mở trên GitHub và viết thử ba comment theo quy ước nhãn (
nit:,question:,blocking:). Tự đánh giá xem comment của bạn có mang tính xây dựng không.
- Cấu hình CODEOWNERS: Tạo file
CODEOWNERScho một repo có ít nhất hai thư mục, gán chủ sở hữu khác nhau, rồi mở PR chạm vào mỗi thư mục để xem reviewer được tự động gán.
Tóm tắt
Pull Request Workflow là trái tim của sự cộng tác trong CI/CD. Một PR đi qua vòng đời chuẩn: tạo nhánh, commit, mở PR, CI chạy tự động, gán reviewer, vòng lặp góp ý, rồi merge. Sức mạnh của quy trình này đến từ sự kết hợp giữa máy (CI kiểm tra build, test, lint, security) và người (reviewer đánh giá logic, thiết kế, tính bảo trì).
Những nguyên tắc cốt lõi cần nhớ: giữ PR nhỏ (dưới 400 dòng) để review có chất lượng; viết mô tả rõ ràng trả lời "vì sao"; dùng branch protection và required checks làm cổng an toàn; phân bổ reviewer bằng CODEOWNERS để tránh nút thắt cổ chai; tự động hóa mọi tranh cãi về style; và xây dựng văn hóa review nhân văn — nhận xét về code chứ không về người.
Ba tình huống thực tế cho thấy: PR khổng lồ giấu bug, bottleneck review giết tốc độ đội, và review thiếu văn hóa phá vỡ tinh thần. Cả ba đều có lời giải, và lời giải luôn nằm ở việc kết hợp công cụ tự động với quy ước con người rõ ràng. Khi làm đúng, PR workflow không chỉ chặn bug — nó còn là nơi cả đội cùng học hỏi và nâng tầm chất lượng mỗi ngày.