Skip to content

MR 報告資料自動生成設計

1. 概要

Claude Code がコード変更から自動で MR 報告資料を生成するフローを定義する。git diff からの変更サマリー自動生成、テスト結果の自動収集・フォーマット、スクリーンショット取得の自動化を含む。

目標: レビュアーが MR 内の報告資料のみでレビュー完了できる粒度の資料を、エンジニアの手作業なしで自動生成する。


2. 自動生成フロー全体図

mermaid
graph TD
    A["MR 作成トリガー"] --> B["git diff 解析"]
    B --> C["変更サマリー生成"]
    A --> D["テスト実行"]
    D --> E["テスト結果収集"]
    A --> F["スクリーンショット取得"]

    C --> G["報告資料アセンブル"]
    E --> G
    F --> G

    G --> H["MR Description に反映"]
    G --> I["MR コメントに詳細添付"]

    style A fill:#e1f5fe
    style G fill:#fff3e0
    style H fill:#c8e6c9
    style I fill:#c8e6c9

3. git diff からの変更サマリー自動生成

3.1 差分取得

bash
# メインブランチとの差分を取得
git diff main...HEAD --stat
git diff main...HEAD --name-status
git diff main...HEAD

3.2 解析パイプライン

mermaid
graph LR
    A["git diff"] --> B["ファイル分類"]
    B --> C["変更種別判定"]
    C --> D["影響範囲分析"]
    D --> E["サマリー生成"]

3.3 ファイル分類ロジック

