Skip to content

3.8 - Developer Workflow thực tế ​

Tóm tắt: Biết lệnh Git chưa đủ; cái quyết định là quy ước của đội. Bài này so sánh hai mô hình phổ biến và đưa ra khuyến nghị rõ ràng: Git Flow với năm loại nhánh sinh ra cho thời kỳ phần mềm phát hành theo phiên bản đóng gói, nên với đội nhỏ deploy vài lần một tuần nó chỉ tạo thêm nghi lễ; trunk-based với nhánh sống dưới một ngày hợp hơn nhiều. Phần thứ hai là quy trình hotfix — thứ bạn chỉ cần tới lúc production đang hỏng và không còn đầu óc để nghĩ ra, nên phải viết sẵn từ trước.

Mục tiêu bài học ​

Sau bài này bạn có thể:

  • Đi hết một chu trình từ nhận việc tới merge mà không bỏ bước nào.
  • So sánh Git Flow với trunk-based và chọn theo quy mô đội.
  • Thực hiện một hotfix đúng thứ tự khi production đang hỏng.
  • Giải thích vì sao feature flag thay thế được nhánh sống lâu.
  • Nhận ra dấu hiệu quy trình của nhóm đang quá nặng.

Cần biết trước: bài 3.4 đến bài 3.7: remote, nhánh, merge/rebase, pull request, CI và xung đột.

Nội dung bài học ​

3.8.1 — Một ngày làm việc ​

bash
# 1. Đồng bộ trước khi bắt đầu
git switch main && git pull

# 2. Nhánh cho đúng một việc
git switch -c feature/CRM-142-revenue-report

# 3. Làm việc, commit nhỏ và thường xuyên
git add -p && git commit -m "feat: add revenue query by branch"

# 4. Trước khi mở PR, đồng bộ lại
git fetch origin && git rebase origin/main

# 5. Đẩy lên và mở PR
git push -u origin feature/CRM-142-revenue-report

# 6. Sau khi PR được merge trên GitHub, dọn
git switch main && git pull
git branch -D feature/CRM-142-revenue-report   # -D vì nhóm dùng squash merge
git fetch --prune

Vì sao bước 6 dùng -D (xoá cưỡng bức) chứ không phải -d? Nhóm dùng squash merge (bài 3.6) nên các commit gốc của nhánh không nằm trong main, và git branch -d sẽ từ chối với lỗi not fully merged (bài 3.5 có output thật). Chỉ dùng -D sau khi đã thấy PR ở trạng thái Merged trên GitHub. Nhóm dùng merge commit thường thì -d vẫn chạy bình thường.

Bước 4 là bước hay bị bỏ nhất, và là bước tiết kiệm nhiều thời gian nhất. Đồng bộ trước khi mở PR nghĩa là bạn giải xung đột trên máy mình, lúc rảnh rang — thay vì giải nó trong giao diện web khi người review đang chờ.

3.8.2 — Git Flow ​

