0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub ActionsでPull Requestの変更範囲を制御するOSS「PR Boundary」を作った

0
Posted at

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で教えてもらえると助かります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?