T-CREATOR

Devin の提案がビルドを壊す時の対処法:差分最小化・二分探索・自動ロールバック

Devin の提案がビルドを壊す時の対処法:差分最小化・二分探索・自動ロールバック

AI エージェント「Devin」が自動で提案するコード変更は便利ですが、時としてビルドエラーを引き起こしてしまうこともあります。そんな時、どこに問題があるのかを見つけ、効率よく修正するための手法を本記事でご紹介しましょう。 差分最小化、二分探索、自動ロールバックという 3 つのアプローチを使えば、Devin の提案を安全に取り込みながら、ビルドの破損を最小限に抑えられます。

背景

AI エージェント Devin とは

Devin は、開発者の作業を支援する AI エージェントとして注目を集めています。コードレビュー、リファクタリング、新機能の実装提案など、多岐にわたるタスクを自動で行ってくれるため、生産性向上に大きく貢献してくれるでしょう。

しかし、Devin が生成するコードは完璧ではありません。依存関係の考慮漏れ、型定義の不一致、環境変数の誤設定など、さまざまな理由でビルドエラーが発生する可能性があります。

ビルドエラーが起こりやすい理由

Devin のようなコード生成 AI は、以下の理由でビルドを壊してしまうことがあります。

  • 依存関係の把握不足: プロジェクト全体の依存関係を完全に理解していない場合がある
  • 型システムの複雑性: TypeScript などの型システムを正確に追えない
  • 環境依存の設定: ローカル環境と本番環境の差異を考慮できていない
  • 複数ファイルの同時変更: 変更箇所が多すぎて、整合性が取れなくなる

以下の図は、AI エージェントによるコード提案から、ビルドエラー発生までの典型的な流れを示しています。

mermaidflowchart TD
  dev["開発者"] -->|タスク依頼| ai["AI エージェント<br/>Devin"]
  ai -->|コード生成| files["複数ファイル変更"]
  files -->|依存関係の<br/>考慮漏れ| error1["型エラー"]
  files -->|環境変数の<br/>設定ミス| error2["実行時エラー"]
  files -->|パッケージ<br/>バージョン不一致| error3["ビルドエラー"]
  error1 --> fail["ビルド失敗"]
  error2 --> fail
  error3 --> fail

このように、AI による提案が複数ファイルに及ぶ場合、どこに問題があるのかを特定するのは容易ではありません。

課題

ビルドエラーの原因特定が困難

Devin が複数のファイルを同時に変更した場合、どのファイルのどの部分がビルドエラーの原因なのかを特定するのは非常に困難です。エラーメッセージが複数出力されることもあり、本質的な原因を見極めるには時間がかかります。

手作業でのロールバックは非効率

問題が起きたときに全変更を手作業で巻き戻すのは、時間がかかるだけでなく、ミスも発生しやすくなります。特に、Git の操作に不慣れな開発者にとっては、大きな負担となるでしょう。

複数の変更が絡み合う問題

以下の表は、Devin の提案によるビルドエラーでよく見られる問題パターンをまとめたものです。

#問題パターン発生原因影響範囲
1型定義の不一致インターフェース変更の反映漏れ複数ファイル
2依存関係の破損パッケージバージョンの不整合プロジェクト全体
3環境変数の誤設定.env ファイルの更新漏れ実行時エラー
4インポートパスの誤りファイル移動時のパス更新漏れコンパイルエラー
5テストの失敗モックやスタブの更新漏れCI/CD パイプライン

これらの問題は、単独で発生することもあれば、複数が同時に発生することもあります。

下図は、複数の変更が絡み合ってビルドエラーを引き起こす様子を示しています。

mermaidflowchart LR
  change1["ファイル A<br/>型定義変更"] -->|影響| file_b["ファイル B"]
  change2["ファイル C<br/>関数追加"] -->|影響| file_b
  change3["ファイル D<br/>インポート変更"] -->|影響| file_b
  file_b -->|型エラー| build_fail["ビルド失敗"]

  style build_fail fill:#ff6b6b

このように、複数の変更が同時に行われると、問題の切り分けが難しくなります。

解決策

ビルドエラーの原因を効率よく特定し、修正するためには、以下の 3 つのアプローチが有効です。それぞれの手法を順番に見ていきましょう。

1. 差分最小化アプローチ

差分最小化とは、変更を小さな単位に分割し、一つずつ適用していく手法です。これにより、どの変更がビルドエラーを引き起こしているのかを特定しやすくなります。

