Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 119 additions & 71 deletions packages/browseros/bos_build/cli/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
import typer

from ..core.context import Context
from ..core.products import ProductDescriptor, get_product_descriptor
from ..lib.notify import slack_subscriber
from ..lib.paths import get_package_root
from ..core.runner import StepExecutionError, run as run_steps
from ..lib.utils import log_info, log_error
from ..products import PRODUCTS

from ..release import (
AVAILABLE_MODULES,
Expand All @@ -20,6 +22,7 @@
PublishModule,
DownloadModule,
)
from ..release.list import DEFAULT_LIST_LIMIT

app = typer.Typer(
help="Release automation commands",
Expand All @@ -35,10 +38,24 @@
)
app.add_typer(github_app, name="github")

_PRODUCT_HELP = f"Product to operate on ({', '.join(PRODUCTS)})"


def _resolve_product(product_id: Optional[str]) -> ProductDescriptor:
"""Resolve --product to a descriptor with a CLI-friendly error."""
try:
return get_product_descriptor(product_id)
except ValueError:
log_error(
f"Unknown product '{product_id}'. Valid: {', '.join(sorted(PRODUCTS))}"
)
raise typer.Exit(1)


def create_release_context(
version: str,
repo: Optional[str] = None,
product: Optional[str] = None,
) -> Context:
"""Create Context for release operations.

Expand All @@ -51,6 +68,7 @@ def create_release_context(
chromium_src=root,
architecture="",
build_type="release",
product=_resolve_product(product),
)
ctx.release_version = version
ctx.github_repo = repo or ""
Expand All @@ -71,51 +89,22 @@ def execute_module(ctx: Context, module) -> None:
@app.callback(invoke_without_command=True)
def main(
ctx: typer.Context,
version: Optional[str] = typer.Option(
None, "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
),
list_artifacts: bool = typer.Option(
False, "--list", "-l", help="List artifacts for version from R2"
),
appcast: bool = typer.Option(
False, "--appcast", "-a", help="Generate appcast XML snippets"
),
publish: bool = typer.Option(
False, "--publish", "-p", help="Publish to download/ paths (make live)"
),
download: bool = typer.Option(
False, "--download", "-d", help="Download artifacts to temp directory"
),
os_filter: Optional[str] = typer.Option(
None, "--os", help="Filter by OS: macos, windows, linux"
),
output: Optional[Path] = typer.Option(
None, "--output", "-o", help="Output directory for downloads (default: temp dir)"
),
show_modules: bool = typer.Option(
False, "--show-modules", help="Show available modules and exit"
),
):
"""Release automation for BrowserOS

\b
Quick Operations (Flags):
browseros release --list # List all available versions
browseros release --list --version 0.31.0 # List artifacts for version
browseros release --version 0.31.0 --appcast # Generate appcast XML
browseros release --version 0.31.0 --publish # Publish to download/ paths
browseros release --version 0.31.0 --download # Download all artifacts
browseros release --version 0.31.0 --download --os macos # Download macOS only
browseros release --version 0.31.0 --download --output ./downloads # Custom dir

\b
GitHub Release (Sub-command):
Commands:
browseros release list # Newest releases per product
browseros release list 0.31.0 # Artifacts for a version
browseros release appcast --version 0.31.0 # Generate appcast XML
browseros release publish --version 0.31.0 # Publish to download/ paths
browseros release download --version 0.31.0 # Download all artifacts
browseros release github create --version 0.31.0
browseros release github create --version 0.31.0 --publish

\b
Show Available Modules:
browseros release --show-modules
Use --product to target a specific product (default: browseros).
"""
if show_modules:
log_info("\n📦 Available Release Modules:")
Expand All @@ -125,49 +114,107 @@ def main(
log_info("-" * 50)
return

# If subcommand invoked, let it handle things
if ctx.invoked_subcommand is not None:
return
if ctx.invoked_subcommand is None:
typer.echo(ctx.get_help())
raise typer.Exit(0)

# Check if any flags specified
has_flags = any([list_artifacts, appcast, publish, download])

if not has_flags:
typer.echo(
"Error: Specify a flag (--list, --appcast, --publish, --download) or use a sub-command\n"
)
typer.echo("Use --help for usage information")
typer.echo("Use --show-modules to see available modules")
raise typer.Exit(1)
@app.command("list")
def list_releases(
version_arg: Optional[str] = typer.Argument(
None, metavar="[VERSION]", help="Show artifact details for this version"
),
version: Optional[str] = typer.Option(
None, "--version", "-v", help="Show artifact details for this version"
),
product: Optional[str] = typer.Option(
None, "--product", help=f"{_PRODUCT_HELP}; default: all products"
),
limit: int = typer.Option(
DEFAULT_LIST_LIMIT, "--limit", "-n", min=1, help="Versions shown per product"
),
show_all: bool = typer.Option(False, "--all", help="Show every version"),
):
"""List releases from R2 (newest first), or artifacts for one version.

