Back
Featured image of post 🦋 Changeset을 활용한 모노레포 자동 배포 구축하기

🦋 Changeset을 활용한 모노레포 자동 배포 구축하기

changeset과 GitHub Actions를 활용한 자동 배포 시스템 구축기

개요

사이드 프로젝트로 모노레포 기반의 디자인 시스템을 개발하면서, 하위 패키지들의 소스코드 수정 및 커밋시 Push를 감지하고 자동으로 배포하는 도구를 찾게 되었습니다. 그 중 changeset 이라는 도구를 알게 되어, GitHub Actions와 연동해 자동 배포 환경을 구축하면서 겪게 된 내용들을 공유합니다.
간략한 개발 환경을 말씀드리자면 yarn-berry 환경의 모노레포로 구성된 프로젝트에서 진행하였습니다.

Changeset 기본 사용법

Changeset은 멀티 패키지 환경(monorepo)에서 상호 의존하는 패키지들의 일관성을 유지하기 위한 라이브러리입니다. 여러 의존된 패키지들을 업데이트할 때 마다 자동으로 Semver규칙에 따라 버전을 관리해주고 간편한 명령셋을 통해 손쉽게 레지스트리에 배포가 가능합니다. 이미 프로젝트가 모노레포 환경으로 구성되어 있기 때문에, 기존 프로젝트 환경에서 적용하기 쉽고, CI/CD 지원을 제공하는 라이브러리에 적합한 Changeset을 선택하게 되었습니다.

설치하기

아래와 같이 changeset 패키지를 설치하고 init을 통해 관리할 하위 패키지들을 선택하실 수 있습니다.

$ yarn add @changesets/cli && yarn changeset init
#or
$ npm install @changesets/cli && npx changeset init

환경 설정

기본적으로 생성되는 config.json 파일에서 프로젝트에 맞게 몇가지 수정이 필요하였습니다. 이 역시도 공식 문서에 친절하게 설명이 나와있어 자신의 프로젝트에 맞게 설정을 하시면 되는데, 적용한 설정을 간략하게 설명드리면 아래와 같습니다.

