| import base64 |
| import httpx |
| from typing import Optional |
| from app.core.config import settings |
| from app.core.logging import get_logger |
| from app.models.schemas import GitHubRepoRequest, GitHubRepoResponse |
|
|
| logger = get_logger("github_service") |
|
|
| class GitHubService: |
| BASE = "https://api.github.com" |
| def __init__(self): |
| self.token = settings.github_token |
| self.org = settings.github_org |
| self.client = httpx.AsyncClient(timeout=60.0) |
|
|
| def _headers(self): |
| return { |
| "Authorization": f"token {self.token}", |
| "Accept": "application/vnd.github.v3+json", |
| } |
|
|
| async def create_repo(self, req: GitHubRepoRequest) -> GitHubRepoResponse: |
| if not self.token: |
| return GitHubRepoResponse(repo_url="", clone_url="", created=False) |
| try: |
| path = f"/orgs/{self.org}/repos" if self.org else "/user/repos" |
| payload = { |
| "name": req.name, |
| "description": req.description or f"Auto-created repo for {req.name}", |
| "private": req.private, |
| "auto_init": True, |
| } |
| resp = await self.client.post(f"{self.BASE}{path}", headers=self._headers(), json=payload) |
| resp.raise_for_status() |
| data = resp.json() |
| repo_url = data["html_url"] |
| clone_url = data["clone_url"] |
| if req.readme_content: |
| await self._update_readme(req.name, req.readme_content) |
| logger.info("repo_created", name=req.name, url=repo_url) |
| return GitHubRepoResponse(repo_url=repo_url, clone_url=clone_url, created=True) |
| except Exception as e: |
| logger.error("repo_creation_failed", name=req.name, error=str(e)) |
| return GitHubRepoResponse(repo_url="", clone_url="", created=False) |
|
|
| async def _update_readme(self, repo_name: str, content: str): |
| try: |
| path = f"{self.BASE}/repos/{self.org or 'user'}/{repo_name}/contents/README.md" |
| if self.org: |
| path = f"{self.BASE}/repos/{self.org}/{repo_name}/contents/README.md" |
| payload = { |
| "message": "docs: update README via AgentWorkforce", |
| "content": base64.b64encode(content.encode()).decode(), |
| } |
| resp = await self.client.put(path, headers=self._headers(), json=payload) |
| resp.raise_for_status() |
| except Exception as e: |
| logger.error("readme_update_failed", repo=repo_name, error=str(e)) |
|
|
| async def create_file(self, repo_name: str, path: str, content: str, message: str = "Auto commit via AgentWorkforce"): |
| if not self.token: |
| return {"status": "skipped"} |
| try: |
| full_repo = f"{self.org}/{repo_name}" if self.org else repo_name |
| url = f"{self.BASE}/repos/{full_repo}/contents/{path}" |
| payload = { |
| "message": message, |
| "content": base64.b64encode(content.encode()).decode(), |
| } |
| resp = await self.client.put(url, headers=self._headers(), json=payload) |
| resp.raise_for_status() |
| return {"status": "created", "path": path} |
| except Exception as e: |
| |
| logger.warning("file_create_failed_trying_update", path=path, error=str(e)) |
| return await self.update_file(repo_name, path, content, message) |
|
|
| async def update_file(self, repo_name: str, path: str, content: str, message: str): |
| try: |
| full_repo = f"{self.org}/{repo_name}" if self.org else repo_name |
| url = f"{self.BASE}/repos/{full_repo}/contents/{path}" |
| |
| r = await self.client.get(url, headers=self._headers()) |
| if r.status_code != 200: |
| return {"status": "error", "detail": r.text} |
| sha = r.json()["sha"] |
| payload = { |
| "message": message, |
| "content": base64.b64encode(content.encode()).decode(), |
| "sha": sha, |
| } |
| resp = await self.client.put(url, headers=self._headers(), json=payload) |
| resp.raise_for_status() |
| return {"status": "updated", "path": path} |
| except Exception as e: |
| logger.error("file_update_failed", path=path, error=str(e)) |
| return {"status": "error", "error": str(e)} |
|
|
| async def create_repo_and_push(self, repo_name: str, files: dict, private: bool = True, description: str = "") -> GitHubRepoResponse: |
| repo = await self.create_repo(GitHubRepoRequest(name=repo_name, private=private, description=description)) |
| if not repo.created: |
| return repo |
| for path, content in files.items(): |
| await self.create_file(repo_name, path, content) |
| return repo |
|
|
| github_service = GitHubService() |
|
|