Webhooks 기능을 활용한 협업 툴 연동(Github, Slack, Asana)

안광의·2023년 9월 11일
0

개발 지식 저장소

목록 보기
7/7
post-thumbnail
post-custom-banner

시작하며

최근 회사에서 사용하는 메신저와 프로젝트 관리 도구가 Slack과 Asana로 바뀌게 되었다. 이전에 사용했던 메신저에는 봇을 지원하지 않아서 알림을 보내주는 기능을 구현할 수 없었는데 Slack으로 바뀌게 되면서 반복되는 업무를 자동화 시켜보기로 했다.



Webhook(웹훅)이란?

Webhook이란 데이터가 변경되었을 때 실시간으로 알림을 받을 수 있는 기능으로 이벤트가 발행하면 HTTP POST 요청을 생성하여 callback URL(endpoint)로 이벤트 정보을 보내게 된다. Github, Asana 모두 Webhook 기능을 제공해주기 때문에 Slack Bot 기능을 사용해서 자동으로 메세지가 전송되도록 구현해보기로 했다.

  1. GitHub -> Slack
    • Pull Request에 reviewr 등록 시 Slack에 알림 메세지 전송
    • Pull Request에 리뷰 제출 시 Slack에 알림 메세지 전송
  2. Asana -> Slack
    • 작업에 특정 규칙으로 댓글 작성 시 Slack에 알림 메세지 전송

Cloudflare Workers

웹훅 기능을 이벤트 발생 시에 원하는 url로 관련된 데이터를 보내주기만 하기 때문에 결국 데이터를 받아서 Slack으로 메세지를 보내주는 서버가 필요하다. 이렇게 트래픽이 적을 때 적합한 서버리스 컴퓨팅 서비스인 Cloudflare Workers가 가장 좋은 선택지였다. AWS Lambda와 동일한 서비스이지만 Cloudflare Workers는 제한된 성능이긴 하지만 결제정보 입력도 없이 무료로 사용이 가능하다.

Cloudflare Wokrers : https://workers.cloudflare.com



Github -> Slack

사실 깃헙과 슬랙을 연동할 수 있는 공식 앱(https://slack.github.com)이 이미 만들어져 있었는데 사용해보니 불필요한 알림이 너무 많이 왔다. 찾아보니 알림 구독을 커스텀할 수 있는 기능을 제공해주지만 내가 원하는 딱 두가지 경우에만 알림을 받고 싶고 메세지 내용을 바꾸고 싶어서 봇을 생성해서 메세지가 보내지도록 구현하였다.

참고 https://github.com/integrations/slack/blob/master/README.md#customize-your-notifications

Slack Bot 생성

  1. Slack apps 접속 (https://api.slack.com/apps) 후 Create an App

  2. From scratch -> App Name 입력, 워크스페이스 선택 후 Create App

  1. Bots -> Review Scopes to Add -> Add an OAuth Scope -> chat:write 선택


  2. Install to Workspace -> 허용

  1. Bot User OAuth Token 값 복사

Cloudflare workers 생성

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');
  },
};

github Webhooks 등록

  1. 깃헙 레포지터리 > Settings > Webhooks > Add wehook

  2. Payload URL입력 -> Content type application/json 선택

  3. Let me select individual events. 선택 -> Pull request reviews, Pull requests 선택 -> Add Webhook



Asana -> Slack

아사나도 마찬가지로 슬랙과 연동할 수 있는 공식 앱(https://asana.com/ko/apps/slack)을 제공해주지만 내가 원하는 기능은 현재 사용하는 플랜에서는 사용할 수 없어서 마찬가지로 웹훅을 사용해서 직접 구현했다.

Asana 토큰 발급

  1. Asana My Apps(https://app.asana.com/0/my-apps) 접속
  2. + 새 토큰 생성 -> 토큰 이름 입력 -> 토큰 생성

  3. 토큰 값 복사

Cloudflare workers 생성

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 요청을 통해 필요한 데이터를 조회하는 과정이 필요했다.

Asana Webhook 등록

아사나는 웹훅을 등록할 수 있는 웹페이지를 제공해주지 않아서 api 문서를 보고 직접 http 요청을 보내야 해서 Postman(https://www.postman.com)을 사용해서 등록하였다.

Asana Webhook 관련 문서 : https://developers.asana.com/reference/createwebhook



마치며

협업 툴을 연동하면서 내가 하고 있는 업무 중에 반복되고 있거나 자동화 할 수 있는 일은 없는지 생각해보고 다른 툴(IDE의 익스텐션 등)들을 활용해서 코딩에 할애할 수 있는 시간을 늘릴 수 있는 방법을 찾아봐야겠다는 생각이 들었다. 이번에는 슬랙에 알림 메세지를 보내주는 정도만 구현했는데 아사나의 업무 프로세스나 규칙들이 확립이 되면 추가적으로 API를 사용해서 자동화 할 수 있을 것 같다. 협업 툴들의 개발자 문서를 살펴보면서 추후에는 API 문서나 Webhooks 기능을 제공할 정도의 규모있는 서비스를 개발해보고 싶다는 생각이 들었다.

참고문서

profile
개발자로 성장하기
post-custom-banner

0개의 댓글