mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-19 03:26:28 +00:00
240 lines
7.3 KiB
Python
Generated
240 lines
7.3 KiB
Python
Generated
#!/usr/bin/env python3
|
|
"""GitHub module - Create GitHub releases from R2 artifacts"""
|
|
|
|
import subprocess
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from ...common.context import Context
|
|
from ...common.module import CommandModule, ValidationError
|
|
from ...common.utils import log_info, log_error, log_success, log_warning
|
|
from ..storage import BOTO3_AVAILABLE
|
|
from .common import (
|
|
PLATFORMS,
|
|
PLATFORM_DISPLAY_NAMES,
|
|
fetch_all_release_metadata,
|
|
generate_appcast_item,
|
|
generate_release_notes,
|
|
get_repo_from_git,
|
|
check_gh_cli,
|
|
)
|
|
|
|
|
|
def create_github_release(
|
|
version: str,
|
|
repo: str,
|
|
title: str,
|
|
notes: str,
|
|
draft: bool = True,
|
|
) -> Tuple[bool, str]:
|
|
"""Create GitHub release via gh CLI"""
|
|
cmd = [
|
|
"gh",
|
|
"release",
|
|
"create",
|
|
f"v{version}",
|
|
"--repo",
|
|
repo,
|
|
"--title",
|
|
title,
|
|
"--notes",
|
|
notes,
|
|
]
|
|
if draft:
|
|
cmd.append("--draft")
|
|
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
|
return True, result.stdout.strip()
|
|
except subprocess.CalledProcessError as e:
|
|
if "already exists" in e.stderr:
|
|
return False, f"Release v{version} already exists"
|
|
return False, e.stderr
|
|
|
|
|
|
def download_file(url: str, dest: Path) -> bool:
|
|
"""Download file from URL using curl"""
|
|
try:
|
|
subprocess.run(
|
|
["curl", "-L", "-o", str(dest), url],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def upload_to_github_release(version: str, repo: str, file_path: Path) -> bool:
|
|
"""Upload file to existing GitHub release"""
|
|
try:
|
|
subprocess.run(
|
|
["gh", "release", "upload", f"v{version}", str(file_path), "--repo", repo],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def normalize_version(version: str) -> str:
|
|
"""Normalize version to MAJOR.MINOR.BUILD (strip patch if present)"""
|
|
parts = version.split(".")
|
|
if len(parts) >= 3:
|
|
return ".".join(parts[:3])
|
|
return version
|
|
|
|
|
|
def download_and_upload_artifacts(
|
|
version: str,
|
|
repo: str,
|
|
metadata: Dict[str, Dict],
|
|
platforms: Optional[List[str]] = None,
|
|
) -> List[Tuple[str, bool]]:
|
|
"""Download artifacts from R2 and upload to GitHub release"""
|
|
if platforms is None:
|
|
platforms = PLATFORMS
|
|
|
|
results = []
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
tmppath = Path(tmpdir)
|
|
|
|
for platform in platforms:
|
|
if platform not in metadata:
|
|
continue
|
|
|
|
for key, artifact in metadata[platform].get("artifacts", {}).items():
|
|
url = artifact["url"]
|
|
filename = artifact["filename"]
|
|
local_path = tmppath / filename
|
|
|
|
log_info(f" Downloading {filename}...")
|
|
if not download_file(url, local_path):
|
|
log_error(f" Failed to download {filename}")
|
|
results.append((filename, False))
|
|
continue
|
|
|
|
log_info(f" Uploading {filename}...")
|
|
if upload_to_github_release(version, repo, local_path):
|
|
log_success(f" Uploaded {filename}")
|
|
results.append((filename, True))
|
|
else:
|
|
log_error(f" Failed to upload {filename}")
|
|
results.append((filename, False))
|
|
|
|
return results
|
|
|
|
|
|
class GithubModule(CommandModule):
|
|
"""Create GitHub release from R2 artifacts"""
|
|
|
|
produces = []
|
|
requires = []
|
|
description = "Create GitHub release from R2 artifacts"
|
|
|
|
def __init__(
|
|
self,
|
|
draft: bool = True,
|
|
skip_upload: bool = False,
|
|
title: Optional[str] = None,
|
|
):
|
|
self.draft = draft
|
|
self.skip_upload = skip_upload
|
|
self.title = title
|
|
|
|
def validate(self, ctx: Context) -> None:
|
|
if not BOTO3_AVAILABLE:
|
|
raise ValidationError(
|
|
"boto3 library not installed - run: pip install boto3"
|
|
)
|
|
|
|
if not ctx.env.has_r2_config():
|
|
raise ValidationError("R2 configuration not set")
|
|
|
|
if not ctx.release_version:
|
|
raise ValidationError("--version is required")
|
|
|
|
if not check_gh_cli():
|
|
raise ValidationError(
|
|
"gh CLI not found. Install from: https://cli.github.com"
|
|
)
|
|
|
|
# Determine repo
|
|
if not ctx.github_repo:
|
|
repo = get_repo_from_git()
|
|
if not repo:
|
|
raise ValidationError(
|
|
"Could not detect repo from git remote. Use --repo flag."
|
|
)
|
|
ctx.github_repo = repo
|
|
|
|
def execute(self, ctx: Context) -> None:
|
|
version = ctx.release_version
|
|
tag_version = normalize_version(version)
|
|
repo = ctx.github_repo
|
|
|
|
metadata = fetch_all_release_metadata(version, ctx.env)
|
|
if not metadata:
|
|
log_error(f"No release metadata found for version {version}")
|
|
return
|
|
|
|
log_info(f"\n{'='*60}")
|
|
log_info(f"Creating GitHub Release: v{tag_version}")
|
|
log_info(f"{'='*60}")
|
|
|
|
for platform, release in metadata.items():
|
|
artifacts = release.get("artifacts", {})
|
|
log_info(f" {PLATFORM_DISPLAY_NAMES[platform]}: {len(artifacts)} artifact(s)")
|
|
|
|
log_info(f" Repo: {repo}")
|
|
log_info(f" Draft: {self.draft}")
|
|
|
|
# Create release
|
|
release_title = self.title or f"v{tag_version}"
|
|
notes = generate_release_notes(tag_version, metadata)
|
|
|
|
log_info("\nCreating GitHub release...")
|
|
success, result = create_github_release(tag_version, repo, release_title, notes, self.draft)
|
|
|
|
if success:
|
|
log_success(f"Release created: {result}")
|
|
else:
|
|
if "already exists" in result:
|
|
log_warning(result)
|
|
else:
|
|
log_error(f"Failed to create release: {result}")
|
|
return
|
|
|
|
# Upload artifacts
|
|
if not self.skip_upload:
|
|
log_info("\nUploading artifacts to GitHub release...")
|
|
results = download_and_upload_artifacts(tag_version, repo, metadata)
|
|
|
|
failed = [f for f, ok in results if not ok]
|
|
if failed:
|
|
log_warning(f"Failed to upload: {', '.join(failed)}")
|
|
|
|
# Print appcast snippet
|
|
if "macos" in metadata:
|
|
log_info("\n" + "=" * 60)
|
|
log_info("APPCAST SNIPPET")
|
|
log_info("=" * 60)
|
|
|
|
release = metadata["macos"]
|
|
sparkle_version = release.get("sparkle_version", "")
|
|
build_date = release.get("build_date", "")
|
|
|
|
arch_to_file = {"arm64": "appcast.xml", "x64": "appcast-x86_64.xml", "universal": "appcast.xml"}
|
|
|
|
for arch in ["arm64", "x64", "universal"]:
|
|
if arch in release.get("artifacts", {}):
|
|
artifact = release["artifacts"][arch]
|
|
log_info(f"\n{arch_to_file[arch]} ({arch}):")
|
|
print(generate_appcast_item(artifact, tag_version, sparkle_version, build_date))
|
|
|
|
log_info(f"\n{'='*60}")
|
|
log_success(f"Release v{tag_version} complete!")
|