Claude Media
Claude CodeでChromaticのビジュアルテストを回す — PRの差分検知とHooks連携

Claude CodeでChromaticのビジュアルテストを回す — PRの差分検知とHooks連携

Claude CodeでStorybookを編集しながらChromaticのビジュアルテストを回す手順です。PR差分検知と、Stopフックの落とし穴を扱います。

Chromaticは、Storybookのストーリーを対象にしたビジュアルリグレッションテストのクラウドサービスです。Claude CodeでUIコンポーネントとストーリーを編集する開発フローに組み込むと、見た目の崩れをPRの段階で検知できます。テストケースと実行結果の管理を任せる構成は、ClaudeとQaseの連携にあります。

本記事では、基本セットアップとGitHub Actions連携に加えて、Claude CodeのStopフックから自動実行する構成を扱います。Stopフックのスクリプトは、git statusの見え方とフックの挙動まで扱います。

Chromaticとは何か

Chromaticは、ストーリーごとにクラウドのブラウザでスクリーンショットを撮り、最後に承認したベースラインとの差分を検出します。開発元はStorybookを作ったチームです。Storybookを使っていない場合も、Vitest・Playwright・Cypressのテストをビジュアルテストに変換して連携できます。

差分が見つかると、レビュアーは変更を承認(Accept)するか却下(Deny)するかを選びます。承認するとその見た目が新しいベースラインになり、承認済みのストーリーは、ブランチをまたいでもマージ後でも再承認が要りません。却下するとビルドは失敗します。却下した変更を直すにはコードを変えて新しいビルドを走らせる必要があります。

却下と未レビューの違いにも注意が要ります。ベースラインの扱いはどちらも同じで、次のビルドは元のベースラインと比べられます。却下は「見たが承認しなかった」変更を残す印で、修正の一覧として使えます。レビューできるのは、そのブランチの最新ビルドだけです。

導入前に確認する前提条件

Storybook 6.5以降が必要です。Nodeは、公式が動作を保証するバージョンとして18・20・21が挙げられています。それ以外のNodeではエラーが出ることがあります。

ただし、同じ公式のGitHub Actionsのサンプルはnode-version: 24.20.0を指定しています。クイックスタートの記述とサンプルでNodeの範囲が食い違っているため、CIのNodeをどう選ぶかは自分のプロジェクトで試して決めることになります。

Gitリポジトリであることも必須です。ChromaticはGit履歴でコミットとPRを結び付け、ベースラインを決めます。GitHub・GitLab・Bitbucketと連携せず「Unlinked」プロジェクトとして作ることもできます。その場合は自動のPRステータスチェックが付かないので、CI側でステータスを自分で設定します。

Chromatic CLIをセットアップする

1. サインアップしてプロジェクトを作成する

GitHub・GitLab・Bitbucket、またはメールアドレスでサインインし、プロジェクトを作成します。プロジェクトごとにトークンが発行され、CLIやCI設定に渡します。

2. CLIをインストールする

npm install --save-dev chromatic

package.jsonにスクリプトを追加しておくと、以降はnpm run chromaticで実行できます。

{
  "scripts": {
    "chromatic": "chromatic"
  }
}

このスクリプトは環境変数CHROMATIC_PROJECT_TOKENからトークンを読みます。CIではリポジトリのシークレットに置きます。

3. 初回ビルドでベースラインを作る

npx chromatic --project-token <your-project-token>

初回ビルドは各ストーリーのスナップショットを撮り、ベースラインとして保存します。以降のビルドは、このベースラインと新しいスナップショットを比べます。

差分をレビューしてPRをマージする

Claude Codeでストーリーやコンポーネントを編集してからChromaticビルドを実行すると、変更前後のスクリーンショットがビルド画面に並びます。すべて承認するとビルドはPassになり、PRの「UI Tests」ステータスチェックも緑になります。

レビュアーは特定のスナップショットにディスカッションを紐づけられます。ディスカッションと却下を組み合わせると、バグが直るまでマージを止められます。「UI Tests」を必須チェックにすると、見た目の崩れがそのままマージされるのを防げます。

GitHub ActionsでPRごとに自動実行する

手元のnpx chromaticだけでなく、CIでpushごとにビルドを走らせると、レビュアーはPR上で最新の差分を見られます。.github/workflows/chromatic.ymlを追加します。

name: "Chromatic"
 
on: push
 