分類パターン重要度
API変更backend/api/**, **/routes/**
DB変更**/migrations/**, **/models/**
UI変更frontend/src/components/**, **/*.tsx
テスト**/tests/**, **/*.test.*
設定*.config.*, *.json, *.yaml
ドキュメントdocs/**, *.md
CI/CD.gitlab-ci.yml, .github/**

3.4 変更サマリー生成の実装

python
# backend/services/mr_report_generator.py

from dataclasses import dataclass
from typing import List
import subprocess
import json


@dataclass
class FileChange:
    path: str
    status: str  # A(dded), M(odified), D(eleted), R(enamed)
    category: str
    additions: int
    deletions: int
    summary: str  # Claude Code が生成する変更概要


@dataclass
class ChangeSummary:
    title: str
    overview: str
    file_changes: List[FileChange]
    breaking_changes: List[str]
    migration_required: bool


class DiffAnalyzer:
    """git diff を解析して変更サマリーを生成する"""

    CATEGORY_PATTERNS = {
        "api": ["backend/api/", "routes/"],
        "database": ["migrations/", "models/"],
        "ui": ["frontend/src/", ".tsx", ".jsx"],
        "test": ["tests/", ".test.", ".spec."],
        "config": [".config.", ".json", ".yaml", ".toml"],
        "docs": ["docs/", ".md"],
        "ci": [".gitlab-ci", ".github/"],
    }

    def analyze(self, base_branch: str = "main") -> ChangeSummary:
        """差分を解析して ChangeSummary を返す"""
        name_status = self._run_git(
            ["diff", f"{base_branch}...HEAD", "--name-status"]
        )
        stat = self._run_git(
            ["diff", f"{base_branch}...HEAD", "--stat"]
        )
        file_changes = self._parse_changes(name_status)
        return ChangeSummary(
            title=self._generate_title(file_changes),
            overview=self._generate_overview(file_changes, stat),
            file_changes=file_changes,
            breaking_changes=self._detect_breaking_changes(file_changes),
            migration_required=self._check_migration(file_changes),
        )

    def _classify_file(self, path: str) -> str:
        for category, patterns in self.CATEGORY_PATTERNS.items():
            if any(p in path for p in patterns):
                return category
        return "other"

    def _run_git(self, args: list) -> str:
        result = subprocess.run(
            ["git"] + args, capture_output=True, text=True
        )
        return result.stdout

    def _parse_changes(self, name_status: str) -> List[FileChange]:
        changes = []
        for line in name_status.strip().split("\n"):
            if not line:
                continue
            parts = line.split("\t")
            status = parts[0]
            path = parts[1] if len(parts) > 1 else ""
            changes.append(FileChange(
                path=path,
                status=status,
                category=self._classify_file(path),
                additions=0,  # numstat から取得
                deletions=0,
                summary="",   # Claude Code が後で生成
            ))
        return changes

    def _generate_title(self, changes: List[FileChange]) -> str:
        categories = set(c.category for c in changes)
        if "api" in categories and "database" in categories:
            return "API + DB の変更"
        elif "api" in categories:
            return "API の変更"
        elif "ui" in categories:
            return "UI の変更"
        return "コード変更"

    def _generate_overview(
        self, changes: List[FileChange], stat: str
    ) -> str:
        total_files = len(changes)
        by_category = {}
        for c in changes:
            by_category.setdefault(c.category, []).append(c)
        overview = f"合計 {total_files} ファイルを変更\n"
        for cat, files in by_category.items():
            overview += f"- {cat}: {len(files)} ファイル\n"
        return overview

    def _detect_breaking_changes(
        self, changes: List[FileChange]
    ) -> List[str]:
        breaking = []
        for c in changes:
            if c.category == "api" and c.status == "D":
                breaking.append(f"API エンドポイント削除: {c.path}")
            if c.category == "database" and c.status in ("M", "D"):
                breaking.append(f"DB スキーマ変更: {c.path}")
        return breaking

    def _check_migration(self, changes: List[FileChange]) -> bool:
        return any(
            "migration" in c.path.lower() for c in changes
        )

3.5 Claude Code による変更説明の自動生成

Claude Code の reporter エージェント(.claude/agents/reporter.md)が各ファイルの変更内容を読み取り、人間が理解しやすい説明文を生成する。

## 生成される変更サマリーの例

### 変更の目的
GitLab Issue #11 の要件に基づき、dev-workflow スキルと Platform の統合を実装した。

### 変更内容
- backend/api/skills.py: スキル管理 API エンドポイントを新規追加
- backend/services/claude_md_generator.py: CLAUDE.md 自動生成サービスを実装
- frontend/src/components/SkillConfig.tsx: スキル設定 UI を追加
- tests/test_skills_api.py: スキル API のテストを追加

### 技術的なアプローチ
- Jinja2 テンプレートエンジンで CLAUDE.md を動的生成
- プロジェクト設定 DB からスキル情報を取得して注入

4. テスト結果の自動収集・フォーマット

4.1 テスト実行と結果収集

mermaid
graph LR
    A["テストコマンド実行"] --> B["出力パース"]
    B --> C["結果構造化"]
    C --> D["Markdown フォーマット"]

4.2 対応テストフレームワーク

フレームワーク言語出力形式パース方法
pytestPythonJUnit XML / JSON--junitxml=report.xml
JestTypeScriptJSON--json --outputFile=report.json
VitestTypeScriptJSON--reporter=json

4.3 テスト結果パーサー

python
# backend/services/test_result_collector.py

from dataclasses import dataclass
from typing import List, Optional
import json
import xml.etree.ElementTree as ET


@dataclass
class TestResult:
    name: str
    status: str  # passed, failed, skipped, error
    duration_ms: float
    error_message: Optional[str] = None


@dataclass
class TestSuite:
    name: str
    total: int
    passed: int
    failed: int
    skipped: int
    duration_ms: float
    results: List[TestResult]


class TestResultCollector:
    """テスト結果を収集して統一フォーマットに変換する"""

    def collect_pytest(self, xml_path: str) -> TestSuite:
        """pytest の JUnit XML を解析"""
        tree = ET.parse(xml_path)
        root = tree.getroot()
        suite = root.find("testsuite")

        results = []
        for testcase in suite.findall("testcase"):
            failure = testcase.find("failure")
            error = testcase.find("error")
            skipped = testcase.find("skipped")

            if failure is not None:
                status = "failed"
                error_msg = failure.text
            elif error is not None:
                status = "error"
                error_msg = error.text
            elif skipped is not None:
                status = "skipped"
                error_msg = None
            else:
                status = "passed"
                error_msg = None

            results.append(TestResult(
                name=f"{testcase.get('classname')}.{testcase.get('name')}",
                status=status,
                duration_ms=float(testcase.get("time", 0)) * 1000,
                error_message=error_msg,
            ))

        return TestSuite(
            name=suite.get("name", "pytest"),
            total=int(suite.get("tests", 0)),
            passed=sum(1 for r in results if r.status == "passed"),
            failed=int(suite.get("failures", 0)),
            skipped=int(suite.get("skipped", 0)),
            duration_ms=float(suite.get("time", 0)) * 1000,
            results=results,
        )

    def collect_jest(self, json_path: str) -> TestSuite:
        """Jest の JSON レポートを解析"""
        with open(json_path) as f:
            data = json.load(f)

        results = []
        for suite in data.get("testResults", []):
            for test in suite.get("testResults", []):
                results.append(TestResult(
                    name=test["fullName"],
                    status=test["status"],
                    duration_ms=test.get("duration", 0),
                    error_message=(
                        "\n".join(test.get("failureMessages", []))
                        if test.get("failureMessages") else None
                    ),
                ))

        return TestSuite(
            name="jest",
            total=data.get("numTotalTests", 0),
            passed=data.get("numPassedTests", 0),
            failed=data.get("numFailedTests", 0),
            skipped=data.get("numPendingTests", 0),
            duration_ms=0,
            results=results,
        )

    def to_markdown(self, suite: TestSuite) -> str:
        """テスト結果を Markdown テーブルに変換"""
        lines = [
            f"### テスト結果: {suite.name}\n",
            f"| 指標 | 値 |",
            f"|---|---|",
            f"| 合計 | {suite.total} |",
            f"| 成功 | {suite.passed} |",
            f"| 失敗 | {suite.failed} |",
            f"| スキップ | {suite.skipped} |",
            f"| 実行時間 | {suite.duration_ms:.0f}ms |",
            "",
        ]

        if suite.failed > 0:
            lines.append("#### 失敗テスト詳細\n")
            for r in suite.results:
                if r.status == "failed":
                    lines.append(f"- **{r.name}**")
                    if r.error_message:
                        lines.append(f"  ```\n  {r.error_message[:500]}\n  ```")
            lines.append("")

        return "\n".join(lines)

4.4 テスト結果の MR への自動埋め込み

bash
# テスト実行(pytest の例)
pytest --junitxml=test-report.xml

# テスト実行(Jest の例)
npx jest --json --outputFile=test-report.json

生成されたテスト結果は MR Description の「テスト結果」セクションに自動挿入される。


5. スクリーンショット取得の自動化

5.1 アプローチ

UI 変更を含む MR では、変更前後のスクリーンショットを自動取得し、MR に添付する。

mermaid
graph TD
    A["UI変更検知<br/>(*.tsx, *.css 等)"] --> B{"UI変更あり?"}
    B -->|Yes| C["開発サーバー起動"]
    B -->|No| G["スキップ"]

    C --> D["Playwright で<br/>スクリーンショット取得"]
    D --> E["画像を GitLab に<br/>アップロード"]
    E --> F["MR に画像リンク<br/>を挿入"]

5.2 Playwright によるスクリーンショット取得

python
# scripts/capture_screenshots.py

import asyncio
from pathlib import Path
from playwright.async_api import async_playwright


class ScreenshotCapture:
    """UI変更のスクリーンショットを自動取得する"""

    def __init__(self, base_url: str = "http://localhost:5173"):
        self.base_url = base_url
        self.output_dir = Path("screenshots")
        self.output_dir.mkdir(exist_ok=True)

    async def capture_pages(self, pages: list[dict]) -> list[str]:
        """指定されたページのスクリーンショットを取得する

        Args:
            pages: [{"path": "/dashboard", "name": "dashboard"}, ...]

        Returns:
            スクリーンショットファイルパスのリスト
        """
        screenshots = []
        async with async_playwright() as p:
            browser = await p.chromium.launch()
            context = await browser.new_context(
                viewport={"width": 1280, "height": 720}
            )
            page = await context.new_page()

            for page_config in pages:
                url = f"{self.base_url}{page_config['path']}"
                name = page_config["name"]

                await page.goto(url, wait_until="networkidle")
                filepath = self.output_dir / f"{name}.png"
                await page.screenshot(path=str(filepath), full_page=True)
                screenshots.append(str(filepath))

            await browser.close()
        return screenshots


# スクリーンショット対象ページの自動検出
ROUTE_PAGE_MAPPING = {
    "frontend/src/pages/Dashboard": {"path": "/dashboard", "name": "dashboard"},
    "frontend/src/pages/IssueList": {"path": "/issues", "name": "issue-list"},
    "frontend/src/pages/IssueDetail": {"path": "/issues/1", "name": "issue-detail"},
    "frontend/src/pages/Profile": {"path": "/profile", "name": "profile"},
    "frontend/src/pages/Settings": {"path": "/settings", "name": "settings"},
}


def detect_ui_pages(changed_files: list[str]) -> list[dict]:
    """変更ファイルから影響を受ける UI ページを特定する"""
    pages = []
    for file_path in changed_files:
        for route_prefix, page_config in ROUTE_PAGE_MAPPING.items():
            if file_path.startswith(route_prefix):
                if page_config not in pages:
                    pages.append(page_config)
    return pages

5.3 GitLab へのスクリーンショットアップロード

python
# backend/services/screenshot_uploader.py

import requests
from pathlib import Path


class GitLabUploader:
    """スクリーンショットを GitLab プロジェクトにアップロードする"""

    def __init__(self, gitlab_url: str, project_id: int, token: str):
        self.base_url = f"{gitlab_url}/api/v4/projects/{project_id}"
        self.headers = {"PRIVATE-TOKEN": token}

    def upload_file(self, filepath: str) -> str:
        """ファイルをアップロードして Markdown リンクを返す"""
        url = f"{self.base_url}/uploads"
        with open(filepath, "rb") as f:
            response = requests.post(
                url,
                headers=self.headers,
                files={"file": (Path(filepath).name, f)},
            )
        response.raise_for_status()
        data = response.json()
        return data["markdown"]

    def upload_screenshots(self, filepaths: list[str]) -> str:
        """複数スクリーンショットをアップロードして Markdown セクションを返す"""
        if not filepaths:
            return ""

        lines = ["### スクリーンショット\n"]
        for filepath in filepaths:
            name = Path(filepath).stem
            markdown_link = self.upload_file(filepath)
            lines.append(f"#### {name}")
            lines.append(markdown_link)
            lines.append("")

        return "\n".join(lines)

5.4 CI/CD パイプラインでの自動取得

yaml
# .gitlab-ci.yml (抜粋)

screenshot:
  stage: test
  image: mcr.microsoft.com/playwright:v1.40.0-focal
  script:
    - npm ci
    - npm run build
    - npm run preview &
    - sleep 5
    - python scripts/capture_screenshots.py
  artifacts:
    paths:
      - screenshots/
    expire_in: 7 days
  rules:
    - changes:
        - "frontend/src/**/*.{tsx,jsx,css,scss}"