以下の図は、差分最小化のプロセスを示しています。

mermaidflowchart TD
  all["全変更を取得"] --> split["変更を小単位に<br/>分割"]
  split --> apply1["変更 1 を適用"]
  apply1 --> test1{ビルド<br/>成功?}
  test1 -->|Yes| apply2["変更 2 を適用"]
  test1 -->|No| fix1["変更 1 を修正"]
  fix1 --> apply1
  apply2 --> test2{ビルド<br/>成功?}
  test2 -->|Yes| apply3["変更 3 を適用"]
  test2 -->|No| fix2["変更 2 を修正"]
  fix2 --> apply2
  apply3 --> done["完了"]

  style done fill:#51cf66

この手法により、問題のある変更を早期に発見でき、修正も容易になります。

変更を小単位に分割する方法

Git のコミット履歴や、Devin が提供する差分情報を活用して、変更を以下のような単位に分割します。

  • ファイル単位
  • 関数単位
  • 機能単位(例:認証機能、データ取得機能など)

Git を使った差分の適用

以下は、Git を使って変更を段階的に適用する手順です。

変更の一時的な退避

bash# 現在の変更を全てスタッシュに退避
git stash

これで、作業ディレクトリがクリーンな状態になります。

変更の一部を適用

bash# スタッシュから特定のファイルだけを復元
git checkout stash@{0} -- path/to/file.ts

ファイルパスを指定することで、必要な変更だけを取り出せます。

ビルドテストの実行

bash# ビルドが成功するか確認
yarn build

ビルドが成功すれば、その変更は問題ないと判断できます。

変更のコミット

bash# 問題なければコミット
git add path/to/file.ts
git commit -m "feat: ファイル A の変更を適用"

この手順を繰り返すことで、安全に変更を取り込んでいけるでしょう。

2. 二分探索アプローチ

変更が多い場合、差分最小化だけでは時間がかかりすぎることがあります。そんな時に有効なのが、二分探索アプローチです。

二分探索は、変更を半分ずつ適用していき、問題のある範囲を効率よく絞り込む手法となります。

二分探索の仕組み

以下の図は、二分探索で問題の変更を特定するプロセスを示しています。

mermaidflowchart TD
  start["全変更<br/>100 件"] --> half1["前半 50 件を<br/>適用"]
  half1 --> test1{ビルド<br/>成功?}
  test1 -->|Yes| half2["後半 50 件を<br/>適用"]
  test1 -->|No| quarter1["前半の前半<br/>25 件を適用"]
  quarter1 --> test2{ビルド<br/>成功?}
  test2 -->|Yes| quarter2["前半の後半<br/>25 件を適用"]
  test2 -->|No| eighth1["12 件に絞り込み"]
  eighth1 --> found["問題の変更を<br/>特定"]

  style found fill:#51cf66

この手法により、100 件の変更から問題を特定する場合でも、最大 7 回程度のテストで原因を見つけられます。

Git bisect を使った実装

Git には、二分探索を自動で行う git bisect というコマンドがあります。ここでは、ビルドエラーの原因特定に応用する方法をご紹介しましょう。

bisect セッションの開始

bash# 二分探索を開始
git bisect start

# 現在のコミット(エラーがある)をマーク
git bisect bad

# エラーが発生していなかった過去のコミットをマーク
git bisect good <commit-hash>

これで Git が自動的に中間のコミットをチェックアウトします。

ビルドテストの実行と結果の報告

bash# ビルドを実行
yarn build

# 成功した場合
git bisect good

# 失敗した場合
git bisect bad

この操作を繰り返すことで、Git が自動的に問題のあるコミットを特定してくれるでしょう。

bisect セッションの終了

bash# 問題のコミットが特定されたら終了
git bisect reset

これで元のブランチの状態に戻ります。

自動化スクリプトの作成

bisect プロセスをさらに効率化するために、ビルドテストを自動化できます。

ビルドテストスクリプトの作成

typescript// scripts/build-test.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

まず、必要なモジュールをインポートします。exec を使ってシェルコマンドを実行し、promisify で Promise 化することで、async/await が使えるようになります。

ビルド実行関数の定義

typescriptasync function runBuildTest(): Promise<boolean> {
  try {
    // ビルドコマンドを実行
    const { stdout, stderr } = await execAsync(
      'yarn build'
    );

    console.log('ビルド成功:', stdout);
    return true;
  } catch (error) {
    console.error('ビルド失敗:', error);
    return false;
  }
}

