kickflow Tech Blog

株式会社kickflowのプロダクト開発本部によるブログ

深夜のE2Eテスト失敗をClaude Codeが自動修正してPRを作る仕組みの構築

フクロウは夜間に獲物の位置を音だけで正確に特定し、静かに仕留める
フクロウは夜間に獲物の位置を音だけで正確に特定し、静かに仕留める

こんにちは、kickflow QAチームの川村です。

E2Eテストの数が増えてくると、毎朝のCI失敗対応が地味に時間を取られます。
kickflowでは約900のPlaywrightテストを深夜のnightly CIで実行していますが、プロダクト側の仕様変更によりテストが失敗することがあります。
翌朝出社して、失敗ログを確認し、原因を特定して、セレクタを修正して、PRを作成する。
この繰り返しを自動化できないかと考え、Claude Codeに深夜のうちに分析から修正PR作成まで全部やってもらう仕組みを作りました。

本記事では、この自動修正システムの設計と実装について解説します。

課題:毎朝の「CI失敗対応」ルーティン

kickflowでは、毎晩深夜0時にnightly CIが実行され、全E2Eテストが12シャードで並列実行されます。
テストの詳細については前回の記事で紹介しました。

失敗する原因はさまざまですが、最も多いのはプロダクト側の仕様変更に起因するものです。

原因 具体例
セレクタ変更 ボタンのテキストが「送信する」→「申請する」に変わった
URL変更 パスが /requests//applications/ に変わった
DOM構造変更 テーブルからリストに変わった
ロール変更 リンクがボタンに変わった

こうした仕様変更は、プロダクトリポジトリのPRとして日々マージされます。
E2Eテストリポジトリはプロダクトとは別リポジトリで管理しているため、プロダクト側の変更がテストに反映されるまでタイムラグが発生します。

翌朝の対応フローはこうでした。

  1. Slackの通知でnightly CIの失敗を確認
  2. GitHub Actionsの失敗ログを開いて、どのテストが落ちたか確認
  3. エラーメッセージからセレクタ変更なのか、フレーキーなのかを判断
  4. プロダクトリポジトリで前日マージされたPRを探す
  5. 差分を読んで、E2Eテストのどこを直せばいいか特定
  6. テストコードを修正してPRを作成

テスト数が少ないうちは問題になりませんでしたが、900テストを超えた今では毎朝30分以上かかることもあります。
しかも、この作業の大部分はパターン化された単純作業です。

解決策:Claude Codeによる自律的な修正

この問題を解決するために、claude-code-actionを使ったGitHub Actionsワークフローを作りました。
深夜3時(nightly CI完了後)に自動起動し、以下をすべて自律的に実行します。

  1. nightly CIの結果を確認
  2. 失敗テストのログを分析
  3. プロダクトリポジトリの前日マージPRを調査
  4. 失敗原因を3カテゴリに分類
  5. 仕様変更起因(spec-change)のテストを自動修正 → PRを作成
  6. フレーキー(flaky)テストを自動修正 → 別PRを作成
  7. CI上でテスト実行して検証
  8. リトライワークフローで再検証
  9. 結果をSlackに通知

翌朝出社したときには、修正PRとSlack通知が届いている状態を目指しました。

システム全体像

全体のアーキテクチャは以下の通りです。

sequenceDiagram
    participant Cron as GitHub Actions<br/>(cron: JST 3:00)
    participant Claude as Claude Code
    participant Skill as SKILL.md<br/>(10ステップの手順書)
    participant GH as GitHub CLI
    participant Product as プロダクトリポジトリ
    participant Slack as Slack

    Cron->>Claude: 起動 + プロンプト
    Claude->>Skill: SKILL.md を読み込み
    Claude->>GH: gh run list(nightly結果確認)
    GH-->>Claude: failure
    Claude->>GH: gh run view --log-failed
    GH-->>Claude: 失敗ログ
    Claude->>Product: curl(マージPR・差分取得)
    Product-->>Claude: PR一覧 + 差分
    Note over Claude: エラー分類<br/>spec-change / flaky / unknown
    Claude->>Claude: テスト修正 + 検証
    Claude->>GH: git push + gh pr create
    Claude->>GH: gh workflow run(リトライ)
    Claude->>Claude: レポートJSON出力
    Note over Cron: Claude完了後
    Cron->>Cron: レポートJSON読み込み
    Cron->>Slack: 分析結果通知(リッチ通知)
    Note over GH: リトライ完了後
    GH->>GH: nightly-summaryジョブ起動
    GH->>Slack: 最終サマリ通知(解消/要対応)

