Skip to content

Commit 0be87c7

Browse files
committed
fix dev tag for sea binary should never be used
1 parent 763131a commit 0be87c7

File tree

5 files changed

+687
-376
lines changed

5 files changed

+687
-376
lines changed

.github/workflows/publish-pypi.yml

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,40 @@ jobs:
5656
repo: 'stagehand',
5757
per_page: 100,
5858
});
59-
const release = data.find(r => typeof r.tag_name === 'string' && r.tag_name.startsWith('stagehand-server-v3/v'));
60-
if (!release) {
61-
core.setFailed('No stagehand-server-v3/v* release found in browserbase/stagehand');
59+
const parseStableTag = (tag) => {
60+
if (typeof tag !== 'string') return null;
61+
const match = /^stagehand-server-v3\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
62+
if (!match) return null;
63+
return match.slice(1).map(Number);
64+
};
65+
66+
let best = null;
67+
for (const release of data) {
68+
if (release.draft || release.prerelease) continue;
69+
const version = parseStableTag(release.tag_name);
70+
if (!version) continue;
71+
if (!best) {
72+
best = { release, version };
73+
continue;
74+
}
75+
76+
const isGreater =
77+
version[0] > best.version[0] ||
78+
(version[0] === best.version[0] && version[1] > best.version[1]) ||
79+
(version[0] === best.version[0] && version[1] === best.version[1] && version[2] > best.version[2]);
80+
81+
if (isGreater) {
82+
best = { release, version };
83+
}
84+
}
85+
86+
if (!best) {
87+
core.setFailed('No stable stagehand-server-v3/vX.Y.Z release found in browserbase/stagehand');
6288
return;
6389
}
64-
core.info(`Using stagehand/server-v3 release tag: ${release.tag_name}`);
65-
core.setOutput('tag', release.tag_name);
66-
core.setOutput('id', String(release.id));
90+
core.info(`Using stagehand/server-v3 release tag: ${best.release.tag_name}`);
91+
core.setOutput('tag', best.release.tag_name);
92+
core.setOutput('id', String(best.release.id));
6793
6894
- name: Download stagehand/server SEA binary (from GitHub Release assets)
6995
env:

scripts/download-binary.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ def _parse_server_tag(tag: str) -> tuple[int, int, int] | None:
5858
return None
5959

6060
ver = tag.removeprefix("stagehand-server-v3/v")
61-
# Drop any pre-release/build metadata (we only expect stable tags here).
62-
ver = ver.split("-", 1)[0].split("+", 1)[0]
61+
# Only accept stable tags. Pre-release/build tags like "-dev" should not
62+
# be used for local binary downloads or release packaging.
63+
if "-" in ver or "+" in ver:
64+
return None
6365
parts = ver.split(".")
6466
if len(parts) != 3:
6567
return None

src/stagehand/lib/sea_binary.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from pathlib import Path
99
from contextlib import suppress
1010

11+
from .._version import __version__
12+
1113

1214
def _platform_tag() -> tuple[str, str]:
1315
plat = "win32" if sys.platform.startswith("win") else ("darwin" if sys.platform == "darwin" else "linux")
@@ -99,7 +101,7 @@ def resolve_binary_path(
99101
if resource_path is not None:
100102
# Best-effort versioning to keep cached binaries stable across upgrades.
101103
if version is None:
102-
version = os.environ.get("STAGEHAND_VERSION", "dev")
104+
version = os.environ.get("STAGEHAND_VERSION") or __version__
103105
return _copy_to_cache(src=resource_path, filename=filename, version=version)
104106

105107
# Fallback: source checkout layout (works for local dev in-repo).

tests/test_sea_binary.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
from pathlib import Path
5+
6+
from stagehand.lib import sea_binary
7+
from stagehand._version import __version__
8+
9+
10+
def _load_download_binary_module():
11+
script_path = Path(__file__).resolve().parents[1] / "scripts" / "download-binary.py"
12+
spec = importlib.util.spec_from_file_location("download_binary_script", script_path)
13+
assert spec is not None
14+
assert spec.loader is not None
15+
16+
module = importlib.util.module_from_spec(spec)
17+
spec.loader.exec_module(module)
18+
return module
19+
20+
21+
download_binary = _load_download_binary_module()
22+
23+
24+
def test_resolve_binary_path_defaults_cache_version_to_package_version(
25+
monkeypatch,
26+
tmp_path: Path,
27+
) -> None:
28+
resource_path = tmp_path / "stagehand-test"
29+
resource_path.write_bytes(b"binary")
30+
31+
captured: dict[str, object] = {}
32+
33+
monkeypatch.delenv("STAGEHAND_VERSION", raising=False)
34+
monkeypatch.setattr(sea_binary, "_resource_binary_path", lambda _filename: resource_path)
35+
36+
def _fake_copy_to_cache(*, src: Path, filename: str, version: str) -> Path:
37+
captured["src"] = src
38+
captured["filename"] = filename
39+
captured["version"] = version
40+
return tmp_path / "cache" / filename
41+
42+
monkeypatch.setattr(sea_binary, "_copy_to_cache", _fake_copy_to_cache)
43+
44+
resolved = sea_binary.resolve_binary_path()
45+
46+
assert resolved == tmp_path / "cache" / sea_binary.default_binary_filename()
47+
assert captured["src"] == resource_path
48+
assert captured["filename"] == sea_binary.default_binary_filename()
49+
assert captured["version"] == __version__
50+
51+
52+
def test_parse_server_tag_rejects_prerelease_tags() -> None:
53+
assert download_binary._parse_server_tag("stagehand-server-v3/v3.20.0-dev") is None
54+
assert download_binary._parse_server_tag("stagehand-server-v3/v3.20.0+build.1") is None
55+
56+
57+
def test_resolve_latest_server_tag_ignores_dev_releases(monkeypatch) -> None:
58+
releases = [
59+
{"tag_name": "stagehand-server-v3/v3.20.0-dev"},
60+
{"tag_name": "stagehand-server-v3/v3.19.1"},
61+
{"tag_name": "stagehand-server-v3/v3.19.0"},
62+
]
63+
64+
monkeypatch.setattr(download_binary, "_http_get_json", lambda _url: releases)
65+
66+
assert download_binary.resolve_latest_server_tag() == "stagehand-server-v3/v3.19.1"

0 commit comments

Comments
 (0)