llm-deployment / utils /github_utils.py
dp291203's picture
push
8afaf1e
Raw
History Blame Contribute Delete
14.2 kB
"""
GitHub API utilities for LLM Deployment System.
This module provides functions for interacting with the GitHub API,
creating repositories, managing webhooks, and handling GitHub Pages.
"""
import os
import time
import base64
import hashlib
import hmac
from typing import Dict, List, Optional, Any, Tuple
from datetime import datetime
import requests
from github import Github, GithubException
from github.Repository import Repository
from github.GitRef import GitRef
from .config import config
from .logger import get_logger, log_github_action
logger = get_logger(__name__)
class GitHubManager:
"""Manager class for GitHub API operations."""
def __init__(self, token: Optional[str] = None):
"""Initialize GitHub manager with token."""
self.token = token or config.GITHUB_TOKEN
if not self.token:
raise ValueError("GitHub token is required")
self.github = Github(self.token)
self.user = self.github.get_user()
def create_repository(self, name: str, description: str = "", private: bool = False) -> Repository:
"""Create a new GitHub repository."""
try:
repo = self.user.create_repo(
name=name,
description=description,
private=private,
auto_init=True,
license_template="mit"
)
log_github_action(logger, "create_repo", name, success=True)
logger.info(f"Created repository: {repo.full_name}")
return repo
except GithubException as e:
log_github_action(logger, "create_repo", name, success=False, error=str(e))
logger.error(f"Failed to create repository {name}: {e}")
raise
def delete_repository(self, repo_name: str) -> bool:
"""Delete a repository."""
try:
repo = self.user.get_repo(repo_name)
repo.delete()
log_github_action(logger, "delete_repo", repo_name, success=True)
return True
except GithubException as e:
log_github_action(logger, "delete_repo", repo_name, success=False, error=str(e))
logger.error(f"Failed to delete repository {repo_name}: {e}")
return False
def get_repository(self, repo_name: str) -> Optional[Repository]:
"""Get a repository by name."""
try:
return self.user.get_repo(repo_name)
except GithubException:
return None
def create_or_update_file(self, repo: Repository, path: str, content: str, message: str, branch: str = "main") -> Dict[str, Any]:
"""Create or update a file in the repository."""
try:
# Check if file exists
try:
contents = repo.get_contents(path, ref=branch)
# File exists, update it
response = repo.update_file(
path=path,
message=message,
content=content,
sha=contents.sha,
branch=branch
)
except GithubException:
# File doesn't exist, create it
response = repo.create_file(
path=path,
message=message,
content=content,
branch=branch
)
logger.info(f"{'Updated' if 'sha' in locals() else 'Created'} file {path} in {repo.full_name}")
return response
except GithubException as e:
logger.error(f"Failed to create/update file {path} in {repo.full_name}: {e}")
raise
def create_initial_commit(self, repo: Repository, files: Dict[str, str], branch: str = "main") -> str:
"""Create initial commit with multiple files."""
try:
# Create a tree with all files
tree_elements = []
for path, content in files.items():
# Encode content as base64
encoded_content = base64.b64encode(content.encode('utf-8')).decode('utf-8')
tree_elements.append({
'path': path,
'mode': '100644', # Regular file
'type': 'blob',
'content': encoded_content
})
# Get the latest commit SHA for the branch
try:
ref = repo.get_git_ref(f"heads/{branch}")
latest_sha = ref.object.sha
except GithubException:
# Branch doesn't exist, get the default branch
latest_sha = repo.get_branch(repo.default_branch).commit.sha
# Create tree
tree = repo.create_git_tree(tree_elements, base_tree=latest_sha)
# Create commit
commit_message = "Initial commit: Project setup"
commit = repo.create_git_commit(
message=commit_message,
tree=tree.sha,
parents=[latest_sha]
)
# Update branch reference
try:
ref = repo.get_git_ref(f"heads/{branch}")
ref.edit(commit.sha)
except GithubException:
# Create new branch
repo.create_git_ref(f"refs/heads/{branch}", commit.sha)
logger.info(f"Created initial commit in {repo.full_name}")
return commit.sha
except GithubException as e:
logger.error(f"Failed to create initial commit in {repo.full_name}: {e}")
raise
def enable_github_pages(self, repo: Repository, branch: str = "main", path: str = "/") -> Dict[str, Any]:
"""Enable GitHub Pages for a repository."""
try:
# Enable Pages
pages_config = {
"source": {
"branch": branch,
"path": path
}
}
# Update repository settings to enable Pages
repo.edit(
has_pages=True,
pages_config=pages_config
)
logger.info(f"Enabled GitHub Pages for {repo.full_name}")
return {"success": True, "pages_url": f"https://{self.user.login}.github.io/{repo.name}"}
except GithubException as e:
logger.error(f"Failed to enable GitHub Pages for {repo.full_name}: {e}")
raise
def get_pages_url(self, repo: Repository) -> Optional[str]:
"""Get the GitHub Pages URL for a repository."""
try:
pages_info = repo.get_pages_info()
if pages_info and pages_info.status == "built":
return pages_info.html_url
except GithubException:
pass
# Fallback to standard Pages URL
return f"https://{self.user.login}.github.io/{repo.name}"
def verify_webhook_signature(self, payload: bytes, signature: str, secret: str) -> bool:
"""Verify GitHub webhook signature."""
if not signature.startswith('sha256='):
return False
expected_signature = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected_signature}", signature)
def create_webhook(self, repo: Repository, url: str, secret: str, events: List[str] = None) -> Dict[str, Any]:
"""Create a webhook for a repository."""
if events is None:
events = ["push", "pull_request", "issues"]
try:
webhook = repo.create_webhook(
name="web",
config={
"url": url,
"content_type": "json",
"secret": secret
},
events=events
)
logger.info(f"Created webhook for {repo.full_name}")
return {"success": True, "webhook_id": webhook.id}
except GithubException as e:
logger.error(f"Failed to create webhook for {repo.full_name}: {e}")
raise
def get_repo_languages(self, repo: Repository) -> Dict[str, int]:
"""Get programming languages used in repository."""
try:
return repo.get_languages()
except GithubException as e:
logger.warning(f"Failed to get languages for {repo.full_name}: {e}")
return {}
def get_repo_size(self, repo: Repository) -> int:
"""Get repository size in bytes."""
try:
return repo.size * 1024 # GitHub returns size in KB
except GithubException:
return 0
def check_license(self, repo: Repository) -> bool:
"""Check if repository has a LICENSE file."""
try:
repo.get_license()
return True
except GithubException:
return False
def check_readme(self, repo: Repository) -> bool:
"""Check if repository has a README file."""
try:
readme = repo.get_readme()
return readme is not None
except GithubException:
return False
def get_commit_count(self, repo: Repository) -> int:
"""Get total number of commits in repository."""
try:
commits = repo.get_commits()
return commits.totalCount
except GithubException:
return 0
def validate_repository(self, repo_url: str) -> Dict[str, Any]:
"""Validate a repository meets basic requirements."""
try:
# Extract repo name from URL
repo_name = repo_url.split('/')[-1].replace('.git', '')
repo = self.get_repository(repo_name)
if not repo:
return {"valid": False, "error": "Repository not found"}
validation = {
"valid": True,
"repo_name": repo.name,
"has_license": self.check_license(repo),
"has_readme": self.check_readme(repo),
"languages": self.get_repo_languages(repo),
"size": self.get_repo_size(repo),
"commit_count": self.get_commit_count(repo),
"pages_enabled": False,
"pages_url": None
}
# Check if Pages is enabled
try:
pages_url = self.get_pages_url(repo)
if pages_url:
validation["pages_enabled"] = True
validation["pages_url"] = pages_url
except:
pass
return validation
except Exception as e:
return {"valid": False, "error": str(e)}
class GitHubUtils:
"""Utility functions for GitHub operations."""
@staticmethod
def generate_repo_name(task_id: str, email: str) -> str:
"""Generate a unique repository name from task ID and email."""
# Create a hash from email for uniqueness while maintaining some readability
email_hash = hashlib.md5(email.encode()).hexdigest()[:8]
return f"{task_id}-{email_hash}"
@staticmethod
def extract_github_username(repo_url: str) -> str:
"""Extract GitHub username from repository URL."""
if 'github.com/' in repo_url:
parts = repo_url.split('github.com/')[1].split('/')
return parts[0] if len(parts) >= 1 else ''
return ''
@staticmethod
def extract_repo_name(repo_url: str) -> str:
"""Extract repository name from repository URL."""
if 'github.com/' in repo_url:
parts = repo_url.split('github.com/')[1].split('/')
return parts[1].replace('.git', '') if len(parts) >= 2 else ''
return ''
@staticmethod
def is_valid_github_url(url: str) -> bool:
"""Check if URL is a valid GitHub repository URL."""
return (
url.startswith('https://github.com/') or
url.startswith('http://github.com/') or
url.startswith('git@github.com:')
)
@staticmethod
def format_github_url(username: str, repo_name: str) -> str:
"""Format GitHub repository URL."""
return f"https://github.com/{username}/{repo_name}"
@staticmethod
def format_pages_url(username: str, repo_name: str) -> str:
"""Format GitHub Pages URL."""
return f"https://{username}.github.io/{repo_name}"
@staticmethod
def generate_license_content(project_name: str, year: int = None) -> str:
"""Generate MIT license content."""
if year is None:
year = datetime.now().year
return f"""MIT License
Copyright (c) {year} {project_name}
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
# Global GitHub manager instance
_github_manager = None
def get_github_manager() -> GitHubManager:
"""Get the global GitHub manager instance."""
global _github_manager
if _github_manager is None:
_github_manager = GitHubManager()
return _github_manager
def init_github_manager(token: str) -> GitHubManager:
"""Initialize GitHub manager with token."""
global _github_manager
_github_manager = GitHubManager(token)
return _github_manager