[Project] 포트폴리오 제작기 -4. Sementic Release로 버저닝 및 CHANGELOG 관리하기

young_pallete·2022년 12월 3일
1

🚀 시작하며

최근에 공부하는 것도 연말을 타는지, 세상 정신없는 하루를 보내고 있어요. 🔥
덕분에 포트폴리오 제작기 시리즈의 연재가 정말 밀리고 있기는 하네요.
(실제로 우선순위에 따라 포트폴리오 리팩토링도 늦어지고 있어요. 😭)

오늘 주제는, 저번 글에 대한 연장선이라고 이해하면 될 것 같아요.
바로 Semantic-release라는 매우 강력한 툴로 배포를 관리하는 건데요!

인상 깊었던 레포를 예전에 보고 언제 한 번 써볼까 했다가, 이번에 자동화 로직을 아예 전면 변경하게 되면서 제 입맛에 맞춰 사용해보기로 했어요.

⚠️ 주의!

저는 release note를 사용하지 않고, 버저닝과 CHANGELOG.md만 변경할 목적으로 설정했어요. 실제로는 release note도 사용할 수 있으니 참고 바랍니다!

또한 이전의 글은 이제 쓸모가 없는 것처럼 보여도, 취향에 따라 이전의 방식이 더 좋을 수 있습니다. semantic-release는 draft가 아닌 바로 릴리즈 노트를 생성해주기 때문에, 초안을 수정할 목적이라면 이전의 글을 참조하시는 것도 좋아보여요 😉

최근 모노레포로 인해 Changeset이 매력도가 올라가기는 했지만, Semantic-release 역시 자동화 툴로는 프로젝트에서 사용하기엔 충분히 매력적인 것 같았어요.
(그렇지만 다음 프로젝트는 모노레포라, Changeset을 사용할 확률이 높군요! 써보고 싶기도 했구요.)

여튼, 주절주절 그만하고, 어떻게 설정하면 되는지 시작해보죠.