Năm loại nhánh: main, develop, feature/*, release/*, hotfix/*.

Hợp khi: phần mềm phát hành theo phiên bản đóng gói, phải hỗ trợ nhiều phiên bản cùng lúc, có giai đoạn QA tách biệt, hoặc khách hàng tự quyết khi nào nâng cấp.

Không hợp khi: đội nhỏ, deploy liên tục, chỉ có một phiên bản đang chạy — tức là phần lớn ứng dụng web nội bộ và SaaS.

Git Flow ra đời năm 2010, cho bối cảnh phát hành theo bản. Chính tác giả của nó sau này đã ghi chú rằng nó không phù hợp với các đội làm sản phẩm web deploy liên tục. Dùng nó cho một API nội bộ deploy ba lần một tuần là mang bộ máy của một nhà xuất bản phần mềm vào một đội năm người.

3.8.3 — Trunk-based ​

Một nhánh chính (trunk, ở đây là main). Nhánh tính năng sống rất ngắn, lý tưởng dưới một ngày. Merge vào main liên tục. main luôn ở trạng thái deploy được.

GitHub Flow là cách làm phổ biến nhất của trunk-based với đội nhỏ: tách nhánh ngắn từ main, mở pull request, review và CI, squash merge, deploy từ main. Hai cái tên không phải hai lựa chọn khác nhau: GitHub Flow là trunk-based có thêm bước pull request. Khóa học dùng cách này.

Điều kiện để chạy được:

  • Bộ test đủ tin cậy, vì không có giai đoạn QA riêng để bắt lỗi.
  • CI nhanh, dưới 10 phút, nếu không người ta sẽ ngại merge.
  • Feature flag cho tính năng chưa hoàn thiện.

Điểm thứ ba là chìa khoá. Nó giải quyết mâu thuẫn "tính năng chưa xong thì merge kiểu gì":

csharp
if (_features.IsEnabled("new-revenue-report"))
{
    return await _newReportService.GenerateAsync(ct);
}
return await _legacyReportService.GenerateAsync(ct);

Code chưa hoàn thiện vẫn vào main mỗi ngày, chỉ là chưa bật cho người dùng. Đổi lại bạn không bao giờ có nhánh sống ba tuần — thứ gây ra phần lớn xung đột nặng.

Cái giá: phải dọn flag sau khi tính năng ổn định. Flag bỏ quên tích tụ thành một dạng nợ kỹ thuật riêng.

3.8.4 — Hotfix: viết sẵn trước khi cần ​

Production đang hỏng. Đây là lúc không nên nghĩ ra quy trình.

bash
# 1. Tách từ TRẠNG THÁI ĐANG CHẠY, không phải từ develop
git switch main && git pull
git switch -c hotfix/negative-order-total

# 2. Sửa NHỎ NHẤT CÓ THỂ — chỉ chặn máu, không tái cấu trúc
git commit -am "fix: reject orders with negative total"

# 3. Vẫn mở PR, vẫn để CI chạy
git push -u origin hotfix/negative-order-total

# 4. Merge và deploy

# 5. Gộp ngược về nhánh phát triển nếu mô hình có nhánh đó
git switch develop && git merge hotfix/negative-order-total

Bốn nguyên tắc, và mỗi cái đều từng bị vi phạm với hậu quả thật:

  1. Tách từ nhánh đang chạy trên production. Tách từ develop là kéo theo mọi thứ chưa được kiểm thử.
  2. Sửa nhỏ nhất có thể. Hotfix không phải lúc dọn dẹp. Ghi lại việc dọn vào một issue riêng.
  3. Vẫn chạy CI. Cám dỗ bỏ qua khi đang gấp là lớn nhất, và đó cũng là lúc một lỗi thứ hai gây thiệt hại nhất.
  4. Đừng quên bước 5. Quên gộp ngược về nhánh phát triển thì lần phát hành sau sẽ xoá mất bản vá, và lỗi quay lại.

Bước 5 là lỗi kinh điển của các đội dùng Git Flow, và là một điểm cộng nữa cho trunk-based — nơi chỉ có một nhánh nên không có gì để quên.

3.8.5 — Chọn mô hình nào ​

Git FlowTrunk-based
Quy mô độiVừa và lớnMọi quy mô, tốt nhất với nhỏ
Nhịp phát hànhTheo bản, có lịchLiên tục
Tuổi nhánhNhiều tuầnDưới một ngày
Cần feature flagKhôngCó
Cần bộ test mạnhVừaRất cần
Độ phức tạp quy trìnhCaoThấp

Khuyến nghị thực dụng: bắt đầu bằng trunk-based theo kiểu GitHub Flow — nhánh ngắn, pull request, squash merge vào main. Chỉ thêm nhánh và quy trình khi có một vấn đề cụ thể đòi hỏi, ví dụ phải duy trì song song nhiều phiên bản đã phát hành. Quy trình nặng rất dễ thêm vào và rất khó bỏ đi.

3.8.6 — Dấu hiệu quy trình đang quá nặng ​

  • Mỗi thay đổi nhỏ mất hơn một ngày để tới production.
  • Có nhánh sống hơn hai tuần.
  • Người ta thường xuyên phải giải cùng một xung đột nhiều lần.
  • Có bước trong quy trình mà không ai giải thích được vì sao tồn tại.
  • Hotfix phải đi vòng qua ba nhánh trước khi tới được production.

Mỗi dấu hiệu trên là một chi phí trả hằng ngày. Quy trình tồn tại để giảm rủi ro — khi nó không còn giảm rủi ro nào cụ thể, nó chỉ còn là chi phí.

3.8.7 — Rà lại quy trình của nhóm ​

Danh sách rà soát workflow của nhóm

  • Nhánh tính năng sống dưới một tuần, lý tưởng là dưới một ngày.
  • Đồng bộ với main trước khi mở PR, không phải sau.
  • main luôn ở trạng thái deploy được.
  • Có quy trình hotfix viết sẵn, và mọi người biết nó ở đâu.
  • Hotfix tách từ nhánh đang chạy trên production.
  • Có cơ chế feature flag cho tính năng chưa hoàn thiện.
  • Có lịch dọn các feature flag đã hết vai trò.
  • Mỗi bước trong quy trình đều giải thích được nó giảm rủi ro gì.

Bài tập áp dụng ​

Bài 1 — Đo tuổi nhánh ​

Dựng kho luyện tập có sẵn vài nhánh ở các "tuổi" khác nhau bằng script dưới đây (chạy trong Git Bash, macOS hoặc Linux), rồi liệt kê nhánh theo ngày commit cuối và quyết định xử lý từng nhánh. Nếu bạn đã làm dự án nhóm ở CLB, chạy thêm lệnh đó trên kho của nhóm (thay refs/heads bằng refs/remotes/origin).

bash
mkdir branch-age && cd branch-age && git init -q -b main
echo base > f.txt && git add . && git commit -q -m "chore: init"
nhanh() {   # nhanh <tên> <số ngày trước> <tác giả>
  git switch -q -c "$1" main
  echo "$1" > "$(echo "$1" | tr / -).txt" && git add .
  d=$(date -d "$2 days ago" --iso-8601=seconds)    # trên macOS: date -v-"$2"d +%Y-%m-%dT%H:%M:%S
  GIT_AUTHOR_DATE="$d" GIT_COMMITTER_DATE="$d" GIT_AUTHOR_NAME="$3" git commit -q -m "wip: $1"
  git switch -q main
}
nhanh fix/order-total 2 An
nhanh feat/revenue-report 9 Binh
nhanh feat/payment-integration 35 Cuong
nhanh refactor/split-service 95 Dung
bash
git for-each-ref --sort=committerdate \
  --format='%(refname:short)  %(committerdate:relative)  %(authorname)' \
  refs/heads

Tiêu chí hoàn thành: bạn đếm được số nhánh sống quá hai tuần, và với mỗi nhánh già bạn ghi được một hành động cụ thể (merge, tách nhỏ, xoá, hay giao người khác).

Gợi ý và lời giải — Bài 1

Gợi ý. Con số thô chưa đủ. Phân loại từng nhánh già theo lý do nó còn sống thì mới ra được hành động.

Lời giải — output thật:

text
refactor/split-service  3 months ago  Dung
feat/payment-integration  5 weeks ago  Cuong
feat/revenue-report  9 days ago  Binh
fix/order-total  2 days ago  An
main  1 second ago  An Nguyen

Hai nhánh sống quá hai tuần: refactor/split-service và feat/payment-integration.

Phân loại và hành động:

Lý do nhánh còn sốngHành động
Đang làm dở, sẽ xong tuần nàyKhông sao
Chờ review đã lâuVấn đề quy trình, không phải kỹ thuật — nhắc hoặc đổi người review
Chờ một phụ thuộc bên ngoàiTách phần độc lập ra merge trước, phần còn lại để sau feature flag
Đã bị bỏXoá — lúc xoá Git in ra mã commit cuối, và reflog còn giữ khoảng 30 ngày
Người viết đã rời dự ánXem còn giá trị không; có thì cử người tiếp quản, không thì xoá

Vì sao nhánh già là vấn đề, cụ thể:

  1. Xung đột tăng theo thời gian. Nhánh sống một ngày lệch với main vài commit; nhánh ba tháng lệch hàng trăm commit, và mỗi commit đó là một cơ hội xung đột.
  2. Xung đột ngữ nghĩa tích tụ. Loại xung đột mà Git không thấy (bài 3.7) sinh ra càng nhiều khi khoảng cách càng lớn.
  3. Công sức có nguy cơ mất trắng. Một nhánh ba tháng không merge được nữa thường bị bỏ hẳn.
  4. Người viết quên mất ngữ cảnh. Sau vài tuần, chính tác giả cũng không nhớ vì sao đã làm như vậy.

Xoá nhánh đã merge, hàng loạt. git branch --merged chỉ nhận ra nhánh được gộp bằng merge thường:

bash
git branch --merged main | grep -vE '^\*|main' | xargs -r git branch -d
git fetch --prune

Với nhánh đã squash merge, Git không biết nhánh đã gộp, nên lệnh trên bỏ qua chúng. Cách phòng tốt hơn cách dọn: bật Automatically delete head branches trong Settings > General của kho trên GitHub, nhánh trên GitHub tự biến mất ngay sau khi PR được merge; trên máy, git fetch --prune rồi git branch -vv sẽ đánh dấu [origin/...: gone] cho những nhánh đó, và bạn xoá chúng bằng -D.

Chỉ số đáng theo dõi. Tuổi trung vị của nhánh đang mở. Vài ngày là lành mạnh; trên hai tuần là dấu hiệu quy trình đang có chỗ tắc, và chỗ tắc thường là review chứ không phải lập trình.

Bài 2 — Viết quy trình hotfix cho nhóm ​

Soạn một tài liệu một trang cho nhóm dự án của bạn (hoặc cho kho có CI bạn đã dựng ở Bài 3 của bài 3.6) theo năm bước ở mục 3.8.4, có tên người chịu trách nhiệm ở từng bước. Đặt nó ở nơi tìm được trong 30 giây, ví dụ file HOTFIX.md ở gốc kho.

Tiêu chí hoàn thành: một người chưa từng làm hotfix đọc xong là thực hiện được, không cần hỏi ai.

Gợi ý và lời giải — Bài 2

Gợi ý. Viết cho người đang hoảng lúc 2 giờ sáng. Nghĩa là: câu ngắn, lệnh sao chép dán được, không có chữ "tuỳ tình huống", và ghi rõ ai quyết định cái gì.

Lời giải — mẫu dùng được ngay:

markdown
# Quy trình hotfix — CRM Backend

> Vị trí: README của kho, mục "Vận hành". Cũng ghim ở kênh #crm-oncall.

## Bước 0 — Trước khi sửa code (2 phút)

- [ ] Báo vào kênh #crm-oncall: triệu chứng, thời điểm bắt đầu, phạm vi ảnh hưởng
- [ ] Quyết định: **quay lui** hay **vá tới**?
      -> Nếu lỗi đến từ lần triển khai gần nhất: QUAY LUI trước, điều tra sau
      -> Người quyết định: trưởng nhóm, hoặc người trực nếu ngoài giờ

## Bước 1 — Tách nhánh từ trạng thái ĐANG CHẠY

    git switch main && git pull
    git switch -c hotfix/<mo-ta-ngan>

KHÔNG tách từ develop hay từ nhánh tính năng.

## Bước 2 — Sửa nhỏ nhất có thể

Chỉ chặn máu. Không tái cấu trúc, không dọn dẹp, không đổi tên.
Việc dọn dẹp -> tạo issue riêng, làm sau.

## Bước 3 — Vẫn mở PR, vẫn để CI chạy

    git push -u origin hotfix/<mo-ta-ngan>

- Người review: bất kỳ ai đang online. Không chờ đúng người.
- CI hỏng -> KHÔNG bỏ qua. Đây là lúc lỗi thứ hai gây thiệt hại nhất.

## Bước 4 — Merge và triển khai

- Người bấm merge: người review
- Người theo dõi triển khai: người viết bản vá
- [ ] Xác nhận lỗi đã hết trên production (nêu rõ cách kiểm tra)

## Bước 5 — Sau khi xong (trong 24 giờ)

- [ ] Gộp ngược về nhánh phát triển, nếu mô hình có nhánh đó
- [ ] Viết báo cáo sự cố: nguyên nhân gốc, dòng thời gian, cách phòng
- [ ] Tạo issue cho phần dọn dẹp đã bỏ qua ở bước 2

## Liên hệ

| Vai trò | Người | Kênh |
|---|---|---|
| Trực chính | (tên) | (điện thoại) |
| Quản trị database | (tên) | (điện thoại) |
| Quyết định thông báo khách hàng | (tên) | (điện thoại) |

Bốn điều làm nên một quy trình hotfix dùng được:

  1. Bước 0 tồn tại. Câu hỏi "quay lui hay vá tới" phải được hỏi trước khi ai đó bắt đầu gõ code. Quay lui thường nhanh hơn và an toàn hơn, nhưng người ta hay bỏ qua nó vì bản năng là muốn sửa.
  2. Có tên người, không chỉ có vai trò. "Trưởng nhóm quyết định" là vô dụng lúc 2 giờ sáng nếu không ai biết số điện thoại.
  3. Lệnh sao chép dán được. Không viết "tách nhánh từ main" mà viết đúng dòng lệnh.
  4. Bước 5 có thời hạn. Không có thời hạn thì nó không bao giờ được làm, và bước gộp ngược bị quên chính là nguyên nhân lỗi quay trở lại ở lần phát hành sau.

Kiểm tra tài liệu có dùng được không. Đưa cho một người chưa từng làm hotfix và hỏi: "đọc xong bạn có biết việc đầu tiên phải làm là gì không?" Nếu họ ngập ngừng, tài liệu chưa đạt.

Bài 3 — Diễn tập hotfix có tính giờ ​

Dùng kho có CI và bảo vệ nhánh bạn đã dựng ở Bài 3 của bài 3.6 (solution có unit test từ bài 0.4), và thêm bạn cùng cặp làm Collaborator nếu chưa thêm. Bạn cùng cặp cố ý merge một thay đổi làm hỏng một hàm (test vẫn xanh, vì chưa có test cho trường hợp đó). Bạn đóng vai người trực: đi hết quy trình hotfix ở Bài 2, viết thêm test tái hiện lỗi, tới lúc bản vá được merge vào main. Bấm giờ từng bước.

Tiêu chí hoàn thành: bạn có bảng thời gian của từng bước (đo bằng đồng hồ của chính bạn), chỉ ra bước chậm nhất, và đề xuất một thay đổi cụ thể để rút ngắn nó.

Gợi ý và lời giải — Bài 3

Gợi ý. Bấm giờ từng bước riêng, không chỉ tổng. Chỗ tắc thường không nằm ở bước viết code, và điều đó chỉ lộ ra khi bạn đo tách bạch. Thời gian chờ CI xem ở tab Actions của kho.

Lời giải — cách ghi. Bảng dưới là ví dụ minh hoạ cách ghi, không phải số đo thật; số của bạn phụ thuộc kho và tốc độ của người review.

BướcThời gianNhận xét
Phát hiện → báo vào nhóm chat...Ai phát hiện, bằng cách nào
Quyết định quay lui hay vá tới...Có rõ ai được quyết không
Tách nhánh, viết test tái hiện, viết bản vá...
Chờ CI...Xem thời gian chạy ở tab Actions
Chờ review...
Merge...

Ba chỗ tắc thường gặp nhất, và cách xử lý:

1. CI chậm. Với dự án lớn, đây thường là bước dài nhất. Một cách rút ngắn là thêm một job nhanh chỉ chạy nhóm test quan trọng cho nhánh hotfix/, chạy song song với job đầy đủ. Đây là đánh đổi có chủ ý: job nhanh cho tín hiệu sớm để người review bắt đầu đọc, còn job đầy đủ vẫn chạy trên chính PR đó và trên mỗi lần push vào main. Bản vá vẫn phải qua CI, đúng nguyên tắc ở mục 3.8.4.

yaml
# .github/workflows/hotfix-fast.yml
name: Hotfix fast check
on:
  pull_request:
    branches: [main]
jobs:
  fast-check:
    if: startsWith(github.head_ref, 'hotfix/')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-dotnet@v6
        with:
          dotnet-version: '10.0.x'
      - run: dotnet test --filter "Category=Critical"

Test thuộc nhóm Critical được đánh dấu bằng trait của xUnit:

csharp
[Fact]
[Trait("Category", "Critical")]
public void Total_IsNeverNegative() { /* ... */ }

