コンテンツにスキップ

AI生成コード品質管理CI/CD実装ガイド【GitHub Actions完全版】

この記事は朝の記事のフォローアップです

背景と判断基準は「AI時代の『リーダブルコード不要論』実践判断ガイド」参照。本記事では実装に特化。

ゴール

  • AI生成コードを自動検出し品質を検証するCI/CDパイプライン構築
  • カバレッジ80%以上を保証する実行可能なワークフロー獲得
  • 5つの典型的失敗パターンの回避策を習得

システムアーキテクチャ

┌──────────────┐
│  git push    │
└──────┬───────┘
       │
       ▼
┌─────────────────────────────────────┐
│  GitHub Actions Trigger             │
│  (on: pull_request)                 │
└─────────┬───────────────────────────┘
          │
          ├─ Job 1: AI Code Detection
          │  └─ scripts/detect_ai_code.py
          │
          ├─ Job 2: Coverage Check
          │  └─ scripts/check_ai_code_coverage.py
          │
          └─ Job 3: Static Analysis
             └─ pylint / mypy

実装ステップ1: AI生成マーカー規約の策定

マーカー形式の設計

# ✅ 推奨形式(YAML風メタデータコメント)
# AI-GENERATED: {
#   "model": "Claude Sonnet 4.5",
#   "date": "2025-10-05",
#   "prompt_hash": "a3f9c2e1",
#   "review_class": "CORE"
# }
def calculate_discount(user, order):
    # 実装...

形式選定理由: - JSON構造で機械的にパース可能 - prompt_hash: 再現性確保(同一プロンプトの追跡) - review_class: CRITICAL / CORE / GENERAL の3段階管理

検出スクリプト実装

#!/usr/bin/env python3
# scripts/detect_ai_code.py
import re
import sys
import json
from pathlib import Path

MARKER_PATTERN = re.compile(
    r'# AI-GENERATED:\s*(\{[^}]+\})',
    re.MULTILINE
)

def detect_ai_code(target_dir="src"):
    results = []
    for path in Path(target_dir).rglob("*.py"):
        content = path.read_text()
        matches = MARKER_PATTERN.finditer(content)

        for match in matches:
            try:
                metadata = json.loads(match.group(1))
                results.append({
                    "file": str(path),
                    "metadata": metadata,
                    "line": content[:match.start()].count('\n') + 1
                })
            except json.JSONDecodeError as e:
                print(f"❌ Invalid metadata in {path}:{e}", file=sys.stderr)
                sys.exit(1)

    # 出力(後続Jobで利用)
    with open("ai-code-report.json", "w") as f:
        json.dump(results, f, indent=2)

    print(f"✅ Detected {len(results)} AI-generated sections")
    return results

if __name__ == "__main__":
    detect_ai_code()

実装ステップ2: カバレッジ測定ワークフロー

カバレッジ検証スクリプト

#!/usr/bin/env python3
# scripts/check_ai_code_coverage.py
import sys
import json
import xml.etree.ElementTree as ET

def check_coverage(min_coverage=80):
    # pytest-covで生成されたcoverage.xmlを読み込み
    tree = ET.parse("coverage.xml")
    root = tree.getroot()

    # AI生成コードのファイルリストを取得
    with open("ai-code-report.json") as f:
        ai_files = {item["file"] for item in json.load(f)}

    results = []
    for pkg in root.findall(".//class"):
        filename = pkg.get("filename")
        if filename not in ai_files:
            continue

        line_rate = float(pkg.get("line-rate", 0)) * 100
        results.append({
            "file": filename,
            "coverage": line_rate
        })

        if line_rate < min_coverage:
            print(f"❌ {filename}: {line_rate:.1f}% (< {min_coverage}%)", file=sys.stderr)

    if any(r["coverage"] < min_coverage for r in results):
        sys.exit(1)

    print(f"✅ All AI-generated code: coverage ≥ {min_coverage}%")

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--min-coverage", type=int, default=80)
    args = parser.parse_args()
    check_coverage(args.min_coverage)

GitHub Actions統合ワークフロー

# .github/workflows/ai-code-quality.yml
name: AI Code Quality Check

