🏷️

AI駆動開発でPRが洪水になる前に、Labelerで可視化しておく

なぜAIのPRを可視化したいのか

みなさんのPR一覧はこうなっていませんか…? ラベルが付いていない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か」でルールを設定しラベルを付けます。パスは **/*.tsmodels.tssrc/api/** のようなglobで、ブランチ名は ^fix/ のような正規表現で書けます。

「中身を見て判断する」のではなく、あくまでパスとブランチ名というルールベースの静的な判定です。逆に言えば、ここに当てはまらないもの(PRのタイトルや本文、差分の行数、作成者)はLabelerだけでは扱えません。このあたりは記事の後半で、補助のワークフローや専用Actionを組み合わせて補っていきます。

なぜLabelerなのか

PRの中身を読んでラベルを付けたいなら、LLMにPRの差分を渡して動的にタグ付けする方法も考えられます。柔軟ですが、APIキーが必要で、PRが出るたびに使った分の料金が発生します。

一方でLabelerはパスとブランチ名のルールに一致させるだけの静的な仕組みなので、追加のAPI利用料はかかりません。判定が単純なぶん挙動も予測しやすく、PRごとに結果がブレることもありません。まずは無料で安定して回るLabelerを土台にして、足りない部分だけを後から足していくのがおすすめです。

どんなラベルを用意すると良いか

ラベルは増やしすぎると一覧がごちゃごちゃしてしまうので、最初は「種類」「領域」「規模」の3方向に絞るとバランスが良いです。ここではWeb開発のプロジェクトを想定したサンプルを挙げます。実際のラベルは、お使いのプロジェクトのディレクトリ構成に合わせて正規表現(glob)を整えてください。

ラベル付ける条件のイメージ何がわかるか
docsdocs/****/*.md を変更ドキュメントだけのPR
depspackage.jsonpnpm-lock.yaml などを変更依存パッケージの更新
frontendsrc/frontend/****/*.tsx を変更フロントエンド側の変更
backendsrc/api/**src/server/** を変更バックエンド側の変更
model**/models/**models.ts を変更データモデル・スキーマの変更
test**/*.test.tstests/** を変更テストの追加・修正
ci/cd.github/workflows/** を変更パイプラインの変更
ai-prompt.claude/** などプロンプト定義を変更AIのプロンプトに関わる変更
size:*差分の行数(後述の補助で算出)PRの規模

docsdepsci/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],
            });

typesedited を入れているので、後からタイトルを直したときにも付け直されます。タイトルが規約に沿っていなければ何もせず終了します。

応用: 変更の規模(size)を判定する

変更の規模(差分の行数)もLabelerでは判定できません。専用のAction(pascalgn/size-label-actionCodelyTV/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:XL501〜

ポイントが2つあります。1つは、再実行時に古い size:* ラベルが残らないよう、今回付けるもの以外のsizeラベルを削除していることです。もう1つは、synchronize(追記のpush)でも再評価されるよう types に含めていることです。これで、PRに追記して差分が増えたときも規模ラベルが付け替わります。

可視化して振り返る

ラベルが付くようになったら、あとはそれを使って眺めるだけです。

一番手軽なのはPR一覧のフィルタです。GitHubの検索ボックスに label:type:fix label:size:XS のように複数のラベルを並べると、AND条件で絞り込めます。「小さいバグ修正のPR」だけを抜き出してまとめてレビューする、といった使い方ができます。

件数を集計して傾向を見たいときは、ghコマンドが便利です。特定のラベルが付いたマージ済みPRの件数を数えることもできます。

直近で何回modelの変更があったか 直近で何回modelの変更があったかを可視化できます。

Issueのサイズが適切に細分化出来たか 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一覧もこのようにざっくり理解できるPRに変化しました。AI駆動開発でPRの数が増えていくほど、この可視化の効果は大きくなっていくはずです。

参考文献

新着記事

関連ネットワーク(beta)

ドラッグで移動 / Ctrl+ホイールでズーム