この関数は、ビルドが成功すれば true、失敗すれば false を返します。

終了コードの設定

typescriptasync function main() {
  const isSuccess = await runBuildTest();

  // 終了コード: 0 = 成功、1 = 失敗
  process.exit(isSuccess ? 0 : 1);
}

main();

process.exit() で終了コードを設定することで、Git bisect がビルドの成否を判断できるようになります。

自動 bisect の実行

bash# スクリプトを使った自動 bisect
git bisect start
git bisect bad
git bisect good <commit-hash>
git bisect run node scripts/build-test.ts

これで、Git が自動的にビルドテストを繰り返し、問題のあるコミットを特定してくれます。

3. 自動ロールバック機能の実装

ビルドエラーが発生した際に、自動的に安全な状態に戻る仕組みを作っておくと、開発の効率が大幅に向上します。

以下の図は、自動ロールバックのフローを示しています。

mermaidflowchart TD
  start["変更適用"] --> backup["現在の状態を<br/>バックアップ"]
  backup --> apply["新しい変更を<br/>適用"]
  apply --> build{ビルド<br/>テスト}
  build -->|成功| commit["変更をコミット"]
  build -->|失敗| rollback["自動ロールバック"]
  rollback --> restore["バックアップから<br/>復元"]
  restore --> notify["開発者に通知"]
  commit --> done["完了"]

  style done fill:#51cf66
  style rollback fill:#ffd43b

この仕組みにより、ビルドが壊れた状態で作業を続けるリスクを減らせます。

ロールバックスクリプトの設計

自動ロールバック機能を実装するために、以下の要素が必要です。

#機能説明実装方法
1状態のバックアップ変更前の状態を保存Git stash / ブランチ作成
2ビルドテスト変更後のビルド確認yarn build / npm run test
3ロールバック処理失敗時の復元Git reset / stash pop
4通知機能開発者への報告コンソール出力 / Slack 通知
5ログ記録エラー情報の保存ファイル出力 / データベース

これらの機能を組み合わせて、堅牢なロールバックシステムを構築しましょう。

TypeScript による実装例

以下は、自動ロールバック機能の実装例です。

必要なモジュールのインポート

typescript// scripts/auto-rollback.ts
import { exec } from 'child_process';
import { promisify } from 'util';
import * as fs from 'fs/promises';
import * as path from 'path';

const execAsync = promisify(exec);

ファイルシステム操作のために fs​/​promises も追加しています。

ログ記録関数の定義

typescriptasync function logError(
  message: string,
  error: any
): Promise<void> {
  const timestamp = new Date().toISOString();
  const logDir = path.join(process.cwd(), 'logs');
  const logFile = path.join(logDir, 'rollback.log');

  // ログディレクトリが存在しない場合は作成
  await fs.mkdir(logDir, { recursive: true });

  // ログメッセージの作成
  const logMessage = `[${timestamp}] ${message}\n${JSON.stringify(
    error,
    null,
    2
  )}\n\n`;

  // ログファイルに追記
  await fs.appendFile(logFile, logMessage);
}

エラー情報をタイムスタンプ付きで記録することで、後から原因を調査しやすくなります。

バックアップ作成関数

typescriptasync function createBackup(): Promise<string> {
  try {
    // ユニークなバックアップ名を生成
    const timestamp = Date.now();
    const backupBranch = `backup/auto-${timestamp}`;

    // 現在の変更を一時的にコミット
    await execAsync('git add .');
    await execAsync(
      `git commit -m "一時保存: ${timestamp}"`
    );

    // バックアップブランチを作成
    await execAsync(`git branch ${backupBranch}`);

    console.log(
      `✓ バックアップを作成しました: ${backupBranch}`
    );
    return backupBranch;
  } catch (error) {
    await logError('バックアップ作成に失敗', error);
    throw error;
  }
}

バックアップブランチを作成することで、いつでも安全な状態に戻れるようにします。

ビルドテスト実行関数

typescriptasync function runBuildTest(): Promise<boolean> {
  try {
    console.log('ビルドテストを実行中...');

    // 依存関係のインストール
    await execAsync('yarn install --frozen-lockfile');

    // ビルドの実行
    await execAsync('yarn build');

    // テストの実行(オプション)
    await execAsync('yarn test');

    console.log('✓ ビルドテストが成功しました');
    return true;
  } catch (error) {
    console.error('✗ ビルドテストが失敗しました');
    await logError('ビルドテスト失敗', error);
    return false;
  }
}

