---
id: git-workflow-release
title: "Branching strategy, tag và release"
shortTitle: "Branching strategy & release"
track: git
trackTitle: "Git & VCS Hosting"
order: 4
level: core
status: published
estimatedMinutes: 45
prerequisites:
  - git-remote-pr
next: git-recovery
reviewedAgainst: 2026-10-12
sources:
  - title: "GitHub flow"
    url: https://docs.github.com/en/get-started/using-github/github-flow
  - title: "DORA — Trunk-based development"
    url: https://dora.dev/capabilities/trunk-based-development/
  - title: "Semantic Versioning 2.0.0"
    url: https://semver.org/
  - title: "Conventional Commits 1.0.0"
    url: https://www.conventionalcommits.org/en/v1.0.0/
  - title: "Pro Git — Tagging"
    url: https://git-scm.com/book/en/v2/Git-Basics-Tagging
  - title: "About releases"
    url: https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases
---

# Branching strategy, tag và release

Workflow mẫu dùng `actions/checkout@v7`, cùng mốc version với bài [GitHub Actions](../cicd-iac/01-github-actions.md).

## Để làm gì

Cả team thống nhất một cách chia branch, để code đi từ máy dev tới production theo một đường rõ ràng. Mỗi bản release được đánh version bằng tag, và pipeline tự build, gắn version, deploy khi có tag mới. Nhờ vậy, lúc production lỗi bạn biết ngay đang chạy version nào, commit nào, và quay về bản trước bằng cách nào.

## Gồm những gì

- Ba chiến lược hay gặp: GitHub flow, trunk-based development, GitFlow.
- Feature flag để merge sớm code chưa xong.
- Semantic Versioning: `MAJOR.MINOR.PATCH`.
- Annotated tag và lightweight tag, push tag, `git describe`.
- Conventional Commits và changelog tự động.
- GitHub Releases.
- Workflow chạy khi push tag: build image gắn version.

Deploy lên Kubernetes hay cloud nằm ở các chặng sau. Bài này dừng ở chỗ bạn có image gắn đúng version.

## Dùng khi nào

- Team mới hoặc repo mới: chọn chiến lược branch trước khi branch tự phát mọc lên khắp nơi.
- Chuẩn bị release đầu tiên lên production: bắt đầu đánh tag `v0.1.0` hoặc `v1.0.0`.
- Khi ai đó hỏi "production đang chạy code nào?": image tag và Git tag phải khớp nhau thì mới trả lời được.
- Khi viết library hoặc API cho người khác dùng: SemVer cho họ biết bản nào có breaking change.

## Kiến thức cốt lõi

### GitHub flow

Docs GitHub mô tả một quy trình nhẹ: tạo branch từ `main`, commit, mở pull request, review, merge, rồi xoá branch. `main` luôn ở trạng thái deploy được. Quy trình này hợp với web app deploy liên tục, nơi chỉ có một version đang chạy.

### Trunk-based development

DORA (nhóm nghiên cứu DevOps Research and Assessment, nay thuộc Google Cloud) xếp trunk-based development vào nhóm năng lực gắn với hiệu quả delivery. Mô tả của DORA:

- Mọi người merge vào trunk (`main`) ít nhất mỗi ngày một lần.
- Branch sống ngắn, thường dưới một ngày, ít hơn ba branch đang mở cùng lúc.
- Không có giai đoạn "code freeze" hay "integration phase" kéo dài.

Code chưa xong vẫn merge được nếu nằm sau **feature flag**, tức một biến config bật tắt tính năng lúc runtime. Code lên production ở trạng thái tắt, và team bật flag khi tính năng sẵn sàng.

GitHub flow với PR nhỏ, merge nhanh, thực chất là một dạng trunk-based.

### GitFlow