//📂.changeset/config.json
{
  "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}
  • access: 액세스 권한 설정 (restricted, public)
  • baseBranch: 변경 감지를 위한 대상 브랜치
  • updateInternalDependencies: 종속된 패키지가 변경될 때 같이 업데이트 patch
  • commit: false를 통해 사용자가 직접 커밋

changeset command

changeset 커맨드를 입력하면 패키지들의 변경 사항을 감지합니다. 그런 다음 semver 규칙에 따라 메이저 버전으로 업데이트할지, 아니면 마이너 버전으로 업데이트할지 질의합니다. 스페이스바로 선택하실 수 있고, 선택하지 않고 넘어가면(Enter) 스킵합니다.

npx changeset
#or
yarn changeset

# step01) 업데이트 패키지가 무엇인지 설정한다.
# 🦋  Which packages would you like to include? ...

# step02) 패키지의 SEMVER를 결정한다. 선택되지 않은 패키지는 minor 버전으로 업데이트
# 🦋  Which packages should have a major bump? ...

# step03) 변경 사항을 간략하게 입력합니다.
# 🦋  Please enter a summary for this change (this will be in the changelogs). Submit empty line to open external

version

배포하기로 결정한 후, 다음과 같이 버전 업데이트를 진행합니다. 설정된 업데이트 규칙에 따라 메이저 또는 마이너 버전이 증가하고, 의존하고있는 패키지들도 같이 업데이트됩니다. 또한 로그 파일(CHANGELOG.md)도 함께 생성됩니다.

npx changeset version
#or
yarn changeset version

이 단계 이후 changeset publish 명령어를 사용해 내부적으로 .npmrc 파일을 참조해 레지스트리에 배포할 수 있습니다. 자동 배포를 원하시면 publish를 GitHub Actions에 스크립트를 작성하고 push를 수행하시면 됩니다.

publish

changeset publish를 실행하면 이전 단계에서 수행한 자동으로 업데이트 예정인 패키지들을 레지스트리에 배포합니다.

npx changeset publish
#or
yarn changeset publish

여기까지가 기본적인 Changeset 사용법 입니다. 팀의 목표는 GitHub 레포지토리의 main 브랜치에 push가 되면, GitHub Actions를 활용해 NPM 레지스트리까지 자동으로 배포되는 파이프라인을 구축하는 것 이었습니다.

자동 배포 구현하기

GitHub Action

친절하게도 changeset에서 ci/cd를 위해 GitHub Action 워크플로우 파일을 제공하고 있습니다. 아래는 예시인데요, actions 문서를 확인하시면 상황에 따라 여러 예시를 제공해주고 있습니다.

name: Release

on:
  push:
    branches:
      - main

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repo
        uses: actions/checkout@v3

      - name: Setup Node.js 16.x
        uses: actions/setup-node@v3
        with:
          node-version: 16.x

      - name: Install Dependencies
        run: yarn

      - name: Create Release Pull Request or Publish to npm
        id: changesets
        uses: changesets/action@v1
        with:
          # This expects you to have a script called release which does a build for your packages and calls changeset publish
          publish: yarn release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Send a Slack notification if a publish happens
        if: steps.changesets.outputs.published == 'true'
        # You can do something when a publish happens.
        run: my-slack-bot send-notification --message "A new version of ${GITHUB_REPOSITORY} was published!"

프로젝트는 NPM 레지스트리에서 관리하고 있기 때문에, GitHub에서 연동할 NPM TOKEN을 직접 발급 받아 프로젝트 레포지토리의 시크릿 키로 등록을 하셔야 합니다. NPM 사이트 접속 후 로그인 → 프로필 → Access Tokens 메뉴에서 발급을 받을 수 있습니다. 토큰 생성시 타입은 꼭 publish 타입으로 발급을 받으셔야 됩니다.
(⚠️ 발급 후 꼭 별도로 보관해주세요, 다시는 토큰 값을 볼 수 없습니다..) 01-npm-token

이제 발급된 키를 깃허브 레포지토리에 secret 키로 등록하실 수 있습니다. secret 키는 GitHub Actions Workflow에서 사용될 변수 값이라고 생각하시면 됩니다.

레포지토리 → Settings 탭→ Secrets and variables → Actions → New repository secret을 통해 키를 발급하실 수 있습니다. 여기에 NPM토큰 값을 Secret 필드에 입력하고, 키 이름을 Name에 입력하시면 됩니다. (이름은 자유이나, 편의상 NPM_TOKEN으로 등록하시는게 나을 것 같습니다.) 02-git-secret-key

자, 이제 준비는 끝났습니다. 위에서 changeset 문서에서 예시로 알려준 Actions workflow 스크립트 파일을 참고하여 YAML 파일을 생성하고 프로젝트 내 .github/workflows 파일에 저장한 후 레포지토리에 push하면 됩니다. 그러나 저는 몇 가지 문제가 발생했기 때문에 아래에서 해결 과정을 말씀드리도록 하겠습니다.

문제 사항

E404 Error

e404-error 우선, 먼저 발생했던 오류는 changeset에서 퍼블리싱 대상은 잘 파악하지만, changeset publish를 수행하는 순간 해당 NPM 레지스트리를 찾을 수 없다는 에러였습니다. 저는 아래와 같이 미흡한 부분을 추가하여 해결할 수 있었습니다.

GithubToken 권한

token 기본적으로 제공되는 GITHUB_TOKEN에도 write 권한을 부여해야 한다는 사실을 알게 되어 권한을 변경했습니다. 권한 변경 방법은 다음과 같습니다.

레포지토리 설정 → Actions 탭 → General → Workflow Permissions에서 Read and write permissions 항목으로 변경 후 저장

organization 계정

제가 구성한 모노레포는 organization 계정으로 되어 있기 때문에, .npmrc 파일에 npm 레지스트리 계정을 입력해야만 404 에러가 발생되지 않았습니다. .npmrc 파일은 npm 설정 파일으로써, 프로젝트에서 관리되는 파일은 프로젝트 외부에서 관리되는 파일보다 우선순위가 높습니다.

//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN} 이 부분에 해당 레포지토리가 배포될 레지스트리 주소를 적으면 됩니다. GitHub Registry를 사용하는 경우도 있지만, 저는 NPM Registry에 배포를 진행하게끔 하였습니다.

⚠️ 주의 사항으로는 프로젝트에 .npmrc가 이미 존재하는 경우, Actions의 실행 과정에서 생성되는 .npmrc 파일은 덮어쓰지 않습니다. 따라서 NPM 토큰을 .env 파일에서 읽어들일지, 아니면 Actions 스크립트에서 아래와 같이 .npmrc 파일을 생성할지 선택해야 합니다. (저는 퍼블릭 레포지토리로 관리 하고있기 때문에 두 번째 방법을 선택했습니다.)

옵션1) Project 내부에서 관리하는 방법

#./.npmrc
email=username@registry-email.com
//registry.npmjs.org/:_authToken=${process.env.NPM_TOKEN}

옵션2) Workflow를 통해 Publish 단계 직전에 생성하는 방법

#📂root/.github/workflows/release-action.yml

#changeset publish 이전 단계에 추가
...
- name: Create .npmrc file
        run: |
          cat << EOF > "$HOME/.npmrc"
            email=username@registry-email.com
            //registry.npmjs.org/:_authToken=$NPM_TOKEN
          EOF
				env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Workspace range 이슈