2. Không rõ ai được quyết. Thời gian ở bước quyết định là thời gian thuần tuý để đi tìm người. Sửa bằng cách ghi thẳng tên và cách liên lạc vào tài liệu như Bài 2.

3. Chờ review. Với hotfix, quy tắc "phải đúng người phụ trách module review" là một chỗ tắc nguy hiểm. Quy tắc thay thế: bất kỳ ai đang online, người phụ trách xem lại sau.

Hai chỉ số đáng theo dõi lâu dài:

Chỉ sốNghĩa
Thời gian tới lúc khắc phụcTừ phát hiện tới lúc hết lỗi
Thời gian đưa một dòng code lên productionTừ commit tới production, lúc bình thường

Chỉ số thứ hai đặt giới hạn dưới cho chỉ số thứ nhất. Nếu bình thường mất 2 giờ để đưa một dòng code lên production, thì lúc khẩn cấp bạn không thể nhanh hơn 2 giờ — trừ khi bỏ qua các bước an toàn, và đó chính là lúc sự cố thứ hai xảy ra.

Vì sao phải diễn tập thật. Quy trình chưa bao giờ chạy thử là quy trình chưa biết có chạy được không. Diễn tập giúp phát hiện những thứ không ai lường trước: thiếu quyền merge, không ai biết người trực là ai, test tái hiện khó viết. Phát hiện lúc diễn tập rẻ hơn nhiều so với lúc production đang hỏng.

