최근 회사에서 사용하는 메신저와 프로젝트 관리 도구가 Slack과 Asana로 바뀌게 되었다. 이전에 사용했던 메신저에는 봇을 지원하지 않아서 알림을 보내주는 기능을 구현할 수 없었는데 Slack으로 바뀌게 되면서 반복되는 업무를 자동화 시켜보기로 했다.
Webhook이란 데이터가 변경되었을 때 실시간으로 알림을 받을 수 있는 기능으로 이벤트가 발행하면 HTTP POST 요청을 생성하여 callback URL(endpoint)로 이벤트 정보을 보내게 된다. Github, Asana 모두 Webhook 기능을 제공해주기 때문에 Slack Bot 기능을 사용해서 자동으로 메세지가 전송되도록 구현해보기로 했다.
- GitHub -> Slack
- Pull Request에 reviewr 등록 시 Slack에 알림 메세지 전송
- Pull Request에 리뷰 제출 시 Slack에 알림 메세지 전송
- Asana -> Slack
- 작업에 특정 규칙으로 댓글 작성 시 Slack에 알림 메세지 전송
웹훅 기능을 이벤트 발생 시에 원하는 url로 관련된 데이터를 보내주기만 하기 때문에 결국 데이터를 받아서 Slack으로 메세지를 보내주는 서버가 필요하다. 이렇게 트래픽이 적을 때 적합한 서버리스 컴퓨팅 서비스인 Cloudflare Workers가 가장 좋은 선택지였다. AWS Lambda와 동일한 서비스이지만 Cloudflare Workers는 제한된 성능이긴 하지만 결제정보 입력도 없이 무료로 사용이 가능하다.
Cloudflare Wokrers : https://workers.cloudflare.com
사실 깃헙과 슬랙을 연동할 수 있는 공식 앱(https://slack.github.com)이 이미 만들어져 있었는데 사용해보니 불필요한 알림이 너무 많이 왔다. 찾아보니 알림 구독을 커스텀할 수 있는 기능을 제공해주지만 내가 원하는 딱 두가지 경우에만 알림을 받고 싶고 메세지 내용을 바꾸고 싶어서 봇을 생성해서 메세지가 보내지도록 구현하였다.
참고 https://github.com/integrations/slack/blob/master/README.md#customize-your-notifications
Slack apps 접속 (https://api.slack.com/apps) 후 Create an App
From scratch
-> App Name 입력, 워크스페이스 선택 후 Create App
Bots
-> Review Scopes to Add
-> Add an OAuth Scope
-> chat:write
선택
Install to Workspace
-> 허용
export default {
async fetch(request, env, ctx) {
const res = await request.json();
const channel = '슬랙 채널 ID';
const memberIds = {
'깃헙 이름' : '슬랙 유저 ID',
...
}
const headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer xoxb-XXXXXXXX' //슬랙 봇 토큰
};
const getMemberId = (name) => {
const id = memberIds?.[name] || ''
return id ? `<@${id}>` : name;
};
const sendMessage = (body) => {
return fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
body: JSON.stringify(body),
headers,
});
};
if(res.action === 'review_requested') {
//리뷰 요청
const reviewers = res?.pull_request.requested_reviewers?.map((user) => (getMemberId(user.login) + '님')).join(',') || '';
const sender = res?.sender?.login || '';
const url = res?.pull_request?.html_url || '';
const text = `${getMemberId(sender)}님이 ${getMemberId(reviewers)}에게 리뷰 요청을 보냈습니다.\n${url}`;
const requestBody = {
channel,
text,
};
const slackResponse = await sendMessage(requestBody)
return new Response(await slackResponse.json())
}
if(res.action === 'submitted'){
//리뷰 제출
const sender = res.sender?.login || '';
const recevier = res.pull_request?.user?.login || '';
const url = res?.pull_request?.html_url || '';
const reviewBody = res?.review?.body || '';
const text = `${getMemberId(sender)}님이 ${getMemberId(recevier)}님에게 리뷰를 제출했습니다.\n${url}\n\n코멘트: ${reviewBody}`;
const requestBody = {
channel,
text,
};
const slackResponse = await sendMessage(requestBody);
return new Response(await slackResponse.json());
}
return new Response('not sended slack message');
},
};
깃헙 레포지터리 > Settings > Webhooks > Add wehook
Payload URL입력 -> Content type application/json
선택
Let me select individual events.
선택 -> Pull request reviews
, Pull requests
선택 -> Add Webhook
아사나도 마찬가지로 슬랙과 연동할 수 있는 공식 앱(https://asana.com/ko/apps/slack)을 제공해주지만 내가 원하는 기능은 현재 사용하는 플랜에서는 사용할 수 없어서 마찬가지로 웹훅을 사용해서 직접 구현했다.
+ 새 토큰 생성
-> 토큰 이름 입력 -> 토큰 생성
export default {
async fetch(request, env, ctx) {
const developers = ['아사나 특정 유저', ...];
const users = {
"아사나 유저이름" : "슬랙 유저 ID",
...
}
const channelIds = {
"아사나 작업 사용자 지정 필드" : "슬랙 채널 ID",
...
};
const secret = request.headers.get('X-Hook-Secret');
const res = await request.json()
const commentId = res?.events?.[0]?.resource?.gid;
const headers = {
'Content-Type': 'application/json',
Authorization: 'Bearer xoxb-XXXXXX' //슬랙 봇 토큰
}
const sendMessage = (body) => {
return fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
body: JSON.stringify(body),
headers,
})}
if(commentId) {
const asanaHeaders = {
"Authorization" : "Bearer 아사나 토큰"
}
const commentResponse = await fetch(`https://app.asana.com/api/1.0/stories/${commentId}`, {
method: 'GET',
headers: asanaHeaders,
});
const commentData = await commentResponse.json();
const createdBy = commentData?.data?.created_by?.name?.replaceAll(' ', '');
if(!developers.includes(createdBy)) {
return new Response('', {
status: 200,
headers: {
"X-Hook-Secret": secret || undefined,
},
});
}
let comment = commentData?.data?.text;
if(!comment?.startsWith('!')) {
return new Response('', {
status: 200,
headers: {
"X-Hook-Secret": secret || undefined,
},
});
}
comment = comment.split('\n').slice(1).join('\n');
const taskId = commentData?.data?.target?.gid;
const taskResponse = await fetch(`https://app.asana.com/api/1.0/tasks/${taskId}`, {
method: 'GET',
headers: asanaHeaders,
});
const taskData = await taskResponse.json();
const taskName = taskData?.data?.name;
const taskLink = taskData?.data?.permalink_url;
const requesterName = taskData?.data?.custom_fields.find((value) => value.gid === '아사나 작업 사용자 지정 필드 Id')?.people_value?.[0].name?.replaceAll(' ', '') || '';
const hospitalName = taskData?.data?.custom_fields.find((value) => value.gid === '아사나 작업 사용자 지정 필드 Id')?.enum_value?.name;
const mentionTag = users?.[requesterName] ? `<@${users?.[requesterName]}> ` : '';
const channel = channelIds?.[hospitalName] || channelIds.default;
const text = `${mentionTag} ${comment}\n\n[아사나 링크]\n${taskName} : ${taskLink}`
const requestBody = {
channel,
text: text,
}
await sendMessage(requestBody)
}
return new Response('', {
status: 200,
headers: {
"X-Hook-Secret": secret || undefined,
},
});
}
};
깃헙 웹훅은 필요한 데이터를 전부 보내주기 때문에 받은 데이터만 가공해서 슬랙 메세지를 전송하면 되는데 아사나는 제한적인 데이터만 보내주어서 추가로 api 요청을 통해 필요한 데이터를 조회하는 과정이 필요했다.
아사나는 웹훅을 등록할 수 있는 웹페이지를 제공해주지 않아서 api 문서를 보고 직접 http 요청을 보내야 해서 Postman(https://www.postman.com)을 사용해서 등록하였다.
Asana Webhook 관련 문서 : https://developers.asana.com/reference/createwebhook
협업 툴을 연동하면서 내가 하고 있는 업무 중에 반복되고 있거나 자동화 할 수 있는 일은 없는지 생각해보고 다른 툴(IDE의 익스텐션 등)들을 활용해서 코딩에 할애할 수 있는 시간을 늘릴 수 있는 방법을 찾아봐야겠다는 생각이 들었다. 이번에는 슬랙에 알림 메세지를 보내주는 정도만 구현했는데 아사나의 업무 프로세스나 규칙들이 확립이 되면 추가적으로 API를 사용해서 자동화 할 수 있을 것 같다. 협업 툴들의 개발자 문서를 살펴보면서 추후에는 API 문서나 Webhooks 기능을 제공할 정도의 규모있는 서비스를 개발해보고 싶다는 생각이 들었다.
- Slack API : https://api.slack.com/docs
- Github Webhook : https://docs.github.com/ko/webhooks
- Asana Developers : https://developers.asana.com/docs/overview