jobs:
  chromatic:
    name: Run Chromatic
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v7
        with:
          fetch-depth: 0
      - uses: actions/setup-node@v7
        with:
          node-version: 24.20.0
      - name: Install dependencies
        run: npm ci
      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

公式のサンプルはfetch-depth: 0を指定します。shallow cloneでは履歴が足りず、祖先コミットのビルドを探せません。

トリガーはpushが推奨です。pull_requestで動かすときの違いは次のとおりです。

くらべる

pushとpull_requestのトリガー

公式の推奨

push

実際のPRブランチのコミットがそのまま対象になります。追加の設定は要りません。

設定が増える

pull_request

GitHubがPRブランチとmainを合成した一時的なブランチで動かします。mainの最新ビルドが祖先に見えるため、誤ったベースラインと比べられます。使う場合は専用ワークフローにし、チェックアウトでref: ${{ github.event.pull_request.head.ref }}を指定します。CHROMATIC_BRANCH・CHROMATIC_SHA・CHROMATIC_SLUGも環境変数で渡します。

大規模なStorybookでは、TurboSnapでビルド時間を抑えられます。onlyChanged: trueを足すと、変更に関係するストーリーだけが再テストされます。既定ではCI上で無効です。

      - name: Run Chromatic
        uses: chromaui/action@latest
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          onlyChanged: true

既定のChromaticは、差分があっても終了コード0を返します。判断はPR上の「UI Tests」バッジに任せる設計です。未レビューの差分でジョブごと失敗させたいときはexitZeroOnChanges: falseを足します。CI全体の組み込みはClaude CodeをGitHub Actionsに組み込む手順が詳しく、同じワークフローにChromaticのジョブを並べられます。

Claude CodeのStopフックでビジュアルテストを自動化する

Claude Codeにストーリーの編集を任せていると、編集のたびにnpx chromaticを手で叩くのは面倒です。フックを使えば、編集内容に応じてビルドを自動実行できます。どのイベントに紐づけるかで運用の重さが変わります。

フックイベント向き不向き理由
Stop(ターン終了時に1回)向き不向き向く理由1回の指示で直した複数のストーリーを、まとめて1ビルドで検証できる
PostToolUse(Edit/Writeごと)向き不向き重い理由ストーリーを1つ直すたびにビルドが走り、待ち時間とビルド数が増える
PreToolUse向き不向き向かない理由ビルドはアップロードと撮影が終わるまで結果が出ず、ツール実行前の判断材料にならない

Stopはmatcherに対応しておらず、ターンが終わるたびに毎回起動します。そのため、スクリプト側でストーリーファイルの変更を見て、あるときだけビルドします。フックの設定全般はClaude Code Hooks完全ガイド、実用レシピはClaude Code Hooks実例カタログにあります。

動作の流れ

手順

Stopフックがビルドを走らせるまで

  1. 1

    ターンが終わる

    Claude Codeが応答を終えると、Stopフックが起動します。入力のJSONにはstop_hook_activeが入っています。

  2. 2

    ストーリーの変更を探す

    スクリプトがgit statusで.stories.ファイルの変更を探します。無ければ何もせず終了します。

  3. 3

    Chromaticビルドを実行する

    変更があればnpx chromaticを同期的に実行し、出力からビルドのURLを抜き出します。

  4. 4

    結果のURLをClaudeに返す

    additionalContextでURLを返すと、会話が続いてClaudeがURLを受け取ります。トランスクリプトにはStop hook feedbackと表示されます。

  5. 5

    続きのターンで止まる

    継続したターンの終わりではstop_hook_activeがtrueになり、スクリプトは何もしません。

スクリプトを書く

.claude/hooks/chromatic-stop-check.shを作成します。

#!/bin/bash
INPUT=$(cat)
 
# Stopフックによる継続ターンでは再ビルドしない(無限ループ防止)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
  exit 0
fi
 
# -uall: 新しいディレクトリ内の未追跡ファイルも1件ずつ列挙する
CHANGED_STORIES=$(git status --porcelain -uall | grep -E '\.stories\.(tsx|jsx|ts|js|mdx)$')
if [ -z "$CHANGED_STORIES" ]; then
  exit 0
fi
 
BUILD_LOG=$(npx chromatic --project-token "$CHROMATIC_PROJECT_TOKEN" --exit-zero-on-changes 2>&1)
BUILD_URL=$(echo "$BUILD_LOG" | grep -oE 'https://www\.chromatic\.com/build\?appId=[^[:space:]]+')
 
