feat(git-ops): add GitHub provider with auto-detection
Implements GitHub API provider following the same pattern as Gitea: - Full PR operations (create, get, list, merge, update, close) - Branch operations via API - Comment and label management - Reviewer request support - Rate limit error handling Server enhancements: - Auto-detect provider from repository URL (github.com vs custom Gitea) - Initialize GitHub provider when token is configured - Health check includes both provider statuses - Token selection based on repo URL for clone/push operations Refs: #110 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ from config import Settings, get_settings
|
||||
from exceptions import ErrorCode, GitOpsError
|
||||
from git_wrapper import GitWrapper
|
||||
from models import MergeStrategy, PRState
|
||||
from providers import GiteaProvider
|
||||
from providers import BaseProvider, GiteaProvider, GitHubProvider
|
||||
from workspace import WorkspaceManager
|
||||
|
||||
# Input validation patterns
|
||||
@@ -77,25 +77,57 @@ logger = logging.getLogger(__name__)
|
||||
_settings: Settings | None = None
|
||||
_workspace_manager: WorkspaceManager | None = None
|
||||
_gitea_provider: GiteaProvider | None = None
|
||||
_github_provider: GitHubProvider | None = None
|
||||
|
||||
|
||||
def _get_provider_for_url(repo_url: str) -> GiteaProvider | None:
|
||||
def _get_provider_for_url(repo_url: str) -> BaseProvider | None:
|
||||
"""Get the appropriate provider for a repository URL."""
|
||||
if not _settings:
|
||||
return None
|
||||
|
||||
# Normalize URL for matching
|
||||
url_lower = repo_url.lower()
|
||||
|
||||
# Check for GitHub URLs
|
||||
if "github.com" in url_lower or (
|
||||
_settings.github_api_url
|
||||
and _settings.github_api_url != "https://api.github.com"
|
||||
and _settings.github_api_url.replace("https://", "").replace("/api/v3", "") in url_lower
|
||||
):
|
||||
return _github_provider
|
||||
|
||||
# Check if it's a Gitea URL
|
||||
if _settings.gitea_base_url and _settings.gitea_base_url in repo_url:
|
||||
if _settings.gitea_base_url and _settings.gitea_base_url.lower() in url_lower:
|
||||
return _gitea_provider
|
||||
|
||||
# Default to Gitea for now
|
||||
# Default: try to detect from URL pattern
|
||||
# If URL contains 'github' anywhere, use GitHub
|
||||
if "github" in url_lower:
|
||||
return _github_provider
|
||||
|
||||
# Default to Gitea for self-hosted instances
|
||||
return _gitea_provider
|
||||
|
||||
|
||||
def _get_auth_token_for_url(repo_url: str) -> str | None:
|
||||
"""Get the appropriate auth token for a repository URL."""
|
||||
if not _settings:
|
||||
return None
|
||||
|
||||
url_lower = repo_url.lower()
|
||||
|
||||
# GitHub token
|
||||
if "github.com" in url_lower or "github" in url_lower:
|
||||
return _settings.github_token if _settings.github_token else None
|
||||
|
||||
# Gitea token (default)
|
||||
return _settings.gitea_token if _settings.gitea_token else None
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||
"""Application lifespan handler."""
|
||||
global _settings, _workspace_manager, _gitea_provider
|
||||
global _settings, _workspace_manager, _gitea_provider, _github_provider
|
||||
|
||||
logger.info("Starting Git Operations MCP Server...")
|
||||
|
||||
@@ -114,6 +146,13 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||
)
|
||||
logger.info(f"Gitea provider initialized: {_settings.gitea_base_url}")
|
||||
|
||||
if _settings.github_token:
|
||||
_github_provider = GitHubProvider(
|
||||
token=_settings.github_token,
|
||||
settings=_settings,
|
||||
)
|
||||
logger.info("GitHub provider initialized")
|
||||
|
||||
logger.info("Git Operations MCP Server started successfully")
|
||||
|
||||
yield
|
||||
@@ -124,6 +163,9 @@ async def lifespan(_app: FastAPI) -> AsyncIterator[None]:
|
||||
if _gitea_provider:
|
||||
await _gitea_provider.close()
|
||||
|
||||
if _github_provider:
|
||||
await _github_provider.close()
|
||||
|
||||
logger.info("Git Operations MCP Server shut down")
|
||||
|
||||
|
||||
@@ -167,6 +209,21 @@ async def health_check() -> dict[str, Any]:
|
||||
else:
|
||||
status["dependencies"]["gitea"] = "not configured"
|
||||
|
||||
# Check GitHub connectivity
|
||||
if _github_provider:
|
||||
try:
|
||||
if await _github_provider.is_connected():
|
||||
user = await _github_provider.get_authenticated_user()
|
||||
status["dependencies"]["github"] = f"connected as {user}"
|
||||
else:
|
||||
status["dependencies"]["github"] = "not connected"
|
||||
is_degraded = True
|
||||
except Exception as e:
|
||||
status["dependencies"]["github"] = f"error: {e}"
|
||||
is_degraded = True
|
||||
else:
|
||||
status["dependencies"]["github"] = "not configured"
|
||||
|
||||
# Check workspace directory
|
||||
if _workspace_manager:
|
||||
try:
|
||||
@@ -442,10 +499,8 @@ async def clone_repository(
|
||||
# Create workspace
|
||||
workspace = await _workspace_manager.create_workspace(project_id, repo_url) # type: ignore[union-attr]
|
||||
|
||||
# Get auth token from provider
|
||||
auth_token = None
|
||||
if _settings and _settings.gitea_token:
|
||||
auth_token = _settings.gitea_token
|
||||
# Get auth token appropriate for the repository URL
|
||||
auth_token = _get_auth_token_for_url(repo_url)
|
||||
|
||||
# Clone repository
|
||||
git = GitWrapper(workspace.path, _settings)
|
||||
@@ -715,10 +770,8 @@ async def push(
|
||||
if not workspace:
|
||||
return {"success": False, "error": f"Workspace not found for project: {project_id}", "code": ErrorCode.WORKSPACE_NOT_FOUND.value}
|
||||
|
||||
# Get auth token
|
||||
auth_token = None
|
||||
if _settings and _settings.gitea_token:
|
||||
auth_token = _settings.gitea_token
|
||||
# Get auth token appropriate for the repository URL
|
||||
auth_token = _get_auth_token_for_url(workspace.repo_url or "")
|
||||
|
||||
git = GitWrapper(workspace.path, _settings)
|
||||
result = await git.push(
|
||||
|
||||
Reference in New Issue
Block a user