Tự kiểm tra ​

Vì sao nên đồng bộ với main trước khi mở PR?

Vì bạn giải xung đột trên máy mình lúc rảnh rang, với đầy đủ công cụ và có thể chạy test ngay. Nếu để sau, bạn phải giải xung đột trong giao diện web khi người review đang chờ, và CI có thể chạy trên một kết quả merge khác với thứ bạn đã kiểm tra.

Khi nào Git Flow phù hợp?

Khi phần mềm phát hành theo phiên bản đóng gói, phải hỗ trợ nhiều phiên bản cùng lúc, có giai đoạn QA tách biệt, hoặc khách hàng tự quyết khi nào nâng cấp. Nó ra đời năm 2010 cho bối cảnh đó. Với đội nhỏ deploy liên tục và chỉ có một phiên bản đang chạy thì nó chỉ tạo thêm nghi lễ.

Trunk-based cần điều kiện gì?

Ba điều kiện: bộ test đủ tin cậy vì không có giai đoạn QA riêng, CI nhanh dưới mười phút nếu không người ta sẽ ngại merge, và feature flag cho tính năng chưa hoàn thiện. Điều kiện thứ ba là chìa khoá vì nó cho phép merge code chưa xong vào main mỗi ngày mà chưa bật cho người dùng. GitHub Flow (nhánh ngắn, pull request, squash merge) là cách làm trunk-based phổ biến với đội nhỏ.

