なぜAIのPRを可視化したいのか
- AIで作成されたPR
- タイトルだけでは中身が想像できないPR
- 規模(差分の大きさ)が開くまでわからないPR
- etc..
AI駆動開発が広がり、Claude CodeやGitHub CopilotといったエージェントがPRを出すことは日常になりました。GitHub Copilotであれば、IssueのAssignにCopilotを指定するだけで、クラウド上でIssueを処理してPRまで作成してくれます。複数のIssueを並列で処理できるようになった一方で、出てきたPRを人間がチェックする作業の比重は増えています。
ここで困るのが、PR一覧を見ても中身の傾向がつかめないことです。タイトルだけでは機能追加・バグ修正・依存更新のどれなのか判別できず、結局ひとつずつ開いて差分を確認することになりがちです。PRの数が増えるほど、このレビュー負荷はじわじわと効いてきます。
そこで役立つのがラベルです。変更の種類(機能追加・バグ修正・依存更新など)や、触っているレイヤー(フロントエンド・バックエンドなど)、変更の規模をラベルとして自動で付けておけば、PR一覧の時点で中身の見当がつきます。例えばモデルの変更が含まれたPRなら、label:modelのように絞り込めます。また、後から「どんな種類のPRが多かったか」を振り返ることもできます。
なお、ここで紹介するラベル付けはAIが出したPRだけに限った話ではありません。人間が出したPRにも同じルールで付くので、チーム全体のPRの傾向を可視化する仕組みとして使えます。
GitHub Labelerとは
Labeler(actions/labeler)は、PRに対してラベルを自動で付けてくれるGitHub公式のActionです。設定したルールに一致したラベルが自動で付きます。
仕組みはシンプルで、ルールを .github/labeler.ymlに書き、それを呼び出すワークフローを .github/workflows/に置くだけです。ルールは「ラベル名」と「そのラベルを付ける条件」の組み合わせで書きます。
# docs/ 配下か Markdown を変更したPRに docs ラベルを付ける
docs:
- changed-files:
- any-glob-to-any-file:
- 'docs/**'
- '**/*.md'
changed-files は変更ファイルのパスを見る条件で、any-glob-to-any-file に書いたglobのいずれかに一致したファイルが含まれていれば、そのラベル(ここでは docs)が付きます。ラベルを増やしたいときは、このブロックをラベルの数だけ並べていきます。
Labelerでできること
Labelerの正規表現が使える素材は次の2つです。
- 変更ファイルのパス(changed-files)
- ブランチ名(head-branch / base-branch)
つまり「どのファイルを変更したPRか」「どんな名前のブランチから出たPRか」でルールを設定しラベルを付けます。パスは **/*.ts や models.ts、src/api/** のようなglobで、ブランチ名は ^fix/ のような正規表現で書けます。
「中身を見て判断する」のではなく、あくまでパスとブランチ名というルールベースの静的な判定です。逆に言えば、ここに当てはまらないもの(PRのタイトルや本文、差分の行数、作成者)はLabelerだけでは扱えません。このあたりは記事の後半で、補助のワークフローや専用Actionを組み合わせて補っていきます。
なぜLabelerなのか
PRの中身を読んでラベルを付けたいなら、LLMにPRの差分を渡して動的にタグ付けする方法も考えられます。柔軟ですが、APIキーが必要で、PRが出るたびに使った分の料金が発生します。
一方でLabelerはパスとブランチ名のルールに一致させるだけの静的な仕組みなので、追加のAPI利用料はかかりません。判定が単純なぶん挙動も予測しやすく、PRごとに結果がブレることもありません。まずは無料で安定して回るLabelerを土台にして、足りない部分だけを後から足していくのがおすすめです。
どんなラベルを用意すると良いか
ラベルは増やしすぎると一覧がごちゃごちゃしてしまうので、最初は「種類」「領域」「規模」の3方向に絞るとバランスが良いです。ここではWeb開発のプロジェクトを想定したサンプルを挙げます。実際のラベルは、お使いのプロジェクトのディレクトリ構成に合わせて正規表現(glob)を整えてください。
| ラベル | 付ける条件のイメージ | 何がわかるか |
|---|---|---|
docs |
docs/**、**/*.md を変更 |
ドキュメントだけのPR |
deps |
package.json、pnpm-lock.yaml などを変更 |
依存パッケージの更新 |
frontend |
src/frontend/**、**/*.tsx を変更 |
フロントエンド側の変更 |
backend |
src/api/**、src/server/** を変更 |
バックエンド側の変更 |
model |
**/models/**、models.ts を変更 |
データモデル・スキーマの変更 |
test |
**/*.test.ts、tests/** を変更 |
テストの追加・修正 |
ci/cd |
.github/workflows/** を変更 |
パイプラインの変更 |
ai-prompt |
.claude/** などプロンプト定義を変更 |
AIのプロンプトに関わる変更 |
size:* |
差分の行数(後述の補助で算出) | PRの規模 |
docs や deps、ci/cd のあたりは、変更ファイルのパスがほぼそのままラベルに対応するので、Labelerと相性が良い部類です。一方で size:* はパスでは決められないため、後半の応用で別の仕組みを使います。
実装: AI由来のPRを判定してラベルを付ける
ここからは、シンプルなTodoアプリ(Node.js + Express)のリポジトリを題材に、実際に動かした設定を載せていきます。まずはパスベースのラベル付けから始めて、そのあとにブランチ名・タイトル・規模の応用を足していく流れです。
labeler.yml でパスベースのラベルを付ける
Labelerを動かすには、ルールを書く .github/labeler.yml と、それを呼び出すワークフローの2つを用意します。
まず呼び出し側のワークフローです。PRをトリガーに、actions/labelerを実行するだけです。
name: Labeler
on:
- pull_request_target
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
pull_request_target で動かすのは、フォークからのPRでも書き込み(ラベル付け)ができるようにするためです。ラベルを付けるには pull-requests: write の権限が必要になります。
次にルールを定義する labeler.yml です。Todoアプリの構成に合わせて、変更ファイルのパスからラベルを決めています。
ci/cd:
- changed-files:
- any-glob-to-any-file:
- '.github/workflows/**'
test:
- changed-files:
- any-glob-to-any-file:
- 'test/**'
- '**/*.test.js'
model:
- changed-files:
- any-glob-to-any-file:
- 'src/models/**'
docs:
- changed-files:
- any-glob-to-any-file:
- '**/*.md'
- 'docs/**'
deps:
- changed-files:
- any-glob-to-any-file:
- 'package.json'
- 'package-lock.json'
any-glob-to-any-file に並べたglobのいずれかに一致したファイルが変更に含まれていれば、そのラベルが付きます。例えば src/models/todo.js を触ったPRには model が、docs/ 配下や任意のMarkdownを触ったPRには docs が付きます。なお、付けようとしたラベルがGitHub上にまだ存在しない場合は、Labelerが自動で作成してくれます。
応用: 変更の種類を判定する(ブランチ名)
ブランチ名の規約が決まっているなら、種類のラベルもLabeler単体で付けられます。github-scriptを書く必要はありません。head-branch にブランチ名の正規表現を並べると、いずれかに一致したときにラベルが付きます。
fix:
- head-branch:
- '^fix/'
- '^hotfix/'
feature:
- head-branch:
- '^feature/'
- '^feat/'
配列はOR条件なので、fix/login-bug でも hotfix/login-bug でも fix ラベルが付きます。
パス条件(changed-files)と組み合わせたいときは all: でまとめるとAND条件になります。次の例は「ブランチ名が feature/ で始まり、かつ src/ 配下を変更した」ときだけ付きます。
feature-src:
- all:
- head-branch:
- '^feature/'
- changed-files:
- any-glob-to-any-file:
- 'src/**'
ただし注意点があります。AIエージェントが出すブランチ名は、必ずしも fix/ や feature/ の規約に従うとは限りません。Claude Codeのように claude/... といった独自のprefixを付けることもあります。その場合はブランチ名だけでは種類を判定できないので、次のPRタイトルでの判定にフォールバックします。
応用: 変更の種類を判定する(PRタイトル)
Labelerはタイトルや本文を見られないので、タイトルから種類を判定したいときは別のワークフローを用意します。ここではactions/github-scriptで、Conventional Commits形式のタイトル(feat: fix: など)を正規表現マッチして type:* ラベルを付けます。
name: PR Title Labeler
on:
pull_request_target:
types:
- opened
- edited
- reopened
- synchronize
permissions:
contents: read
pull-requests: write
jobs:
title-label:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const title = context.payload.pull_request.title || '';
// Conventional Commits の type → 付与するラベル
const rules = [
{ re: /^feat(\(.+\))?!?:/i, label: 'type:feat' },
{ re: /^fix(\(.+\))?!?:/i, label: 'type:fix' },
{ re: /^docs(\(.+\))?!?:/i, label: 'type:docs' },
{ re: /^(chore|build|ci|refactor|perf|style|test)(\(.+\))?!?:/i, label: 'type:chore' },
];
const matched = rules.find((r) => r.re.test(title));
if (!matched) {
core.info(`No conventional-commits type matched in title: "${title}"`);
return;
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: [matched.label],
});
types に edited を入れているので、後からタイトルを直したときにも付け直されます。タイトルが規約に沿っていなければ何もせず終了します。
応用: 変更の規模(size)を判定する
変更の規模(差分の行数)もLabelerでは判定できません。専用のAction(pascalgn/size-label-action や CodelyTV/pr-size-labeler)を使う手もあります。ここではactions/github-scriptで additions + deletions を集計して size:* を付ける自前実装を載せます。
name: PR Size Labeler
on:
pull_request_target:
types:
- opened
- synchronize
- reopened
permissions:
contents: read
pull-requests: write
jobs:
size-label:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const total = pr.additions + pr.deletions;
// total <= max の最初にマッチしたものを採用。閾値はカスタム可能。
const thresholds = [
{ max: 10, label: 'size:XS' },
{ max: 50, label: 'size:S' },
{ max: 200, label: 'size:M' },
{ max: 500, label: 'size:L' },
{ max: Infinity, label: 'size:XL' },
];
const target = thresholds.find((t) => total <= t.max).label;
const allSizeLabels = thresholds.map((t) => t.label);
// 既存の size:* ラベルのうち、今回付けるものと違うものは外す
const current = (pr.labels || []).map((l) => l.name);
for (const name of current) {
if (allSizeLabels.includes(name) && name !== target) {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name,
}).catch(() => {});
}
}
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: [target],
});
閾値は次のようにしています。プロジェクトの粒度に合わせて調整してください。
| ラベル | 変更行数 (additions + deletions) |
|---|---|
size:XS |
〜10 |
size:S |
〜50 |
size:M |
〜200 |
size:L |
〜500 |
size:XL |
501〜 |
ポイントが2つあります。1つは、再実行時に古い size:* ラベルが残らないよう、今回付けるもの以外のsizeラベルを削除していることです。もう1つは、synchronize(追記のpush)でも再評価されるよう types に含めていることです。これで、PRに追記して差分が増えたときも規模ラベルが付け替わります。
可視化して振り返る
ラベルが付くようになったら、あとはそれを使って眺めるだけです。
一番手軽なのはPR一覧のフィルタです。GitHubの検索ボックスに label:type:fix label:size:XS のように複数のラベルを並べると、AND条件で絞り込めます。「小さいバグ修正のPR」だけを抜き出してまとめてレビューする、といった使い方ができます。
件数を集計して傾向を見たいときは、ghコマンドが便利です。特定のラベルが付いたマージ済みPRの件数を数えることもできます。
直近で何回modelの変更があったかを可視化できます。

Issueのサイズが適切に細分化できたかを見ることもできます。size:XL ばかりが並ぶようなら、もう少し小さく分割してからAIに渡したほうが良い、といった気づきにつながります。
さらに進めるなら、この集計を定期的に回してダッシュボード化したり、新しいPRにラベルが付いたタイミングでSlackへ通知したり、といった発展も考えられます。まずはフィルタとghでの集計から始めて、必要になったら自動化していくのがおすすめです。
まとめ
LabelerでPRを可視化する流れを、土台から応用まで見てきました。
- 土台: Labelerで変更ファイルのパスからラベルを付ける(
docs,deps,modelなど)。無料で安定して回る - 応用: ブランチ名で種類を判定する。規約があればLabeler単体で完結する
- 応用: PRタイトルや差分の行数は、github-scriptや専用Actionで補う(
type:*size:*) - 活用: PR一覧のフィルタやghコマンドの集計で、PRの傾向を振り返る
まずはパスベースのラベルだけでも、PR一覧の見通しはぐっと良くなります。

ラベルの設定をすることで、冒頭に見たPR一覧もこのようにざっくり理解できるPRに変化しました。AI駆動開発でPRの数が増えていくほど、この可視化の効果は大きくなっていくはずです。
参考文献