참고로 제가 쓴 글은 다음 기술스택을 따라갑니다.
@semantic-release/*의 패키지는 default로도 제공된다고 하니, 참고하세요!

{
  "packageManager": "yarn@3.3.0",
  "devDependencies": {
    "@commitlint/cli": "^17.3.0",
    "@commitlint/config-conventional": "^17.3.0",
    "@semantic-release/changelog": "^6.0.2",
    "@semantic-release/commit-analyzer": "^9.0.2",
    "@semantic-release/git": "^10.0.1",
    "@semantic-release/release-notes-generator": "^10.0.3",
    "commitizen": "^4.2.5",
    "conventional-changelog-conventionalcommits": "^5.0.0",
    "cz-customizable": "^7.0.0",
    "husky": "^8.0.2"
  }
}

레포를 보고 참고하고 싶다면, 다음 레포 링크를 통해 CI 설정 결과물을 확인할 수 있습니다!

🚦 본론

commitizen, commitllint

conventional-commit에 엄청 도움을 주는 라이브러리입니다.
이 친구들 덕분에 커밋 관리를 좀 더 컨벤셔널하게 가져갈 수 있어요.
이 친구들을 적용할 때에는 어떤 방식으로 관리할 것인지 어댑터를 달아줘야 하는데요.

커밋린트에는 컨벤션을 따르도록 @commitlint/config-conventional을 설치한 후, commitizen(cz)cz-customizable을 통해 customization을 가능하게 합니다.

여기서 호불호가 갈립니다. commitizen의 경우 전역으로 설치하는 경우가 많은데 저는 해당 패키지에서 설치합니다.
이유는 특정 프로젝트를 포크하는 어떤 사람이든, 똑같은 컨벤션을 강제하고 싶었기 때문입니다.

저는 컨벤션하게 작성을 하되, 몇 가지를 추가하고 싶었어요. 예컨대 moveremove같은 것들 말이죠.

이후 commitizen은 자동으로 .cz-config.js를 찾는데요.
.cz-config.js에서는 다음과 같이 설정해주면 됩니다.

module.exports = {
  types: [
    { value: 'feat', name: 'feat | 기능을 추가해요.' },
    { value: 'fix', name: 'fix | 버그를 수정해요.' },

    { value: 'perf', name: 'perf | 성능을 개선해요.' },
    { value: 'refactor', name: 'refactor | 코드를 리팩토링해요.' },
    { value: 'style', name: 'style | 포맷팅이나 컨벤션에 따라 수정 사항을 반영해요.' },
    { value: 'docs', name: 'docs | 문서의 내용을 일부 변경해요.' },
    { value: 'test', name: 'test | 테스트 코드를 추가하거나 리팩토링해요.' },
    { value: 'chore', name: 'chore | 사소한 변경사항이나, 패키지매니저를 관리해요.' },
    { value: 'revert', name: 'revert | 이전의 코드로 되돌려요.' },
    { value: 'move', name: 'move | 디렉토리, 파일이나 코드를 새로운 위치로 이동시켜요.' },
    { value: 'remove', name: 'revert | 쓸모없는 디렉토리, 파일이나 코드를 삭제해요.' },
    { value: 'ci', name: 'ci | CI를 업데이트해요.' },
  ],
  scopes: [
    'component',
    'css-style',
    'custom-hook',
    'store',
    'util',
    'api',

    'wrong codes',
    'spaghetti codes',
    'alien codes',
    
    'assets',
    'package',

    'lint',
    'formatting',
    
    'config',
    'workflow',

    // NOTE: .releaserc.js
    'breaking',
    'no-release',
    'README'
  ],
  allowCustomScopes: true,
};

참고로 scope의 경우 저는 파일 경로를 알려주는 것을 선호하지 않아요.
왜냐하면 파일이 중첩되었을 때 읽기도 힘들고, "파일에서 뭐가 문제인지"를 정확한 범위만 정확히 알려주는 방식을 선호하기 때문이에요.
(실제로 conventionalcommits를 참고해봐도, 스코프는 좀 더 범위에 대해 명확하게 말해주는 편이지요.)

따라서 스코프를 명시적으로 선택해서 가져갈 수 있도록 위와 같이 제 입맛대로 추가해주었답니다 🥰
마지막에 커스텀 스코프를 써준다는 것은 true로 체크한 부분을 놓치지 말아주세요!

husky로 Client hook 추가하기

Git hooks는 보통 클라이언트 훅과 서버 훅으로 나뉘죠.
저는 허스키를 통해, 커밋 단계에서 좀 더 빠르게 깃 훅을 사용하기로 했습니다.
husky를 잘 모르겠다면 가비아의 글을 읽어보시는 것도 좋을 것 같아요 😉

설치

우선 설치를 해주어야겠죠?

yarn add -D husky

설치 한 이후에는 저는 yarn-berry 이므로 husky 공식문서의 글을 따라갔습니다.

yarn husky install

npm publish 는 없을 예정이므로 pinst는 설치하지 않고, 허스키만 깃 훅을 쓸 수 있도록 하죠!

클라이언트 훅 생성

그러면 이제 .husky라는 폴더가 생성이 되었을 건데요.
여기에서 저는 다음과 같이 클라이언트 훅을 넣어주었어요.

commit-msg

커밋 메시지를 남기는 때에는 commitlint가 작동하도록 합니다.

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

echo "🚦 COMMIT-MSG | commitlint check..."

yarn commitlint --edit ${1}

prepare-commit-msg

커밋 메시지를 준비하는 때에는 cz가 작동하도록 합니다.
(여기서 czcommitizen의 약자입니다!)

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

echo "🚦 PREPARE-COMMIT-MSG | Start cz with cz-customizable..."
exec < /dev/tty && yarn cz --hook || true

이러면 커밋할 때마다 여러 메시지가 뜰 거에요.
양식에 맞게 작성해주시면 됩니다!

중간 정리 - 커밋 작성과 훅

후! 이제 끝났군요. 생각보다 글이 길어지네요 😭
다시금 우리가 잘 하고 있는지를 살펴보기 위해 중간 정리를 해볼까요?

  1. commitizencommitlint는 컨벤셔널하게 커밋 메시지를 작성하는 데 도움을 준다. 이는 Semantic-release에서 자동화할 때 큰 영향을 준다.
  2. 이를 일일이 다시 작동하는 건 번거롭다. 따라서 husky를 통해 각 단계에서 작동을 깃 훅을 사용하여 자동화시켜준다.

Semantic-release 사용하기

다음을 설치해주어야겠어요!


yarn add -D @semantic-release/changelog
yarn add -D @semantic-release/commit-analyzer
yarn add -D @semantic-release/git
yarn add -D @semantic-release/release-notes-generator
yarn add -D conventional-changelog-conventionalcommits

🤯 엇! 왜 @semantic-release는 설치하지 않나요?

저는 포트폴리오에서 eslint(v7.32.0~v8.28.0)를 같이 쓰고 있었는데요. eslint에서의 의존성 패키지인 debug에서 의존하는 ms 패키지와 @semantic-releasedebug에 있는 ms 패키지가 호환이 충돌되더라구요. 😖
따라서 어차피 Github Action에서 설치해주면 그만이기 때문에 위의 네 패키지만 설치했어요. 위 패키지를 설치하지 않고 나중에 워크플로우 때 설치해도 무방하겠네요!

이후에는 다음과 같이 작성해줍니다.

module.exports = {
  "branches": [
    "main"
  ],
  "plugins": [
    [
      "@semantic-release/commit-analyzer", 
      {
        "preset": "conventionalcommits",
        "releaseRules": [
          /**
           * @inner
           * NOTE: Commits with scope no-release will not be associated with a release type 
           * even if they have a breaking change or the type 'feat', 'fix' or 'perf'
           * 
           * @see
           * https://github.com/semantic-release/commit-analyzer#releaserules
           */
          {"scope": "no-release", "release": false},
          {"scope": "breaking", "release": "major"},

          {"type": "docs", "scope": "README", "release": "patch"},
          
          {"type": "feat", "release": "minor"},
          {"type": "fix", "release": "patch"},
          
          {"type": "refactor", "scope": "core-*", "release": "minor"},
          {"type": "refactor", "release": "patch"},

          {"type": "style", "release": "patch"},
          {"type": "perf", "release": "patch"},
          {"type": "revert", "release": "patch"},
          
          {"type": "move", "release": false},
          {"type": "remove", "release": false},
          {"type": "chore", "release": false},
          {"type": "ci", "release": false},
          {"type": "test", "release": false},
        ],
        "parserOpts": {
          "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"]
        }
      }
    ],
    [
      '@semantic-release/release-notes-generator',
      {
        preset: 'conventionalcommits',
        presetConfig: {
          types: [
            /**
             * @inner
             * 아래 변화들은 보이도록 한다. 
             * (presetConfig는 conventional-changelog 방식을 따른다.)
             * 
             * @see
             * https://github.com/semantic-release/release-notes-generator
             * https://github.com/conventional-changelog/conventional-changelog-config-spec/blob/master/versions/2.0.0/README.md
             */
            { type: 'feat', section: '✨ Features', hidden: false },
            { type: 'fix', section: '🐛 Bug Fixes', hidden: false },
            { type: 'perf', section: '🌈 Performance', hidden: false },
            { type: 'refactor', section: '♻️ Refactor', hidden: false },
            { type: 'docs', section: '📝 Docs', hidden: false },
            { type: 'style', section: '💄 Styles', hidden: false },
            { type: 'revert', section: '🕐 Reverts', hidden: false },
            { type: 'ci', section: '💫 CI/CD', hidden: false },
            
            /**
             * @inner
             * 아래 변화들은 보이지 않게 한다.
             */
            { type: 'test', section: '✅ Tests', hidden: true },
            { type: 'chore', section: '📦 Chores', hidden: true },
            { type: 'move', section: '🚚 Move Files', hidden: true },
            { type: 'remove', section: '🔥 Remove Files', hidden: true },        
          ],
        },
      },
    ],
    [
      "@semantic-release/changelog",
      {
        "changelogFile": "CHANGELOG.md",
        "changelogTitle": "# 🚦 CHANGELOG | 변경 사항을 기록해요."
      }
    ],
    [
      "@semantic-release/git",
      {
        "assets": ["CHANGELOG.md"]
      }
    ]
  ]
}

