🧩 Design System8. Pipeline & Distribution03 — Changesets로 SemVer 자동화: snapshot·prerelease·publish

03 — Changesets로 SemVer 자동화: snapshot·prerelease·publish

이 문서가 답하는 질문: 모노레포 안의 7~10개 패키지를 어떻게 기능 단위로 버저닝하고, PR 흐름에 어떻게 녹이며, snapshot/prerelease 같은 비공식 릴리스는 어떻게 다루는가. 한 줄 답 (Pyramid Top): Changesets는 **(1) 개발자가 PR에 변경 의도 markdown을 동봉 → (2) 메인 머지 시 CI가 자동으로 "Version Packages" PR을 생성 → (3) 그 PR이 머지되면 publish**의 3-스텝 SemVer 자동화다.


Why — 왜 Changesets인가

풀려는 문제이전 해법한계
모노레포 패키지별 다른 버전Lerna independent modepublish 단계가 수동·실수 잦음
”어떤 PR이 어떤 패키지를 깼는가” 추적커밋 메시지 컨벤션(feat:, fix:) + semantic-release단일 패키지만 잘 동작
의존 관계 따라 버전 자동 bumpLerna의 --conventional-graduate토큰 minor bump → react도 minor가 과한 경우 있음
Release notes 자동 생성conventional-changelog커밋 메시지에 의존 — 사람이 잘 안 씀
변경 의도와 영향 범위를 PR 단위로 명시Changesets가 등장

Changesets는 **“개발자가 PR마다 ‘이 변경이 어떤 패키지를 어떻게 깰지’ markdown으로 적어둔다”**는 단순한 사회적 합의를 자동화의 입력으로 삼는다.


How — 3-스텝 워크플로

핵심은 두 단계 PR:

  1. 기능 PR — 코드 + changeset markdown
  2. Version PR — CI가 자동 생성, package.json·CHANGELOG.md만 변경

이 분리가 **“기능 머지 ≠ 릴리스”**를 강제한다 — 릴리스는 Version PR 머지 시점에만 일어난다.


What — 실제 설정과 명령어

초기 설정

pnpm add -D -w @changesets/cli
pnpm changeset init

생성되는 .changeset/config.json:

{
  "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
  "changelog": [
    "@changesets/changelog-github",
    { "repo": "org/design-system" }
  ],
  "commit": false,
  "fixed": [],
  "linked": [
    ["@org/react", "@org/tailwind-preset", "@org/panda-preset"]
  ],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@org/docs"],
  "snapshot": {
    "useCalculatedVersion": true,
    "prereleaseTemplate": "{tag}-{datetime}"
  }
}
옵션의미
fixed항상 같은 버전으로 publish할 패키지 그룹
linkedbump 시 같이 올라가야 하는 패키지 그룹 (다른 패키지는 영향 없으면 그대로)
updateInternalDependencies워크스페이스 의존성 자동 bump 단위 (patch/minor)
ignorepublish 제외 (docs 앱처럼 private인 것)
access: "public"scoped 패키지(@org/...)의 기본 publish 권한

Changeset 생성

pnpm changeset

대화형:

🦋  Which packages would you like to include? (Use arrow keys/space)
  ◯ @org/tokens
  ◉ @org/react
  ◯ @org/icons

🦋  Which packages should have a major bump?
  ◯ @org/react

🦋  Which packages should have a minor bump?
  ◉ @org/react

🦋  Please enter a summary for this change:
  > Button: size prop "xl" 추가

생성되는 .changeset/lucky-cats-jump.md:

---
"@org/react": minor
---
 
Button: size prop "xl" 추가
 
- 새로운 `size="xl"` 옵션
- BREAKING이 아닌 추가만

CI 워크플로

.github/workflows/release.yml:

name: Release
 
on:
  push:
    branches:
      - main
 
concurrency: ${{ github.workflow }}-${{ github.ref }}
 
jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      id-token: write   # npm provenance
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
 
      - uses: pnpm/action-setup@v4
        with:
          version: 9
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
          registry-url: https://registry.npmjs.org
 
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run build typecheck test
 
      - name: Create Release PR or Publish
        id: changesets
        uses: changesets/action@v1
        with:
          publish: pnpm release
          version: pnpm version-packages
          commit: "chore: version packages"
          title: "chore: version packages"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
          NPM_CONFIG_PROVENANCE: true

루트 package.json:

{
  "scripts": {
    "version-packages": "changeset version && pnpm install --no-frozen-lockfile",
    "release": "pnpm turbo run build && changeset publish"
  }
}

changesets/action두 모드로 동작한다:

  • 대기 중인 changeset이 있음 → “Version Packages” PR 생성/업데이트
  • 대기 중인 changeset이 없음 (Version PR이 머지된 직후) → publish 실행

CHANGELOG 자동 생성 예시

@org/react/CHANGELOG.md:

# @org/react
 
## 1.2.0
 
### Minor Changes
 
