コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

フロントエンドのバンドル差分をPull Requestコメントから見れるようにしました

こんにちは!プロダクト開発部の吉田(よしよし)です。

ハンディタイプのかき氷機を買ったら、我が家で空前のかき氷ブームがおき、3日おきに新色のシロップが増えていっています。よくばるほど微妙な色になる…という人生の学びをものともせず、5歳の息子は全色混ざった茶色がお好みです🍧

さて、この記事では、フロントエンドのバンドル差分有無をPull Requestのコメントで確認できるようにし、ライブラリ更新などの本番影響の有無を判断しやすくした話を書きたいと思います。

モチベーション

コドモンのとあるプロダクトでは、フロントエンドのビルド成果物は100ファイルを超えます。ファイル名にはコンテンツハッシュが含まれているため、main.abc123.jsmain.def456.jsのように、同じファイルでも中身が少しでも変化するとファイル名が変わってしまいます。

それらの差分をIDEで見ようとすると、名前が一致しないので比較しづらく、コツコツ対応するファイルを選択して見比べたり、ビルド設定を変えてhashを無効にしてから比較したり……。そしてminifyされた中身はそもそも比較に向かないのでフォーマッタをかけて……など、とにかく地味に手のかかる作業でした。
結果、ライブラリ更新は億劫になりがちでした。

さらに「○○は開発系パッケージで、△△でしか使われていないから本番に影響はない」といった個人の知見頼みの判断も多く、それら判断の根拠を属人化させず仕組みで支えたいという思いがありました。

ライブラリの脆弱性対策の文脈でも事情は同じです。緊急のパッチ適用では、利用バージョンが大きくジャンプすることもあります。そうした変更をすばやく本番影響あり/なしに切り分け、本当に確認すべき差分だけに集中できる状態を整えておきたい。これも大きな動機でした。

仕組み化のきっかけ

実は前述の「ファイル名が変わって比較しづらい」問題、git diffではすでに解決されています。
git diff --find-renamesを使うと、ファイル名が変わっていても内容の類似度をもとにリネームを検出してくれます。つまり、hash付きのファイル名でも「同じファイルの変化前・変化後」として追跡できるのです。デフォルトでONになっています。

前節で触れた「hashを無効化して比較する」手もありますが、それだと本番とは異なるビルド設定でビルドした物を比較することになってしまいます。この--find-renamesなら、ビルド設定を変えずにそのまま本番に出る成果物の差分を追えるのが利点です。

IDEで差分を見がちな身としてはこの機能を知らなかったのですが、この存在を知ってから「なにか仕組み化できるかも…」という期待がふくらんでいました。

そしてそんな中で出会ったのが、以下のテックタッチさんの記事でした。

tech.techtouch.jp

biomeとGitHubを組み合わせて、JSバンドル差分を表示する仕組みの整備について丁寧に解説されています。
作業頻度が多くないが故についつい手作業でやってしまっていましたが、この記事をきっかけに本格的に仕組み化について考え始めました。

コドモンで仕組み化する上で大事にしたところ

元記事のアイデアをきっかけに仕組み化するにあたって、コドモンの状況や普段の作業に合わせて以下の方針を決めました。

CIだけでなくローカル(CLI)でも動かせること

ライブラリ更新の影響が大きい場合など、試行錯誤しながらバージョン選定やビルド設定を決めたいケースがあります。その場合は、いちいちpushしてワークフローの結果を待つ、ということをしたくありません。素早くフィードバックがほしい。なのでローカル用のCLIツールも用意することにしました。それぞれの良さは生かしつつ、できるだけ同じスクリプトをCIとCLIツールで共有する形を目指しました。

Pull Request内で完結する

元記事では、GitHub上の差分表示を利用するために比較専用のブランチを作成して差分を見る構成をとられていました。ただ、コドモンでは、過去にGitHub上にブランチが溜まりすぎて、ある機能がタイムアウトを起こすようになりブランチを大掃除した……ということがあり、ちょっとブランチ増には慎重になっています💦
「不要になれば削除する」という処理を整備すれば良いだけの話ではありますが、今回は別のアプローチをとってPull Request内で完結させる方針を取りました。

AIの力を借りて、人間が判断するときの根拠を補助してもらう

