Product Management
Đăng nhập
ESC

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

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

Bài 35 — Release Notes & Changelog Automation

Đã đọc 0/13 mục · còn khoảng 12 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 bạn là kỹ sư trực (on-call) lúc 2 giờ sáng. Một dịch vụ thanh toán vừa lỗi hàng loạt sau khi bản v3.14.0 được deploy lên production. Câu hỏi đầu tiên cấp trên hỏi bạn là: "Bản này thay đổi những gì so với bản trước?". Nếu team bạn có một file CHANGELOG.md sạch sẽ, được sinh tự động, bạn chỉ mất 30 giây để biết bản mới có commit fix: điều chỉnh timeout gateway VNPay từ 30s xuống 5s — và bạn có ngay nghi phạm để rollback. Nếu không, bạn phải mò git log thô, đọc hàng trăm commit lẫn lộn kiểu "fix bug", "update", "asdf", và mất cả tiếng đồng hồ trong lúc khách hàng đang không thanh toán được.

Release notes và changelog không phải là "giấy tờ hành chính" cho vui. Chúng là hợp đồng giao tiếp giữa team phát triển với ba nhóm đối tượng: (1) chính developer trong tương lai khi debug sự cố, (2) team QA/vận hành cần biết deploy gì để kiểm thử và theo dõi, và (3) người dùng cuối hoặc khách hàng doanh nghiệp muốn biết sản phẩm có gì mới. Vấn đề là: viết tay thì mệt, dễ quên, dễ thiếu nhất quán, và luôn là việc bị bỏ lại cuối cùng khi deadline dí.

Bài này tập trung đúng một chuyện: làm sao để changelog và release notes được sinh ra tự động trong pipeline CI/CD, dựa trên chính lịch sử commit của bạn — không cần ai ngồi gõ tay. Đây là mảnh ghép nằm ngay giữa quy trình release: sau khi code đã merge, trước khi (hoặc ngay khi) tạo tag và publish bản mới.

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

Phân biệt Changelog và Release Notes

Hai khái niệm này hay bị dùng lẫn lộn nhưng có mục đích khác nhau:

  • Changelog (CHANGELOG.md): một file kỹ thuật, liệt kê có cấu trúc mọi thay đổi đáng kể theo từng phiên bản. Đối tượng đọc chính là developer. Chuẩn phổ biến nhất là Keep a Changelog, với các nhóm cố định: Added, Changed, Deprecated, Removed, Fixed, Security.
  • Release Notes: thường mang tính "kể chuyện" hơn, đăng lên GitHub Releases, gửi qua email, đăng blog hoặc thông báo trong app. Đối tượng là người dùng cuối, đôi khi cả bộ phận kinh doanh. Ngôn ngữ thân thiện hơn, có thể kèm ảnh, GIF.
Điểm mấu chốt: cả hai đều có thể sinh ra từ cùng một nguồn dữ liệu — lịch sử commit — nếu commit của bạn có kỷ luật.

Conventional Commits — nền tảng của tự động hóa

Bạn không thể tự động hóa cái gì không có cấu trúc. Đây là lý do Conventional Commits ra đời — một quy ước đặt tên commit message theo mẫu:

<type>[scope tùy chọn]: <mô tả>

[phần thân tùy chọn]

[footer tùy chọn]

Các type phổ biến:

  • feat: — tính năng mới (map vào nhóm Added).
  • fix: — sửa lỗi (map vào nhóm Fixed).
  • docs:, style:, refactor:, test:, chore: — các thay đổi không ảnh hưởng trực tiếp người dùng, thường bị ẩn khỏi release notes.
  • perf: — cải thiện hiệu năng.
  • BREAKING CHANGE: trong footer, hoặc dấu ! sau type (ví dụ feat!:) — báo hiệu thay đổi phá vỡ tương thích.
Ví dụ commit chuẩn:

feat(payment): thêm cổng thanh toán MoMo

Tích hợp SDK MoMo v2, hỗ trợ QR động.

BREAKING CHANGE: đổi tên tham số `gateway_id` thành `provider_id`