Vì sao hotfix phải tách từ nhánh đang chạy trên production?

Vì tách từ nhánh phát triển sẽ kéo theo mọi thay đổi chưa được kiểm thử vào bản vá khẩn cấp. Khi production đang hỏng, bạn muốn đưa lên đúng một thay đổi nhỏ nhất có thể, không kèm theo hai chục thứ khác.

Bước nào trong quy trình hotfix hay bị quên nhất?

Bước gộp ngược bản vá về nhánh phát triển. Quên bước này thì lần phát hành sau sẽ xoá mất bản vá và lỗi quay trở lại. Đây là lỗi kinh điển của các đội dùng Git Flow, và cũng là một điểm cộng cho trunk-based, nơi chỉ có một nhánh nên không có gì để quên.

Feature flag có cái giá gì?

Phải dọn sau khi tính năng đã ổn định. Flag bỏ quên tích tụ thành một dạng nợ kỹ thuật riêng: code có hai nhánh chạy song song, không ai nhớ nhánh nào còn dùng, và việc kiểm thử nhân đôi số tổ hợp. Nên đặt lịch rà soát và xoá flag hết vai trò.

Kết luận ​

Ba điều đáng nhớ nhất:

  1. Nhánh sống lâu là nguồn gốc của phần lớn đau khổ. Mọi khuyến nghị khác đều xoay quanh việc rút ngắn tuổi nhánh.
  2. Quy trình hotfix phải viết trước khi cần. Lúc production hỏng không phải lúc thiết kế quy trình.
  3. Mỗi bước trong quy trình phải trả lời được nó giảm rủi ro gì. Không trả lời được thì đó là chi phí thuần.

Tham khảo ​

Điều hướng ​

Phát hành theo giấy phép MIT.