생각보다 길죠? 좀 부연설명이 필요할 것 같아요.

Semantic-release에서의 단계

Semantic-release 역시 뚝딱하고 없던 것을 창조해내는 게 아니에요.
일련의 과정을 거쳐 배포 시의 chores를 자동화해주는 건데요!

이 각 단계에 맞춰서 세팅해주시면 됩니다.
실제로 플러그인들은 독립되어 있지 않고, 이 단계에 맞춰 의존되어 있는 경우가 있기 때문에 플러그인을 넣는 순서라던지, 사용 유무에 있어 주의해주셔야 해요.

예컨대, @semantic-release/changelog@semantic-release/release-notes-generator에 완전히 의존하고 있어요. changelog 플러그인 이전에 release-notes-generator을 플러그인을 설정하지 않는다면 완전히 CHANGELOG.md에 대한 작업을 하지 않습니다.
(이것 때문에 release note만 생성하는 줄 알고 설치 안 했다가 3시간을 날렸네요. 😭)

만약 릴리즈 노트를 만들고 싶다면?
PublishNotify 단계에서 처리하는 친구를 찾으면 되겠죠?
제 경험상으로는 @semantic-release/github이 이를 담당했던 것 같아요. 참고해주세요!

각 플러그인에 대한 설정 설명

