최근에 회사에 사람이 많아지다 보니, MR(Merge Request) 리뷰 하는 걸 자꾸 까먹게 된다...ㅎㅎ Merge Request(MR)이 쌓이다 보니 마지막쯤에 몰아서 하다 보니 병합 이슈가 생기기도 하고, 스트레스가 장난이 아니었다. 팀 프로젝트를 진행할 때, 병합되지 않은 MR을 추적하는 것은 매우 중요하다. 특히 여러 명이 다양한 브랜치에서 작업할 때는 MR이 누락되지 않도록 관리해야 한다. 이를 자동화하기 위해, GitLab에서 병합되지 않은 MR 목록을 매일 아침 9시에 Slack 채널로 보내주는 봇을 만들었다.
예전에 배민 기술블로그에서 코드리뷰에 관한 글을 읽은 적이 있는데, 이번 기회에 한 번 적용시켜 보면 어떨까 싶어서 만들어봤다.
개발 환경
- Slack
- Node.js
- GitLab
자동화 봇 Sequence
너무 간단해서 sequence diagram이라고 하기도 뭐하지만, 쨋든 이런식으로 구성해볼 계획이다.
1단계: Slack API 설정
먼저, Slack 앱을 설정해서 특정 채널에 메시지를 보낼 수 있게 만들어야 한다.
Slack 앱 생성
Slack API 대시보드로 이동해 새로운 앱을 생성한다.
봇의 이름으로 메세지를 보내기만 하면 되기 때문에 권한은 chat:write
만 있으면 된다.
Slack 봇 토큰 생성
OAuth & Permissions 탭에서 워크스페이스에 앱을 설치하고, 발급된 봇 토큰을 복사한다. 이 토큰은 Node.js 스크립트에서 Slack web api 클라이언트를 구성할 때 사용된다.
2단계: GitLab API 설정
GitLab 프로젝트 액세스 토큰 생성: GitLab 프로필에서 api 권한이 있는 개인 액세스 토큰을 생성한다. 이 토큰은 GitLab 프로젝트에서 MR 정보를 가져올 때 사용된다.
GitLab 프로젝트 ID
GitLab의 각 프로젝트는 고유한 ID를 가지고 있다. 이름이 아닌 ID로 API로 프로젝트에 접근하기 때문에 프로젝트 ID를 알아놔야 한다.
3단계: Node.js 스크립트 작성
이제 API 사용을 위한 준비되었으니, 실제 스크립트를 작성해 보자.
WebClient 초기화
아까 발급받은 슬랙 봇 토큰을 사용해서 클라이언트를 만들어준다.
const { WebClient } = require('@slack/web-api');
const client = new WebClient(process.env.SLACK_BOT_TOKEN);
MR 리스트 가져오기
GitLab API는 모두 Restful로 작성되어 있다. 개발 문서 페이지에 자세히 적혀있으니 참고하여 API를 사용하면 된다.
일단 우리가 필요한건 MR API이기 때문에 개발 문서를 먼저 살펴보자. 특정 프로젝트에 대한 MR을 가져와야 되기 때문에 밑으로 스크롤 쭉쭉 내려서 project id가 있는 항목을 보자.
모두 GET으로 이루어져있는데, 현재 진행 중인 마일스톤에 해당하는 MR만 가져오면 좋을 것 같다.
async function getMergeRequestList() {
try {
var url = `${process.env.GITLAB_API_URL}/projects/${process.env.PROJECT_ID}/merge_requests`
console.log(url)
const response = await axios.get(url, {
headers: {
'Content-Type': 'application/json',
'Private-Token': process.env.PROJECT_ACCESS_TOKEN,
},
params: {
state: 'opened',
order_by: 'created_at',
sort: 'desc'
}
});
return response.data
} catch (error) {
console.error('GITLAB ERROR: getMRList - ', error.response ? error.response.data : error.message);
}
}
base URL은 사용하는 Gitlab의 주소에 /api/v4 만 뒤에 붙여주면 된다.
http://{깃랩 baseURL}/api/v4
참고로 axios를 사용한 이유는 개인적으로 메서드 구성이라던가, 파라미터 구성이 가장 직관적이라고 생각해서 썼다.
GitLab API Response
GET을 때려보면 GitLab 콘솔에서 볼 수 있는 모든 정보를 다 준다.
여기서 나는 제목(title), MR 링크 (web_url), 작성자(author_name), 브랜치(source_branch, target_branch) 등 알림에 필요한 내용만 추출했다.
[
{
"id": 1,
"iid": 1,
"project_id": 3,
"title": "test1",
"description": "fixed login page css paddings",
"state": "merged",
"imported": false,
"imported_from": "none",
"merged_by": { // Deprecated and will be removed in API v5, use `merge_user` instead
"id": 87854,
"name": "Douwe Maan",
"username": "DouweM",
"state": "active",
"locked": false,
"avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
"web_url": "https://gitlab.com/DouweM"
},
"merge_user": {
"id": 87854,
"name": "Douwe Maan",
"username": "DouweM",
"state": "active",
"locked": false,
"avatar_url": "https://gitlab.example.com/uploads/-/system/user/avatar/87854/avatar.png",
"web_url": "https://gitlab.com/DouweM"
},
"merged_at": "2018-09-07T11:16:17.520Z",
"merge_after": "2018-09-07T11:16:00.000Z",
"prepared_at": "2018-09-04T11:16:17.520Z",
"closed_by": null,
"closed_at": null,
"created_at": "2017-04-29T08:46:00Z",
"updated_at": "2017-04-29T08:46:00Z",
"target_branch": "main",
"source_branch": "test1",
"upvotes": 0,
"downvotes": 0,
"author": {
"id": 1,
"name": "Administrator",
"username": "admin",
"state": "active",
"locked": false,
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
"assignee": {
"id": 1,
"name": "Administrator",
"username": "admin",
"state": "active",
"locked": false,
"avatar_url": null,
"web_url" : "https://gitlab.example.com/admin"
},
"assignees": [{
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"locked": false,
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/axel.block"
}],
"reviewers": [{
"id": 2,
"name": "Sam Bauch",
"username": "kenyatta_oconnell",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon",
"web_url": "http://gitlab.example.com//kenyatta_oconnell"
}],
"source_project_id": 2,
"target_project_id": 3,
"labels": [
"Community contribution",
"Manage"
],
"draft": false,
"work_in_progress": false,
"milestone": {
"id": 5,
"iid": 1,
"project_id": 3,
"title": "v2.0",
"description": "Assumenda aut placeat expedita exercitationem labore sunt enim earum.",
"state": "closed",
"created_at": "2015-02-02T19:49:26.013Z",
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": "2018-09-22",
"start_date": "2018-08-08",
"web_url": "https://gitlab.example.com/my-group/my-project/milestones/1"
},
"merge_when_pipeline_succeeds": true,
"merge_status": "can_be_merged",
"detailed_merge_status": "not_open",
"sha": "8888888888888888888888888888888888888888",
"merge_commit_sha": null,
"squash_commit_sha": null,
"user_notes_count": 1,
"discussion_locked": null,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
"web_url": "http://gitlab.example.com/my-group/my-project/merge_requests/1",
"reference": "!1",
"references": {
"short": "!1",
"relative": "!1",
"full": "my-group/my-project!1"
},
"time_stats": {
"time_estimate": 0,
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
},
"squash": false,
"squash_on_merge": false,
"task_completion_status":{
"count":0,
"completed_count":0
},
"has_conflicts": false,
"blocking_discussions_resolved": true,
"approvals_before_merge": 2
}
]
API 요청 시 state == open
인 것들만 요청했으니, 따로 필터링할 필요는 없다. 이제 이 정보를 가지고 슬랙으로 알림을 보내보자.
Slack Block
슬랙 메세지는 블럭이라는 개념으로 이루어져 있다. 블럭 안에 rich text, 버튼, 인풋 필드 등을 구성할 수 있다.
근데 나는 MR 별로 밑에 사진처럼 색이 있는 줄로 구분하고 싶었다. 블럭은 이 기능이 없어서 legacy이긴 하지만 attachment를 사용했다.
Block Kit Builder
블럭을 구성할 때 슬랙에서 Block kit builder (링크)에서 블럭을 어떻게 구성할지 미리 볼 수 있다. 나는 attachment를 사용할거기 때문에 왼쪽 위에서 Attachment Preview로 바꾸면 attachment 형태로 변경된다. 근데 legacy라 그런지 잘 안된다. 그냥 block 형태에서 보는 게 낫다.
메시지 구성하기
Attachment 구조는 이렇게 생겼다. attachment 리스트에 색을 지정해 주고, block에 형태에 맞게 구성해 주면 된다.
블럭은 Slack API 문서에서 어떻게 구성하는지 확인할 수 있다.
{
"attachments": [
{
"color": "#000000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "111"
}
}
]
},
{
"color": "#000000",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "22222"
}
}
]
}
]
}
그래서 좀 사용하기 쉽게 여러 개 블럭을 미리 만들어놓고 사용했다.
const markdown = (text) => {
return {
type: 'section',
text: {
type: 'mrkdwn',
text: text
}
}
}
const divider = () => {
return {
"type": "divider"
}
}
attachment 구성하기
const makeAttachment = mr => {
var mrTitle = mr.title
var mrId = mr.reference
var createdDay = timePassed(mr.created_at)
var updatedDay = timePassed(mr.updated_at)
var mrLink = mr.web_url
return {
"color": attachmentColor.green,
"blocks": [
markdown(`<${mrLink}|${mrTitle}>\n`),
markdown(`${mrId} (created *${createdDay}* & updated *${updatedDay}*)\n`),
markdown(`*${mr.source_branch}* -> *${mr.target_branch}*\n`),
markdown(`Created by @${mr.author.username}`),
]
}
}
attachment 만들어서 알림 보내기
async function notifyMRs(client, channel, caller) {
console.log('#### NOTIFY MERGE REQUESTS #####')
var mrList = await getMergeRequestList();
// console.log(mrList)
caller = caller === undefined ? '@here' : `<@${caller}>`
let blocks = [
markdown(`${caller} 머지 리퀘스트가 여러분의 관심을 기다리고 있어요:`),
]
let attachments = [];
mrList.map(x => {
attachments.push(makeAttachment(x))
})
sendMessage(client)(channel)(blocks, attachments)
}
메시지 보내기
메시지 보내는 건 간단하다. 아까 초기화시킨 client로 client.chat.postMessage
를 호출하면 된다.
async function sendMessageWithBlocks(client, channelId, text, blocks, attachments){
try {
await client.chat.postMessage({
channel: channelId,
text,
blocks,
attachments
});
console.log(`Message sent to ${channelId}`);
} catch (error) {
console.error('Error sending message:', error);
}
}
여기서 channelID는 슬랙 채널 정보에서 확인할 수 있다.
최종 모습
멘트랑 모양은 배민 기술 블로그 참고했습니다. 배민 감사합니다😁😁
태그 안 되는 문제
위 메시지를 보면 어떤 이름은 태그가 안되는데, 이건 간단하게 해결할 수 있다.
유저 프로필 클릭 -> User settings -> Account -> Change username -> Path
여기서 Path에 있는 url 마지막 이름을 슬랙에서 사용하는 ID랑 통일시켜 주면 된다.
그러면, API로 mr 정보를 가져와서 author.username에 path에 설정된 이름을 던져주는데, 슬랙에서 태그 할 때 @author.username을 보내면 태그가 된다.
4단계: 봇 자동화
이제 스크립트가 준비되었으니, 매일 아침 9시에 실행되도록 설정만 하면 된다. node-cron
을 사용해서 매일 평일 아침 9시마다 실행되도록 했다.
모듈 설치
npm install --save node-cron
스케줄 등록
node-cron
은 string 값으로 반복되는 스케줄을 등록할 수 있다. (npm 링크)
┌────────────── second (optional)
│ ┌──────────── minute
│ │ ┌────────── hour
│ │ │ ┌──────── day of month
│ │ │ │ ┌────── month
│ │ │ │ │ ┌──── day of week
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
위 그럼 매일 평일 9시는 0 9 * * 1-5
이다.
여기서 중간에 * 하나 더 넣으면 평일 매 시간 9분이 되니까 주의할 것!!
실수로 그렇게 올렸다가 한 시간마다 알림이 가서 매우 당황스러웠다..
cron.schedule('0 9 * * 1-5', async () => {
// 머시기 머시기
},
{
timezone: "Asia/Seoul"
})
이로써 매일 평일 아침 9시에 MR 목록이 자동으로 Slack에 알림으로 전송된다.
후기
이렇게 해서 GitLab의 병합되지 않은 MR을 매일 아침 Slack 채널로 알림을 보내주는 자동화 봇을 완성했다. 아직 적용한 지 얼마 안 돼서 얼마만큼의 효과가 있는지는 아직 잘 모르겠다. 그래도 이제 팀원들 모두가 매일 아침 MR 현황을 확인할 수 있게 되어, MR에 대한 경각심(?)을 주는 것만으로도 성공이라고 본다.
사람이 많아지면서 코드 리뷰의 중요성을 정말 뼈저리게 느끼고 있다. 매일 반복되는 작업을 자동화함으로써, 팀은 더 중요한 일에 집중할 수 있고 병합 충돌 같은 문제도 미리 예방할 수 있지 않을까 싶다.
지금은 좀 기능을 더 추가해서, slack slash command로 MR 코드 리뷰 요청하기, milestone 알리미 같은 것도 넣었다.
GitLab API와 Slack API를 활용해 자신의 팀에 맞는 자동화 툴을 만들어 보면 어떨까? 이 과정을 통해 느낀 건, 자동화는 생각보다 어렵지 않고, 한번 만들어 두면 효율성이 극대화된다는 것이다. 앞으로도 다양한 작업을 자동화해 더 나은 협업 환경을 만들어 나가고자 한다.
Reference
AI 업무 관리 및 생산성 도구
Slack은 팀과 커뮤니케이션할 수 있는 새로운 방법입니다. 이메일보다 빠르고, 더 조직적이며, 훨씬 안전합니다.
slack.com
Building with Block Kit
String the atoms together into molecules and inject them into messages and modals.
api.slack.com
Merge requests API | GitLab
Documentation for the REST API for merge requests in GitLab.
docs.gitlab.com
node-cron
A simple cron-like task scheduler for Node.js. Latest version: 3.0.3, last published: a year ago. Start using node-cron in your project by running `npm i node-cron`. There are 1305 other projects in the npm registry using node-cron.
www.npmjs.com
공통시스템개발팀 코드 리뷰 문화 개선 이야기 | 우아한형제들 기술블로그
안녕하세요. 공통시스템개발팀 배대준입니다. Merge Request(Pull Request)를 생성했는데 리뷰어는 묵묵부답이고 직접 요청하자니 업무를 방해하는 건 아닌가 걱정하신 적이 있으신가요? 작년에 저희
techblog.woowahan.com
'프로그래밍 > 개발 이야기' 카테고리의 다른 글
AWS S3 Presigned URL: 쉽고 빠르게 파일 업로드/다운로드하기 (0) | 2025.03.31 |
---|---|
[Notion API] 리소스 업로더 만들어보기 - 1 (API 연결, Database Query) (4) | 2024.01.30 |
[Google Analytics] 앱 분석해보기 (Unity) (2) | 2023.12.25 |
티스토리로 이전하며 (2) | 2023.12.06 |