差分を見ること自体、ある程度の経験や勘所が必要です。
差分の内容を解釈する部分をAIに補助してもらうことで、追加の判断材料が得られるようにしました。



実際のPull Requestコメント出力例

先にお見せすると、以下のようなかんじでPull Request(以下、PR)にコメントを出しています。

差分がない場合のPRコメント

差分がある場合のPRコメント

🔍Compare on GitHubのリンクから、GitHubのコミット比較画面で差分を確認可能です。

GitHub上で差分比較を確認

▶︎ ここで差分を表示するからdiffをインライン展開できるようにもしています。差分が少ないときはこれで十分です。

PRコメント上で差分をインラインに展開

差分が大きくてWeb上ではとてもレビューできない……というときのために、artifactにもdiffファイルをアップロードしています。

仕組みの全体像

GitHub Actions

GitHubのCI上では、大まかに以下のジョブフローで動きます。

差分生成ワークフロー

目的がライブラリの更新判断なので、package.jsonpnpm-*.yamlの変更を含むPRで発火するようにしています。

まず、ベース側とPR側のブランチをそれぞれ並列にビルドし、oxfmtでフォーマットを揃えた上で、git diff --find-renamesで差分ファイルを生成します。差分の有無によって、以下のように分岐します。

  • 差分なし → 差分がない旨をPRコメントに投稿
  • 差分あり → 差分データをPRコメントに投稿(後述)した上で、Claude Code にも差分分析と結果の投稿を依頼

ローカルのCLI

ローカルのCLIは、差分の確認とAIによる分析を手元で回すためのものです。差分が大きいときにじっくり検証したり、ライブラリ更新を試行錯誤したりする場面で使います。PRコメントの投稿は行いません。

ローカルのCLIの流れは以下です。

CLIの処理フロー

CIと共通なのはオレンジの枠で、そこは個別にサブコマンドとして呼び出せるようにしています。
つまりローカル実行は、worktreeの作成・破棄もしつつ、それらサブコマンドをただ呼び出すだけのツールとして作っています。

# ビルド・oxfmtでフォーマット
./scripts/build-diff.sh --build <output-dir>

# diff生成
./scripts/build-diff.sh --diff <base-dir> <current-dir>

# ローカルのCLIとして↑を連続実行
./scripts/build-diff.sh --local

余談ですが、中身はTypeScriptで書いています。複雑なシェルスクリプトはメンテがつらいので。。

#!/usr/bin/env bash

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# devDependenciesに入れている oxfmt を使うため pnpm exec 経由で実行
exec pnpm exec node "${SCRIPT_DIR}/build-diff.ts" "$@"

ローカルで実行すると、現在のブランチに加えて、デフォルトブランチとの共通祖先をベースとしてビルドします。

ベース側のビルドはgit worktreeで作成した別ディレクトリで行うので、現在の作業途中のファイルをstashしたりcheckoutしたりする必要はありません。共通祖先のハッシュが変わらないうちはビルド済みを再利用し、無駄にビルドしないようにしています。

仕組み的には並列ビルドもできますが、弊社のフロントエンドはファイル数が多くIOが重く、計測してもほとんど恩恵がありませんでした。そもそもローカルで何度も試すときは(前述のビルド済みの再利用で)ベース側のビルドはスキップされるので、並列化できても実質初回のみです。それなら、ローカル用CLIを複雑にしてまで並列化する必要はない、と判断して直列にしています。

実装で工夫した点

以下では、実装にあたり工夫した点をいくつか紹介したいと思います。
もし同様な仕組みを作りたい!という場合に、わずかでも参考になれば幸いです。

CIで高速にデフォルトブランチとの共通祖先を見つける

差分は、現在のブランチとデフォルトブランチとの共通祖先(以降、ベースSHA)との差分を確認しています。

gitコマンドでは、merge-baseサブコマンドで取得できます。

git merge-base HEAD origin/main

通常、GitHub Actionsでソースコードをcheckoutするときはcommit履歴なしで素早く展開するかと思います。しかしベースSHAを解決するためにはcommit情報が必要で、これがリポジトリの歴史が深いととても時間がかかります。
コドモンでは数分レベルでかかってしまいました。。