사실 이건 제가 이야기하는 것보다는 공식문서가 훨~씬 잘 되어 있어요.
제 글은 완전한 정답이 아닙니다. 결국 제 입맛에 맞게 쓸 뿐이니까요.
공식문서를 찬찬히 읽어보시고, 자신에게 더 나은 방식이 있다면 적용하는 것을 추천드려요!

GitHub Actions로 push할 때 자동화하기

제 경우에는 다음과 같이 설정해주었습니다!
GitHub Action의 워크플로우만 아신다면 너무 간단해서, 설명을 생략해도 좋을
것 같아요 🥰

생각해보니, 이 역시 yarn-berry로 설정했다는 것에 유의해주세요!
클래식 버전에서는 돌아가지 않을 것 같군요. install 하는 부분들을 클래식 버전에 맞게 고쳐주셔야 할 거에요!

# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Semantic-release

on:
  push:
    branches: ['main']

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [16.x]

    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v3

      - name: Install dependencies
        run: yarn

      - name: Semantic-release
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
        run: yarn dlx semantic-release

🔥 주의 사항

이렇게 모든 설정이 끝났군요.
메인 브랜치에 푸시해보세요. 잘 되시나요?! 그렇다면 성공하신 거에요. 😉

제 설정대로 하면 기댓값은

  1. 태그 버저닝은 커밋의 타입과 스코프의 타입에 따라 commit-analyzer에 맞춰 변경되어야 한다.
  2. CHANGELOG.mdrelease-notes-generator 설정값에 맞추어 생성되어야 한다.
  3. 릴리즈 노트는 생성되지 않아야 한다.(@semantic-release/github 사용을 하지 않았으므로)

이어야 합니다.

다만 안 되는 경우가 있어요.
대표적인 걸로 임의로 tag를 바꾸거나, push --force를 빈번히 하는 경우입니다.
특정 부분에서 안 되신다면, 다음 트러블 슈팅 문서를 참조하시기를 권합니다 😉

🎉 마치며

잠깐 쓰고 넘어가려 했는데, 어쩌다 보니 글 쓰는 시간이 꽤 걸렸어요!
어제 내내 이것을 설정하고 원하는 대로 적용하기까지.. 거의 하루를 버렸네요.
그렇지만 저 역시 제가 했던 작업들을 복습하는 차원에서 충분한 글이었던 것 같아요.

누군가는 저처럼 삽질하지 않고(...) 멋진 자동화를 하시길 바라며. 글을 마칠게요!


참고자료

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글