自社のDB操作系のPRについてはApproveを2つ以上という制約があり、最初ブランチプロテクションでやろうと思ったのだが、マージ先に対するルールしかかけないことでやりたいことをやるには多段マージが必要になり、それはあまりに手間なので、Actionsでやった。このActionをマージ条件に加えておけば、指定したパスのファイルの変更があった場合はApproveを定義した数以上もらうことを強制できる。
name: Require Two Approvals for Protected Paths
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, edited]
pull_request_review:
types: [submitted]
permissions:
contents: read
pull-requests: read
jobs:
require-two-approvals:
# Draft PRは対象外
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
env:
# 監視対象のパス(正規表現)。例: db/migrations配下
PROTECTED_PATTERN: ^db/migrations
# 必要な最小承認数
REQUIRED_APPROVALS: "2"
steps:
# (任意)レポジトリのチェックアウトが必要な場合だけ
- name: Checkout
uses: actions/checkout@v5
- name: Detect if protected paths changed
id: diff
uses: actions/github-script@v7
with:
# 正直これはgit diffでもいい
script: |
const pattern = new RegExp(process.env.PROTECTED_PATTERN || '');
const {owner, repo, number} = context.issue;
const per_page = 100;
let page = 1;
const changed = [];
while (true) {
const {data} = await github.rest.pulls.listFiles({
owner, repo, pull_number: number, per_page, page
});
if (!data.length) break;
changed.push(...data.map(f => f.filename));
if (data.length < per_page) break;
page++;
}
core.info(`Changed files:\n${changed.join('\n') || '(none)'}`);
const touched = changed.some(f => pattern.test(f));
core.setOutput('touched', String(touched));
- name: Count current approvals (latest state per user)
if: steps.diff.outputs.touched == 'true'
id: approvals
uses: actions/github-script@v7
with:
script: |
const {owner, repo, number} = context.issue;
const per_page = 100;
let page = 1;
const latest = new Map(); // userId -> latest review state
while (true) {
const {data} = await github.rest.pulls.listReviews({
owner, repo, pull_number: number, per_page, page
});
if (!data.length) break;
for (const r of data) {
// 各レビュアーの「最新状態」で上書き(APPROVED / CHANGES_REQUESTED / COMMENTED / DISMISSED)
latest.set(r.user.id, r.state);
}
if (data.length < per_page) break;
page++;
}
const approvals = [...latest.values()].filter(s => s === 'APPROVED').length;
core.notice(`Approvals for protected path changes: ${approvals}`);
core.setOutput('approvals', String(approvals));
- name: Enforce minimum approvals for protected paths
if: steps.diff.outputs.touched == 'true'
run: |
echo "Approvals: ${{ steps.approvals.outputs.approvals }} / required: ${REQUIRED_APPROVALS}"
if [ "${{ steps.approvals.outputs.approvals }}" -lt "${REQUIRED_APPROVALS}" ]; then
echo "❌ This PR changes protected paths and requires at least ${REQUIRED_APPROVALS} approvals."
exit 1
else
echo "✅ Enough approvals for protected path changes."
fi
書くまで知らなかったんだけど、
pull_request_review:
types: [submitted]
を使うと、レビューコメントにフックできるから便利だった。