저는 깃애니몰즈라는 사이드 프로젝트에 프런트엔드 개발자로 참여하고 있습니다. 개발 기반이 궤도에 오른 상태에서 중간에 합류했는데요, 프런트엔드 프로젝트의 일부 영역에서 GitHub Actions를 활용한 자동화 구축이 되어 있었습니다.
그러던 중 이번에 새롭게 시작하는 SEO 학습 프로젝트에 GitHub Actions로 자동화를 적용해보면 좋겠다는 생각이 들었습니다. 그래서 기존 코드와 공식문서를 참고하고 보다 더 깊게 학습하며 포스트를 작성해보겠습니다.
이번 포스트를 통해 이런 것들이 가능합니다.
- (PR 기능) Pull Request 추가시
개발 카테고리 Label자동으로 달기- feature, bugfix, refactor 등
- (PR 기능) Pull Request 추가시
개발 영역(area) Label자동으로 달기- area:ui, area:hooks 등
- (PR 기능) Pull Request 추가시
코드 변화량(diff) Label자동으로 달기- diff:xl, diff:sm 등
- (Issue 기능) Issue 생성시
이슈 카테고리 Label자동으로 달기- feature, bugfix 등
- (학습) labeler의 설정법
오늘 언급되는 코드는 template화하여 별도 repository로 구분했습니다. 내용 작성 중에 이 repository의 코드를 일부 인용할 예정입니다.
미리 알고 계시면 좋아요.
- 이번 포스팅은 Docker와 배포를 다루지 않습니다. Docker Image 생성이나 Deploy까지 고려하신다면 GitHub Action으로 CI/CD 구축하기 포스트를 참고하시기 바랍니다.
- CI 코드와 여러 키워드의 모든 설명은 하지 않을 예정입니다. 본 포스트의 목적은 label 자동화의 편의성 확보입니다. CI/CD 관련해서는 정확히 학습하여 기회가 된다면 별도의 포스팅으로 준비해보겠습니다.
label을 붙이는 작업은 여러 labeler 오픈소스가 도와줍니다.
GitHub workflows에는 공식 labeler가 있습니다. 스타는 2.2K정도 되죠. 하지만 저는 srvaoa/labeler를 사용할 겁니다.
이름으로 아실 수 있듯이 개인 개발자의 labeler 프로젝트입니다. star가 86에 불과하는 작은 프로젝트예요. 그럼에도 왜 이 labeler를 선택했는지 설명이 필요할 것 같습니다.
우선 공식 labeler를 살펴보시면 아시겠지만, 기능이 많지 않다고 느껴졌습니다. branch명이나 특정 파일 경로, 파일 확장자의 변경을 감지하는 게 전부죠. 하지만 srvaoa/labeler는 앞서 설명한 기능은 물론이고, 변경의 크기 비교, 제목, 본문, 작성자/작성자그룹 등 다양한 실행 조건들을 지원합니다.
공식 labeler는 TypeScript로 작성된 반면, srvaoa/labeler는 Go 언어로 작성되어 빠른 속도를 자랑합니다. 프로젝트 규모와 관계 없이 보통 7초 내외로 Job이 종료되는 퍼포먼스를 보여줬습니다.
공식 labeler의 기능이 부족하기만 한 것은 아닐 겁니다. 공식인데 이유가 있겠죠. 다만 공식 문서 페이지가 없고, README에는 제한적인 기능만 소개하고 있습니다. 그래서 제 상황에 필요한 기능이 한 눈에 봤을 때 부족했다고만 설명하겠습니다.
글을 보시는 분들은 상황에 맞춰 적절한 오픈소스를 선택하시면 됩니다. srvaoa/labeler를 사용하시겠다면 글을 이어서 봐주시면 되겠습니다.
본격적으로 CI 파이프라인을 구성해보겠습니다. 우선 Pull Request label 자동화부터 해보겠습니다. 프로젝트 root 위치에 .github/workflows/pr_labeler.yml를 추가하고 내용을 이렇게 구성합니다.
출처: git-animals-client/pr_labeler.yml
# .github/workflows/pr_labeler.yml
name: "Pull Request Labeler"
permissions:
contents: read
pull-requests: write
on:
pull_request:
branches:
- "**"
types:
- opened
- reopened
- synchronize
- ready_for_review
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@v1.13.0
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"간단히 설명하면
- 모든 branch의 pull request에 대하여
- opened, …, ready_for_review 이벤트 타입일 때,
opened: PR이 처음 생성되었을 때reopened: 닫혔던 PR이 다시 열렸을 때synchronize: PR branch에 새로운 커밋의 push로 업데이트될 때ready_for_review: Draft 상태의 PR이 리뷰 준비가 완료되어 Ready for review 상태로 변경될 때
- 이 job을 실행한다.
라고 생각해주시면 됩니다. 자세한 이벤트 type에 대한 정보는 GitHub Actions - activity types를 참고해주세요.
참고 코드는 srvaroa/labeler@master로 되어 있지만, master 버전은 추후 언제든 바뀔 수 있으니 안정성을 고려해 버전을 v1.13.0(현재 최신 버전)으로 고정해보았습니다.
파일에는 이런 코드가 있는데요, 어딘가에 GITHUB_TOKEN을 넣어주어야만 할 것 같이 생겼습니다.
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'하지만 걱정하지 마세요. GitHub이 자동으로 생성해줍니다. (출처: GitHub Docs - 자동 토큰 인증)
permissions는 GitHub Actions가 얼마나 권한을 가질 것인지를 명시적으로 지정합니다. GitHub Actions는 최소 권한 정책을 따르기에 이후에 소개될 작업들에 권한 문제가 발생할 수 있습니다. 명시적으로 지정하여 안전하게 사용하는 걸 권하기에 추가해주었습니다.
job-level에서도 지정할 수 있기 때문에 이렇게 세부 지정해줘도 됩니다.
jobs:
labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: srvaroa/labeler@v1.13.0
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"이번에는 Pull Request가 아니라, issue에도 자동으로 label을 달아볼 겁니다. .github/workflows/issue_labeler.yml 파일을 만들고 코드를 이렇게 구성해보겠습니다.
# .github/workflows/issue_labeler.yml
name: "Issue Labeler"
permissions:
contents: read
issues: write
on:
issue:
types:
- opened
- reopened
- edited
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@v1.13.0
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
위에서 대부분 설명을 했기에 가볍게 넘어가겠습니다.
왜 pr_labeler.yml과 issue_labeler.yml을 나눠야 하나요?
물론 이렇게 한 번에 실행할 수도 있습니다. (코드가 세로로 너무 길어지기에 activity type은 리스트 형식으로 묶어 가독성을 확보했습니다.)
# .github/workflows/pr_issue_labeler.yml
name: "Pull Request & Issue Labeler"
permissions:
contents: read
pull-requests: write
issues: write
on:
pull_request:
branches:
- "**"
types: [opened, reopened, synchronize, ready_for_review]
issue:
types: [opened, reopened, edited]
jobs:
labeler:
runs-on: ubuntu-latest
steps:
- uses: srvaroa/labeler@v1.13.0
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"하지만, 제가 파일을 나눈 이유는 이렇습니다.
pull request와issue라는 관심사에 따라 구분할 수 있다.- job의 세부 설정이 복잡하다면 구분하는 것이 가독성의 이점이 있다.
- job이 trigger되는 상황을 구분하여 labeler의 확인 조건을 나눠 성능 이점을 가질 수 있다.
그리고 그 밖에도 Pull Request 시에 이렇게 yaml name이 표시되어 용도가 명확하지 않았죠.
이건 기호에 따라 맞춰 선택해주시면 되겠습니다.
다음은 본격적으로 각 label들이 붙는 조건들을 설정해보겠습니다. 프로젝트 root 위치에 .github/labeler.yml를 만들고 코드를 이렇게 추가합니다.
코드가 너무 길어서 내용 이해를 위한 일부 코드만 남깁니다.
코드 전문은 labeler-template - labeler.yml을 참고해주세요.
# .github/labeler.yml
version: 1
issues: True
labels:
## Issue ========================================================
### Title -------------------------------------------------------
- label: "❓question"
type: "issue"
title: "^(Q.|Question).*"
- label: "❓question"
type: "issue"
title: ".*\\?$"
### Body --------------------------------------------------------
- label: "🐞bug"
type: "issue"
body: "- \\[x\\] 버그"
## Pull Request ===================================================
### File / Directory Based (PRs only) -----------------------------
- label: "📝docs"
files:
- ".*\\.md"
- label: "✅test"
files:
- ".*\\.test\\.(ts|tsx)$"
### Branch (PRs only) ---------------------------------------------
- label: "✨feature"
branch: "(feat|feature)/.*"
### Size (PRs only) -----------------------------------------------
- label: "📏diff:XS"
size:
below: 50
...
- label: "📏diff:XL"
size:
above: 500여기부터는 프로젝트 구조에 따라 달라집니다.
프로젝트마다 참조하는 파일의 구조가 다르거든요.
코드 설명해드릴테니 직접 구성해봅시다.
labeler 공식문서 - Conditions를 보며 설정에 필요한 요소들을 하나하나 쪼개서 해석해보겠습니다.
labels:
- label: "(label_name)"
(condition1)
(condition2)
...이렇게 구성되는데요, condition이란 label이 붙는 조건에 해당합니다. 이렇게 한 label에 쌓으면 and 조건, 그리고 같은 label을 여러 문 만들면 or 조건을 만들 수 있습니다.
한 번 label condition을 알아보겠습니다.
변경 파일의 경로로 label을 붙입니다.
- docs: labeler - files
- 보통은 디렉토리 레벨로 설정합니다.
- 일부 경우엔 파일 레벨로 설정합니다.
labels:
- label: "📝docs"
files:
- ".*\\.md"
- label: "✅test"
files:
- ".*\\/__tests__\\/.*"만약 monorepo를 사용하고 있다면 app과 package 레벨로 설정하여 어떤 범위가 변경되었는지 쉽게 파악할 수 있습니다.
- label: 'area:Web'
files:
- 'apps/web/*'변경 라인의 숫자로 label을 붙입니다.
- docs: labeler - size
- 변경 라인은 추가된 라인과 삭제된 라인의 합으로 계산됩니다.
above,below조건으로 범위를 설정할 수 있습니다.
- label: "📏diff:S"
size:
above: 50
below: 150브랜치 이름으로 label을 붙입니다.
- docs: labeler - branch
- 정규표현식을 지원합니다.
- label: "✨feature"
branch: "(feat|feature)/.*"label을 붙이는 action의 타입을 제한합니다.
- docs: labeler - type
- label: "🛠️bugfix"
type: "pull_request"
branch: "^fix/.*"타이틀 텍스트로 label을 붙입니다.
- docs: labeler - title
- 정규표현식을 지원합니다.
- label: "❓question"
type: "issue"
title: "^(Q.|Question).*"본문 텍스트로 label을 붙입니다.
- docs: labeler - body
- 정규표현식을 지원합니다.
- label: "🛠️enhancement"
type: "issue"
body: "- \\[x\\] 기능 개선"그 밖에도 쓰면 좋은 조건들이 있습니다.
negate: 조건을 반대로 해석합니다.appendOnly:true로 설정하면 조건이 나중에 바뀌어도 제거하지 않습니다.- ...
작성자와 팀 단위로 label을 붙이는 등 다양한 조건들을 활용해 다채롭게 고도화할 수 있습니다. 나머지는 labeler - conditions를 참고해주세요.
사실 이 포스트를 쓰는 게 예상보다 오래 걸렸습니다. 정규표현식을 사용할 수 있다고 하는데 계속 조건이 적용되지 않았거든요.
알고보니 Go 언어를 사용한 이 GitHub Action은 정규표현식을 사용할 때 탈출문자(escape character)를 single backslash(\)가 아니라 double backslash(\\)로 처리해야 했습니다. labeler yaml에서 backslash를 하나 소비하고, golang에서 backslash를 하나 더 소비하기 때문이라고 하네요. (출처: srvaroa/labeler - Some regex not working in filter via property)
- 공식문서: https://github.com/marketplace/actions/auto-assign-action
- 주요 기능
- PR 리뷰어(reviewer) 자동 설정
- PR 담당자(assignee) 자동 설정
.github/auto-assign.yml 파일에서 코드를 전개합니다.
공식 문서에서는 CI레벨의 job까지 소개해주는데, 보통은 CI까지 필요 없이 이 정도면 될 것 같네요.
(출처: Auto Assign Action > Single Reviewers List)
# .github/auto-assign.yml
addReviewers: true
addAssignees: false
reviewers:
- reviewerA
- reviewerB
- reviewerC
numberOfReviewers: 0더더욱 일반적으로는 PR을 올린 본인이 담당자가 되기 때문에 “작성자(author) 본인을 담당자로 해줘.”라는 설정도 있습니다. 이렇게요.
(출처: Auto Assign Action > Assign Author as Assignee)
# .github/auto-assign.yml
addAssignees: author분량을 고려해서 각 keyword와 고급 기능은 생략했는데요, 자세한 내용은 Auto Assign Action 공식문서를 참고해주세요.
job이 실행되면 labeler.yml 파일에 정의된 label은 자동으로 생성됩니다. 하지만 label은 색상이나 설명 등 부가 정보들이 있고, 이걸 명시적으로 만들고 싶을 수 있겠죠.
그런데 이게 하나하나 만들면 상당히 귀찮습니다. 그래서 shell script로 만들어보려 합니다.
환경은 MacOS + brew를 사용합니다.
# GitHub CLI 설치
brew install gh
# GitHub 로그인
gh auth login저는 Web UI를 사용해 로그인하는 게 편하기 때문에 아래와 같은 옵션을 선택해 로그인 했습니다.
> gh auth login
? Where do you use GitHub? GitHub.com
? What is your preferred protocol for Git operations on this host? HTTPS
? Authenticate Git with your GitHub credentials? Yes
? How would you like to authenticate GitHub CLI? Login with a web browser
! First copy your one-time code: 6E83-3946
Press Enter to open https://github.com/login/device in your browser...
✓ Authentication complete.
- gh config set -h github.com git_protocol https
✓ Configured git protocol
✓ Logged in as Orchemi저는 root 경로에서 /scripts 폴더를 하나 만들고, create-labels.sh라는 이름의 shell script 파일을 만들었습니다. 그리고 이렇게 코드를 구성했습니다.
#!/bin/bash
# GitHub repository 정보
REPO_OWNER="username"
REPO_NAME="repo-name"
# 라벨 정보를 배열로 정의
LABELS=(
# Common
"60A5FA / ✨feature / 기능 신규 추가"
"3B82F6 / 🛠️enhancement / 기존 기능 개선"
"6B7280 / ⚙️chore / 기타 작업"
# Issue
"DC2626 / 🐞bug / 오류"
"CA8A04 / ❓question / 질문"
# Pull Request
"F5F5F5 / 📝docs / 문서 변경"
"F87171 / 🛠️bugfix / 오류 수정"
"A78BFA / 🧹refactor / 코드 리팩토링"
"34D399 / ✅test / 테스트 코드 추가"
## Size
"F3F4F6 / 📏diff:XS / 50라인 이내 변경"
"E5E7EB / 📏diff:S / 50~150라인 변경"
"D1D5DB / 📏diff:M / 150~250라인 변경"
"9CA3AF / 📏diff:L / 250~500라인 변경"
"6B7280 / 📏diff:XL / 500라인 초과 변경"
)
# 각 라벨 생성
for LABEL_INFO in "${LABELS[@]}"; do
IFS=' / ' read -r LABEL_COLOR LABEL_NAME LABEL_DESCRIPTION <<< "$LABEL_INFO"
echo "Creating label: $LABEL_NAME with color $LABEL_COLOR"
gh api repos/$REPO_OWNER/$REPO_NAME/labels -f name="$LABEL_NAME" -f color="$LABEL_COLOR" -f description="$LABEL_DESCRIPTION"
done
echo "All labels have been created successfully!" 우선 이런 식으로 구성됩니다.
REPO_OWNER와REPO_NAME의 정보를 기반으로 하여 repository를 찾아갑니다.색상 / 이름 / 설명으로 구성된 label 요소들의 배열LABELS을 만듭니다.LABELS의 label 정보를 순회하며, 각각/로 쪼개어 변수로 지정합니다.- 위의 모든 정보를 조합해 github label 생성 api를 보냅니다.
구분점이나 순서 등 정보들은 원하시는대로 바꿔서 구성하시면 됩니다.
참고로 첫 줄의 주석 #!/bin/bash은 shebang(샤뱅)이라고 하는데, OS가 어떤 프로그램으로 이 shell script를 실행할지 알게 해주는 역할을 합니다. 꼭 넣어주세요.
# 실행권한 부여
chmod +x scripts/create-labels.sh
# 코드 실행
./scripts/create-labels.sh이렇게 shell script를 통해 쉽게 디테일한 label을 만들어 봤습니다.
(안 익숙하신 분들을 위해) 참고로 script를 실행하면 이런 게 나올텐데 이 상태를 나가는 단축키는 q입니다.
{
"id": ...,
"node_id": "...",
"url": "https://api.github.com/repos/Orchemi/huns-seo/labels/✨feature",
"name": "✨feature",
"color": "60A5FA",
"default": false,
"description": "기능 신규 추가"
}
(END)body condition을 언급하며, issue body에 포함된 문자열 확인을 통해 label을 붙이는 예시를 들었습니다. 하지만 issue의 경우 GitHub Issue Template을 활용하면 label의 지정도, default title, author, body 등도 사전에 설정할 수 있습니다. 한 마디로 issue는 굳이 labeler가 필요하지 않을 수 있다는 것이죠. Repository Issue 템플릿 구성 문서를 참고해주세요.
추가로 Issue Form을 사용하면 yaml 구조로 더 고도화된 Issue Template을 만들 수 있으니 참고하시면 좋습니다.
사실 최근 회사 GitLab CI/CD를 구축하고 모듈화하면서 CI 코드에 대해 많이 공부하게 되었는데요, 이번에 GitHub CI 코드를 학습하고 적용할 때 큰 도움이 되었습니다. 방식이 크게 다르진 않더라고요. 새로운 학습을 할 때 배경지식은 역시 중요한가 봅니다.
간단히 내용 정리만 하려 했는데 생각보다 R&D 시간이 길어졌습니다. 이런 이유 때문이었죠.
- Golang에서 정규표현식을 다루는 방식이 익숙치 않았어요.
- labeler가 제 예상보다 정말 많은 기능들을 지원했어요. 다 훑어보느라 좀 걸렸어요.
- 참고했던 코드에서도 오류가 조금 있었어요. 정규표현식에서요.
제 주말을... 많이 썼지만 많이 알아가고 내용도 잘 정리해서 뿌듯합니다.
모르는 걸 공부하면서 보기 쉽게 정리하는 과정이었고, 저는 최선을 다했습니다. 하지만 더 좋은 방법이 있거나, 어쩌면 제 설명에 오류가 있을 수도 있겠죠. 그런 점이 있다면 피드백은 언제든지, 얼마든지 환영이니 댓글이나 이메일로 알려주세요😁😁
재미있는 연구였습니다. 많은 도움이 되시길 바랄게요. 감사합니다!