Từ một commit như vậy, công cụ có thể tự suy ra: đây là tính năng mới, thuộc scope payment, và có breaking change. Điều này còn liên kết trực tiếp với Semantic Versioning (SemVer) — dạng MAJOR.MINOR.PATCH:

  • fix: → tăng PATCH (2.3.1 → 2.3.2).
  • feat: → tăng MINOR (2.3.1 → 2.4.0).
  • BREAKING CHANGE → tăng MAJOR (2.3.1 → 3.0.0).
Nghĩa là commit message không chỉ sinh changelog, mà còn quyết định luôn số phiên bản tiếp theo một cách tự động.

Bộ công cụ thường dùng

  • semantic-release: "ông trùm" trong hệ JavaScript/Node. Phân tích commit → tính version mới → sinh CHANGELOG → tạo git tag → tạo GitHub Release → publish npm. Tất cả trong một lệnh. Triết lý: không con người nào chạm tay vào số version.
  • standard-version / commit-and-tag-version: nhẹ hơn, chỉ lo bump version và sinh changelog cục bộ, không tự publish. Phù hợp team muốn kiểm soát bước release thủ công.
  • release-please (Google): chạy như một GitHub Action, tạo sẵn một "Release PR" gom các thay đổi. Khi bạn merge PR đó thì mới thực sự release. Rất phù hợp cho team muốn có một bước "phê duyệt" trước khi phát hành.
  • git-cliff: viết bằng Rust, cực nhanh, cấu hình bằng template linh hoạt, không phụ thuộc ngôn ngữ dự án — hợp với monorepo hoặc dự án không phải Node.
  • GitHub Release Notes tự động: GitHub có tính năng "Generate release notes" gom PR theo label, cấu hình qua file .github/release.yml.

Tình huống thực tế

Tình huống 1 — Tiki và bài toán "changelog mồ côi"

Giả sử một team backend tại một sàn thương mại điện tử lớn của Việt Nam (đặt tên là "TikiPay") có 40 kỹ sư, deploy 15-20 lần mỗi ngày. Trước đây họ viết changelog tay trong Confluence. Kết quả: sau 6 tháng, file changelog cập nhật lần cuối cách đó... 4 tháng. Mỗi khi có sự cố, không ai tin vào changelog nữa vì nó luôn lỗi thời.

Họ chuyển sang release-please. Mỗi khi có commit feat: hoặc fix: được merge vào nhánh main, một bot tự động mở/cập nhật một PR tên "chore: release 2.8.0", trong đó có sẵn đoạn CHANGELOG được sinh ra. Team lead chỉ cần liếc qua PR đó, thấy đủ 3 tính năng và 5 fix của sprint, rồi bấm merge. Ngay khi merge, tag v2.8.0 được tạo và Docker image được đóng gói với đúng nhãn version.

Bài học: vấn đề của changelog thủ công không phải là "lười", mà là nó không nằm trong luồng công việc tự nhiên. Khi changelog trở thành một PR phải review như code, nó tự động được duy trì. Con số quan trọng: thời gian trung bình để trả lời "bản này có gì" giảm từ ~45 phút xuống dưới 1 phút.

Tình huống 2 — Startup fintech Singapore và cái giá của breaking change bị bỏ sót

Một startup fintech ở Singapore (gọi là "PayWave") cung cấp API cho các đối tác. Một developer đổi tên trường trong response API từ amount thành amount_cents, commit với message cụt lủn refactor: cleanup response. Vì không đánh dấu BREAKING CHANGE, công cụ semantic-release chỉ bump PATCH (từ 1.4.2 lên 1.4.3) và changelog không hề nhắc đến thay đổi này. Ba đối tác tích hợp bị vỡ tích hợp trong đêm, thiệt hại giao dịch trong 6 tiếng.

Sau sự cố, PayWave thêm một commit-lint vào pre-commit hook và CI: mọi commit không đúng chuẩn Conventional Commits sẽ bị chặn không cho merge. Họ cũng thêm rule: bất kỳ thay đổi nào trong thư mục api/contracts/ mà không kèm footer BREAKING CHANGE sẽ bị pipeline cảnh báo bắt buộc reviewer xác nhận.

Bài học: tự động hóa changelog chỉ tốt bằng chất lượng commit đầu vào. "Garbage in, garbage out". Bạn phải cưỡng chế kỷ luật commit bằng công cụ, không thể trông chờ vào thiện chí con người. Một BREAKING CHANGE bị bỏ sót có thể tốn hàng nghìn đô.

