|
15 | 15 | from __future__ import annotations |
16 | 16 |
|
17 | 17 | import sys |
| 18 | +import os |
| 19 | +import json |
18 | 20 | import argparse |
19 | 21 | import platform |
20 | 22 | import urllib.error |
21 | 23 | import urllib.request |
22 | 24 | from pathlib import Path |
| 25 | +from typing import Any |
23 | 26 |
|
24 | 27 |
|
25 | 28 | def get_platform_info() -> tuple[str, str]: |
@@ -49,6 +52,71 @@ def get_local_filename(plat: str, arch: str) -> str: |
49 | 52 | name = f"stagehand-{plat}-{arch}" |
50 | 53 | return name + (".exe" if plat == "win32" else "") |
51 | 54 |
|
| 55 | +def _parse_server_tag(tag: str) -> tuple[int, int, int] | None: |
| 56 | + # Expected: stagehand-server/vX.Y.Z |
| 57 | + if not tag.startswith("stagehand-server/v"): |
| 58 | + return None |
| 59 | + |
| 60 | + ver = tag.removeprefix("stagehand-server/v") |
| 61 | + # Drop any pre-release/build metadata (we only expect stable tags here). |
| 62 | + ver = ver.split("-", 1)[0].split("+", 1)[0] |
| 63 | + parts = ver.split(".") |
| 64 | + if len(parts) != 3: |
| 65 | + return None |
| 66 | + try: |
| 67 | + return int(parts[0]), int(parts[1]), int(parts[2]) |
| 68 | + except ValueError: |
| 69 | + return None |
| 70 | + |
| 71 | + |
| 72 | +def _http_get_json(url: str) -> Any: |
| 73 | + headers = { |
| 74 | + "Accept": "application/vnd.github+json", |
| 75 | + "User-Agent": "stagehand-python/download-binary", |
| 76 | + } |
| 77 | + # Optional, but helps avoid rate limits in CI. |
| 78 | + token = (os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") or "").strip() |
| 79 | + if token: |
| 80 | + headers["Authorization"] = f"Bearer {token}" |
| 81 | + |
| 82 | + req = urllib.request.Request(url, headers=headers) |
| 83 | + with urllib.request.urlopen(req, timeout=30) as resp: |
| 84 | + data = resp.read() |
| 85 | + return json.loads(data.decode("utf-8")) |
| 86 | + |
| 87 | + |
| 88 | +def resolve_latest_server_tag() -> str: |
| 89 | + """Resolve the latest stagehand-server/v* tag from GitHub releases.""" |
| 90 | + repo = "browserbase/stagehand" |
| 91 | + releases_url = f"https://api.github.com/repos/{repo}/releases?per_page=100" |
| 92 | + try: |
| 93 | + releases = _http_get_json(releases_url) |
| 94 | + except urllib.error.HTTPError as e: # type: ignore[misc] |
| 95 | + raise RuntimeError(f"Failed to query GitHub releases (HTTP {e.code}): {releases_url}") from e # type: ignore[union-attr] |
| 96 | + except Exception as e: |
| 97 | + raise RuntimeError(f"Failed to query GitHub releases: {releases_url}") from e |
| 98 | + |
| 99 | + if not isinstance(releases, list): |
| 100 | + raise RuntimeError(f"Unexpected GitHub API response for releases: {type(releases).__name__}") |
| 101 | + |
| 102 | + best: tuple[tuple[int, int, int], str] | None = None |
| 103 | + for r in releases: |
| 104 | + if not isinstance(r, dict): |
| 105 | + continue |
| 106 | + tag = r.get("tag_name") |
| 107 | + if not isinstance(tag, str): |
| 108 | + continue |
| 109 | + parsed = _parse_server_tag(tag) |
| 110 | + if parsed is None: |
| 111 | + continue |
| 112 | + if best is None or parsed > best[0]: |
| 113 | + best = (parsed, tag) |
| 114 | + |
| 115 | + if best is None: |
| 116 | + raise RuntimeError("No stagehand-server/v* GitHub Releases found for browserbase/stagehand") |
| 117 | + |
| 118 | + return best[1] |
| 119 | + |
52 | 120 |
|
53 | 121 | def download_binary(version: str) -> None: |
54 | 122 | """Download the binary for the current platform.""" |
@@ -123,12 +191,18 @@ def main() -> None: |
123 | 191 | ) |
124 | 192 | parser.add_argument( |
125 | 193 | "--version", |
126 | | - default="v3.2.0", |
127 | | - help="Version to download (default: v3.2.0)", |
| 194 | + default=None, |
| 195 | + help="Stagehand server release tag/version to download (e.g. v3.2.0 or stagehand-server/v3.2.0). Defaults to latest stagehand-server/* GitHub Release.", |
128 | 196 | ) |
129 | 197 |
|
130 | 198 | args = parser.parse_args() |
131 | | - download_binary(args.version) |
| 199 | + version = str(args.version).strip() if args.version is not None else "" |
| 200 | + if not version: |
| 201 | + latest_tag = resolve_latest_server_tag() |
| 202 | + download_binary(latest_tag) |
| 203 | + return |
| 204 | + |
| 205 | + download_binary(version) |
132 | 206 |
|
133 | 207 |
|
134 | 208 | if __name__ == "__main__": |
|
0 commit comments