기존에는 자동 배포를 수행하지 않고 yarn 커맨드를 통해 publish를 수행하면 workspace 범위에 맞게 버전이 명시되었습니다. 그러나 changeset publish를 통해 배포하면 제대로 된 범위가 설정되지 않고 그대로 배포되는 문제가 발생했습니다. 너무나 많은 패키지들이 서로 의존하고 있어서 업데이트될 때마다 의존된 패키지들을 지정해줄 수 없는 상황입니다. workspace

감사하게도, 어떤 사용자가 git issue 에서 해결 방안을 제안해주셨습니다. 이 방안은 루트 패키지에서 하위 패키지를 순회하여 yarn pack 명령어를 사용해 패키징한 후, 별도의 디렉토리로 이동하여 압축을 풀고 해당 디렉토리를 배포하는 방식이었습니다.

//📂root/package.json
...
  "scripts": {
		...
    "allpackages": "yarn workspaces foreach --no-private --verbose",
    "build": "yarn workspaces foreach run build",
    "build:release": "yarn allpackages exec rm -rf _release && yarn allpackages pack && yarn allpackages exec \"mkdir _release && tar zxvf package.tgz --directory _release && rm package.tgz\"",
    "release": "yarn build:release && changeset publish"
  },

최종적으로 아래와 같이 루트-하위 패키지를 구성하였습니다.

// 📂root/package.json
{
  "name": "project-name",
  "version": "1.0.2",
  "packageManager": "yarn@3.3.1",
  "homepage": "your repository url..",
  "scripts": {
		...
    "allpackages": "yarn workspaces foreach --no-private --verbose",
    "build": "yarn workspaces foreach run build",
    "build:release": "yarn allpackages exec rm -rf _release && yarn allpackages pack && yarn allpackages exec \"mkdir _release && tar zxvf package.tgz --directory _release && rm package.tgz\"",
    "version": "changeset version",
    "release": "yarn build:release && changeset publish"
  },
  "workspaces": [
    "packages/*"
  ],
  "devDependencies": {
		...
  },
  "dependencies": {
		...
  }
}

//📂root/packages/some-package/pacakge.json (하위 패키지)
{
  "name": "@orgname/some-package",
  "packageManager": "yarn@3.3.1",
  "version": "0.5.0",
  "main": "src/index.ts",
  "sideEffects": false,
  "files": [
    "dist"
  ],
  "scripts": {
		...
    "build": "tsup src --dts --format esm,cjs",
    "build:clean": "rimraf dist/"
  },
  "dependencies": {
    "@orgname/A-Package": "workspace:*",
    "@orgname/B-Package": "workspace:*",
		...
  },
  "devDependencies": {
		...
  },
  "peerDependencies": {
		...
  },
  "publishConfig": {
    "access": "public",
    "directory": "_release/package",
    "main": "./dist/index.js",
    "module": "./dist/index.mjs",
    "types": "./dist/index.d.ts"
  }
}
name: Release Action
on:
  push:
    branches:
      - main
  #GitHub Action 수동 실행 허용
  workflow_dispatch:
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      #Node 셋팅 및 레포지토리 체크아웃
      - name: checkout
        uses: actions/checkout@v2
      - name: Use Node.js 16.x
        uses: actions/setup-node@v2
        with:
          node-version: '16.x'
          token: ${{ secrets.WORKFLOW_TOKEN }}
          fetch-depth: '0'
      - name: Install dependencies
        env:
          YARN_ENABLE_IMMUTABLE_INSTALLS: 'false'
        run: yarn install
        # .npmrc 파일 생성
      - name: Configure npm
        run: |
          cat << EOF > "$HOME/.npmrc"
            email=user-id@email.com #NPM 계정 주소
            //registry.npmjs.org/:_authToken=$NPM_TOKEN 
          EOF          
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }} #NPM Token
        #패키지들을 빌드
      - name: Build packages
        run: yarn build
        ## Changeset Action을 사용해 NPM Resistry에 배포
      - name: Create Release Pull Request or Publish to npm
        id: changesets
        uses: changesets/action@v1
        with:
          publish: yarn release
        env:
          NPM_TOKEN: ${{secrets.NPM_TOKEN}}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

이로써 changeset을 통해 하위 컴포넌트 패키지들에 대한 수정이 발생할 때 마다 사용자는 수동으로 의존성 체크와 배포 과정을 할 필요가 없어 개발 생산성을 높일 수 있었습니다. 특히 저 혼자 하는 프로젝트가 아니기 때문에 더 크게 작용하는 것 같습니다. 또한.. GitHub Issus를 통해 yarn workspace 이슈를 제대로 확인하지 못했다면 다른 배포 도구를 찾아서 해매고 있었을 것 입니다. yarn에서 많이 사용하고 있는 다른 유명한 배포 라이브러리로는 lerna도 있으니 한번 고민해보는 것도 좋을 것 같습니다. :-)

참고 자료