GitFlow do Vincent Driessen đề xuất năm 2010: branch `develop` để tích hợp, `feature/*` tách từ `develop`, `release/*` để chuẩn bị bản phát hành, `hotfix/*` sửa gấp trên `main`. Năm 2020, chính tác giả thêm ghi chú vào bài gốc: với phần mềm deploy liên tục như web app, nên dùng quy trình đơn giản hơn như GitHub flow. GitFlow vẫn hợp khi phải duy trì nhiều version cùng lúc, ví dụ app mobile hoặc phần mềm cài đặt tại chỗ khách hàng.

| | GitHub flow / trunk-based | GitFlow |
| --- | --- | --- |
| Branch lâu dài | `main` | `main`, `develop` |
| Độ dài feature branch | Giờ đến vài ngày | Có thể vài tuần |
| Release | Merge là deploy được, tag khi cần | Qua branch `release/*` |
| Hợp với | Web app, service, CI/CD liên tục | Nhiều version song song |

### Semantic Versioning

SemVer 2.0.0 quy định version `MAJOR.MINOR.PATCH`:

- **MAJOR** tăng khi có thay đổi không tương thích ngược (breaking change).
- **MINOR** tăng khi thêm tính năng mà vẫn tương thích.
- **PATCH** tăng khi sửa lỗi tương thích.

Version `0.y.z` là giai đoạn phát triển ban đầu, API có thể đổi bất cứ lúc nào. Pre-release ghi sau dấu gạch: `1.4.0-rc.1`. Một version đã phát hành thì không được sửa nội dung; sửa thì ra version mới.

Với app nội bộ, không ai phụ thuộc vào nó theo kiểu library, nhưng SemVer vẫn có ích: nhìn tag là biết bản nào chỉ sửa lỗi, bản nào đổi lớn.

### Tag

Tag là một cái tên gắn cố định vào một commit. Khác với branch, tag không di chuyển khi có commit mới.

- **Lightweight tag**: chỉ là con trỏ, như branch đứng yên.
- **Annotated tag**: là một object riêng, có người tạo, ngày, message, có thể ký. Pro Git khuyên dùng annotated tag cho release.

`git push` không gửi tag. Bạn phải push tag riêng, hoặc dùng `--follow-tags` để gửi kèm các annotated tag nằm trên commit đang push.

`git describe --tags` in version gần nhất cộng số commit kể từ đó, ví dụ `v1.2.0-3-g1a2b3c4` nghĩa là đã qua `v1.2.0` ba commit và đang ở commit `1a2b3c4`. Lệnh này tiện để gắn version cho các bản build không phải release.

### Conventional Commits

Format message có cấu trúc:

```text
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

Theo spec 1.0.0, `fix:` tương ứng PATCH, `feat:` tương ứng MINOR, còn `BREAKING CHANGE:` trong footer hoặc dấu `!` sau type (`feat!:`) tương ứng MAJOR. Các type khác như `docs:`, `chore:`, `ci:`, `refactor:` được phép, không ảnh hưởng version. Nhờ quy ước này, tool có thể tự tính version kế tiếp và sinh changelog từ history.

### GitHub Releases

Release trên GitHub gắn với một tag, có release notes và file đính kèm. GitHub có thể tự sinh release notes từ các PR đã merge kể từ tag trước.

### Một Git tag, một image tag

Nguyên tắc trong pipeline: mỗi tag `v1.4.2` build ra đúng một image `api:1.4.2`, và chính image đó đi qua mọi environment (staging rồi production). Pipeline không build lại cho từng environment. Nhờ vậy, thứ đã test ở staging đúng là thứ chạy ở production, và nhìn image tag là truy ra được commit.

## Lệnh và code mẫu

Đánh tag và push:

```bash
git switch main
git pull --ff-only
git tag -a v1.0.0 -m "First production release"
git push origin v1.0.0

