【GitHubActions】PullRequestでレビューした結果をまとめてCSV出力できるようにする

## GitHub上でのPullRequestについて
GitHub上のPullRequestベースでドキュメントレビューやコードレビューを行うのが好きなんですが、何事にもメリット・デメリットがあります。

### メリット
* このファイルのここ！って指定してコメントできる
* 特定コメントに対してリプライしたり、Resolveでクローズもできる
* レビューをしてOKになるまでマージ不可！というブランチ保護ができる

### デメリット
* レビュー結果の残り方が証跡として確認しづらい
* レビュー結果を元にしたデータ分析ができない

要は、レビューというもの自体を管理するときにちょっと物足りないって感じです。
品質管理・改善をしよう！となったときに、レビュー内容を監視・管理したい、というのはあるあるかな、と思います。

## やったこと
GitHubActionsで、
プルリクエストおよびそのレビューコメントをCSV出力するPythonを走らせて、
出力したCSVを同じリポジトリにコミットしておく。

CSV出力しておけば、つぶしが効きます。
Excel化して渡したりとか、DBに流し込んだりとか。

GitHubActionsはすべてGitHubの中で完結するので、
今回のようなケースにはピッタリです！似たようなCIツールに比べて安いし。

## やり方
### 1. GitHub Actionsを書く
今回は、テスト的な意味合いもあったので、
気軽に試せるように手動トリガーにしてみました。

```run-CSVcreate-python.yml
name: run-CSVCreate-python

on: [ workflow_dispatch ]

jobs:
  build:
    runs-on: ubuntu-latest
    env:
     token: ${{secrets.GITHUB_TOKEN}}
     repo: ${{github.repository}}
    steps:
    - uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        python -m pip install flake8 pytest
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    - name: Run python
      run: python createCSV.py
    - name: CSV commit
      run: |
          git config --global user.email ${{secrets.EMAIL}}
          git config --global user.name ${{secrets.NAME}}
          git add .
          git commit -m "add CSV"
          git push origin main
```
### 2.Pythonを書く
[GitHubのREST API](https://docs.github.com/ja/rest)

```createCSV.py
import json
import requests
import os
import pandas as pd
import requests.packages.urllib3

requests.packages.urllib3.disable_warnings()
    
def main():
    os.environ['no_proxy'] = 'localhost'
    token = os.environ['token']
    repo = os.environ['repo']
    urldomain = 'https://api.github.com/repos/' + repo
    pr_df = pd.DataFrame(columns=["プルリクエストID","タイトル","内容","プルリクエストした人","更新日時","URL"]) #PRのカラム
    rv_df = pd.DataFrame(columns=["プルリクエストID","プルリクエストレビューID","レビューコメント","レビューした人","更新日時","URL"])#PRに対するレビューのカラム
    cm_df = pd.DataFrame(columns=["プルリクエストID","コメントID","対象","コメント","コメントした人","更新日時","URL","プルリクエストレビューID","親コメントID"])#PRに対するコメントのカラム
    
    pr_url = urldomain + '/pulls?access_token='+ token +'&state=all' #　全てのPRを取得する
    pr_response = requests.get(pr_url, verify=False)
    json_dict = json.loads(pr_response.text)
    
    for i,pr in enumerate(json_dict):
       cnt=str(i+1)
       pr_se = pd.Series([cnt,pr['title'],pr['body'],pr['user']['login'],pr['updated_at'],pr['html_url']], index=pr_df.columns)
       pr_df = pr_df.append(pr_se,ignore_index=True)
        
       rv_url = urldomain +'/pulls/' + cnt + '/reviews?access_token=' + token # PRのFileChanged->Reviewchangesで残したコメント
       rv_response = requests.get(rv_url, verify=False)
       rv_dict = json.loads(rv_response.text)
               
       for review in rv_dict:
          rv_se = pd.Series([cnt,review['id'],review['body'],review['user']['login'],review['submitted_at'],review['html_url']], index=rv_df.columns)
          rv_df = rv_df.append( rv_se, ignore_index=True)
       
    issuecm_url = urldomain +'/issues/comments?access_token=' + token # PRConversationで残したコメント 
    issuecm_response = requests.get(issuecm_url, verify=False)
    issuecm_dict = json.loads(issuecm_response.text)
               
    for issuecm in issuecm_dict:
        issuecm_se = pd.Series([cnt,issuecm['id'],"",issuecm['body'],issuecm['user']['login'],issuecm['updated_at'],issuecm['html_url'],"",""], index=cm_df.columns)
        cm_df = cm_df.append( issuecm_se, ignore_index=True)
          
    cm_url = urldomain + '/pulls/comments?access_token=' + token # PRのFileChangedでAddsinglecomment/Startareviewで残したコメント
    cm_response = requests.get(cm_url, verify=False)
    cm_dict = json.loads(cm_response.text)
       
    for cm in cm_dict:
        cm_parentid = cm.get('in_reply_to_id',None)
        cm_prid = cm.get('pull_request_review_id',None)
              
        cm_se = pd.Series([cnt,cm['id'],cm['path'],cm['body'],cm['user']['login'],cm['updated_at'],cm['html_url'],cm_prid,cm_parentid], index=cm_df.columns)
        cm_df = cm_df.append( cm_se, ignore_index=True)
              
    print(pr_df)
    print(rv_df)
    print(cm_df)
    
    pr_df.to_csv("output_pr.csv", index=False)
    rv_df.to_csv("output_rv.csv", index=False)
    cm_df.to_csv("output_cm.csv", index=False)

if __name__ == '__main__':
    main()
```

### 3.GitHub Actionsを動かす
Actionsタブ⇒手動実行ボタンを押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/666457/f9a9dabe-fa37-aadc-6f94-0d9c7682630b.png)