Tình huống 3 — Team sản phẩm mobile và release notes cho người dùng cuối

Một team làm ứng dụng gọi xe ở Đông Nam Á cần release notes cho App Store và Google Play mỗi 2 tuần. Vấn đề: changelog kỹ thuật đầy fix(auth): refactor token refresh thì người dùng cuối chẳng hiểu và cũng chẳng quan tâm.

Giải pháp của họ là dùng hai kênh, một nguồn. Trong CI, git-cliff sinh ra CHANGELOG.md kỹ thuật đầy đủ cho team. Song song, họ dùng label PR: chỉ những PR gắn nhãn user-facing mới được gom vào một file RELEASE_NOTES_STORE.md với ngôn ngữ đơn giản do product owner biên tập lại (ví dụ: "Đăng nhập nhanh hơn, ít bị văng ra ngoài hơn"). Bước biên tập cuối cùng vẫn là con người, nhưng phần gom nhặt thô đã được máy làm.

Bài học: đừng cố ép một changelog kỹ thuật phục vụ luôn cho người dùng cuối. Tự động hóa lo phần thu thập và phân loại; con người lo phần diễn giải và kể chuyện cho đúng đối tượng.

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

Dưới đây là quy trình dựng tự động hóa changelog cho một dự án Node điển hình dùng GitHub Actions và semantic-release. Nguyên tắc áp dụng được cho hầu hết công cụ khác.

Bước 1 — Thiết lập kỷ luật commit. Cài commitlint và husky để chặn commit sai chuẩn ngay tại máy dev:

npm install --save-dev @commitlint/cli @commitlint/config-conventional husky
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js
npx husky init
echo 'npx --no -- commitlint --edit $1' > .husky/commit-msg

Từ giờ, một commit update stuff sẽ bị từ chối ngay lập tức với thông báo lỗi rõ ràng.

Bước 2 — Cài semantic-release và cấu hình. Tạo file .releaserc.json:

{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
    "@semantic-release/github",
    ["@semantic-release/git", {
      "assets": ["CHANGELOG.md", "package.json"],
      "message": "chore(release): ${nextRelease.version} [skip ci]"
    }]
  ]
}

Thứ tự plugin quan trọng: phân tích commit → sinh nội dung notes → ghi vào CHANGELOG → tạo GitHub Release → commit ngược file changelog trở lại repo.

Bước 3 — Thêm job vào pipeline. Tạo .github/workflows/release.yml:

name: Release
on:
  push:
    branches: [main]
permissions:
  contents: write
  issues: write
  pull-requests: write
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # RẤT quan trọng: cần toàn bộ lịch sử git
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Lưu ý fetch-depth: 0 — nếu quên, công cụ chỉ thấy commit mới nhất và không tính được version.

Bước 4 — Xác định điểm kích hoạt. Hãy quyết định: bạn muốn release mỗi lần merge vào main (continuous), hay theo lịch/thủ công? Với continuous, mỗi merge có feat/fix sẽ tạo bản mới. Nếu muốn kiểm soát hơn, dùng release-please để tạo Release PR và chỉ release khi merge PR đó.

Bước 5 — Nối release notes vào kênh thông báo. Sau khi GitHub Release được tạo, thêm một bước gửi nội dung sang Slack, Telegram hoặc email cho các bên liên quan. Nội dung release notes chính là biến ${nextRelease.notes} mà semantic-release đã sinh.

Bước 6 — Kiểm chứng bằng dry-run. Trước khi bật thật, chạy npx semantic-release --dry-run để xem công cụ sẽ tính version nào và sinh notes gì mà không thực sự publish. Luôn làm bước này khi thiết lập lần đầu.

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

Lỗi 1 — Quên fetch-depth: 0. Đây là lỗi số một khiến người mới bối rối: pipeline chạy nhưng version không bao giờ bump, hoặc changelog trống. Nguyên nhân là checkout mặc định chỉ lấy 1 commit. Luôn đặt fetch-depth: 0.

Lỗi 2 — Vòng lặp CI vô hạn. Khi công cụ commit ngược file CHANGELOG.md vào repo, commit đó lại kích hoạt pipeline on: push một lần nữa. Giải pháp: thêm [skip ci] vào commit message release (như đã làm ở Bước 2), hoặc lọc bỏ nhánh/tác giả bot.