git tag --list "v1.*"
git show v1.0.0
git describe --tags
```

Tag cho một commit cũ:

```bash
git tag -a v0.9.0 -m "Beta" 3f2a9c1
```

Xoá tag gõ sai (chỉ khi chưa ai dùng):

```bash
git tag -d v1.0.1
git push origin --delete v1.0.1
```

Commit theo Conventional Commits:

```bash
git commit -m "feat(api): add /healthz endpoint"
git commit -m "fix: return 503 when database is unreachable"
git commit -m "feat!: drop support for Node 18" -m "BREAKING CHANGE: runtime image now requires Node 24."
```

Tạo GitHub Release từ tag, tự sinh notes:

```bash
gh release create v1.0.0 --generate-notes
```

Workflow build image khi push tag, đặt ở `.github/workflows/release.yml`. Image đẩy lên GitHub Container Registry bằng `GITHUB_TOKEN`. Tên image trên GHCR phải viết thường; thay `my-org/api` bằng tên của bạn:

```yaml
name: Release

on:
  push:
    tags: ["v*.*.*"]

permissions:
  contents: read
  packages: write

env:
  IMAGE: ghcr.io/my-org/api

jobs:
  image:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Login to GHCR
        run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin

      - name: Build and push
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          docker build \
            --label "org.opencontainers.image.revision=${GITHUB_SHA}" \
            -t "$IMAGE:$VERSION" .
          docker push "$IMAGE:$VERSION"
