guardia-itsm/routers/changelog_tracker.py

138 lines
4.3 KiB
Python

"""성장일지 — git 변경이력 자동 생성·조회."""
from __future__ import annotations
import asyncio, logging, subprocess
from datetime import datetime
from fastapi import APIRouter, BackgroundTasks, Depends, Query
from pydantic import BaseModel
from sqlalchemy import select, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user, require_admin_role
from database import get_db
from models import User, ChangelogEntry
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/changelog", tags=["성장일지-변경이력"])
GIT_ROOT = "/opt/guardia"
def _run_git(cmd: list[str]) -> str:
try:
r = subprocess.run(
["git", "-C", GIT_ROOT] + cmd,
capture_output=True, text=True, timeout=15
)
return r.stdout.strip()
except Exception as e:
return str(e)
async def _collect_and_save():
"""최신 커밋 50개에서 ChangelogEntry 자동 생성."""
log = _run_git([
"log", "--oneline", "--no-merges", "-50",
"--format=%H|||%s|||%an|||%ad", "--date=short"
])
if not log:
return 0
from database import AsyncSessionLocal
async with AsyncSessionLocal() as db:
count = 0
for line in log.splitlines():
parts = line.split("|||")
if len(parts) < 4:
continue
commit_hash, subject, author, date = parts[0], parts[1], parts[2], parts[3]
existing = await db.execute(
select(ChangelogEntry).where(ChangelogEntry.commit_hash == commit_hash).limit(1)
)
if existing.scalar_one_or_none():
continue
cat = "feat"
for prefix in ("fix", "docs", "refactor", "test", "chore", "style"):
if subject.lower().startswith(prefix):
cat = prefix
break
entry = ChangelogEntry(
version="auto",
category=cat,
title=subject[:290],
commit_hash=commit_hash,
author=author,
created_at=datetime.fromisoformat(date) if date else datetime.utcnow(),
)
db.add(entry)
count += 1
await db.commit()
return count
@router.post("/collect")
async def collect_changelog(
background_tasks: BackgroundTasks,
user: User = Depends(require_admin_role),
):
"""git 커밋 이력에서 변경이력 자동 수집."""
background_tasks.add_task(_collect_and_save)
return {"ok": True, "message": "변경이력 수집 시작됨 (백그라운드)"}
@router.get("/list")
async def list_changelog(
limit: int = 50,
category: str = Query(None),
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""변경이력 목록."""
q = select(ChangelogEntry).order_by(desc(ChangelogEntry.created_at)).limit(limit)
if category:
q = q.where(ChangelogEntry.category == category)
rows = await db.execute(q)
return [{
"id": e.id, "version": e.version, "category": e.category,
"title": e.title, "author": e.author,
"commit_hash": e.commit_hash, "created_at": e.created_at,
} for e in rows.scalars().all()]
@router.get("/summary")
async def changelog_summary(db: AsyncSession = Depends(get_db), user: User = Depends(get_current_user)):
"""카테고리별 변경 수 요약."""
from sqlalchemy import func as sqlfunc
rows = await db.execute(
select(ChangelogEntry.category, sqlfunc.count().label("cnt"))
.group_by(ChangelogEntry.category)
.order_by(desc("cnt"))
)
return {"summary": [{"category": r.category, "count": r.cnt} for r in rows.all()]}
class ManualEntry(BaseModel):
version: str
category: str = "feat"
title: str
description: str = ""
@router.post("/entry")
async def add_entry(
entry: ManualEntry,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin_role),
):
"""수동 변경이력 추가."""
row = ChangelogEntry(
version=entry.version, category=entry.category,
title=entry.title, description=entry.description,
author=user.username, created_at=datetime.utcnow(),
)
db.add(row)
await db.commit()
await db.refresh(row)
return {"id": row.id, "ok": True}