- abc1234: Button: size prop "xl" 추가 ([#142](https://github.com/org/ds/pull/142))
 
### Patch Changes
 
- Updated dependencies [def5678]
  - @org/tokens@1.1.1

updateInternalDependencies: "patch" 덕분에 @org/tokens가 patch bump 됐을 때 @org/react도 자동으로 patch 올라가고, 그 사실이 CHANGELOG에 기록된다.


Snapshot release — PR 미리보기 배포

PR이 머지되기 전에 임시 버전으로 npm에 올려 디자이너·사용자가 테스트할 수 있게 한다.

# PR 브랜치에서
pnpm changeset version --snapshot pr-142
pnpm changeset publish --tag pr-142 --no-git-tag

생성되는 버전: @org/react@0.0.0-pr-142-20260519143022

사용자:

pnpm add @org/react@pr-142

CI에서 자동화:

# .github/workflows/snapshot.yml
on:
  pull_request:
    types: [labeled]
 
jobs:
  snapshot:
    if: github.event.label.name == 'snapshot'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run build
      - run: |
          pnpm changeset version --snapshot pr-${{ github.event.pull_request.number }}
          pnpm changeset publish --tag pr-${{ github.event.pull_request.number }} --no-git-tag
      - uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '📦 Snapshot published: `pnpm add @org/react@pr-${{ github.event.pull_request.number }}`'
            })

PR에 snapshot 라벨을 붙이면 자동 publish + PR 댓글로 install 명령 게시.


Prerelease mode — 메이저 변경 점진 출시

다음 메이저(2.0.0)를 준비할 때 next 채널로 배포한다.

pnpm changeset pre enter next
# 이제 모든 publish가 2.0.0-next.0, 2.0.0-next.1, ... 로 나감
 
# 평소처럼 작업
pnpm changeset
git commit ...
# main 머지 → 자동으로 2.0.0-next.X publish
 
# 모두 끝나면 종료
pnpm changeset pre exit
# 다음 publish는 2.0.0 정식

사용자:

pnpm add @org/react@next   # prerelease 채널
pnpm add @org/react@latest # 안정 채널 (기본)

dist-tag 시스템이 두 채널을 분리해 안정 사용자가 prerelease를 받지 않게 한다.


What-if — 잘못 쓰면 어떻게 깨지는가

  • 함정 1 — changeset 누락: PR이 changeset 없이 머지되어 변경 사항이 사라짐. 대응: .github/workflows/ci.yml에 changeset 검사 추가.
- name: Check for changesets
  run: |
    if [ -z "$(ls -A .changeset/*.md 2>/dev/null | grep -v README)" ]; then
      echo "No changeset found. Run 'pnpm changeset' or add 'no-changeset' label."
      exit 1
    fi

또는 bot으로 자동 댓글changesets/bot GitHub App이 PR에 “changeset이 없습니다”를 댓글로 단다.

  • 함정 2 — linked 잘못 설정: reacttokens를 linked로 묶었더니, tokens 패치마다 react도 패치 올라감 → 사용자 update 부담. 대응: linked같이 메이저 변화하는 패키지에만. 의존 관계는 updateInternalDependencies가 처리.

  • 함정 3 — provenance 미설정: npm 7+에서 publish 출처 증명(sigstore)이 표준이 되었는데, NPM_CONFIG_PROVENANCE: true를 빠뜨려 사용자에게 “unverified publisher” 경고. 대응: GitHub Actions에서 id-token: write 권한 + 환경변수.

  • 함정 4 — Version PR 머지 후 publish 실패: NPM_TOKEN 만료/권한 부족. CHANGELOG와 package.json은 올라갔는데 npm에는 없는 유령 버전. 대응: 토큰 만료 알림 + 실패 시 Slack alert. Version PR을 revert하지 말고 patch bump로 재배포 (revert 시 사용자가 이미 본 버전을 못 받음).

  • 함정 5 — snapshot이 latest tag로 publish됨: --tag 옵션 빠뜨림 → latest 채널 오염. 대응: snapshot 명령은 항상 --tag pr-XXX. CI에서 강제.

  • 함정 6 — 의존 그래프 무한 bump: A→B→C 의존 시 A의 patch가 B의 patch, B가 C의 patch까지 cascade. 한 번의 변경이 모든 패키지에 영향. 대응: 진짜 런타임 의존만 dependencies로. 빌드 시점만 쓰는 것은 devDependencies로.


Insight — semantic-release를 안 쓰는 이유

항목semantic-releaseChangesets
입력커밋 메시지 (Conventional Commits).changeset/*.md
모노레포semantic-release-monorepo로 부분 지원1급 지원
Release notes커밋 메시지에서 추출markdown에서 추출
의도 표현”fix: …” “feat: …” 한 줄자유 형식 markdown (여러 줄)
PR 단위 통합어려움자연스러움
학습 곡선가파름 (Conventional Commits 강제)평탄

개발자가 왜 이 변경이 minor인지 설명할 수 있는 공간이 Changesets에는 있고, semantic-release에는 없다. 디자인 시스템처럼 의도 설명이 중요한 도메인에서 이 차이가 컸다.

반전: 2022년 Vercel·Atlassian·Shopify·Adobe Spectrum이 동시에 Changesets로 이전했다. Spectrum 팀의 이전 후기에 따르면, “semantic-release 시절엔 commit message 컨벤션 위반으로 release가 깨지는 일이 월 5~6회였는데, Changesets로 옮긴 후 0회”.


요약

  • Changesets는 (1) changeset markdown → (2) Version PR → (3) publish의 3-스텝 자동화.
  • 모노레포에서 영향받은 패키지만 SemVer를 올린다 (의존 관계 따라 cascade).
  • Snapshot release는 PR 미리보기 (--snapshot pr-142), Prerelease는 메이저 채널 분리 (pre enter next).
  • 핵심 강제 셋: changeset 검사, provenance, 의존 그래프 폭주 방지.