このシステムは4つのファイルで構成されています。

ファイル 役割
.github/workflows/auto-fix-nightly.yml GitHub Actionsワークフロー(cronトリガー)
.claude/skills/auto-fix-nightly/SKILL.md Claude Codeへの指示書(10ステップの手順)
.github/scripts/slack-notify.sh 分析結果Slack通知スクリプト(リッチ/シンプル切り替え)
scripts/nightly-final-summary.js 最終サマリ通知スクリプト(3データ突き合わせ)

Claude Code Skillとは

Claude Codeのカスタムスキルは、Markdownファイル1つでAIの振る舞いを定義する仕組みです。
プログラムではなく自然言語で手順を書くだけで、Claude Codeがその通りに動作します。

前回の記事で紹介した /codegen-test スキルと同じ仕組みですが、今回の /auto-fix-nightly はCI上で自動実行されるのが特徴です。

GitHub Actionsワークフロー

ワークフローの全体像は以下の通りです。

name: Auto Fix Nightly Failures

on:
  workflow_dispatch:      # 手動実行
    inputs:
      nightly_run_id:
        description: '対象のnightly run ID(空欄の場合は最新を自動取得)'
        required: false
        type: string
  repository_dispatch:    # 外部トリガー
    types: [auto-fix-nightly]
  schedule:
    - cron: '0 18 * * 0-4'  # JST 3:00(月-金の早朝)

3つのトリガーを用意しています。

トリガー 用途
schedule 毎朝JST 3:00に自動実行(メインの用途)
workflow_dispatch 手動で実行したいとき。nightly_run_id を指定すると特定のrunを対象にできる
repository_dispatch 外部システムからの実行(将来の拡張用)

Claude Codeの実行部分はclaude-code-actionを使っています。

- name: Run Claude Code to analyze and fix
  id: claude
  timeout-minutes: 60
  uses: anthropics/claude-code-action@v1
  with:
    anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
    claude_args: '--dangerously-skip-permissions'
    prompt: /auto-fix-nightly
    show_full_output: true

prompt にスラッシュコマンド /auto-fix-nightly を指定するだけで、Claude Codeがプロジェクト内の SKILL.md を自動的に読み込んで実行します。

いくつかのポイントがあります。

  • claude_args: '--dangerously-skip-permissions':Claude Codeはデフォルトでは gh コマンドなどの外部コマンド実行に確認を求めますが、CI上では対話的な確認ができないため、このフラグで権限チェックをスキップしています
  • show_full_output: true:Claude Codeの内部ログ(どのコマンドを実行したか、どう判断したか)をGitHub Actionsのログに出力します。問題発生時のデバッグに不可欠です また、CI上でテスト検証を行うため、Playwrightブラウザのインストールやテスト用の環境変数(sandbox環境への接続情報)もワークフロー内で設定しています。

エラー分類ロジック

自動修正の精度を上げるために、失敗テストを3つのカテゴリに分類しています。

spec-change(仕様変更起因)

プロダクトリポジトリの develop ブランチ(デプロイ済み)と main ブランチの差分を取得し、テストの失敗箇所と突合して判定します。

# develop と main の差分を取得(mainにまだ入っていない変更)
curl -s -H "Authorization: token ${PRODUCT_REPO_TOKEN}" \
  "https://api.github.com/repos/<org>/<product-repo>/compare/main...develop" \
  | jq '.files[] | {filename, status, patch}' | head -200

日付ベースではなくブランチ差分を見ることで、月曜実行時に金曜のマージが漏れるといった問題を回避しています。

ここでの「突合」は、プログラム的なマッチングではなくClaude Codeの自然言語推論に委ねています。
SKILL.mdに「失敗ログとプロダクトリポジトリの差分を比較して分類せよ」と指示しているだけで、具体的なマッチングルールはLLMが文脈から判断します。

たとえば、テストが getByRole('button', { name: '送信する' }) で失敗していて、develop ブランチのdiffにボタンのテキストを「申請する」に変更するパッチが含まれていれば、Claude Codeがそれを読み取って spec-change と判定します。
厳密なルールベースではなくLLMの推論に頼っているため、判断が曖昧なケースでは unknown に分類されます。この「迷ったら人間に委ねる」設計が、自動修正の暴走を防ぐ安全弁になっています。

修正箇所には原因PRへのトレーサビリティを残します。

// 仕様変更対応: <org>/<product-repo>#1234
await page.getByRole('button', { name: '申請する' }).click();

