import os import requests import uvicorn import logging from fastapi import FastAPI, Request, Header, HTTPException from typing import Optional logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI() TEAMS_WEBHOOK_URL = os.getenv("TEAMS_WEBHOOK_URL") WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET") HF_TOKEN = os.getenv("HF_TOKEN") @app.get("/") def root(): return {"status": "running"} def get_commit_info(repo_type, repo_name, head_sha): type_map = {"model": "models", "dataset": "datasets", "space": "spaces"} api_type = type_map.get(repo_type, "models") headers = {} if HF_TOKEN: headers["Authorization"] = f"Bearer {HF_TOKEN}" commit_title = head_sha[:8] commit_author = "unknown" parent_sha = None changed_files = [] # 1) 커밋 목록에서 커밋 메시지, 작성자, 부모 SHA 조회 try: commits_url = f"https://huggingface.co/api/{api_type}/{repo_name}/commits/main" resp = requests.get(commits_url, headers=headers, timeout=10) if resp.ok: commits = resp.json() for i, commit in enumerate(commits): if commit.get("id", "").startswith(head_sha[:8]): commit_title = commit.get("title", head_sha[:8]) authors = commit.get("authors", []) if authors: commit_author = authors[0].get("user", "unknown") # 바로 다음이 부모 커밋 if i + 1 < len(commits): parent_sha = commits[i + 1].get("id") break except Exception as e: logger.error(f"Commits API error: {e}") # 2) 현재 커밋과 부모 커밋의 파일 트리 비교 if parent_sha: try: def get_tree(revision): tree_url = f"https://huggingface.co/api/{api_type}/{repo_name}/tree/{revision}" logger.info(f"Fetching tree: {tree_url}") r = requests.get(tree_url, headers=headers, timeout=10, params={"recursive": "true"}) logger.info(f" Tree status: {r.status_code}") if r.ok: return {f.get("path"): f.get("oid") for f in r.json() if f.get("type") == "file"} return {} current_files = get_tree(head_sha) parent_files = get_tree(parent_sha) # 추가된 파일 for path in current_files: if path not in parent_files: changed_files.append(f"🆕 {path}") elif current_files[path] != parent_files[path]: changed_files.append(f"✏️ {path}") # 삭제된 파일 for path in parent_files: if path not in current_files: changed_files.append(f"🗑️ {path}") except Exception as e: logger.error(f"Tree compare error: {e}") return commit_title, commit_author, changed_files @app.post("/webhook") async def handle_webhook( request: Request, x_webhook_secret: Optional[str] = Header(default=None) ): if WEBHOOK_SECRET: if x_webhook_secret != WEBHOOK_SECRET: raise HTTPException(status_code=403, detail="Invalid secret") payload = await request.json() # 전체 payload 로깅 (디버깅용) logger.info(f"=== WEBHOOK RECEIVED ===") logger.info(f"Full payload keys: {list(payload.keys())}") logger.info(f"Event: {payload.get('event')}") logger.info(f"Repo: {payload.get('repo')}") event = payload.get("event", {}) repo = payload.get("repo", {}) if event.get("action") == "update" and event.get("scope", "").startswith("repo.content"): repo_name = repo.get("name", "unknown") repo_type = repo.get("type", "model") head_sha = repo.get("headSha", "") commit_title, commit_author, changed_files = get_commit_info(repo_type, repo_name, head_sha) if changed_files: files_text = "\n".join(changed_files[:15]) if len(changed_files) > 15: files_text += f"\n...외 {len(changed_files) - 15}개 파일" else: files_text = "(변경 파일 조회 실패 - Space 로그를 확인하세요)" card = { "type": "message", "attachments": [{ "contentType": "application/vnd.microsoft.card.adaptive", "contentUrl": None, "content": { "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "version": "1.4", "body": [ { "type": "TextBlock", "text": f"📦 {repo_name}", "wrap": True, "weight": "Bolder", "size": "Large" }, { "type": "FactSet", "facts": [ {"title": "커밋", "value": commit_title}, {"title": "작성자", "value": commit_author}, {"title": "SHA", "value": head_sha[:8]} ] }, { "type": "TextBlock", "text": "**변경된 파일:**", "wrap": True, "spacing": "Medium" }, { "type": "TextBlock", "text": files_text, "wrap": True, "fontType": "Monospace", "size": "Small" }, { "type": "ActionSet", "actions": [{ "type": "Action.OpenUrl", "title": "커밋 보기", "url": f"https://huggingface.co/{repo_name}/commit/{head_sha}" }] } ] } }] } resp = requests.post(TEAMS_WEBHOOK_URL, json=card) return {"sent": True, "teams_status": resp.status_code} return {"sent": False, "reason": "not a repo content update"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)