[ python + github Action ] PR 리스트 자동화 하기 (with. pygithub)
🌈 프로그래밍/Python

[ python + github Action ] PR 리스트 자동화 하기 (with. pygithub)

반응형

 

코드 리뷰 문화를 제대로 정착하기 위해서 팀 레포에 대한 PR 목록들을 자동화하여 리스트업 할 필요가 있었다.

그러기 위해서 좀 리서치를 해본 결과 아래의 좋은 레퍼런스가 있었다.

 

공통시스템개발팀 코드 리뷰 문화 개선 이야기 | 우아한형제들 기술블로그

{{item.name}} 안녕하세요. 공통시스템개발팀 배대준입니다. Merge Request(Pull Request)를 생성했는데 리뷰어는 묵묵부답이고 직접 요청하자니 업무를 방해하는 건 아닌가 걱정하신 적이 있으신가요? 작

techblog.woowahan.com

 

코드 리뷰 in 뱅크샐러드 개발 문화 | 뱅크샐러드

안녕하세요, 뱅크샐러드 BanksaladX iOS Engineer…

blog.banksalad.com

 

 

실현 가능성이 있는 리뷰를 위해 내가 생각하는 자세는 다음과 같았다.

Author 

  • 리뷰 작성자는 최대한 자세하지만 간단명료하게 PR을 작성하도록 한다.
  • 모르는 내용과 질문이 있다면 PR 작성 시 포함하도록 한다.
  • 리뷰어가 자세하게 봐주었으면 하는 내용을 강조하도록 한다.
  • PR 등록 후 "D-N" 규칙에 맞게 태그를 설정한다.
    • Why?
      • 각 리뷰 우선순위를 가늠하기 위해, 리뷰어의 일정 내에서 우선순위를 결정하고 리뷰에 치중되지 않도록 환경을 조성하기 위함.
      • 각 PR 작성자는 스스로 판단하여 태그를 설정하도록 한다.
    • "D-N" 규칙?
      • PR에는 "Label"이라는 것을 추가할 수 있다.
      • 그래서 각 PR을 들여다보기 전에 어떤 부분을 수정했는지, 긴급하게 고쳐야 하는 부분을 수정했는지에 대한 정보이다.

PR에 달릴 D-N 라벨 예시

Reviewer

  • 리뷰어는 PR 작성자가 강조한 내용을 중점적으로 확인한다.
  • 문제가 발생할만한 부분에 대해서 코멘트를 남긴다.
  • 고칠 수 있는 부분 혹은 의견이 있다면 추가적인 코멘트를 남긴다.
  • 코멘트의 규칙은 "PN 규칙"에 맞게 표기한다.
    • Why?
      • 커뮤니케이션 비용을 줄이기 위함이다. 만약 나의 PR에 코멘트가 달렸다. 예를 들어 "~~ 부분은 ~~ 했으면 좋겠어요." 혹은 "~~ 것도 좋아 보이네요"라고만 달린다면 이 부분을 정말로 바꿔야 하는지? 바꾸지 않아야 하는 건지? 애매한 부분이 존재하기 때문이다.
      • 따라서 아래와 같이 코멘트 앞에 "PN"을 붙여서 코멘트를 남기면 좀 더 명확해지는 부분이 있을 것 같다고 생각했다.
P1: 꼭 반영해 주세요 (Request changes)
P2: 적극적으로 고려해 주세요 (Request changes)
P3: 웬만하면 반영해 주세요 (Comment)
P4: 반영해도 좋고 넘어가도 좋습니다 (Approve)
P5: 그냥 사소한 의견입니다 (Approve)

 

pygithub 모듈 사용하기

위에서는 간략하게 코드 리뷰를 위한 PR 작성자와 리뷰어가 해야 하는 실질적인 행동에 대해서 정리해보았고,

이제는 태그를 직접 관리하기가 애매하고, 매번 github 페이지에 들어가서 어떤 리뷰가 급한지 확인해야 하는 번거로움을 조금이나마 덜기 위해서 자동화를 생각하였다.

찾아보니 pygithub라는 python 모듈이 있었다.

이 모듈은 github API를 사용하는 모듈이며, 기본적으로 github Access Token이 필요하다.

 

Creating a personal access token - GitHub Docs

Warning: Treat your access tokens like passwords. To access GitHub from the command line, consider using GitHub CLI or Git Credential Manager instead of creating a personal access token. When using a personal access token in a script, consider storing your

docs.github.com

생성하기 위해서 위의 링크에 들어가면서 Step by Step으로 진행하면 쉽게 만들 수 있다.

주의사항으로는 토큰의 만료기간은 최대 1년까지 생성 가능하며, 토큰의 권한을 줄 때 모든 권한을 주면 위험하다.

그래서 나는 특정 private 레포지토리에만 접근할 예정이기 때문에 권한을 아래와 같이 설정해 주었다.

특정 repo에만 접근할 수 있도록 접근 권한 제한

또한 해당 레포지토리에 PR 목록을 확인하려면 아래의 추가 권한이 필요하다. 체크 해주자!

토큰 생성 전, 권한 주기

아래와 같이 권한을 설정해주었다.

권한 상세 설정

그런 뒤, 토큰을 발급받고 이제 사용하러 가보자!