ビルドだけでなく、テストも実行することで、より確実な検証ができます。

ロールバック実行関数

typescriptasync function performRollback(
  backupBranch: string
): Promise<void> {
  try {
    console.log(`ロールバックを実行中: ${backupBranch}`);

    // バックアップブランチに切り替え
    await execAsync(`git checkout ${backupBranch}`);

    // 元のブランチに戻る
    await execAsync('git checkout -');

    // バックアップの内容で上書き
    await execAsync(`git reset --hard ${backupBranch}`);

    console.log('✓ ロールバックが完了しました');
  } catch (error) {
    await logError('ロールバック失敗', error);
    throw error;
  }
}

Git の機能を使って、安全かつ確実にロールバックを実行します。

メイン処理の実装

typescriptasync function main() {
  let backupBranch: string | null = null;

  try {
    // 1. バックアップを作成
    backupBranch = await createBackup();

    // 2. ビルドテストを実行
    const buildSuccess = await runBuildTest();

    // 3. 結果に応じて処理を分岐
    if (buildSuccess) {
      console.log('✓ すべてのテストが成功しました');
      console.log(
        `バックアップブランチ ${backupBranch} は不要なら削除できます`
      );
      process.exit(0);
    } else {
      console.error(
        '✗ ビルドテストが失敗したため、ロールバックします'
      );
      await performRollback(backupBranch);
      process.exit(1);
    }
  } catch (error) {
    console.error('予期しないエラーが発生しました:', error);

    // バックアップがあればロールバックを試みる
    if (backupBranch) {
      try {
        await performRollback(backupBranch);
      } catch (rollbackError) {
        console.error(
          'ロールバックも失敗しました:',
          rollbackError
        );
      }
    }

    process.exit(1);
  }
}

main();

エラーハンドリングを含めた完全な自動ロールバック処理です。予期しないエラーが発生した場合でも、可能な限りロールバックを試みます。

実行方法

作成したスクリプトは、以下のように実行します。

スクリプトのビルド

bash# TypeScript をコンパイル
yarn tsc scripts/auto-rollback.ts --outDir scripts/dist

TypeScript ファイルを JavaScript にコンパイルします。

スクリプトの実行

bash# ロールバック機能付きでビルドテスト
node scripts/dist/auto-rollback.js

これで、ビルドエラーが発生した場合に自動的にロールバックされます。

package.json への統合

json{
  "scripts": {
    "safe-build": "node scripts/dist/auto-rollback.js",
    "build": "next build",
    "test": "jest"
  }
}

npm scripts に登録しておくことで、簡単に実行できるようになります。

bash# safe-build コマンドで安全なビルドを実行
yarn safe-build

このコマンド一つで、バックアップ、ビルド、必要に応じたロールバックが自動的に行われます。

具体例

ここでは、実際のプロジェクトで Devin の提案がビルドを壊してしまった場合の対処例を見ていきましょう。

シナリオ: Devin による大規模リファクタリング

あるプロジェクトで、Devin に「認証機能を最新のライブラリに移行する」というタスクを依頼しました。Devin は以下の変更を提案しています。

  • auth.ts: 認証ロジックの更新(50 行)
  • middleware.ts: ミドルウェアの変更(30 行)
  • user.interface.ts: ユーザー型定義の更新(20 行)
  • login.tsx: ログイン画面の修正(40 行)
  • package.json: 依存関係の更新

これら全ての変更を一度に適用したところ、ビルドエラーが多数発生してしまいました。

以下の図は、この問題を解決するプロセスを示しています。

mermaidflowchart TD
  error["ビルドエラー<br/>発生"] --> analyze["エラーログを<br/>確認"]
  analyze --> strategy{解決<br/>戦略}
  strategy -->|変更が少ない| minimal["差分最小化<br/>アプローチ"]
  strategy -->|変更が多い| bisect["二分探索<br/>アプローチ"]
  minimal --> apply_one["1 ファイルずつ<br/>適用"]
  bisect --> apply_half["半分ずつ<br/>適用"]
  apply_one --> test1{テスト}
  apply_half --> test2{テスト}
  test1 -->|失敗| fix1["修正"]
  test1 -->|成功| next1["次へ"]
  test2 -->|失敗| narrow["範囲を<br/>絞り込み"]
  test2 -->|成功| next2["次へ"]
  fix1 --> apply_one
  narrow --> apply_half
  next1 --> complete["完了"]
  next2 --> complete

  style complete fill:#51cf66

