O hirunewani blog

Github Actionsを利用してReviewer全員のApprovedを必須にする

Created at

GithubでReviewer全員のApprovedを必須にする方法を紹介するが、おすすめしない。

指定したレビュワー全員に見てほしい場合、常に見てほしい人数が固定であるならば、Githubのブランチプロテクションを利用すれば良い。 一方、PRによって見てほしい人数が異なる場合は、ブランチプロテクションでは制御出来ない。

「コメントで指摘した内容などが修正されていないにも関わらず他の人がマージしてしまう」という相談をされた際に考えた内容を元に、Github Actionsを利用してReviewer全員のApprovedを必須にする方法を紹介する。

GitHub Scriptを利用してレビューの状態を取得する

Github Actionsでは、actions/github-scriptを利用することでoctokitを利用してGithub APIを叩くことができる。 そこでまず手元でoctokitを利用してレビュー状態を取得するスクリプトを書いた。

const { data: pull_request } = await github.rest.pulls.get({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: context.payload.pull_request.number,
});

if (pull_request.requested_reviewers.length > 0) {
  throw new Error("There are reviewers who have not reviewed.");
}

const { data: reviews } = await github.rest.pulls.listReviews({
  owner: context.repo.owner,
  repo: context.repo.repo,
  pull_number: context.payload.pull_request.number,
});

console.log(reviews);

レビューをフィルタリングする

レビューを一覧で取得すると、そのまま使うには次の2つの問題があると分かる。

  • 自分自身のレビューも含まれている。
  • 同一人物による複数回のレビューが全て含まれている。

そこで、これをフィルタリングする必要がある。

自分自身の除外

次のようにしてPRを作成した人物を除外できる。

const reviewsWithoutAuthor = reviews.filter(review => {
  return review.user.login !== context.payload.pull_request.user.login;
});

ステータスの確認

レビューのステータスは、単純に最新の状態を取得するだけでは不十分である。次のことを考慮する必要がある。

  • CHANGE_REQUESTEDとAPPROVEDは現在の状態を上書きする。
  • COMMENTEDは、CHANGE_REQUESTEDとAPPROVEDを上書きできない。

次のようにしてステータスを確認できる。

/**
 * @type {{[reviewerName: string]: "APPROVED" | "CHANGES_REQUESTED" | "COMMENTED"}}
 */
const reviewStatus = reviewsWithoutAuthor.reduce((acc, review) => {
  if (review.state === "CHANGES_REQUESTED" || review.state === "APPROVED") {
    acc[review.user.login] = review.state;
  }
  if (review.state === "COMMENTED" && acc[review.user.login] === undefined) {
    acc[review.user.login] = review.state;
  }
  return acc;
}, {});

全員がApprovedしているかどうかは次のようにすればいい。

const allApproved = Object.values(reviewStatus).every(state => {
  return state === "APPROVED";
});

Resusable workflowとして作る

以上のスクリプトを利用して、レビューの状態を取得し、全員がAPPROVEDであるかを確認することができる。

全てをまとめてResusable workflowとして作成すると次のようになる。

name: Approved Checker
on: workflow_call

jobs:
  main:
    runs-on: ubuntu-latest
    steps:
      - name: Get pull request reviews
        uses: actions/github-script@v6
        with:
          script: |
             const { data: { requested_reviewers } } = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number
            });

            if(requested_reviewers.length > 0) {
              throw new Error('There are reviewers who have not reviewed.');
            }

            const { data: reviews } = await github.rest.pulls.listReviews({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number
            });

            const reviewsWithoutAuthor = reviews.filter((review) => {
              return review.user.login !== context.payload.pull_request.user.login;
            });

            if(reviewsWithoutAuthor.length === 0) {
              throw new Error('No pull request reviews have been submitted.');
            }

            const reviewStatus = reviewsWithoutAuthor.reduce((acc, review) => {
              if (review.state === "CHANGES_REQUESTED" || review.state === "APPROVED") {
                acc[review.user.login] = review.state;
              }
              if (review.state === "COMMENTED" && acc[review.user.login] === undefined) {
                acc[review.user.login] = review.state;
              }
              return acc;
            }, {});

            const allApproved = Object.values(reviewStatus).every(state => {
              return state === "APPROVED";
            });

            if (!allApproved) {
              throw new Error('Not all pull request reviews have been approved.');
            }

次のようにして利用すれば、PRを操作する度に走らせてレビューの状態を確認できる。

name: Approved Checker

on:
  pull_request:
    types:
      [opened, reopened, synchronize, review_requested, review_request_removed]
  pull_request_review:
    types: [submitted, edited, dismissed]

jobs:
  check_review_status:
    uses: org_name/workflow_repository_name/.github/workflows/approved-checker.yml@main

誰か1人にだけ見てほしい場合

例えば、ラベルなどを利用して次のように制御できる。

name: Approved Checker
on: workflow_call

jobs:
  main:
    runs-on: ubuntu-latest
    if: contains(github.event.pull_request.labels.*.name, 'or-review') == false
    steps:
      - name: Get pull request reviews
        uses: actions/github-script@v6

Reusable Workflowを利用する側ではラベルの変更をトリガーにする。

on:
  pull_request:
    types:
      [
        opened,
        reopened,
        synchronize,
        review_requested,
        review_request_removed,
        labeled,
        unlabeled,
      ]

もっと簡単な解決策

コメントを見てほしいならChanged Requestするべきだ。 Changed Requestがあれば、他の人がApprovedしていてもマージできない状態に簡単にできる。

Changed Requestが拒絶しているような印象を与え相手を傷つけないようにしているのかもしれないし、ただレビュワーの怠惰かもしれないが、だからといって別の手段に頼るのはあまりにも不毛な上、健全で開発チームであるとはとても思えない。