### 4.CSVが3つ出力されて、メインブランチにコミットされてる！
- プルリクエストの一覧CSV
- プルリクエスト全体へのコメントの一覧CSV
- プルリクエストの差分に対するコメントの一覧CSV

1~3を実行すると、上記CSVが出力されます。やった～:v:
動かしたリポジトリのすべてのプルリクエスト、コメントを取得してそれぞれ一覧にするようにしています。

CSVの分け方については、今回扱っているデータのリレーションが下記のようになるので、この後DBに流し込むとしたときに使いやすいデータモデルになるように正規化しました。

①プルリクエスト
②プルリクエスト全体へのコメント
③プルリクエストの差分に対するコメント
④プルリクエストの差分に対するコメントへのコメント

※④にさらにコメントしても、全て③にぶら下がるという扱われ方になるようです。

## ハマったポイント
### APIの使い分け
PullRequest周りで付けるコメントを全て取得するためには3種類のAPIを叩く必要があった。
見え方としてはぜんぶPRに対するコメントとして捉えることもできるのだが、②はissueのAPIになるようで、②だけは一覧で一気に取ってくるAPIが存在しないようだ。

GitHubAPI使うのが初めてでよく分かっていなかった…。

①PRのFileChanged->Reviewchangesで残したコメント
https://docs.github.com/ja/rest/reference/pulls#list-reviews-for-a-pull-request

②PRのConversationで残したコメント
https://docs.github.com/ja/rest/reference/issues#get-an-issue-comment

③PRのFileChangedでAdd single comment/Start a reviewで残したコメント
https://docs.github.com/ja/rest/reference/pulls#list-review-comments-in-a-repository

## 今後
CSVがあれば処理の最初に読み込んで、差分更新とかやってあげた方がいいかな？かえって処理時間延びちゃうか？

### 活用面
AWS RDSでDB立てて、SuperSetとかで可視化するとかしてあげると、
DB触れる人もそうでない人にもやさしくなりそうです。

DBに流し込むのもGitHub Actionsでやってあげると管理もラクそう。

SuperSetについて分かりやすかったブログ
https://blog.engineer.adways.net/entry/advent_calendar_2017/10