사용하기 전에 내가 기획한 내용은 아래와 같다.

- 매일(평일) 오전에 특정 슬랙 채널에 PR 목록 알림 예정

- 리뷰가 진행 중인(review comment로 구분) PR은 목록에서 제외하도록 한다.

- PR 링크를 클릭하면 바로 해당 PR로 이동한다.
  - 만약 PR에 D-N 태그가 달려있지 않았다면 기본값으로 D-5 태그로 설정한다.
    - 태그가 달려있다면 하루를 차감하도록 한다.
      - (ex) D-5 → D-4
  - 만약 D-0 태그라면 따로 수정하지 않는다.

- Github Action을 통해서 실행하도록 구성 예정

 

새로운 폴더를 하나 만들고, 가상 환경을 구성해준 뒤 아래의 명령어로 pygithub를 설치한다.

$ pip install pygithub

설치한 뒤, 아래와 같이 py 파일을 하나 만들어주고 내용을 작성해주자.

# main.py

from github import Github

# First create a Github instance:

# using an access token
g = Github("access_token")

# Github Enterprise with custom hostname
g = Github(base_url="https://{hostname}/api/v3", login_or_token="access_token")

# Then play with your Github objects:
for repo in g.get_user().get_repos():
    print(repo.name)

설치가 정상적으로 완료되었고, github Access token 또한 잘 만들어주었다면 repo 이름이 정상적으로 잘 출력될 것이다.

 

자동화 로직을 구성하기 위해서 크게 5개의 함수로 구분하였다.

  1. _send_slack
  2. _set_label_decrease
  3. _make_pr_link_with_no
  4. _get_total_pull_requests
  5. set_pull_requests_tags

 

import requests
from github import Github

private_access_token = "access_token"
SLACK_TOKEN = "slack_token"

g = Github(private_access_token)
repo = g.get_repo("myorganization/privaterepo")

labels = repo.get_labels()


def _send_slack(msg: str):
    response = requests.post(
        "https://slack.com/api/chat.postMessage",
        headers={"Authorization": "Bearer " + SLACK_TOKEN},
        data={"channel": "#channel", "text": msg},
    )


def _set_label_decrease(pull, before_label) -> str:
    if before_label == "D-5":
        pull.set_labels("D-4")
        return "D-4"

    elif before_label == "D-4":
        pull.set_labels("D-3")
        return "D-3"

    elif before_label == "D-3":
        pull.set_labels("D-2")
        return "D-2"

    elif before_label == "D-2":
        pull.set_labels("D-1")
        return "D-1"
    else:
        pull.set_labels("D-0")
        return "D-0"


def _make_pr_link_with_no(pr_no: int) -> str:
    link = f"https://github.com/myorganization/privaterepo/pull/" + str(pr_no)
    return link


def _get_total_pull_requests():
    count = 0
    pull_requests_list = []
    # 현재 열려있는 PR 목록들을 가져온다.
    for pull in repo.get_pulls(
        state="open",
        sort="updated",
    ):
        pr_comments_count = pull.review_comments
        if pr_comments_count != 0:
            # 리뷰가 진행중인 PR인 경우는 목록에서 제외
            pass
        else:
            count += 1
            pull_requests_list.append(pull)
    return count, pull_requests_list


def set_pull_requests_tags():
    cnt, pulls = _get_total_pull_requests()
    pr_msg_to_slack = (
        f"<!here> 👋🏻 총 {cnt}개의 Pull Request가 리뷰를 기다리고 있어요! :revolving_hearts:\n"
    )

    for pull in pulls:

        pr_link = _make_pr_link_with_no(pull.number)

        if pull.labels == []:
            # D-5 라벨 설정
            pull.set_labels("D-5")
            pr_msg_to_slack += f"> <{pr_link}|[ D-5 ] " + pull.title + ">" + "\n"

        elif pull.get_labels()[0].name == "D-0":
            # D-0 라벨인 경우
            pr_msg_to_slack += f"> <{pr_link}|[ D-0 ] " + pull.title + ">" + "\n"

        else:
            # D-0 라벨이 아닌 라벨이 있다면, 하루를 줄인 태그를 설정한다.
            before_label = pull.get_labels()[0].name

            after_label = _set_label_decrease(pull, before_label)

            pr_msg_to_slack += (
                f"> <{pr_link}|[ " + after_label + " ] " + pull.title + ">" + "\n"
            )
    _send_slack(pr_msg_to_slack)


set_pull_requests_tags()

 

자세한 설명은 나중에 차차 적어봐야겠다. 수정사항도 생길 수 있으니..!

 

Github Action으로 실행하기

이제 마지막으로 작성한 python 파일을 주기적으로만 실행시킨다면 원하는 slack 채널에 PR 목록들을 쏴줄 수 있다!

name: privateRepo PR list notification

on:
  # 스케쥴러로 실행
  schedule:
    - cron: "0 1 * * 1-5"

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    - name: Set up Python 3.9
      uses: actions/setup-python@v3
      with:
        python-version: "3.9"
	
    # 의존성 설치
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    # python 파일 실행
    - name: run main.py
      run: |
        python main.py

crontab 표현식은 구글링 하면 쉽게 찾을 수 있으니 설명은 생략.

반응형