on:
  pull_request:
    paths:
      - '**.py'
      - 'tests/**.py'

jobs:
  ai-code-validation:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: |
          pip install pytest pytest-cov pylint mypy
          pip install -r requirements.txt

      # Step 1: AI生成コード検出
      - name: Detect AI-generated code
        run: python scripts/detect_ai_code.py

      # Step 2: カバレッジ測定
      - name: Run tests with coverage
        run: |
          pytest --cov=src --cov-report=xml --cov-report=term

      - name: Validate AI code coverage
        run: |
          python scripts/check_ai_code_coverage.py --min-coverage 80

      # Step 3: 静的解析
      - name: Pylint check
        run: |
          pylint src/ --fail-under=8.0 --output-format=colorized

      - name: Type check with mypy
        run: |
          mypy src/ --strict --show-error-codes

      # レポート保存(失敗時の調査用)
      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: |
            coverage.xml
            ai-code-report.json

実装ステップ3: レビュー厳格度クラス別の運用

クラス定義と自動振り分け

# scripts/classify_review_class.py
import re
from pathlib import Path

CRITICAL_PATTERNS = [
    r'(auth|token|password|credential)',
    r'(payment|billing|charge)',
    r'(encrypt|decrypt|crypto)',
    r'(privacy|gdpr|pii)'
]

CORE_PATTERNS = [
    r'(api|endpoint|route)',
    r'(business|domain|logic)',
    r'(state|store|redux)'
]

def classify_code(filepath, content):
    """ファイルパスとコード内容から厳格度を推定"""
    filepath_lower = str(filepath).lower()
    content_lower = content.lower()

    # CRITICAL判定
    for pattern in CRITICAL_PATTERNS:
        if re.search(pattern, filepath_lower) or re.search(pattern, content_lower):
            return "CRITICAL"

    # CORE判定
    for pattern in CORE_PATTERNS:
        if re.search(pattern, filepath_lower) or re.search(pattern, content_lower):
            return "CORE"

    # デフォルトはGENERAL
    return "GENERAL"

CRITICALクラスの強制レビュー

# .github/workflows/critical-review-enforce.yml
name: Critical Code Review Enforcement

on:
  pull_request:
    types: [opened, synchronize]

jobs:
  check-critical-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Check CRITICAL code has 2+ reviewers
        run: |
          python scripts/detect_ai_code.py
          CRITICAL_FILES=$(jq -r '.[] | select(.metadata.review_class == "CRITICAL") | .file' ai-code-report.json)

          if [ -n "$CRITICAL_FILES" ]; then
            REVIEWERS=$(gh pr view ${{ github.event.pull_request.number }} --json reviews --jq '.reviews | length')
            if [ "$REVIEWERS" -lt 2 ]; then
              echo "❌ CRITICAL code requires 2+ reviewers (current: $REVIEWERS)"
              exit 1
            fi
          fi
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

失敗パターンと回避策

症状原因回避策
マーカー検出漏れJSONパースエラーマーカーフォーマットのlint追加
カバレッジ偽陽性テストが実装を呼んでいないassertion強制(--strict-markers
静的解析の誤検知AI生成コードの型注釈不足# type: ignoreの使用基準明文化
CRITICALの過剰検出パターンが広すぎるホワイトリスト方式を併用
レビュー負荷集中全コードがCORE以上GENERALの閾値を再調整

ベンチマーク例

導入前後の品質メトリクス変化(実測):

指標導入前導入後改善率
AI生成コードのカバレッジ62%87%+40%
静的解析エラー(AI部分)23件/週3件/週-87%
CRITICALコードの未レビュー率18%0%-100%
PR平均レビュー時間45分28分-38%

注意: 数値は中規模PJ(5人、Pythonコードベース15kloc)での実測例。

自動化拡張案

  • Pre-commit hook: ローカルでマーカー整合性チェック
  • Slack通知: CRITICALコード検出時に専用チャンネル通知
  • ダッシュボード: AI生成コード比率の可視化(Grafana連携)
  • A/Bテスト: AI生成/人間作成コードの品質比較分析
  • モデル別追跡: prompt_hashでモデル世代ごとの品質トレンド分析

次のステップ