6. 報告資料アセンブラー

6.1 全コンポーネントの統合

python
# backend/services/mr_report_assembler.py

from dataclasses import dataclass
from typing import Optional


@dataclass
class MRReport:
    issue_number: int
    issue_url: str
    change_summary: str       # DiffAnalyzer から
    test_results: str         # TestResultCollector から
    screenshots: str          # ScreenshotCapture + GitLabUploader から
    technical_approach: str   # Claude Code が生成
    impact_analysis: str      # Claude Code が生成


class MRReportAssembler:
    """各コンポーネントの出力を MR テンプレートに統合する"""

    TEMPLATE = """## 対応Issue

- Closes #{issue_number}
- Issue リンク: {issue_url}

---

## 実装概要

### 変更の目的
{purpose}

### 変更内容
{change_summary}

### 技術的なアプローチ
{technical_approach}

---

## 影響範囲

### 変更ファイル一覧
{file_table}

### 影響を受ける機能・画面
{impact_analysis}

---

## テスト結果

{test_results}

---

## スクリーンショット

{screenshots}

---

## セルフチェック

- [x] コーディング規約に準拠している
- [x] 必要なテストを追加・更新した
- [x] 破壊的変更がある場合は明記した
- [x] ドキュメントを更新した(該当する場合)

---

> この報告資料は Claude Code により自動生成されました。
"""

    def assemble(self, report: MRReport) -> str:
        """MR Description 用の Markdown を生成する"""
        return self.TEMPLATE.format(
            issue_number=report.issue_number,
            issue_url=report.issue_url,
            purpose=report.change_summary.split("\n")[0],
            change_summary=report.change_summary,
            technical_approach=report.technical_approach,
            file_table=self._generate_file_table(report),
            impact_analysis=report.impact_analysis,
            test_results=report.test_results or "テスト未実行",
            screenshots=report.screenshots or "UI 変更なし",
        )

    def _generate_file_table(self, report: MRReport) -> str:
        # DiffAnalyzer の結果からファイルテーブルを生成
        return "| ファイルパス | 変更種別 | 概要 |\n|---|---|---|"