# Version is required for all flags except --list
requires_version = any([appcast, publish, download])
if requires_version and not version:
log_error("--version is required for this operation")
\b
Examples:
browseros release list # Newest 5 per product
browseros release list --all # Every version
browseros release list -n 10 # Newest 10 per product
browseros release list --product browserclaw # One product only
browseros release list 0.31.0 # Artifact details
"""
if version_arg and version and version_arg != version:
log_error(f"Conflicting versions: '{version_arg}' vs --version '{version}'")
raise typer.Exit(1)
resolved_version = version_arg or version

# Create context
release_ctx = create_release_context(version or "")

# Execute requested modules
if list_artifacts:
if version:
log_info(f"📋 Listing artifacts for v{version}")
else:
log_info("📋 Listing all available releases")
if resolved_version:
release_ctx = create_release_context(resolved_version, product=product)
log_info(f"📋 Listing artifacts for v{resolved_version}")
execute_module(release_ctx, ListModule())
return

products = [_resolve_product(product)] if product else list(PRODUCTS.values())
release_ctx = create_release_context("", product=product)
log_info("📋 Listing available releases")
execute_module(
release_ctx,
ListModule(products=products, limit=None if show_all else limit),
)

if appcast:
log_info(f"📝 Generating appcast for v{version}")
execute_module(release_ctx, AppcastModule())

if publish:
log_info(f"🚀 Publishing v{version} to download/ paths")
execute_module(release_ctx, PublishModule())
@app.command("appcast")
def appcast(
version: str = typer.Option(
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
),
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
):
"""Generate Sparkle appcast XML snippets."""
release_ctx = create_release_context(version, product=product)
log_info(f"📝 Generating appcast for v{version}")
execute_module(release_ctx, AppcastModule())


@app.command("publish")
def publish(
version: str = typer.Option(
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
),
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
):
"""Publish versioned artifacts to download/ paths (make live)."""
release_ctx = create_release_context(version, product=product)
log_info(f"🚀 Publishing v{version} to download/ paths")
execute_module(release_ctx, PublishModule())

if download:
log_info(f"📥 Downloading artifacts for v{version}")
execute_module(release_ctx, DownloadModule(os_filter=os_filter, output_dir=output))

@app.command("download")
def download(
version: str = typer.Option(
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
),
os_filter: Optional[str] = typer.Option(
None, "--os", help="Filter by OS: macos, windows, linux"
),
output: Optional[Path] = typer.Option(
None, "--output", "-o", help="Output directory for downloads (default: temp dir)"
),
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
):
"""Download release artifacts to a local directory.

\b
Examples:
browseros release download --version 0.31.0
browseros release download --version 0.31.0 --os macos
browseros release download --version 0.31.0 --output ./downloads
"""
release_ctx = create_release_context(version, product=product)
log_info(f"📥 Downloading artifacts for v{version}")
execute_module(release_ctx, DownloadModule(os_filter=os_filter, output_dir=output))


@github_app.command("create")
Expand All @@ -190,6 +237,7 @@ def github_create(
publish_to_download: bool = typer.Option(
False, "--publish", "-p", help="Also publish to download/ paths after creating release"
),
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
):
"""Create GitHub release from R2 artifacts

Expand All @@ -199,7 +247,7 @@ def github_create(
browseros release github create --version 0.31.0 --publish # Also publish to download/
browseros release github create --version 0.31.0 --no-draft # Create published release
"""
ctx = create_release_context(version, repo)
ctx = create_release_context(version, repo, product)

log_info(f"🚀 Creating GitHub release for v{version}")
module = GithubModule(
Expand Down
Loading
Loading