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:#c8e6c93. git diff からの変更サマリー自動生成
3.1 差分取得
bash
# メインブランチとの差分を取得
git diff main...HEAD --stat
git diff main...HEAD --name-status
git diff main...HEAD3.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 対応テストフレームワーク
| フレームワーク | 言語 | 出力形式 | パース方法 |
|---|---|---|---|
| pytest | Python | JUnit XML / JSON | --junitxml=report.xml |
| Jest | TypeScript | JSON | --json --outputFile=report.json |
| Vitest | TypeScript | JSON | --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 pages5.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 をリンク
- 承認推奨: テスト全パス + 品質スコア基準クリア時に自動承認を推奨