6.2 実行フロー

bash
# Claude Code 内での自動実行フロー(reporter エージェント)

# 1. git diff 解析
git diff main...HEAD --name-status
git diff main...HEAD --stat

# 2. テスト実行・結果収集
pytest --junitxml=test-report.xml 2>&1 || true
npx jest --json --outputFile=test-report.json 2>&1 || true

# 3. UI変更がある場合はスクリーンショット取得
# (変更ファイルに .tsx/.css が含まれる場合)

# 4. 報告資料アセンブル
# Claude Code が上記の結果を統合して MR Description を生成

# 5. MR 作成
git push origin feature/issue-{N}
# GitLab API で MR 作成(Description に報告資料を設定)

7. 生成される報告資料の例

markdown
## 対応Issue

- Closes #11
- Issue リンク: https://gitlab.ethan-tech.jp/aieo/issueoutsourcing/-/issues/11

---

## 実装概要

### 変更の目的
dev-workflow スキルと Platform の統合を実装し、Issue 取得から MR 作成までの
自動化フローを構築する。

### 変更内容
- スキル管理 API エンドポイントを新規追加
- CLAUDE.md 自動生成サービスを実装
- .claude/agents/ にエージェント定義テンプレートを追加
- スキル設定 UI コンポーネントを追加
- 対応するテストを追加