```

`GITHUB_REF_NAME` là tên tag, ví dụ `v1.4.2`. `${GITHUB_REF_NAME#v}` bỏ chữ `v` đầu, còn `1.4.2`. Label `revision` ghi commit SHA vào image để truy ngược.

## Thực hành

1. Trong repo lab, viết ba commit theo Conventional Commits: một `fix:`, một `feat:`, một `docs:`.
2. Đánh annotated tag `v0.1.0`, chạy `git show v0.1.0` để thấy tagger và message.
3. Thêm hai commit, chạy `git describe --tags`.
4. Tự tính version kế tiếp theo SemVer dựa trên các commit kể từ `v0.1.0`, rồi đánh tag đó.
5. Nếu có repo GitHub: thêm `release.yml` cùng một `Dockerfile` đơn giản, push tag, xem workflow chạy và image xuất hiện ở tab Packages.
6. Ghi ra giấy: team bạn hiện theo GitHub flow, trunk-based hay GitFlow, và branch sống lâu nhất hiện đã bao nhiêu ngày.

## Kết quả mong đợi

- `git show v0.1.0` in dòng `Tagger:` và message, rồi mới tới commit.
- `git describe --tags` in dạng `v0.1.0-2-g<hash>`.
- Commit `feat:` kể từ tag trước thì version kế là `v0.2.0`; chỉ có `fix:` thì `v0.1.1`.
- Workflow Release chỉ chạy khi push tag, không chạy khi push commit thường.
- Image trên GHCR có tag `0.1.1` hoặc `0.2.0`, không có chữ `v`.

## Checklist

- [ ] So sánh được GitHub flow, trunk-based và GitFlow, và chọn được cho một web app.
- [ ] Giải thích feature flag giúp merge sớm thế nào.
- [ ] Tăng đúng phần MAJOR, MINOR hoặc PATCH theo loại thay đổi.
- [ ] Tạo annotated tag và push nó lên remote.
- [ ] Viết commit theo Conventional Commits, gồm cả breaking change.
- [ ] Viết workflow chỉ chạy khi push tag `v*.*.*`.
- [ ] Giải thích vì sao build một image rồi dùng cho mọi environment.

## Thẻ ôn

### git-workflow-release-image
kind: decide
cue: Staging đã test xanh mà production lại build image mới thì bản production không còn là bản đã test. Một tag build một image, rồi đưa đúng image đó đi tiếp.

Q: Staging đã chạy một image và bộ test trên image đó xanh. Lúc deploy production, pipeline không lấy image đó mà build lại từ cùng commit, ra một image mới. Chỗ nào sai, và một tag như `v1.4.2` nên đi qua các môi trường thế nào?

A: Hai lần build, dù cùng commit, không bảo đảm ra từng byte giống nhau: thời điểm kéo dependency, cache, hoặc toolchain có thể khác. Bản đang chạy production lúc đó không phải bản đã được test ở staging. Với tag `v1.4.2`, pipeline build một lần thành đúng một image, ví dụ `api:1.4.2`. Chính image đó được đưa sang staging, test, rồi promote sang production. Nhìn tag của image là truy ra commit đã test. Đừng build lại cho từng môi trường.

### git-workflow-release-latest
kind: trap
cue: Production kéo image tag `latest` thì không biết đang chạy bản nào, và không có tag cũ để rollback.

Q: Manifest production ghi image tag `latest` cho đỡ phải sửa tag mỗi lần phát hành. Sự cố giữa đêm, bạn cần quay về bản đã chạy ổn trước đó. Rollback về đâu?

A: `latest` là một cái tên bị đẩy đi mỗi lần có bản mới. Nó không nói production đang chạy version nào, cũng không giữ lại tên của bản trước. Kéo lại `latest` có thể ra đúng bản vừa hỏng, hoặc một bản mới hơn nữa. Gắn image theo version, ví dụ `1.4.2`, hoặc theo commit SHA. Rollback là đổi manifest về tag đã biết, không phải hy vọng `latest` trỏ lại quá khứ.

### git-workflow-release-tag
kind: command
cue: `git push` không gửi tag. Workflow release chờ tag trên remote, nên phải `git push origin v1.4.2` hoặc `--follow-tags`.

Q: Bạn tạo tag `v1.4.2` trên máy, push commit lên remote, và ngồi đợi workflow release. Workflow được cấu hình chạy khi có tag khớp `v*.*.*`. Nó không chạy. Thiếu bước nào?

A: `git push` gửi commit, không gửi tag. Tag `v1.4.2` vẫn chỉ nằm trên máy bạn, remote chưa có sự kiện push tag, nên workflow không được kích. Gửi riêng tag bằng `git push origin v1.4.2`, hoặc `git push --follow-tags` để kèm các annotated tag đang trỏ vào commit vừa push. Push commit thường không làm workflow này chạy.

### git-workflow-release-move-tag
kind: trap
cue: Tag `v1.4.2` đã push và pipeline đã build thì đừng trỏ tag đó sang commit khác. Bản đã phát hành thì ra tag mới.

Q: Tag `v1.4.2` đã được push, pipeline đã build image từ commit tag đó trỏ vào. Bạn phát hiện một sửa nhỏ và muốn trỏ cùng tag sang commit mới cho khỏi tăng version. Người và pipeline đã lấy tag cũ sẽ thấy gì?

A: Họ giữ một tên `v1.4.2` nhưng nội dung không còn là nội dung bạn vừa đổi. Máy đã fetch tag cũ không tự biết tag đã bị chuyển. Image đã build mang commit cũ trong khi tài liệu nói tag đó là commit mới. Cùng một tên, hai nội dung. Bản đã phát hành thì để yên, sửa thì ra tag mới.

### git-workflow-release-flag
kind: decide
cue: Tính năng chưa xong mà nhánh sống lâu thì merge sau feature flag, flag tắt lúc runtime. Web app một version thì bắt đầu với GitHub flow.

Q: Một nhánh tính năng đã sống nhiều ngày, mỗi sáng lại conflict với `main`, nhưng tính năng chưa cho user dùng. Làm sao merge sớm mà không bật tính năng trên production? Với web app chỉ có một version đang chạy, có nên dựng thêm `develop` và `release/*` kiểu GitFlow không?

A: Để code sau feature flag, tức một biến config bật tắt lúc runtime. Merge khi flag tắt: code đã nằm trên `main`, conflict ngừng chất đống, user chưa thấy tính năng. Bật flag khi tính năng sẵn sàng, không cần một nhánh dài riêng. Web app một version không cần giữ nhiều dòng release. Bắt đầu với GitHub flow: nhánh ngắn, pull request, merge vào `main`. Thêm `develop` và `release/*` khi thật sự phải giữ nhiều version cùng lúc, không thêm sẵn cho đủ kiểu.

### git-workflow-release-semver
kind: command
cue: Kể từ tag trước, chỉ có `fix:` thì tăng PATCH. Có `feat:` thì tăng MINOR. Breaking change thì tăng MAJOR. Nhớ push tag, vì `git push` không gửi tag.

Q: Tag gần nhất là `v0.1.0`. Các commit kể từ đó theo Conventional Commits và chỉ có `fix:`. Version kế tiếp là gì? Nếu trong đợt đó có thêm một commit `feat:` thì đổi thế nào? Annotated tag hơn lightweight tag ở điểm nào khi phát hành?

A: Chỉ có `fix:` thì tăng PATCH, thành `v0.1.1`. Có `feat:` thì tăng MINOR, thành `v0.2.0`, dù kèm các `fix:`. Breaking change tăng MAJOR. Annotated tag là một object riêng: có người tạo, ngày và message, và có thể ký. Lightweight tag chỉ là một cái tên trỏ vào commit, không mang thông tin phát hành. Tạo annotated tag cho release. `git push` không gửi tag một mình, phải push tag riêng hoặc `--follow-tags`.

## Lỗi thường gặp

**Tạo tag nhưng quên push.** Tag chỉ nằm trên máy bạn nên pipeline không chạy. Dùng `git push origin <tag>` hoặc `git push --follow-tags`.

**Sửa tag đã push, trỏ nó sang commit khác.** Người và pipeline đã lấy tag cũ sẽ thấy cùng một tên nhưng hai nội dung khác nhau. Bản đã phát hành thì đừng sửa; hãy ra tag mới.

**Image dùng tag `latest` trên production.** Bạn không biết production đang chạy bản nào, và lúc rollback cũng không rõ quay về đâu. Gắn image theo version hoặc commit SHA.

**Build lại image riêng cho production.** Khi đó bản chạy production không phải bản đã test ở staging. Build một lần rồi promote cùng một image.

**Áp GitFlow cho một web app chỉ có một version.** Bạn thêm `develop` và `release/*` mà không được lợi gì, chỉ thấy merge vòng vèo và conflict nhiều hơn. Bắt đầu với GitHub flow, và chỉ thêm branch release khi thật sự phải duy trì nhiều version.

## Nguồn

1. [GitHub flow](https://docs.github.com/en/get-started/using-github/github-flow) — GitHub Docs.
2. [DORA — Trunk-based development](https://dora.dev/capabilities/trunk-based-development/) — định nghĩa và các dấu hiệu nhận biết từ nghiên cứu DORA.
3. [A successful Git branching model](https://nvie.com/posts/a-successful-git-branching-model/) — bài gốc GitFlow của Vincent Driessen, có ghi chú năm 2020 của tác giả. Đây là blog, dùng để dẫn nguồn gốc mô hình.
4. [Semantic Versioning 2.0.0](https://semver.org/) — spec SemVer.
5. [Conventional Commits 1.0.0](https://www.conventionalcommits.org/en/v1.0.0/) — spec format commit và liên hệ với SemVer.
6. [Pro Git — Tagging](https://git-scm.com/book/en/v2/Git-Basics-Tagging) và [git-tag](https://git-scm.com/docs/git-tag), [git-describe](https://git-scm.com/docs/git-describe) — annotated tag, push tag.
7. [About releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) và [Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) — GitHub Releases.
8. [Workflow syntax — on.push.tags](https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax) và [Working with the Container registry](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) — trigger theo tag, GHCR.

## Tiếp theo

[Sửa sai: revert, reset, reflog và secret lỡ commit](./05-recovery.md)
