GitHub の PR に緊急 Approve するワークフロー

GitHub の PR に緊急 Approve するワークフロー

想定シチュエーション #

  • GitHub の PR で承認が無いとマージできないルールになっています。
  • ホットフィックス等、緊急でマージが必要です。
  • しかし承認権限を持つコードオーナーが不在です。

実現する機能 #

  • GitHub Actions を使って実現します。
  • 承認権限を持つコードオーナーの代わりに承認を付けてくれます。
  • ワークフローを実行するとコードオーナーが事前に設定したアクセストークンを使って承認をつけます。
  • ワークフローを実行できるユーザは事前に設定している人に限られます(乱用防止)。
  • ワークフローを実行した際はその記録が残ります。

ワークフロー定義 #

.github/workflows/force-approve.yaml
name: Force Approve

on:
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: read
  issues: read

env:
  FORCE_APPROVERS: '["alice", "bob", "charlie"]'
  FORCE_APPROVE_COMMENT: "FORCE_APPROVE"

jobs:
  force-approve:
    runs-on: ubuntu-latest
    env:
      FORCE_APPROVER_TOKEN: ${{ secrets.FORCE_APPROVER_PAT }}

    steps:
      - name: Get repository metadata
        id: repository-metadata
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          result-encoding: string
          script: |
            const githubRepository = process.env.GITHUB_REPOSITORY;
            const [owner, repo] = githubRepository.split("/");
            core.info(`Repository: ${githubRepository}`);
            return JSON.stringify({ owner, repo });

      - name: List open pull requests
        id: open-pull-requests
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          result-encoding: string
          script: |
            const repoMeta = JSON.parse('${{ steps.repository-metadata.outputs.result }}');

            const openPrs = await github.paginate(
              github.rest.pulls.list,
              {
                owner: repoMeta.owner,
                repo: repoMeta.repo,
                state: "open",
                per_page: 100,
              }
            );
            const openPrNumbers = openPrs.map(pr => pr.number);
            core.info(`Found ${openPrs.length} PR(s): ${JSON.stringify(openPrNumbers)}`);

            return JSON.stringify(openPrNumbers);

      - name: Find force approve pull requests
        id: force-approve-pull-requests
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          result-encoding: string
          script: |
            const repoMeta = JSON.parse('${{ steps.repository-metadata.outputs.result }}');
            const openPrNumbers = JSON.parse('${{ steps.open-pull-requests.outputs.result }}');
            const forceApprovers = JSON.parse(process.env.FORCE_APPROVERS);
            const forceApproveComment = process.env.FORCE_APPROVE_COMMENT;

            core.info(`Force approvers: ${forceApprovers}`);
            core.info(`Force approve comment: "${forceApproveComment}"`);

            const forceApprovePrs = [];

            for (const openPrNumber of openPrNumbers) {
              core.info(`Checking comments for PR #${openPrNumber}...`);

              const comments = await github.paginate(
                github.rest.issues.listComments,
                {
                  owner: repoMeta.owner,
                  repo: repoMeta.repo,
                  issue_number: openPrNumber,
                  per_page: 100,
                }
              );

              const isForceApproved = comments.some((c) => {
                const user = c.user?.login;
                const body = c.body?.trim();
                const isValidUser = forceApprovers.includes(user)
                const isValidComment = body === forceApproveComment;
                return isValidUser && isValidComment;
              });

              if (isForceApproved) {
                core.info(`PR #${openPrNumber} is marked with FORCE_APPROVE.`);
                forceApprovePrs.push(openPrNumber);
              }
            }

            core.info(`Found ${forceApprovePrs.length} PR(s): ${JSON.stringify(forceApprovePrs)}`);

            return JSON.stringify(forceApprovePrs);

      - name: Approve pull requests
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const repoMeta = JSON.parse('${{ steps.repository-metadata.outputs.result }}');
            const forceApprovePrs = JSON.parse('${{ steps.force-approve-pull-requests.outputs.result }}');
            const forceApproverToken = process.env.FORCE_APPROVER_TOKEN;

            const baseUrl = "https://api.github.com";
            const headers = {
              "Authorization": `Bearer ${forceApproverToken}`,
              "Accept": "application/vnd.github+json",
              "X-GitHub-Api-Version": "2022-11-28",
              "Content-Type": "application/json",
            };
            const message = "FORCE_APPROVED";

            for (const forceApprovePr of forceApprovePrs) {
              core.info(`Approving PR #${forceApprovePr}`);

              const url = `${baseUrl}/repos/${repoMeta.owner}/${repoMeta.repo}/pulls/${forceApprovePr}/reviews`;
              const response = await fetch(url, {
                method: "POST",
                headers: headers,
                body: JSON.stringify({
                  event: "APPROVE",
                  body: message,
                }),
              });

              if (response.ok) {
                core.info(`Approved PR #${forceApprovePr}.`);
              } else {
                const text = await response.text();
                core.error(`Failed to approve PR #${forceApprovePr}: ${response.status} ${response.statusText} - ${text}`);
              }
            }

            core.info("Force approve workflow finished.");

準備 #

リポジトリに上記のワークフロー定義を追加します。

その後ワークフロー定義内の env 部分を調整します。

env:
  FORCE_APPROVERS: '["alice", "bob", "charlie"]'
  FORCE_APPROVE_COMMENT: "FORCE_APPROVE"
  • FORCE_APPROVERS には GitHub のユーザ名を設定します。ここに設定したユーザがワークフローを実行したときに強制 Approve の処理が行われます。設定していないユーザが実行した場合は Approve されません。
  • FORCE_APPROVE_COMMENT は強制 Approve 実行する PR を指定するための文字列です。こだわりがなければ “FORCE_APPROVE” のままでも問題ありません。

そして、GitHub 設定ページから以下のキーでシークレットを設定します。

FORCE_APPROVER_PAT

ここには承認権限を持つユーザのプライベートアクセストークンを設定します。

ワークフローの処理では、ここに設定された PAT を用いて PR に Approve をつけます。

使い方 #

  1. FORCE_APPROVERS で指定したユーザが緊急 Approve をつけたい PR に FORCE_APPROVE_COMMENT で指定した文字列をコメントします。
  2. Force Approve のワークフローを動かします。
  3. (Force Approve の処理によって、1でコメントをつけた PR に FORCE_APPROVER_PAT を用いて Approve をつけます。)