Skip to content

Commit bd594ff

Browse files
authored
fix(browseros): product-aware release CLI with bounded listing (#1505)
* fix(browseros): derive download alias map from product artifact prefix * fix(browseros): list release versions under productized R2 layout * feat(browseros): bounded per-product release list with legacy tagging * fix(browseros): thread product into appcast and download metadata fetch * feat(browseros): product-aware release subcommands * fix(browseros): address review findings for release listing
1 parent db79cbc commit bd594ff

9 files changed

Lines changed: 700 additions & 151 deletions

File tree

packages/browseros/bos_build/cli/release.py

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@
77
import typer
88

99
from ..core.context import Context
10+
from ..core.products import ProductDescriptor, get_product_descriptor
1011
from ..lib.notify import slack_subscriber
1112
from ..lib.paths import get_package_root
1213
from ..core.runner import StepExecutionError, run as run_steps
1314
from ..lib.utils import log_info, log_error
15+
from ..products import PRODUCTS
1416

1517
from ..release import (
1618
AVAILABLE_MODULES,
@@ -20,6 +22,7 @@
2022
PublishModule,
2123
DownloadModule,
2224
)
25+
from ..release.list import DEFAULT_LIST_LIMIT
2326

2427
app = typer.Typer(
2528
help="Release automation commands",
@@ -35,10 +38,24 @@
3538
)
3639
app.add_typer(github_app, name="github")
3740

41+
_PRODUCT_HELP = f"Product to operate on ({', '.join(PRODUCTS)})"
42+
43+
44+
def _resolve_product(product_id: Optional[str]) -> ProductDescriptor:
45+
"""Resolve --product to a descriptor with a CLI-friendly error."""
46+
try:
47+
return get_product_descriptor(product_id)
48+
except ValueError:
49+
log_error(
50+
f"Unknown product '{product_id}'. Valid: {', '.join(sorted(PRODUCTS))}"
51+
)
52+
raise typer.Exit(1)
53+
3854

3955
def create_release_context(
4056
version: str,
4157
repo: Optional[str] = None,
58+
product: Optional[str] = None,
4259
) -> Context:
4360
"""Create Context for release operations.
4461
@@ -51,6 +68,7 @@ def create_release_context(
5168
chromium_src=root,
5269
architecture="",
5370
build_type="release",
71+
product=_resolve_product(product),
5472
)
5573
ctx.release_version = version
5674
ctx.github_repo = repo or ""
@@ -71,51 +89,22 @@ def execute_module(ctx: Context, module) -> None:
7189
@app.callback(invoke_without_command=True)
7290
def main(
7391
ctx: typer.Context,
74-
version: Optional[str] = typer.Option(
75-
None, "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
76-
),
77-
list_artifacts: bool = typer.Option(
78-
False, "--list", "-l", help="List artifacts for version from R2"
79-
),
80-
appcast: bool = typer.Option(
81-
False, "--appcast", "-a", help="Generate appcast XML snippets"
82-
),
83-
publish: bool = typer.Option(
84-
False, "--publish", "-p", help="Publish to download/ paths (make live)"
85-
),
86-
download: bool = typer.Option(
87-
False, "--download", "-d", help="Download artifacts to temp directory"
88-
),
89-
os_filter: Optional[str] = typer.Option(
90-
None, "--os", help="Filter by OS: macos, windows, linux"
91-
),
92-
output: Optional[Path] = typer.Option(
93-
None, "--output", "-o", help="Output directory for downloads (default: temp dir)"
94-
),
9592
show_modules: bool = typer.Option(
9693
False, "--show-modules", help="Show available modules and exit"
9794
),
9895
):
9996
"""Release automation for BrowserOS
10097
10198
\b
102-
Quick Operations (Flags):
103-
browseros release --list # List all available versions
104-
browseros release --list --version 0.31.0 # List artifacts for version
105-
browseros release --version 0.31.0 --appcast # Generate appcast XML
106-
browseros release --version 0.31.0 --publish # Publish to download/ paths
107-
browseros release --version 0.31.0 --download # Download all artifacts
108-
browseros release --version 0.31.0 --download --os macos # Download macOS only
109-
browseros release --version 0.31.0 --download --output ./downloads # Custom dir
110-
111-
\b
112-
GitHub Release (Sub-command):
99+
Commands:
100+
browseros release list # Newest releases per product
101+
browseros release list 0.31.0 # Artifacts for a version
102+
browseros release appcast --version 0.31.0 # Generate appcast XML
103+
browseros release publish --version 0.31.0 # Publish to download/ paths
104+
browseros release download --version 0.31.0 # Download all artifacts
113105
browseros release github create --version 0.31.0
114-
browseros release github create --version 0.31.0 --publish
115106
116-
\b
117-
Show Available Modules:
118-
browseros release --show-modules
107+
Use --product to target a specific product (default: browseros).
119108
"""
120109
if show_modules:
121110
log_info("\n📦 Available Release Modules:")
@@ -125,49 +114,107 @@ def main(
125114
log_info("-" * 50)
126115
return
127116

128-
# If subcommand invoked, let it handle things
129-
if ctx.invoked_subcommand is not None:
130-
return
117+
if ctx.invoked_subcommand is None:
118+
typer.echo(ctx.get_help())
119+
raise typer.Exit(0)
131120

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

135-
if not has_flags:
136-
typer.echo(
137-
"Error: Specify a flag (--list, --appcast, --publish, --download) or use a sub-command\n"
138-
)
139-
typer.echo("Use --help for usage information")
140-
typer.echo("Use --show-modules to see available modules")
141-
raise typer.Exit(1)
122+
@app.command("list")
123+
def list_releases(
124+
version_arg: Optional[str] = typer.Argument(
125+
None, metavar="[VERSION]", help="Show artifact details for this version"
126+
),
127+
version: Optional[str] = typer.Option(
128+
None, "--version", "-v", help="Show artifact details for this version"
129+
),
130+
product: Optional[str] = typer.Option(
131+
None, "--product", help=f"{_PRODUCT_HELP}; default: all products"
132+
),
133+
limit: int = typer.Option(
134+
DEFAULT_LIST_LIMIT, "--limit", "-n", min=1, help="Versions shown per product"
135+
),
136+
show_all: bool = typer.Option(False, "--all", help="Show every version"),
137+
):
138+
"""List releases from R2 (newest first), or artifacts for one version.
142139
143-
# Version is required for all flags except --list
144-
requires_version = any([appcast, publish, download])
145-
if requires_version and not version:
146-
log_error("--version is required for this operation")
140+
\b
141+
Examples:
142+
browseros release list # Newest 5 per product
143+
browseros release list --all # Every version
144+
browseros release list -n 10 # Newest 10 per product
145+
browseros release list --product browserclaw # One product only
146+
browseros release list 0.31.0 # Artifact details
147+
"""
148+
if version_arg and version and version_arg != version:
149+
log_error(f"Conflicting versions: '{version_arg}' vs --version '{version}'")
147150
raise typer.Exit(1)
151+
resolved_version = version_arg or version
148152

149-
# Create context
150-
release_ctx = create_release_context(version or "")
151-
152-
# Execute requested modules
153-
if list_artifacts:
154-
if version:
155-
log_info(f"📋 Listing artifacts for v{version}")
156-
else:
157-
log_info("📋 Listing all available releases")
153+
if resolved_version:
154+
release_ctx = create_release_context(resolved_version, product=product)
155+
log_info(f"📋 Listing artifacts for v{resolved_version}")
158156
execute_module(release_ctx, ListModule())
157+
return
158+
159+
products = [_resolve_product(product)] if product else list(PRODUCTS.values())
160+
release_ctx = create_release_context("", product=product)
161+
log_info("📋 Listing available releases")
162+
execute_module(
163+
release_ctx,
164+
ListModule(products=products, limit=None if show_all else limit),
165+
)
159166

160-
if appcast:
161-
log_info(f"📝 Generating appcast for v{version}")
162-
execute_module(release_ctx, AppcastModule())
163167

164-
if publish:
165-
log_info(f"🚀 Publishing v{version} to download/ paths")
166-
execute_module(release_ctx, PublishModule())
168+
@app.command("appcast")
169+
def appcast(
170+
version: str = typer.Option(
171+
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
172+
),
173+
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
174+
):
175+
"""Generate Sparkle appcast XML snippets."""
176+
release_ctx = create_release_context(version, product=product)
177+
log_info(f"📝 Generating appcast for v{version}")
178+
execute_module(release_ctx, AppcastModule())
179+
180+
181+
@app.command("publish")
182+
def publish(
183+
version: str = typer.Option(
184+
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
185+
),
186+
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
187+
):
188+
"""Publish versioned artifacts to download/ paths (make live)."""
189+
release_ctx = create_release_context(version, product=product)
190+
log_info(f"🚀 Publishing v{version} to download/ paths")
191+
execute_module(release_ctx, PublishModule())
167192

168-
if download:
169-
log_info(f"📥 Downloading artifacts for v{version}")
170-
execute_module(release_ctx, DownloadModule(os_filter=os_filter, output_dir=output))
193+
194+
@app.command("download")
195+
def download(
196+
version: str = typer.Option(
197+
..., "--version", "-v", help="Version to operate on (e.g., 0.31.0)"
198+
),
199+
os_filter: Optional[str] = typer.Option(
200+
None, "--os", help="Filter by OS: macos, windows, linux"
201+
),
202+
output: Optional[Path] = typer.Option(
203+
None, "--output", "-o", help="Output directory for downloads (default: temp dir)"
204+
),
205+
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
206+
):
207+
"""Download release artifacts to a local directory.
208+
209+
\b
210+
Examples:
211+
browseros release download --version 0.31.0
212+
browseros release download --version 0.31.0 --os macos
213+
browseros release download --version 0.31.0 --output ./downloads
214+
"""
215+
release_ctx = create_release_context(version, product=product)
216+
log_info(f"📥 Downloading artifacts for v{version}")
217+
execute_module(release_ctx, DownloadModule(os_filter=os_filter, output_dir=output))
171218

172219

173220
@github_app.command("create")
@@ -190,6 +237,7 @@ def github_create(
190237
publish_to_download: bool = typer.Option(
191238
False, "--publish", "-p", help="Also publish to download/ paths after creating release"
192239
),
240+
product: Optional[str] = typer.Option(None, "--product", help=_PRODUCT_HELP),
193241
):
194242
"""Create GitHub release from R2 artifacts
195243
@@ -199,7 +247,7 @@ def github_create(
199247
browseros release github create --version 0.31.0 --publish # Also publish to download/
200248
browseros release github create --version 0.31.0 --no-draft # Create published release
201249
"""
202-
ctx = create_release_context(version, repo)
250+
ctx = create_release_context(version, repo, product)
203251

204252
log_info(f"🚀 Creating GitHub release for v{version}")
205253
module = GithubModule(

0 commit comments

Comments
 (0)