O hirunewani blog

AIなどの特定ユーザーの作成したPRに要求するApproval数を増やす

Created at

Botなどが作成したPull Requestに対して、GitHub Actionsを利用して通常より多くのApproveを要求する方法

現時点でRulesetやBranch protection rulesでは、ユーザーやヘッドブランチのパターンに応じてルールを適用することはできない。

そのため、AIなどのBotアカウントによって作成されたPRに対して、通常よりも厳格なルールを適用したい場合は、GitHub Actionsを利用する必要がある。

例:DevinのPRに2個以上のApprovalsを要求する

PRが変更またはPRがレビューされる度に、現在のApproval数を確認し、指定された数に達していない場合は失敗したステータスを発行し、指定された数に達した場合は成功したステータスを発行することで実現できる。

この例ではDevinを対象にしているが、AI_USERNAMEを変更することで任意のユーザーを対象にすることができる。

name: Check Approvals for Devin PRs

on:
  pull_request_review:
    types: [submitted]
  pull_request:
    types: [opened, synchronize, reopened]

env:
  AI_USERNAME: devin-ai-integration[bot]
  REQUIRED_APPROVALS: 2

permissions:
  pull-requests: read
  statuses: write

jobs:
  check-ai-prs:
    runs-on: ubuntu-latest
    steps:
      - name: Check AI PRs
        if: github.event.pull_request.user.login == env.AI_USERNAME
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // 略:GitHub Scripts部分を参照
      - name: Skip Check (Not an AI PR)
        if: github.event.pull_request.user.login != env.AI_USERNAME
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const { owner, repo } = context.repo;

            await github.rest.repos.createCommitStatus({
              owner,
              repo,
              sha: context.payload.pull_request.head.sha,
              state: 'success',
              context: `AI Approval Requirement`,
              description: 'This PR was not created by the AI bot. Skipping approval check.',
              target_url: `https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id}}`
            });

GitHub Scripts部分:

/*レビューの取得  */
const { owner, repo } = context.repo;
// pull_request_reviewの場合でもpull_requestと同様に取得できる
const pr = context.payload.pull_request;
const requiredApprovals = ${{ env.REQUIRED_APPROVALS }};
const { data: reviews } = await github.rest.pulls.listReviews({
    owner,
    repo,
    pull_number: pr.number
});

/* 最終的なステータスがApprovedのユーザーを取得 */
const approvers = new Set();
const latestReviewStates = new Map();
for (const review of reviews) {
    // 後からコメントされたケースでもApprovedとして扱われるためコメントは無視 
    if (review.state === 'COMMENTED') {
        continue;
    }
    latestReviewStates.set(review.user.login, review.state);
}
for (const [user, state] of latestReviewStates.entries()) {
    if (state === 'APPROVED') {
        approvers.add(user);
    }
}
console.log(`Current unique approvers: ${approvers.size} (${Array.from(approvers).join(', ')})`);

/* 状況に合わせてステータスを発行 */
const isSuccess = approvers.size >= requiredApprovals;
await github.rest.repos.createCommitStatus({
    owner,
    repo,
    sha: pr.head.sha,
    state: isSuccess ? 'success' : 'failure',
    context: `AI Approval Requirement`,
    description: isSuccess 
    ? 'All required approvals have been received.'
    : `Waiting for ${requiredApprovals - approvers.size} more approval(s).`,
    target_url: `https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id}}`
});

ルールの設定とステータスを発行する理由

このワークフローによって発行されるステータス(例ではAI Approval Requirement)をRulesetやBranch protection rulesで必須にすることで、DevinのPRに対しては2個以上のApprovalがないとマージが出来ないようになる。

ワークフロー自体のステータスではないため注意。

どのトリガーに対しても常に同じステータスを発行してほしい場合は、実行したワークフローのステータスとは別にGitHub Status APIを利用してステータスを発行し、そのステータスを扱う必要がある。

なぜなら、GitHub Actionsで自動的に生成されるステータスがトリガー毎に生成される上、ルールで指定した場合すべてのトリガーでsuccessすることが要求されるようになってしまうためである。具体的に例ではCheck Approvals for Devin PRs (pull_request)Check Approvals for Devin PRs (pull_request_review)の2つを満たす必要が発生するが、pull_requestの方はレビューで発火しないためapprovalsが2個以上になっても失敗したままになってしまう。