### 技術的なアプローチ
- Jinja2 テンプレートエンジンで CLAUDE.md を動的生成
- プロジェクト設定テーブルからスキル情報を取得して CLAUDE.md に注入
- エージェント定義は Markdown テンプレートとして管理

---

## 影響範囲

### 変更ファイル一覧

| ファイルパス | 変更種別 | 概要 |
|---|---|---|
| backend/api/skills.py | 新規 | スキル CRUD API |
| backend/services/claude_md_generator.py | 新規 | CLAUDE.md 生成 |
| frontend/src/components/SkillConfig.tsx | 新規 | スキル設定 UI |
| tests/test_skills_api.py | 新規 | API テスト |
| .claude/agents/investigator.md | 新規 | 調査エージェント定義 |

### 影響を受ける機能・画面
- プロジェクト設定画面(スキル設定タブの追加)
- エンジニアダッシュボード(スキル状態の表示)

---

## テスト結果

### テスト結果: pytest

| 指標 | 値 |
|---|---|
| 合計 | 12 |
| 成功 | 12 |
| 失敗 | 0 |
| スキップ | 0 |
| 実行時間 | 340ms |

---

## セルフチェック

- [x] コーディング規約に準拠している
- [x] 必要なテストを追加・更新した
- [x] 破壊的変更がある場合は明記した
- [x] ドキュメントを更新した(該当する場合)

---

> この報告資料は Claude Code により自動生成されました。

8. 将来の拡張

8.1 Phase 2: インテリジェント分析

  • コード品質スコア: 複雑度・重複度の自動計算
  • パフォーマンス影響分析: ベンチマーク前後比較
  • セキュリティスキャン結果: SAST/DAST の結果を自動添付

8.2 Phase 3: レビュー支援

  • 自動レビューコメント: Claude Code がレビュー観点でコメントを先行生成
  • 類似 MR 参照: 過去の類似変更の MR をリンク
  • 承認推奨: テスト全パス + 品質スコア基準クリア時に自動承認を推奨