Cách dùng Changesets để tự động hóa release trong monorepo
Tích hợp @changesets/cli v2.31.0 vào monorepo pnpm + Turborepo — từ changeset file đầu tiên đến pipeline publish tự động trên GitHub Actions.
Bởi Ethan
2.248 từ · 12 phút đọc
Nếu bạn đang phải bump version thủ công cho từng package và tự tay cập nhật changelog, Changesets sẽ giúp bạn lấy lại khoản thời gian đó ngay trong tuần đầu. Hướng dẫn này tích hợp @changesets/cli v2.31.0 vào monorepo pnpm + Turborepo 2.x, từ lúc cài đặt ban đầu đến khi có pipeline publish tự động trên GitHub Actions.
Dành cho ai
Developer từ mid-level trở lên đang có sẵn monorepo pnpm + Turborepo và đang bump version thủ công. Nếu bạn chưa cài pnpm workspaces, hãy đọc Cách thiết lập monorepo pnpm + Turborepo từ đầu trước.
Changesets là gì (và không phải là gì)
Changesets là công cụ quản lý release cho monorepo. Nó xử lý ba việc: package nào cần bump version, bump loại nào (patch/minor/major), và nội dung changelog sẽ là gì.
Điều nó không làm: build, publish orchestration, hay CI. Nó chỉ ghi file. Bạn quyết định khi nào sử dụng chúng.
Vòng đời gồm ba lệnh:
changeset add → developer khai báo intent (loại bump + tóm tắt)
changeset version → áp dụng vào package.json + CHANGELOG.md
changeset publish → npm publish cho mọi package có version mới
Chỉ vậy thôi. Phần còn lại là cấu hình xung quanh ba bước này.
developer PR
│
▼
changeset add
│ ghi .changeset/<tên-ngẫu-nhiên>.md
▼
merge vào main
│
▼
changeset version
│ bump package.json, ghi CHANGELOG.md
│ xóa các file .changeset/*.md
▼
pnpm install ← bắt buộc: cập nhật lockfile trước khi publish
│
▼
changeset publish
│ npm publish cho mỗi package đã bump
▼
git tag + push
Yêu cầu
- Node 20+
- pnpm 9+ (khuyến nghị pnpm 10)
- Turborepo 2.x
- Tài khoản npm với quyền publish cho các scoped package của bạn
Cài đặt và khởi tạo
Cài tại workspace root:
pnpm add -Dw @changesets/cli
Flag -w cài vào workspace root. Không cài riêng cho từng package.
Khởi tạo:
pnpm changeset init
Lệnh này tạo .changeset/config.json và .changeset/README.md. Commit cả hai file.
Cấu hình .changeset/config.json
Config mặc định hoạt động tốt cho hầu hết các trường hợp, nhưng có ba trường cần bạn cân nhắc:
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
| Trường | Mặc định | Ý nghĩa |
|---|---|---|
access | "restricted" | Quyền publish lên npm cho scoped packages |
commit | false | changeset version có tự động commit không |
fixed | [] | Các package phải bump version cùng nhau |
linked | [] | Các package dùng chung version nếu bất kỳ package nào bump |
baseBranch | "main" | Branch Changesets dùng để diff |
Cạm bẫy với access: giá trị mặc định là "restricted", nghĩa là npm sẽ từ chối publish với bất kỳ scoped package nào (@org/pkg) trừ khi bạn trả phí cho npm private packages. Đặt thành "public" nếu các scoped package của bạn là public:
{
"access": "public"
}
fixed vs linked: cả hai đều nhóm các package lại, nhưng hoạt động khác nhau.
fixed buộc tất cả package được liệt kê phải bump lên cùng version, kể cả khi chỉ một package trong số đó có changeset:
{
"fixed": [["@myorg/core", "@myorg/react", "@myorg/vue"]]
}
linked chỉ đồng bộ các package đã có changeset — nếu @myorg/react có patch changeset còn @myorg/vue thì không, linked sẽ bump react và để nguyên vue. fixed thì bump cả hai.
Dùng fixed khi các package luôn được ship cùng nhau (design system tokens + components). Dùng linked khi các package dùng chung version range nhưng có thể release độc lập.
Thêm changeset
Khi sắp mở PR có ảnh hưởng đến một package, thêm changeset:
pnpm changeset
Prompt tương tác hỏi hai điều: package nào bị ảnh hưởng, và loại bump nào (patch / minor / major). Sau đó nó yêu cầu một dòng tóm tắt — viết cho người đọc changelog, không phải cho bản thân bạn.
Lệnh này ghi một file vào .changeset/ với nội dung như sau:
---
"@myorg/core": minor
"@myorg/utils": patch
---
Add `createToken()` utility and expose `BaseComponent` as a named export from core.
Commit file này cùng với PR. Reviewer có thể thấy ngay release impact của PR mà không cần commit “bump version” riêng.
Versioning khi merge
Khi các PR được merge vào main, áp dụng các changeset đã tích lũy:
pnpm changeset version
Lệnh này đọc tất cả file .changeset/*.md, áp dụng version bump vào từng package.json, ghi changelog, và xóa các changeset file.
Bước bắt buộc với pnpm: sau changeset version, hãy chạy pnpm install trước khi publish. Các file package.json đã thay đổi nên lockfile hiện đang lỗi thời. Publish mà bỏ bước này sẽ bị cảnh báo hoặc fail tùy version pnpm của bạn.
pnpm changeset version
pnpm install # ← bắt buộc: cập nhật pnpm-lock.yaml
pnpm changeset publish
Publish
pnpm changeset publish
Lệnh này chạy npm publish cho mọi package có version trong package.json chưa tồn tại trên registry. Nó cũng tạo git tag cho từng package.
Bạn cần NPM_TOKEN trong environment. Nếu token bị thiếu, publish sẽ fail với lỗi 401 hiển thị như một lỗi auth chung — không phải lúc nào cũng rõ ràng. Set trước khi chạy:
export NPM_TOKEN=your-token-here
pnpm changeset publish
Sau khi publish, push các tag:
git push --follow-tags
Nếu bạn đang publish một package độc lập thay vì trong monorepo, hãy xem Cách publish npm package đúng chuẩn trong năm 2026 để tham khảo quy trình cho package đơn lẻ.
Tự động hóa với GitHub Actions
Official Changesets action xử lý vòng lặp versioning → PR → publish tự động. Đây là workflow hoạt động được cho pnpm:
name: Release
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build
- name: Create Release PR or publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version && pnpm install --no-frozen-lockfile
commit: 'chore: version packages'
title: 'chore: version packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Action này hoạt động theo hai chế độ tùy vào trạng thái của main:
- Có changeset đang chờ: nó mở (hoặc cập nhật) PR “Version Packages”. PR đó hiển thị diff về version và changelog. Khi bạn merge PR này, action chạy lại ở chế độ 2.
- Không có changeset: nó publish những package có version mới.
Lưu ý pnpm install --no-frozen-lockfile trong input version. Lệnh pnpm install --frozen-lockfile thông thường sẽ fail ở đây vì changeset version vừa thay đổi các file package.json, khiến lockfile bị lỗi thời. Flag --no-frozen-lockfile cho phép pnpm tự cập nhật lockfile cho version PR.
Nâng cao: private packages và kiểm soát access
Package nào bạn không muốn publish có thể loại khỏi Changesets hoàn toàn:
{
"ignore": ["@myorg/internal-app", "@myorg/test-utils"]
}
Package trong ignore sẽ không bao giờ được áp dụng changeset. Chúng vẫn có thể xuất hiện như dependent — nếu core bump, internal-app sẽ không bump, nhưng tham chiếu đến core trong package.json của nó vẫn được cập nhật.
Với package chỉ tồn tại như workspace internal và không bao giờ được publish, đặt "private": true trong package.json của nó. Changesets tôn trọng flag này và bỏ qua chúng lúc publish mà không cần thêm vào mảng ignore.
Nâng cao: snapshot releases
Snapshot releases cho phép bạn publish một pre-release version từ bất kỳ branch nào — hữu ích để test một PR trong downstream project trước khi merge.
pnpm changeset version --snapshot alpha
pnpm changeset publish --tag alpha --no-git-tag
Version được publish sẽ có dạng như 1.2.0-alpha-20260529120000. Flag --no-git-tag ngăn các snapshot tag làm rối lịch sử tag của bạn.
Consumer có thể test bằng:
pnpm add @myorg/core@alpha
Snapshot releases không yêu cầu phải có changeset — nếu không có changeset file nào, version snapshot được tính từ version stable mới nhất với timestamp suffix.
Nâng cao: pre-release mode
Pre-release mode dùng để quản lý branch beta hoặc RC theo thời gian. Bật trên branch pre-release của bạn:
pnpm changeset pre enter beta
Từ đây, changeset version sẽ tạo ra các version như 1.0.0-beta.0, 1.0.0-beta.1, v.v. Changeset tích lũy qua các commit trên branch này.
Khi sẵn sàng đưa lên stable:
pnpm changeset pre exit
pnpm changeset version # tạo ra version stable cuối cùng
Pre-release mode được theo dõi trong .changeset/pre.json. Commit file đó — nó cho Changesets biết branch đang ở chế độ nào và package nào có pre-release bump đang chờ.
Tích hợp Turborepo
Đừng đặt tên script publish của bạn là publish. npm và pnpm xử lý publish như một lifecycle hook và có thể chạy nó vào những lúc không mong muốn. Dùng publish-packages thay thế:
{
"scripts": {
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"publish-packages": "turbo run build lint test && changeset version && changeset publish"
}
}
Thêm các lệnh Changesets vào turbo.json. Lưu ý: key config của Turborepo 2.x là tasks, không phải pipeline:
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["build"]
}
}
}
changeset version và changeset publish không cần Turborepo task — chúng hoạt động ở workspace level, không phải per-package. Chạy turbo run build lint test trước chúng để đảm bảo mọi package đều được build và pass test trước khi bất kỳ version nào được bump hay publish.
Changesets vs các lựa chọn thay thế
| Changesets | semantic-release | release-it | Lerna | |
|---|---|---|---|---|
| Monorepo-first | Có | Qua plugin | Qua plugin | Có |
| Changelog thủ công | Có (changeset files) | Không (auto từ commits) | Cả hai | Cả hai |
| pnpm workspaces | Hỗ trợ trực tiếp | Qua plugin | Qua plugin | Hạn chế |
| Phối hợp nhiều package | Nhóm fixed / linked | Config per-package | Config per-package | Flag --since |
| Pre-release | Built-in (pre enter) | Built-in | Built-in | Built-in |
| Độ phức tạp config | Thấp | Trung bình | Trung bình | Cao |
| Đang được maintain | Có | Có | Có | Có (bởi Nx team) |
Changesets là lựa chọn mặc định hợp lý cho monorepo pnpm. Mô hình changeset-file của nó phù hợp tốt với repo nhiều team, nơi PR author khai báo intent thay vì phụ thuộc vào convention trong commit message.
semantic-release và release-it tỏa sáng khi bạn muốn không phải tốn công sức — tự động phát hiện version từ conventional commits, không cần quyết định gì lúc mở PR. Đánh đổi là mọi người trong team phải tuân thủ commit convention một cách nhất quán.
Lerna đã được Nx team duy trì tích cực từ năm 2022. Đây là lựa chọn hợp lý nếu bạn đã trong hệ sinh thái Nx, nhưng sẽ thêm overhead nếu không.
Những lỗi thường gặp
access: "restricted" cho scoped packages
Đây là nguyên nhân publish fail phổ biến nhất. Config mặc định có "access": "restricted", đúng cho npm private packages nhưng sai cho public. Nếu bạn thấy lỗi 402 hoặc 403 từ registry khi publish @scope/package, đây chính là nguyên nhân. Đặt "access": "public" trong config.json.
Quên chạy pnpm install giữa changeset version và changeset publish
changeset version ghi lại các file package.json. Lockfile hiện bị lỗi thời. Nếu bạn publish mà không chạy pnpm install, bạn sẽ nhận cảnh báo hoặc fail tùy cài đặt frozen-lockfile. Luôn chạy pnpm install sau khi version.
pnpm catalog + Changesets (kiểm tra trước khi publish)
pnpm catalogs (giao thức catalog: trong pnpm-workspace.yaml) chưa được Changesets hỗ trợ natively. PR #1714 (--enable-pnpm-catalog) đang mở và chưa được merge tính đến tháng 5 năm 2026. Nếu bạn đang dùng catalog, hãy kiểm tra xem PR này đã được merge chưa trước khi publish. Workaround: thêm manual patch changeset cho bất kỳ package nào dùng catalog: dependency khi các catalog entry đó thay đổi.
Đặt tên script là publish
Nếu package.json root của bạn có "publish": "...", pnpm sẽ chạy nó như lifecycle hook khi bất kỳ package nào publish, không phải như script top-level bạn định dùng. Hãy dùng publish-packages hoặc bất kỳ tên nào khác.
commit: true trong Turborepo setups
commit: true trong config.json khiến changeset version tự động commit version bump. Điều này có thể xung đột với CI pipeline của Turborepo khi chúng expect version commit đến từ PR merge. Để nguyên false (mặc định) và để Changesets GitHub Action hoặc CI của bạn xử lý commit.