0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actionsで変更された機能だけをテストするCI/CD設計

Posted at

はじめに

モノレポで複数の機能を管理していると、CI/CDの実行時間が課題になります。すべての機能のテストを毎回実行していると、開発効率が下がってしまいます。

今回は、変更された機能だけを自動検出してテストを実行するGitHub Actionsワークフローを実装した事例を紹介します。この仕組みにより、CIの実行時間を大幅に短縮できました。

また、approve時などに全テストを実行することで、品質の担保を行うことも可能です。

課題と解決策

課題

  • 5つの機能(function-a、function-b、function-c、function-d、function-e)を持つ統合システム
  • 従来は全機能のテストを毎回実行(約15-20分)
  • 変更していない機能のテストも実行される無駄

解決策

  • 変更されたファイルを自動検出
  • 影響を受けた機能のみテストを実行
  • 堅牢な変更検出ロジックでエッジケースにも対応

実装の詳細

1. 変更検出ジョブの設計

まず、変更されたファイルを検出するジョブを作成します:

detect_changed_files:
  name: Detect changed files
  runs-on: normal
  container: /ci-images/golang:latest
  outputs:
    function-a: ${{ steps.changes.outputs.function-a }}
    function-b: ${{ steps.changes.outputs.function-b }}
    function-c: ${{ steps.changes.outputs.function-c }}
    function-d: ${{ steps.changes.outputs.function-d }}
    function-e: ${{ steps.changes.outputs.function-e }}

2. 堅牢な変更検出ロジック

# より堅牢な変更検出ロジック
if git rev-parse --verify origin/main >/dev/null 2>&1; then
  CHANGED_FILES=$(git diff --name-only origin/main...HEAD)
  echo "✅ Comparing against origin/main"
else
  # 既存のフォールバックロジック
  if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then
    CHANGED_FILES=$(git ls-tree --name-only HEAD)
    echo "⚠️ New branch: listing all files"
  else
    CHANGED_FILES=$(git diff --name-only "${{ github.event.before }}" "${{ github.sha }}")
    echo "⚠️ Fallback: comparing against previous commit"
  fi
fi

このロジックのポイント:

  1. origin/mainとの比較を優先: 最も確実な比較方法
  2. フォールバック機能: 新規ブランチや特殊なケースにも対応
  3. エラーハンドリング: 各段階で適切な処理を実行

3. 機能別の変更検出

function-a_changed=false
function-b_changed=false
function-c_changed=false
function-d_changed=false
function-e_changed=false

while read -r file; do
  if [[ "$file" == function-a/* ]]; then
    function-a_changed=true
  elif [[ "$file" == function-b/* ]]; then
    function-b_changed=true
  elif [[ "$file" == function-c/* ]]; then
    function-c_changed=true
  elif [[ "$file" == function-d/* ]]; then
    function-d_changed=true
  elif [[ "$file" == function-e/* ]]; then
    function-e_changed=true
  fi
done <<< "$CHANGED_FILES"

4. 構造化ログ出力

デバッグとモニタリングのために、JSON形式でログを出力:

echo "{"
echo "  \"workflow\": \"selective_function_test\","
echo "  \"event\": \"${{ github.event_name }}\","
echo "  \"branch\": \"${{ github.ref_name }}\","
echo "  \"functions_affected\": {"
echo "    \"function-a\": $function-a_changed,"
echo "    \"function-b\": $function-b_changed,"
echo "    \"function-c\": $function-c_changed,"
echo "    \"function-d\": $function-d_changed,"
echo "    \"function-e\": $function-e_changed"
echo "  }"
echo "}"

5. 条件付きテスト実行

各機能のテストジョブは、変更検出の結果に基づいて条件付きで実行されます:

test_function-a:
  name: Run Unit test and Integration (function-a)
  runs-on: normal
  needs: [detect_changed_files]
  if: needs.detect_changed_files.outputs.function-a == 'true'
  container:
    image: /ci-images/golang:latest
  functions:
    sales_db:
      image: /databases/sales-db:latest
      env:
        MYSQL_ALLOW_EMPTY_PASSWORD: true
      options: >-
        --health-cmd "mysqladmin ping -h localhost"
        --health-interval 20s
        --health-timeout 10s
        --health-retries 10
  steps:
    - name: Test
      run: make test/function-a

実際の効果

Before(改善前)

  • 全機能のテスト実行: 15-20分
  • 変更していない機能もテスト実行
  • リソースの無駄遣い

After(改善後)

  • 変更された機能のみテスト実行: 3-8分
  • 実行時間を60-70%短縮
  • 開発者の待ち時間を大幅削減

実装時の注意点

1. 依存関係の考慮

機能間の依存関係がある場合は、追加のロジックが必要です:

# 例:function-dが変更された場合、function-eもテストする
if [[ "$file" == function-d/* ]]; then
  function-d_changed=true
  function-e_changed=true  # 依存機能も含める
fi

2. 共通ライブラリの変更

共通ライブラリが変更された場合は、全機能をテストする必要があります:

# 共通ライブラリの変更検出
if [[ "$file" == cmd/* ]] || [[ "$file" == go.mod ]] || [[ "$file" == go.sum ]]; then
  function-a_changed=true
  function-b_changed=true
  function-c_changed=true
  function-d_changed=true
  function-e_changed=true
fi

注意: この記事では基本的な変更検出ロジックを紹介しています。実際の実装では、共通ライブラリの変更検出や機能間の依存関係の考慮など、追加のロジックが必要になる場合があります。

3. テスト結果の集約

最後に、テスト結果を集約してビルドと通知を行います:

build:
  name: Build
  runs-on: normal
  needs:
    - test_function-a
    - test_function-b
    - test_function-c
    - test_function-d
  if: >-
    always() && (
      needs.test_function-a.result == 'success' ||
      needs.test_function-b.result == 'success' ||
      needs.test_function-c.result == 'success' ||
      needs.test_function-d.result == 'success'
    )

応用可能なパターン

このパターンは他のプロジェクトでも応用できます:

  1. マイクロ機能: 各機能を独立してテスト
  2. フロントエンド: コンポーネント別のテスト実行
  3. インフラ: 環境別のデプロイメント
  4. ドキュメント: 変更されたドキュメントのみビルド

まとめ

変更された機能だけをテストするCI/CD設計により、以下の効果を実現しました:

  • 実行時間の大幅短縮(60-70%削減)
  • 開発効率の向上
  • リソースコストの削減
  • 堅牢な変更検出ロジック

ぜひ皆さんのプロジェクトでも試してみてください!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?