想定シチュエーション #
- 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 をつけます。
使い方 #
FORCE_APPROVERSで指定したユーザが緊急 Approve をつけたい PR にFORCE_APPROVE_COMMENTで指定した文字列をコメントします。- Force Approve のワークフローを動かします。
- (Force Approve の処理によって、1でコメントをつけた PR に
FORCE_APPROVER_PATを用いて Approve をつけます。)