Pull Requestで、
今回はドキュメントだけ修正してほしい
というつもりだったのに、実際のPRでは、
docs/**
README.md
.github/workflows/**
package.json
まで変更されていた、ということがあります。
人間が作るPRでも起こりますし、Dependabotやbot、coding agentが作るPRでも起こり得ます。
もちろんdiffをレビューすれば分かります。
ただ、コードレビューとは別に、
「このPRで変更してよい範囲そのもの」を機械的に制限したい
と思い、GitHub Actionを作りました。
PR Boundary というOSSです。
GitHub Marketplaceでも公開しています。
何をするActionなのか
例えばPRに、
scope:docs
というlabelを付けます。
repository側には、.github/pr-scope.ymlでdocs scopeの許可範囲を定義します。
version: 1
label_prefix: "scope:"
profiles:
docs:
allow:
- "README.md"
- "docs/**"
- "CONTRIBUTING.md"
- "SECURITY.md"
- "CHANGELOG.md"
protected:
- ".github/workflows/**"
- ".github/pr-scope.yml"
- "action.yml"
- "package.json"
defaults:
no_profile: review
この状態で、
README.md
docs/install.md
だけを変更したPRなら、
PASS
です。
一方、
README.md
.github/workflows/release.yml
まで変更すると、
BLOCKED
になります。
考え方としては、
trusted base policy
+
PRのscope
+
実際のchanged files
↓
PASS / REVIEW_REQUIRED / BLOCKED
というだけです。
LLMは使いません。API keyも不要です。
GitHub Actionとして使う
workflowは次のようになります。
name: PR Boundary
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
- labeled
- unlabeled
- edited
- ready_for_review
concurrency:
group: pr-boundary-${{ github.event.pull_request.number }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: read
statuses: write
jobs:
scope:
runs-on: ubuntu-latest
steps:
- uses: fp-fuyutsuki/pr-boundary@8f40124c658331bf4ec752462a19242ba0272661
with:
github-token: ${{ github.token }}
例ではActionをcommit SHAでpinしています。
PR Boundaryは判定結果を、
pr-boundary/scope
というcommit statusとして、評価対象のPR head SHAに付けます。
Rulesetと組み合わせてmerge gateにする
PR Boundaryが判定するだけでは、merge自体は禁止されません。
GitHub Rulesetsやbranch protectionで、
pr-boundary/scope
をrequired status checkにすると、merge gateとして使えます。
例えば、
scope labelなし
REVIEW_REQUIRED: NO_PROFILE
→ required statusを満たさない
scope:docs + docsだけ変更
PASS
→ merge可能
scope:docs + workflowも変更
BLOCKED
→ merge不可
という運用にできます。
PR Boundary自身のrepositoryでも、この構成でdogfoodしています。
PR自身にpolicyを書き換えさせない
実装上、特に重視したのがpolicyの取得元です。
もしPRが、
.github/pr-scope.yml
を書き換えて、
workflowも変更してよい
というpolicyを追加し、その新しいpolicyで自分自身を評価できると、scope gateとして意味がありません。
そこでPR Boundaryでは、評価対象PRのpolicyを使わず、
exact PR base SHA
↓
.github/pr-scope.yml
からpolicyを取得します。
つまり、
PR内でpolicyを緩和
↓
そのpolicyで同じPRを正当化
ということはできません。
このケースは実際のGitHub PRでもAcceptance testしました。
pull_request_targetなのでPR codeは実行しない
PR Boundaryではpull_request_targetを使っています。
このeventは便利ですが、PR側のコードをcheckoutして実行するようなworkflowにすると危険です。
そのためPR Boundary用workflowでは、
PR codeのcheckout
install
build
test
import
execute
を行いません。
扱うのはPR metadataとchanged filesです。
permissionも、
contents: read
pull-requests: read
statuses: write
だけです。
statuses: writeはpr-boundary/scopeをpublishするために使います。
secretsやOIDC、environmentも不要です。
changed filesを全部確認できなければPASSしない
scope gateなので、
実はchanged filesを全部取得できていなかったがPASSした
という挙動は避けたいところです。
そこでchanged-file APIはpaginationし、取得件数の完全性も確認します。
完全なfile listを証明できない場合は、
CHANGED_FILES_INCOMPLETE
としてREVIEW_REQUIREDにします。
大規模PRについても、無理に独自fallbackを作ってPASSさせるのではなく、判定不能ならfail closedにしています。
renameは旧pathと新pathの両方を見る
renameも新しいpathだけを見ると問題があります。
例えば、
.github/workflows/release.yml
を、
docs/release.yml
へrenameした場合、新pathだけならdocs/**です。
そのためrenameでは、
previous_filename
filename
の両方を評価します。
どちらかがscope外ならPASSしません。
実GitHubで試して見つかったrace condition
実際にdogfoodしている途中で、1つbugを見つけました。
PRへscope:docsを付けた直後なのに、
REVIEW_REQUIRED: NO_PROFILE
になることがありました。
原因は、label追加直後のGitHub REST APIが一時的に古いlabel状態を返すread-after-write raceでした。
現在は、
-
labeledなら、そのlabelがREST snapshotへ反映されたことを確認 -
unlabeledなら、そのlabelが消えたことを確認 - 不一致の場合だけbounded retry
- 最終的にも一致しなければ
PR_STATE_CHANGED / REVIEW_REQUIRED
としています。
event payloadのlabel自体をprofile選択のauthorityにはしていません。
unit testだけでは見つからず、実GitHubで動かして見つかったケースでした。
AI専用ツールではない
coding agentの利用が増えると、
指示した範囲より広く変更された
という問題は意識しやすくなります。
ただ、PR Boundary自体はAI専用ではありません。
対象は同じです。
human contributors
Dependabot
bots
coding agents
誰がPRを作ったかではなく、
実際にどのpathを変更したか
だけを見ます。
CODEOWNERSとは役割が違う
CODEOWNERSは主に、
このファイルを変更したとき、誰のreviewが必要か
を表現します。
PR Boundaryが見るのは、
このPRが、今回許可された変更範囲に収まっているか
です。
例えばscope:docsなら、
docs/** → 許可
.github/workflows/** → 不許可
というPR単位の境界を作ります。
CODEOWNERSの代替ではなく、別の役割です。
PR Boundaryがやらないこと
PR Boundaryは、
- security review
- code quality review
- malware detection
- semantic diff
- AI-generated code detection
をしません。
判定するのは、
このPRのchanged pathsが、
repository側で決めたscopeに収まっているか
だけです。
Scope compliance is not a security review.
現在の状態
2026年8月時点でv0.1.0を公開しています。
- Apache-2.0
- GitHub Marketplace公開済み
- 自身のrepositoryでdogfood中
- GitHub Rulesetのrequired statusとして運用中
Repositoryはこちらです。
仕様とthreat modelもrepository内で公開しています。
おわりに
コードレビューでは、
この実装は正しいか
を見る必要があります。
それとは別に、
そもそも今回変更してよい場所だけを変更しているか
を機械的なgateにできると、PR運用を少し分離できるのではないかと考えて作りました。
まだv0.1.0なので、実際のrepositoryで使ったときに分かりにくい点や、想定していなかったケースがあればGitHub Issueで教えてもらえると助かります。