ステップ 1: エラーログの確認

まず、ビルドエラーの内容を確認します。

bash# ビルドを実行してエラーを確認
yarn build

以下のようなエラーが出力されました。

typescriptError: TS2339: Property 'userId' does not exist on type 'User'.
Error: TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
Error: Module '"next-auth"' has no exported member 'SessionProvider'.

複数の型エラーとインポートエラーが発生しています。

ステップ 2: 差分最小化アプローチを適用

変更ファイルが 5 つと比較的少ないため、差分最小化アプローチを選択します。

すべての変更を一旦退避

bash# 現在の変更をスタッシュに保存
git stash save "Devin の認証機能リファクタリング提案"

これで、作業ディレクトリがクリーンな状態に戻りました。

型定義から段階的に適用

依存関係を考慮して、型定義から適用していきます。

bash# ステップ 1: 型定義の変更を適用
git checkout stash@{0} -- src/types/user.interface.ts

# ビルドテスト
yarn build

この段階でビルドが成功することを確認します。

bash# ステップ 2: 依存関係の更新
git checkout stash@{0} -- package.json
yarn install

# ビルドテスト
yarn build

ここでエラーが発生しました。package.json の変更に問題があることが判明しました。

ステップ 3: 問題の修正

package.json を確認すると、next-auth のバージョンが互換性のないものに更新されていました。

問題のある変更を修正

json{
  "dependencies": {
    "next-auth": "^4.24.5"
  }
}

Devin の提案では ^5.0.0 になっていましたが、プロジェクトの他の部分がまだ v4 に依存しているため、バージョンを戻します。

bash# 修正後、再度インストール
yarn install

# ビルドテスト
yarn build

今度はビルドが成功しました。

ステップ 4: 残りの変更を適用

bash# ステップ 3: 認証ロジックの変更
git checkout stash@{0} -- src/lib/auth.ts
yarn build  # 成功を確認

# ステップ 4: ミドルウェアの変更
git checkout stash@{0} -- src/middleware.ts
yarn build  # 成功を確認

# ステップ 5: UI コンポーネントの変更
git checkout stash@{0} -- src/components/login.tsx
yarn build  # 成功を確認

すべての変更を段階的に適用し、それぞれのステップでビルドが成功することを確認できました。

ステップ 5: 最終確認とコミット

bash# すべてのテストを実行
yarn test

# 問題なければコミット
git add .
git commit -m "feat: 認証機能を next-auth v4 対応にリファクタリング"

このように、差分最小化アプローチを使うことで、問題のある変更を素早く特定し、修正できました。

結果の比較

以下の表は、従来の方法と本記事で紹介した手法の比較です。

#項目従来の方法差分最小化二分探索自動ロールバック
1問題特定時間2-3 時間30-60 分15-30 分自動(数分)
2修正の確実性★★☆☆☆★★★★☆★★★★★★★★★★
3学習コスト
4自動化可能性なし部分的可能完全
5チーム適用性個人依存標準化可能標準化可能標準化可能

これらの手法を組み合わせることで、Devin のようなコード生成 AI を安全に活用できるようになります。

まとめ

Devin が提案するコード変更でビルドが壊れてしまった場合でも、適切な手法を使えば効率よく問題を解決できます。本記事で紹介した 3 つのアプローチを振り返りましょう。

差分最小化アプローチは、変更を小さな単位に分割して段階的に適用する手法です。変更が比較的少ない場合や、依存関係が明確な場合に効果的でしょう。Git のスタッシュ機能を活用することで、安全に変更を取り込めます。

二分探索アプローチは、変更が多い場合に威力を発揮します。git bisect を使えば、数十から数百の変更の中から問題のあるコミットを効率よく特定できるでしょう。自動化スクリプトと組み合わせることで、さらに効率が向上します。

自動ロールバック機能は、ビルドエラーが発生した際に自動的に安全な状態に戻す仕組みです。バックアップ作成、ビルドテスト、ロールバック処理を自動化することで、開発者の負担を大幅に軽減できます。

これらの手法を組み合わせることで、AI エージェントの提案を安全かつ効率的に取り込めるようになります。最初は差分最小化から始めて、慣れてきたら二分探索や自動ロールバックにも挑戦してみてください。

AI との協働開発は、適切なツールと手法を使うことで、より安全で生産的なものになるでしょう。ぜひ、今回紹介した方法を実践してみてください。

関連リンク