いきなりフルでcheckoutせず少しずつ fetch-depth を増やしながら共通祖先を辿るアプローチも取れますが、ベースSHAとそのTree hashを以下のGitHub APIで簡単に取得できることに気づき、こちらを採用しました。

jobs:
  resolve_base:
    name: ベースSHA解決
    runs-on: ubuntu-slim
    permissions:
      contents: read
    outputs:
      base_sha: ${{ steps.resolve.outputs.base_sha }}
      base_tree: ${{ steps.resolve.outputs.base_tree }}
    steps:
      - name: ベースSHAを解決
        id: resolve
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          REPO: ${{ github.repository }}
          BASE_REF: ${{ github.base_ref }}
          HEAD_REF: ${{ github.head_ref }}
        run: |
          BASE_SHA=$(gh api "repos/${REPO}/compare/${BASE_REF}...${HEAD_REF}" --jq '.merge_base_commit.sha')
          BASE_TREE=$(gh api "repos/${REPO}/git/commits/${BASE_SHA}" --jq '.tree.sha')
          echo "base_sha=${BASE_SHA}" >> $GITHUB_OUTPUT
          echo "base_tree=${BASE_TREE}" >> $GITHUB_OUTPUT

このステップは数秒で終わります。後続ジョブの分岐に必要な情報を軽量に取得できています。

注: github.base_refgithub.head_refはpull_requestイベント専用のコンテキスト変数です。もし他のイベントトリガーから流用する場合は別途変数の取得方法を調整してください。

複数同時PR作成でのベース側のバンドル再利用

ローカルと同様に、CIでもベース側のビルドは使い回したいところです。とくにdependabotのように、デフォルトブランチに対して複数のPRが同時に作られる場合、各PRが同じベースを別々にビルドするのは無駄になります。

そこで、ベースが同じ(tree hashが一致する)ビルドは1回だけ走らせ、結果をartifact経由で共有するようにしました。 ベース側のビルド時に、既にartifactへキャッシュがあればビルド自体をスキップします。

これだけでは複数のPRが同時で走ると「キャッシュがない→ビルドする」という流れも重なるため、tree hashでconcurrency groupを指定し、1つのインスタンスがビルドしてartifactに登録し終えるまで、他は待つようにしました。

jobs:
  # ...
  build_base:
    name: ベースをビルド
    needs: resolve_base
    runs-on: ubuntu-latest
    concurrency:
      group: build-diff-base-${{ needs.resolve_base.outputs.base_tree }}
      cancel-in-progress: false
    steps:
      # ... artifact に登録済みかを確認 → なければビルド → ビルド結果を artifact として保存

この仕組みにより、複数のPRが同時に作成されても、ベース側が同じtree hashであれば最初の1回のみビルドされ、他はその結果を再利用できます。

ビルド成果物の比較に合わせたgit diffの調整

ベース側と現在側のビルド成果物を比較する際は、以下のオプションを付けてgit diffを呼び出しています。

git --no-pager -c diff.renameLimit=0 \
    diff --no-index --no-prefix --find-renames=30% --unified=2 \
    <base-dir> <current-dir>

特に意識しているのは以下のオプションです。

  • --find-renames=30%
    • リネーム検出の類似度しきい値です。デフォルトは50%ですが、コンテンツハッシュ付きのファイル名は、minify結果が少し変わるだけでもファイル全体の見た目の差分が大きくなりやすく、30%まで下げて追跡されやすくしています。
      この値は細かく比較評価したわけではなく、誤検出は影響評価のノイズになる可能性があるので、今後の結果によって調整するかもしれません。とはいえチャンク全体がこれ以上大きく変わる場合は、そもそもこの差分の仕組みに頼らず検証するべき、という割り切りもあります。
  • --unified=2
    • 差分の前後に表示するコンテキスト行数をデフォルトの3行から2行に減らしています。diff自体をコンパクトにし、PRコメントに投稿する文字列サイズを抑えるのに加え、AIの使用コンテキストを節約する目的での調整です。
  • -c diff.renameLimit=0
    • リネーム検出のファイル数上限を無制限にする指定です。上限(デフォルト1000)を超えると総当たりのリネーム検出がスキップされ、rename扱いされなくなります。今回の規模では達しませんが、ファイル数によらず全リネームを検出させる意図を明示するために付けています。