cat <<JSON
{
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Chromaticビルドを実行しました。差分レビュー: ${BUILD_URL}"
  }
}
JSON
chmod +x .claude/hooks/chromatic-stop-check.sh

.claude/settings.jsonにフックを登録します。ビルドが長引く大きなStorybookではtimeout(秒)も足します。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/chromatic-stop-check.sh",
            "timeout": 900
          }
        ]
      }
    ]
  }
}

Stopフックスクリプトの落とし穴

次の挙動は、npxを偽のコマンドにした一時Gitリポジトリで再現できます。

ストーリーを変更済みの状態でstop_hook_activeをfalseにして実行すると、フックは次のJSONを返します。

{"hookSpecificOutput":{"hookEventName":"Stop","additionalContext":"Chromaticビルドを実行しました。差分レビュー: https://www.chromatic.com/build?appId=abc123&number=12"}}

変更が無いときとstop_hook_activeがtrueのときは、どちらも何も出力せず終了コード0で終わります。

見落としやすいのは、新しいディレクトリに作った新規ストーリーです。src/Card/Card.stories.tsxを作った直後のgit status --porcelainは、次のように出力します。

?? src/Card/

ファイルでなくディレクトリ1行にまとめられるため、.stories.tsxで終わる行を探す正規表現に掛からず、ビルドが飛びます。-uallを付けるとファイル単位で表示され、検出できました。リネームはR 旧 -> 新と出て、行末が新しい名前なので検出できます。

次の性質も、運用では効いてきます。

  • ストーリーをコミットするまで、次のターンの終わりごとに、stop_hook_activeがfalseの通常ターンで再びビルドが走ります。1回の依頼ごとにビルド1回で足りるなら、コミットまで含めて任せます
  • additionalContextを返すと、会話は続きます。Claudeがもう1往復してURLを報告するぶん、トークンを使います
  • Stopフックの継続は連続8回までで、超えると次のブロックは無視されてターンが終わります。上限はCLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられます
  • コマンドフックのtimeoutの既定は600秒です。時間切れになるとフックはキャンセルされ、出力も捨てられます
  • ビルドではbuild-storybook.log・chromatic.log・chromatic-build-*.xml・chromatic-diagnostics.jsonといったログが作業ツリーに残り、git status -uallにも出ます。ストーリーではないので無害で、.gitignoreに加えておくと見通しが良くなります

Storybook側のストーリー生成にはStorybook MCPサーバーをClaude Codeに繋ぐ手順を組み合わせられます。

よくあるつまずき

git log -n 1のエラーでビルドが失敗する

Chromaticはgitコマンドが$PATHにあることを前提にしています。Dockerのイメージにgitが無い、Heroku CIでGit履歴が渡されない、Google Cloud CIで.gitフォルダが無視される、といったケースで起きます。手元でgit log -n 1 --format="%H,%ct,%ce,%cn"を実行して、エラーが出ないか見ます。

Build X is based on a commit without ancestor buildsと表示される

祖先コミットのビルドが見つからないときのメッセージです。原因はshallow clone、rebaseやsquash mergeによる履歴の書き換え、ブランチの切り替えなどです。shallow cloneが原因なら、チェックアウトで全履歴を取得します。

ERR_INVALID_ARG_TYPEが出る

PlaywrightやCypressのプロジェクトで、package.jsonのoverridesに"storybook": "$storybook"が残っていると、互換性のないStorybookが使われてこのエラーになります。この指定を削除します。

PRのステータスチェックが更新されない

Unlinkedプロジェクトでは、ChromaticがGitホストへ自動で書き込めないため、「UI Tests」バッジが付きません。CI側でステータスを自分で設定します。CLIのJUnit XMLレポートを使えます。

まとめ

導入はサインアップ・CLIのインストール・初回ビルドの3ステップで終わり、PRごとの検証はpushトリガーのGitHub Actionsで足ります。Stopフックでつなぐ場合は、git status -uallで新規ファイルまで拾い、ビルドが長いときはtimeoutを伸ばします。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. chromatic.com/docs/quickstart
  2. chromatic.com/docs/github-actions
  3. code.claude.com/docs/en/hooks-guide
  4. code.claude.com/docs/en/hooks
  5. chromatic.com/docs/configure
この記事を共有:XはてブLinkedIn