temp / student /github_manager.py
CheeksTheGeek's picture
Fix: Use GitHub REST API directly to enable Pages
5613139 unverified
"""GitHub repository management and Pages deployment."""
import shutil
import time
from pathlib import Path
import git
from github import Github, GithubException
from shared.config import settings
from shared.logger import setup_logger
from shared.utils import sanitize_repo_name
logger = setup_logger(__name__)
class GitHubManager:
"""Manage GitHub repository creation, pushing, and Pages deployment."""
def __init__(self) -> None:
"""Initialize GitHub manager."""
self.github = Github(settings.github_token)
self.user = self.github.get_user()
logger.info(f"Initialized GitHubManager for user {self.user.login}")
def create_and_deploy(
self, task_id: str, code_dir: Path
) -> tuple[str, str, str]:
"""Create repo, push code, enable Pages, and return URLs.
Args:
task_id: Unique task identifier
code_dir: Directory containing generated code
Returns:
Tuple of (repo_url, commit_sha, pages_url)
"""
repo_name = sanitize_repo_name(task_id)
logger.info(f"Creating and deploying repository: {repo_name}")
# Create GitHub repository
repo_url = self._create_repo(repo_name)
# Initialize git and push
commit_sha = self._push_code(repo_name, code_dir)
# Enable GitHub Pages
pages_url = self._enable_pages(repo_name)
# Wait for Pages to be ready
self._wait_for_pages(pages_url)
logger.info(
f"Successfully deployed {repo_name}: "
f"repo={repo_url}, commit={commit_sha}, pages={pages_url}"
)
return repo_url, commit_sha, pages_url
def update_and_redeploy(
self, repo_name: str, code_dir: Path
) -> tuple[str, str]:
"""Update existing repo with new code and return new commit.
Args:
repo_name: Repository name
code_dir: Directory containing updated code
Returns:
Tuple of (repo_url, commit_sha)
"""
logger.info(f"Updating repository: {repo_name}")
try:
repo = self.user.get_repo(repo_name)
repo_url = repo.html_url
# Clone, update, and push
commit_sha = self._update_code(repo_name, code_dir)
logger.info(f"Successfully updated {repo_name}: commit={commit_sha}")
return repo_url, commit_sha
except GithubException as e:
logger.error(f"Failed to update repo {repo_name}: {e}")
raise
def _create_repo(self, repo_name: str) -> str:
"""Create a new GitHub repository.
Args:
repo_name: Repository name
Returns:
Repository URL
"""
try:
# Check if repo already exists
try:
existing_repo = self.user.get_repo(repo_name)
logger.warning(f"Repository {repo_name} already exists, using it")
return existing_repo.html_url
except GithubException:
pass # Repo doesn't exist, create it
# Create new repo
repo = self.user.create_repo(
name=repo_name,
description=f"Auto-generated repository for task {repo_name}",
private=False, # Must be public for Pages
auto_init=False,
)
logger.info(f"Created repository: {repo.html_url}")
return repo.html_url
except GithubException as e:
logger.error(f"Failed to create repository {repo_name}: {e}")
raise
def _push_code(self, repo_name: str, code_dir: Path) -> str:
"""Initialize git repo and push code.
Args:
repo_name: Repository name
code_dir: Directory containing code
Returns:
Commit SHA
"""
try:
# Ensure LICENSE file exists
self._ensure_license(code_dir)
# Initialize git repo
repo = git.Repo.init(code_dir)
# Configure git
with repo.config_writer() as config:
if not config.has_option("user", "name"):
config.set_value("user", "name", settings.github_username)
if not config.has_option("user", "email"):
config.set_value("user", "email", settings.student_email)
# Add all files
repo.index.add("*")
# Create .gitignore if needed
gitignore_path = code_dir / ".gitignore"
if not gitignore_path.exists():
gitignore_path.write_text("*.pyc\n__pycache__/\n.DS_Store\n")
repo.index.add([".gitignore"])
# Commit
commit = repo.index.commit("Initial commit: Generated application")
commit_sha = commit.hexsha
# Rename master to main if needed
try:
repo.git.branch("-M", "main")
except Exception:
pass # Already on main or error, continue
# Add remote
remote_url = f"https://{settings.github_token}@github.com/{settings.github_username}/{repo_name}.git"
try:
origin = repo.remote("origin")
origin.set_url(remote_url)
except ValueError:
origin = repo.create_remote("origin", remote_url)
# Push
origin.push(refspec="main:main", force=True)
logger.info(f"Pushed code to {repo_name}: commit {commit_sha}")
return commit_sha
except Exception as e:
logger.error(f"Failed to push code to {repo_name}: {e}")
raise
def _update_code(self, repo_name: str, code_dir: Path) -> str:
"""Update existing repository with new code.
Args:
repo_name: Repository name
code_dir: Directory containing updated code
Returns:
New commit SHA
"""
try:
# Clone the repo
temp_dir = settings.generated_repos_dir / f"{repo_name}_temp"
if temp_dir.exists():
shutil.rmtree(temp_dir)
clone_url = f"https://{settings.github_token}@github.com/{settings.github_username}/{repo_name}.git"
repo = git.Repo.clone_from(clone_url, temp_dir)
# Copy new files (except .git)
for item in code_dir.iterdir():
if item.name == ".git":
continue
dest = temp_dir / item.name
if item.is_file():
shutil.copy2(item, dest)
elif item.is_dir():
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(item, dest)
# Ensure LICENSE
self._ensure_license(temp_dir)
# Commit and push
repo.index.add("*")
commit = repo.index.commit("Update: Revised application")
commit_sha = commit.hexsha
origin = repo.remote("origin")
origin.push()
# Cleanup
shutil.rmtree(temp_dir)
logger.info(f"Updated code in {repo_name}: commit {commit_sha}")
return commit_sha
except Exception as e:
logger.error(f"Failed to update code in {repo_name}: {e}")
raise
def _ensure_license(self, code_dir: Path) -> None:
"""Ensure MIT LICENSE file exists in code directory.
Args:
code_dir: Directory to add license to
"""
license_path = code_dir / "LICENSE"
if not license_path.exists():
year = time.strftime("%Y")
license_text = f"""MIT License
Copyright (c) {year} {settings.github_username}
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.
"""
license_path.write_text(license_text)
logger.debug(f"Created MIT LICENSE in {code_dir}")
def _enable_pages(self, repo_name: str) -> str:
"""Enable GitHub Pages for repository.
Args:
repo_name: Repository name
Returns:
Pages URL
"""
import requests
try:
# Use GitHub REST API to enable Pages
api_url = f"https://api.github.com/repos/{settings.github_username}/{repo_name}/pages"
headers = {
"Accept": "application/vnd.github+json",
"Authorization": f"Bearer {settings.github_token}",
"X-GitHub-Api-Version": "2022-11-28"
}
data = {
"source": {
"branch": "main",
"path": "/"
}
}
response = requests.post(api_url, headers=headers, json=data)
if response.status_code == 201:
logger.info(f"Enabled GitHub Pages for {repo_name}")
elif response.status_code == 409:
logger.info(f"GitHub Pages already enabled for {repo_name}")
else:
logger.warning(f"Pages enable response: {response.status_code} - {response.text}")
# Construct Pages URL
pages_url = f"https://{settings.github_username}.github.io/{repo_name}/"
return pages_url
except Exception as e:
logger.error(f"Failed to enable Pages for {repo_name}: {e}")
# Return expected URL even if enabling fails
return f"https://{settings.github_username}.github.io/{repo_name}/"
def _wait_for_pages(self, pages_url: str, max_wait: int = 60) -> bool:
"""Wait for GitHub Pages to be ready.
Args:
pages_url: Pages URL to check
max_wait: Maximum wait time in seconds
Returns:
True if ready, False if timeout
"""
import requests
logger.info(f"Waiting for Pages to be ready: {pages_url}")
for i in range(max_wait):
try:
response = requests.get(pages_url, timeout=5)
if response.status_code == 200:
logger.info(f"Pages ready after {i+1} seconds")
return True
except requests.RequestException:
pass
time.sleep(1)
logger.warning(f"Pages not ready after {max_wait} seconds")
return False