差分を確認する方法

差分がなければ、確認するまでもなく ✅ がコメントとして見えます。

差分がない場合のPRコメント(再掲)

差分が出た場合、PRから差分を確認できる経路を3つ用意しました。

  1. PRコメント内へのインライン埋め込み: unified diffをPRコメント内に折りたたんで貼り付けます。GitHubのコメント文字数上限(65,536文字)を超えないよう、余裕を持って65,000文字でカットしています。

     <details><summary>ここで差分を表示する</summary>
     ```diff
     {diffをここに記載する(65,000文字でカット)}
     ```
     </details>
    

    差分がある場合のPRコメント(再掲)

    PRコメント上で差分をインラインに展開(再掲)

  2. GitHub Compare URL: ビルド成果物を一時的なorphanブランチとしてpushし、そのブランチはpush直後に削除します。pushとほぼ同時に削除してもcommitは残り、commitハッシュから生成したCompare URLはしばらく有効であることを期待しています。到達可能なrefがなくなったコミットはいずれGitHub側のGCのタイミングで消えます。いつまであるかの保証はなく、「PR作成からレビューまでの短い期間にGitHub画面上で見られたらよい」という割り切りの機能として整備しました。

     git checkout --orphan "$TMP_BRANCH"
     # baseビルドをコミット → currentビルドをコミット (省略)
     git push origin "$TMP_BRANCH"
     git push origin ":$TMP_BRANCH"  # すぐ削除
    

    差分表示のURL:

     https://github.com/{org}/{repository}/compare/{base-commit-sha}...{current-commit-sha}
    

    GitHub上で差分比較を確認(再掲)

  3. artifactとしてダウンロード: コメントに収まらない長大な差分が出た場合のため、生のdiffファイルをartifactにアップロードしています。

Claude Codeによるビルド差分の分析

差分がある場合、GitHub Action上でClaude Codeが自動的に差分を分析してPRにコメントを投稿するようにしました。
改善の余地はまだ多いですが、チャンク構成の変化、新しいグローバル変数・副作用といった観点でパターンを探し、各変化についてのリスクの評価を試しています。

実際に使っているプロンプトはこのような形です:(現状のお試し構成であり、継続的に見直し予定)

以下はプロダクションビルドの差分です。
$DIFF_FILE

---

## 依頼

ビルドの差分全体を通じて、以下の観点でパターンを探して。

- チャンク構成・ファイル構造の変化
- 新しいグローバル変数・副作用
- 制御フロー・クラス構造の意味的変化

変数名のリネームや空白・並び替えなど、minify由来のノイズは無視すること。

## 出力形式
### ビルド差分の分析
各変化を項目ごとに以下の形式で列挙:

1. **変化**: (何が変わったか)
  - **原因**: (なぜ変わったか)
  - **リスク**: 無/低/中/高 — (根拠を1〜2文で)

### 総合リスク評価

**リスク: 無/低/中/高**
(評価の根拠を簡潔に述べる)

---

## 補足

* 原因特定に必要あれば、ソースコードの差分を確認可:
  git diff $BASE_SHA..HEAD

## 制約

- リスク評価はビルド差分に実際に現れた変化のみを根拠にすること
- バージョン番号・semver・リリースノートによる推測不可(例:「パッチバージョンだから安全」は不可)
- 通常リリースフローで当然の作業(ステージング確認・E2Eテスト等)は言及不要

ただし、差分が15,000行を超える場合はClaude Codeの分析をスキップします。大量のdiffを渡すと分析に時間がかかる上にトークン消費も大きくなり、そして大抵その詳細分析は必要ない(対応自体の見直し or 機能確認による品質担保を行う)ためです。「差分が多い」という情報で十分です。

経験的に15,000行で切っていますが、ここは今後の調整対象です。たとえば、15,000行を超えてスキップとなったPRで「これはPR上で分析結果を見たかった」というケースが増えれば閾値を引き上げ、逆に閾値付近で時間やコストの割に分析結果を見ることが少なければ(差分が多すぎる時点で更新の進め方を見直すケースがほとんどであれば)引き下げようと思います。

以下、結果例を2つ貼ります。プロンプトの見直しや差分次第で変わるので雰囲気が伝わればと思います。