flaky(フレーキーテスト)

タイミング依存の失敗です。
TimeoutErrorstrict mode violation など、テスト実行のタイミングで再現性が変わるエラーがこれに該当します。

このカテゴリのテストは spec-change とは別ブランチ・別PR で自動修正します。
修正の原則は「テストロジックは変更せず、待機処理の追加やセレクタの限定化に留める」です。

unknown(判定不能)

上記のどちらにも分類できないケースです。
プロダクト側のバグの可能性もあるため、人間の判断が必要です。

修正パターン

spec-change と判定されたテストに対して、以下のパターンで自動修正を行います。

セレクタのテキスト変更:

// Before
await page.getByRole('button', { name: '送信する' }).click();
// After(仕様変更対応: <org>/<product-repo>#1234)
await page.getByRole('button', { name: '申請する' }).click();

URL変更への追従:

// Before
await page.waitForURL(/.*\/requests\/.*/);
// After(仕様変更対応: <org>/<product-repo>#1234)
await page.waitForURL(/.*\/applications\/.*/);

DOM構造変更への追従:

// Before
await page.getByRole('table').getByText('申請者').click();
// After(仕様変更対応: <org>/<product-repo>#1234)
await page.getByRole('listitem').getByText('申請者').click();

flaky 修正パターン

flaky と判定されたテストに対しては、spec-change とは別のブランチ(fix/auto-nightly-flaky-YYYYMMDD)で以下のパターンの修正を行います。

待機処理の追加:

具体的な応答を待てる場合は waitForResponsewaitForURL を優先します。

// Before(待機なし)
await page.getByRole('button', { name: '保存' }).click();
await expect(page.getByText('完了')).toBeVisible();
// After(flaky対応: API応答を待ってからアサーション)
const responsePromise = page.waitForResponse(
  resp => resp.url().includes('/api/') && resp.status() === 200
);
await page.getByRole('button', { name: '保存' }).click();
await responsePromise;
await expect(page.getByText('完了')).toBeVisible({ timeout: 15000 });

応答を特定しにくい場合は waitForLoadState('networkidle') を使うこともあります。

// After(ネットワーク通信の収束を待つ)
await page.getByRole('button', { name: '保存' }).click();
await page.waitForLoadState('networkidle');
await expect(page.getByText('完了')).toBeVisible({ timeout: 15000 });

strict mode violation の解消:

// Before(複数要素にマッチ)
await page.getByText('編集').click();
// After(flaky対応: スコープを絞る)
await page.getByRole('row', { name: 'ワークフロー名' }).getByText('編集').click();

重要なのは、テストロジックは変更しないという原則です。
セレクタや期待値といった「UIの表面的な変更」だけを修正し、テストシナリオの意図は維持します。

自動検証とPR作成

修正したテストは、CI上でPlaywrightを実行して検証します。

cd playwright && npx playwright test <修正ファイル> --project=chromium --retries=0

3回修正しても通らない場合は unknown に分類し直してスキップします。
無理に修正して壊れたテストをマージするよりも、人間に判断を委ねる方が安全です。

この「3回」というルールはSKILL.mdに自然言語で記述しているだけです。
LLMは非決定的な挙動をするため、同じテストに対して毎回異なる修正を試みる可能性があります。
安定性の担保は「修正後に必ずPlaywrightで検証し、通らなければ採用しない」という検証ゲートで行っています。
つまり、LLMの出力品質に依存するのではなく、テストの成否という客観的な基準で修正の正しさを判定する設計です。

検証に成功したテストは、分類結果テーブルを含むPRとして作成されます。
spec-change と flaky は別ブランチ・別PRで作成されるため、レビュー時にそれぞれの修正意図が明確になります。

ブランチ 内容
fix/auto-nightly-spec-change-YYYYMMDD 仕様変更起因のセレクタ・URL修正
fix/auto-nightly-flaky-YYYYMMDD 待機処理追加・タイムアウト調整
## 分類結果

| テストファイル | 分類 | 原因 | 関連PR |
|---|---|---|---|
| `scenarios/workflow/create.spec.ts` | spec-change | ボタンテキスト変更 | <org>/<product-repo>#1234 |
| `scenarios/ticket/list.spec.ts` | flaky | TimeoutError | - |

さらに、spec-change と flaky 両方の修正をマージした統合ブランチfix/auto-nightly-combined-YYYYMMDD)を作成し、そこからリトライワークフローをトリガーします。
対象は修正済みのテストだけでなく、nightly で失敗した全テストファイルです。flaky なテストはリトライで成功することがあるため、未修正のものも含めて再実行します。
統合ブランチを使うことで「全修正が当たった状態」での最終検証になり、サマリ通知の判定が正確になります。