Lỗi 3 — Squash merge làm mất type commit. Nhiều team bật "Squash and merge" trên GitHub. Nếu tiêu đề squash không theo chuẩn Conventional Commits, mọi thông tin type/scope bị mất. Mẹo: bật rule bắt buộc tiêu đề PR đúng chuẩn, và cấu hình GitHub dùng tiêu đề PR làm commit message khi squash.

Lỗi 4 — Nhồi nhét mọi thứ vào changelog. Người dùng không cần biết bạn chore: bump eslint. Cấu hình để chỉ feat, fix, perf và breaking change xuất hiện trong release notes; các loại còn lại ẩn đi hoặc gom vào một mục "Nội bộ".

Lỗi 5 — Bỏ qua quyền hạn token. GitHub Action mặc định có quyền hạn giới hạn. Nếu thiếu contents: write, bước tạo tag và release sẽ thất bại với lỗi 403. Khai báo permissions rõ ràng.

Mẹo — Dùng scope để nhóm. Với monorepo, feat(web): và feat(api): giúp bạn lọc changelog theo từng service. git-cliff và release-please đều hỗ trợ tách changelog riêng cho từng package.

Mẹo — Đừng chỉnh CHANGELOG.md bằng tay. Một khi đã tự động, coi file đó là "sản phẩm sinh ra" (generated artifact). Muốn sửa nội dung, hãy sửa commit message hoặc cấu hình template, đừng edit trực tiếp — nếu không lần chạy sau sẽ ghi đè.

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

  • Chuẩn hóa commit. Lấy một repo cá nhân bất kỳ, cài commitlint + husky như Bước 1. Thử tạo một commit sai chuẩn (git commit -m "abc") và xác nhận nó bị chặn. Sau đó tạo 3 commit đúng chuẩn: một feat:, một fix:, và một feat!: có breaking change.
  • Dry-run version. Cài semantic-release, chạy npx semantic-release --dry-run trên local. Quan sát: với 3 commit ở trên, công cụ đề xuất version nào? Giải thích tại sao nó lại chọn tăng MAJOR chứ không phải MINOR.
  • Dựng pipeline thật. Tạo workflow GitHub Actions như Bước 3, push lên nhánh main, và xác nhận một GitHub Release cùng file CHANGELOG.md được sinh tự động. Cố tình quên fetch-depth: 0 một lần để trực tiếp thấy triệu chứng lỗi, rồi sửa lại.
  • Tách hai kênh. Thêm bước gom các PR gắn nhãn user-facing thành một release notes ngôn ngữ thân thiện (có thể làm thủ công phần biên tập). So sánh nó với changelog kỹ thuật và nhận xét sự khác biệt về giọng văn và mức chi tiết.
  • Nối thông báo. Thêm một bước gửi nội dung release mới vào một kênh Telegram hoặc Slack test của bạn, dùng nội dung notes do công cụ sinh ra.

Tóm tắt

Release notes và changelog thủ công luôn thất bại vì chúng nằm ngoài luồng công việc tự nhiên và phụ thuộc vào thiện chí con người trong lúc bận rộn. Chìa khóa để tự động hóa là kỷ luật commit có cấu trúc (Conventional Commits), làm nền cho công cụ suy ra version theo SemVer và sinh nội dung. Bạn chọn công cụ theo hệ sinh thái: semantic-release cho Node/continuous release, release-please cho quy trình có bước phê duyệt qua Release PR, git-cliff cho dự án đa ngôn ngữ hoặc monorepo.

Ba nguyên tắc cần nhớ: (1) cưỡng chế chất lượng commit bằng commitlint và CI, vì "garbage in, garbage out" — nhất là với breaking change; (2) coi CHANGELOG.md là sản phẩm sinh ra, đừng sửa tay; (3) tách bạch changelog kỹ thuật (cho developer, máy sinh) với release notes cho người dùng cuối (con người biên tập lại từ nguyên liệu máy gom). Khi làm đúng, câu hỏi "bản này thay đổi những gì?" lúc 2 giờ sáng sẽ có câu trả lời trong 30 giây thay vì một tiếng đồng hồ căng thẳng.

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