レガシーなwebpackビルド設定のESM移行
Vueプロジェクトのライブラリ更新

Claude Codeの呼び出し方

GitHub Action上ではAnthropic公式の claude-code-action を使って呼び出しています。Bedrock経由で利用しているため、AWS OIDCで認証してから prompt に先ほどのプロンプトを渡し、claude_args で許可するツールを絞っています。

- name: AWS OIDC 認証 (for Bedrock)
  if: steps.conditions.outputs.run_claude == 'true'
  uses: aws-actions/configure-aws-credentials@ec61189d14ec14c8efccab744f656cffd0e33f37 # v6.1.0
  with:
    role-to-assume: ${{ vars.AWS_BEDROCK_ROLE_TO_ASSUME }}
    aws-region: ap-northeast-1

- name: Claude 分析・PRコメント投稿
  if: steps.conditions.outputs.run_claude == 'true'
  uses: anthropics/claude-code-action@476e359e6203e73dad705c8b322e333fabbd7416 # v1.0
  with:
    use_bedrock: 'true'
    github_token: ${{ secrets.GITHUB_TOKEN }}
    prompt: ${{ steps.prepare_prompt.outputs.content }}
    claude_args: |
      --bare
      --model ${{ vars.BEDROCK_CLAUDE_MODEL_BALANCED }}
      --max-turns 50
      --allowedTools "
        Read(${{ github.workspace }}/**),
        Glob,
        Grep,
        Bash(gh pr comment:*),
        Bash(gh pr view:*),
        Bash(git diff:*),
        Bash(git log:*),
        Bash(git show:*),
        Bash(grep:*),
        Bash(sort:*),
        Bash(jq:*)
      "
使用モデルは変数vars.BEDROCK_CLAUDE_MODEL_BALANCEDで入れ替えられるようにしています。2026年6月頭現在でjp.anthropic.claude-sonnet-4-6を指定しています。

ローカル実行時は対話モードで素直に呼び出しています。

claude {プロンプト}

-p(非対話モード)をつけていないのには2つ理由があって、つけると分析中の進捗が見えづらいことと、環境によってはツールの許可をその場でインタラクティブに判断できる方が便利なためです。AIに追加質問をすることも可能になります。

上記は2026年6月頭時点のもので、すぐに陳腐化すると思います。実際に構築する際は最新の公式ドキュメント等を参照してください。



使ってみてどうだったか

dependabotのPRなど、「✅ 差分はありません」が表示されると迷わず素早くマージできます。(UTやビルドのCIは別途PASSしている前提です。)

差分がある場合でも、動作に影響のない差分だけかどうかを楽に判断できるようになりました。
20〜30分ほどかかっていた確認作業が、数分で終わるようになりました。しかしそれ以上に大きいのは、「本番には全く影響がない」「xxの部分のみ影響あり」がはっきりわかり、マージに自信をもてるようになったことです。担当者にとってもレビュワーにとっても、この安心感が嬉しいです。

ライブラリ脆弱性対応のバージョンアップ以外にも、ビルド設定の大きめな変更対応のほかViteなどビルドシステムのメジャーバージョンアップにも効果がありました。いずれも想定外の変化への不安がつきまとう作業ですが、それをほとんど感じずに進められました。

Claude Code による判定も、差分を掘り下げて確認するための重要なヒント情報になると感じています。一方で、AIによる差分の分析結果はあくまで判断材料の一つであり、差分発生時の影響判断の主軸は信頼できるテストに置くべきだとも考えています。別角度からの裏付け/補強の一つとして活用できればと思います。

おわりに

以前から課題と感じていたバンドル差分確認について、PR上で差分を確認・評価できる仕組みを整備しました。 ビルドや差分生成といった機械的な作業を自動化するだけでなく、「この差分は何?」の解釈にAIの補助も取り入れる構成を試しました。

今回目指したのは、開発者が安心して判断できる状態をつくることです。 振り返ってみれば、バンドル差分の読み方は「一般的だが、知見・経験が問われる」領域で、ちょうどAIの活用に合う題材だったように思います。

今回の記事が、同じような課題を抱えている方にとって、自社に適用する際の手段や選択肢のヒントとしてわずかでも寄与できたら嬉しいです!