Slack通知

なぜワークフローステップで通知するのか

当初はClaude Code自身がSlack Webhookで通知する設計でしたが、運用してみるとclaude-code-actionのサンドボックス環境では env: で渡した環境変数(SLACK_WEBHOOK_URL など)がClaude Codeの Bash ツールから見えないことがわかりました。

そのため、Slack通知の責任をClaude Codeからワークフローの後続ステップに移し、Claude Codeには分析結果をJSONファイルとして出力させる設計に変更しました。

レポートJSONによる連携

スキルの最終ステップで、Claude Codeが分析結果を /tmp/auto-fix-report.json に出力します。

{
  "nightly_run_id": "23151728113",
  "date": "2026-03-17",
  "summary": "15件の失敗テストを分析。4件を修正PRで対応。",
  "spec_change": [
    {"file": "各管理者ロール.spec.ts", "detail": "f8ebca99 で修正済み", "related_pr": "kickflow/kickflow#1234"}
  ],
  "flaky": [
    {"file": "audit-pipeline.spec.ts", "detail": "読み込み待機不足"}
  ],
  "unknown": [
    {"file": "承認者フィールド.spec.ts:439", "detail": "routeCode 不一致"}
  ],
  "pr_created": true,
  "pr_number": 1685,
  "flaky_pr_number": 1686,
  "retry_run_url": null
}

ワークフローの Notify Slack ステップが .github/scripts/slack-notify.sh を実行し、このJSONを読み込んでリッチな通知を構築します。

  • レポートファイルがある場合: spec-change / flaky / unknown の分類結果、各テストファイルの詳細、PRリンクを含むリッチ通知
  • レポートファイルがない場合(Claude Codeが失敗した場合など): Nightly Run / Auto Fix Run / 結果のシンプル通知にフォールバック

nightly CIが全テスト成功の場合は通知しません。
失敗したときだけ通知が届くため、ノイズになりません。

最終サマリ通知

上記の slack-notify.sh の通知は auto-fix-nightly の分析結果(何をどう分類してPRを作ったか)を伝えるものです。
これとは別に、リトライワークフローの末尾で 「最終的に何件解消・何件要対応」 をまとめた最終サマリ通知が届きます。

リトライワークフロー(playwright-retry-failed.yml)は、nightly_run_id が渡された場合に限り、全テスト完了後に nightly-summary ジョブを実行します。
このジョブが scripts/nightly-final-summary.js を呼び出し、以下の3つのデータを突き合わせます。

データ 取得元
Nightly の失敗テスト一覧 GCS(kicktify バケットの report-data.json
Auto Fix の分類結果 GitHub Artifacts(auto-fix-report-{nightly_run_id}
リトライ結果 リトライワークフロー内のステップレポート

突き合わせた結果、各テストを「解消済み」か「要対応」に振り分けてSlackに通知します。

Nightly CI 最終サマリ (2026-04-01)

Nightly: 5件失敗 → 最終: 1件要対応

✅ 解消済み (4件)
  修正済み (自動修正): 2件
  リトライで成功 (flaky): 2件

❌ 要対応 (1件)
• scenarios/approval/flow.spec.ts → unknown (要手動調査)

🔗 Nightly Run | Retry Run | spec-change PR #1685 | flaky PR #1686

朝出社したときに「今日は1件だけ手動で見ればいい」とすぐわかるため、トリアージの時間が大幅に短縮されました。

CI上での工夫

直近4時間のnightly runのみを対象にする

auto-fix-nightlyは gh run list で最新のnightly runを取得しますが、単に「最新の1件」を取ると問題が起きます。
たとえば金曜にnightly CIを止めた場合、木曜の古い失敗を再処理してしまいます。

これを防ぐため、直近4時間以内に完了したrunのみを対象にしています。
auto-fixはJST 3:00に実行され、nightly CIはJST 0:00に実行されるため、4時間の時間窓で確実にキャッチできます。
nightly CIが実行されなかった日は該当するrunがないため、何もせず終了します。

shallow checkoutへの対応

GitHub Actionsの actions/checkoutfetch-depth: 1 でshallow cloneを行います。
通常の git checkout main && git pull ではエラーになるため、ブランチ作成時に工夫が必要でした。

# shallow cloneでも動作するブランチ作成
git fetch origin main --depth=1
git checkout -B main origin/main
git checkout -b "fix/auto-nightly-spec-change-${DATE}"

ブランチ重複ガード

同日に複数回実行された場合に備えて、既存ブランチのチェックを行います。

if git ls-remote --exit-code --heads origin "${BRANCH}" > /dev/null 2>&1; then
  echo "${BRANCH} already exists. Skip."
  exit 0
fi

claude-code-actionのワークフロー検証制約

claude-code-actionはOIDCトークンの取得時にワークフローファイルがデフォルトブランチ(main)と一致しているかを検証します。
そのため、フィーチャーブランチでワークフローファイルを変更した場合、Claude Codeステップは Workflow validation failed エラーで失敗します。

Action failed with error: Workflow validation failed.
The workflow file must exist and have identical content
to the version on the repository's default branch.

この制約により、ワークフロー自体の変更はmainにマージしてからでないと動作確認ができません
ワークフロー内のスクリプト(.github/scripts/slack-notify.sh など)やスキルファイル(SKILL.md)の変更はこの制約を受けないため、ロジックはなるべくこれらの外部ファイルに切り出しておくと開発がスムーズになります。

また、Claude Codeのサンドボックス環境では env: で渡した環境変数の一部がClaude Code内部から参照できないこともわかりました。
Slack Webhook URLなどの秘匿情報をClaude Codeに渡す場合は、Claude Codeの後続ステップで処理する設計にすることを推奨します。

実際の動作

実際にnightly CIが失敗した際の動作例を紹介します。

  1. 深夜0時にnightly CIが実行され、3つのテストが失敗
  2. 深夜3時にauto-fix-nightlyワークフローが起動
  3. Claude Codeが失敗ログを分析
  4. プロダクトリポジトリのブランチ差分と突合し、2つを spec-change、1つを flaky と分類
  5. spec-change の2テストを自動修正
  6. CI上でテスト実行して成功を確認
  7. spec-change / flaky それぞれの修正PRを作成
  8. 両修正を統合したブランチを作成し、リトライワークフローを nightly_run_id 付きでトリガー
  9. 分析結果をレポートJSONとして出力
  10. ワークフローの後続ステップがレポートを読み込んでSlackに分析結果通知を送信
  11. リトライワークフローが全失敗テストを再実行
  12. リトライ完了後、nightly-summary ジョブが最終サマリ通知をSlackに送信

翌朝出社すると、分析結果通知・修正PRリンク・最終サマリ(要対応件数)がSlackに届いており、PRをレビューしてマージするだけで対応完了です。

今後の展望

  • 分類精度の向上: 現在は unknown に分類されるケースが一定数あるため、過去の分類結果をフィードバックして精度を上げていきたい
  • 修正パターンの拡充: 現在はセレクタ・URL・待機処理の修正が中心だが、テストデータの依存関係やAPI応答の変化への対応も視野に入れたい
  • 他チームへの横展開: この仕組みはE2Eテストに限らず、ユニットテストやインテグレーションテストの自動修正にも応用できる可能性がある

まとめ

Claude Codeのカスタムスキルとclaude-code-actionを組み合わせることで、nightly CIの失敗テストを自動分析・修正する仕組みを構築しました。

  • SKILL.md(Markdown 1ファイル)で10ステップの修正手順を定義
  • GitHub Actionsのcronで毎朝自動実行
  • エラー分類(spec-change / flaky / unknown)で修正対象を絞り込み
  • クロスリポジトリ分析でプロダクト側の仕様変更と突合
  • CI上での自動検証で修正の正しさを担保
  • レポートJSON + 外部スクリプトで分析結果のSlack通知を実現
  • 最終サマリ通知でリトライ後の「解消/要対応」件数を朝一で把握
  • show_full_output: true でClaude Codeの判断過程を可視化し、問題発生時のデバッグを容易に

運用上の知見として、claude-code-actionのサンドボックス制約(環境変数の不可視、OIDC検証によるワークフロー変更制限)を理解し、ロジックを外部ファイルに切り出す設計が重要でした。

毎朝30分以上かかっていたCI失敗対応が、PRのレビューとマージだけで完了するようになりました。
「AIに任せられる単純作業は任せて、人間はテスト戦略やシナリオ設計に集中する」という方針を、また一歩前に進められたと思います。


kickflowでは、一緒にプロダクトを創り上げてくれる仲間を募集しています!
少しでもご興味をお持ちいただけた方は、ぜひ採用サイトをご覧ください。

careers.kickflow.co.jp