Skip to content

Commit 798c057

Browse files
committed
Added local binary logic
1 parent f24078b commit 798c057

File tree

13 files changed

+1036
-20
lines changed

13 files changed

+1036
-20
lines changed

.github/workflows/publish-pypi.yml

Lines changed: 147 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,170 @@
11
# This workflow is triggered when a GitHub release is created.
22
# It can also be run manually to re-publish to PyPI in case it failed for some reason.
3-
# You can run this workflow by navigating to https://www.github.com/browserbase/stagehand-python/actions/workflows/publish-pypi.yml
43
name: Publish PyPI
4+
55
on:
66
workflow_dispatch:
7+
inputs:
8+
stagehand_tag:
9+
description: "Stagehand repo git ref to build SEA binaries from (e.g. @browserbasehq/stagehand@3.0.6)"
10+
required: true
11+
type: string
712

813
release:
914
types: [published]
1015

1116
jobs:
17+
build_wheels:
18+
name: build wheels (${{ matrix.binary_name }})
19+
strategy:
20+
fail-fast: false
21+
matrix:
22+
include:
23+
- os: ubuntu-latest
24+
binary_name: stagehand-linux-x64
25+
output_path: src/stagehand/_sea/stagehand-linux-x64
26+
wheel_platform_tag: linux_x86_64
27+
- os: macos-latest
28+
binary_name: stagehand-darwin-arm64
29+
output_path: src/stagehand/_sea/stagehand-darwin-arm64
30+
wheel_platform_tag: macosx_11_0_arm64
31+
- os: macos-13
32+
binary_name: stagehand-darwin-x64
33+
output_path: src/stagehand/_sea/stagehand-darwin-x64
34+
wheel_platform_tag: macosx_10_9_x86_64
35+
- os: windows-latest
36+
binary_name: stagehand-win32-x64.exe
37+
output_path: src/stagehand/_sea/stagehand-win32-x64.exe
38+
wheel_platform_tag: win_amd64
39+
40+
runs-on: ${{ matrix.os }}
41+
permissions:
42+
contents: read
43+
44+
steps:
45+
- uses: actions/checkout@v4
46+
47+
- name: Install uv
48+
uses: astral-sh/setup-uv@v5
49+
with:
50+
version: "0.9.13"
51+
52+
- name: Checkout stagehand (server source)
53+
uses: actions/checkout@v4
54+
with:
55+
repository: browserbase/stagehand
56+
ref: ${{ inputs.stagehand_tag || vars.STAGEHAND_TAG }}
57+
path: _stagehand
58+
fetch-depth: 1
59+
# If browserbase/stagehand is private, set STAGEHAND_SOURCE_TOKEN (PAT) in this repo.
60+
token: ${{ secrets.STAGEHAND_SOURCE_TOKEN || github.token }}
61+
62+
- name: Setup pnpm
63+
uses: pnpm/action-setup@v4
64+
65+
- name: Setup Node.js
66+
uses: actions/setup-node@v4
67+
with:
68+
node-version: "23"
69+
cache: "pnpm"
70+
cache-dependency-path: _stagehand/pnpm-lock.yaml
71+
72+
- name: Build SEA server binary (from source)
73+
shell: bash
74+
run: |
75+
set -euo pipefail
76+
77+
if [[ -z "${{ inputs.stagehand_tag }}" && -z "${{ vars.STAGEHAND_TAG }}" ]]; then
78+
echo "Missing stagehand ref: set repo variable STAGEHAND_TAG or provide workflow input stagehand_tag." >&2
79+
exit 1
80+
fi
81+
82+
# Ensure we only ship the binary built for this runner's OS/arch.
83+
python - <<'PY'
84+
from pathlib import Path
85+
sea_dir = Path("src/stagehand/_sea")
86+
sea_dir.mkdir(parents=True, exist_ok=True)
87+
for p in sea_dir.glob("stagehand-*"):
88+
p.unlink(missing_ok=True)
89+
for p in sea_dir.glob("*.exe"):
90+
p.unlink(missing_ok=True)
91+
PY
92+
93+
pushd _stagehand >/dev/null
94+
pnpm install --frozen-lockfile
95+
CI=true pnpm --filter @browserbasehq/stagehand-server build:binary
96+
popd >/dev/null
97+
98+
cp "_stagehand/packages/server/dist/sea/${{ matrix.binary_name }}" "${{ matrix.output_path }}"
99+
chmod +x "${{ matrix.output_path }}" 2>/dev/null || true
100+
101+
- name: Build wheel
102+
env:
103+
STAGEHAND_WHEEL_TAG: py3-none-${{ matrix.wheel_platform_tag }}
104+
run: uv build --wheel
105+
106+
- name: Upload wheel artifact
107+
uses: actions/upload-artifact@v4
108+
with:
109+
name: wheel-${{ matrix.binary_name }}
110+
path: dist/*.whl
111+
retention-days: 7
112+
113+
build_sdist:
114+
name: build sdist
115+
runs-on: ubuntu-latest
116+
permissions:
117+
contents: read
118+
steps:
119+
- uses: actions/checkout@v4
120+
121+
- name: Install uv
122+
uses: astral-sh/setup-uv@v5
123+
with:
124+
version: "0.9.13"
125+
126+
- name: Build sdist
127+
run: uv build --sdist
128+
129+
- name: Upload sdist artifact
130+
uses: actions/upload-artifact@v4
131+
with:
132+
name: sdist
133+
path: dist/*.tar.gz
134+
retention-days: 7
135+
12136
publish:
13137
name: publish
138+
needs: [build_wheels, build_sdist]
14139
runs-on: ubuntu-latest
15-
140+
permissions:
141+
contents: read
16142
steps:
17143
- uses: actions/checkout@v4
18144

19145
- name: Install uv
20146
uses: astral-sh/setup-uv@v5
21147
with:
22-
version: '0.9.13'
148+
version: "0.9.13"
23149

24-
- name: Publish to PyPI
150+
- name: Download build artifacts
151+
uses: actions/download-artifact@v4
152+
with:
153+
path: dist
154+
155+
- name: Flatten dist directory
156+
shell: bash
25157
run: |
26-
bash ./bin/publish-pypi
158+
set -euo pipefail
159+
mkdir -p dist_out
160+
find dist -type f \( -name "*.whl" -o -name "*.tar.gz" \) -print0 | while IFS= read -r -d '' f; do
161+
cp -f "$f" dist_out/
162+
done
163+
ls -la dist_out
164+
165+
- name: Publish to PyPI
27166
env:
28167
PYPI_TOKEN: ${{ secrets.STAGEHAND_PYPI_TOKEN || secrets.PYPI_TOKEN }}
168+
run: |
169+
set -euo pipefail
170+
uv publish --token="$PYPI_TOKEN" dist_out/*

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ dist
99
.venv
1010
.idea
1111

12+
.DS_Store
13+
src/stagehand/_sea/stagehand-*
14+
src/stagehand/_sea/*.exe
15+
bin/sea/
1216
.env
1317
.envrc
1418
codegen.log

CONTRIBUTING.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,47 @@ Most of the SDK is generated code. Modifications to code will be persisted betwe
3838
result in merge conflicts between manual patches and changes from the generator. The generator will never
3939
modify the contents of the `src/stagehand/lib/` and `examples/` directories.
4040

41+
## Setting up the local server binary (for development)
42+
43+
The SDK supports running a local Stagehand server for development and testing. To use this feature, you need to download the appropriate binary for your platform.
44+
45+
### Quick setup
46+
47+
Run the download script to automatically download the correct binary:
48+
49+
```sh
50+
$ uv run python scripts/download-binary.py
51+
```
52+
53+
This will:
54+
- Detect your platform (macOS, Linux, Windows) and architecture (x64, arm64)
55+
- Download the latest stagehand-server binary from GitHub releases
56+
- Place it in `bin/sea/` where the SDK expects to find it
57+
58+
### Manual download (alternative)
59+
60+
You can also manually download from [GitHub releases](https://github.com/browserbase/stagehand/releases):
61+
62+
1. Find the latest `stagehand/server vX.X.X` release
63+
2. Download the binary for your platform:
64+
- macOS ARM: `stagehand-server-darwin-arm64`
65+
- macOS Intel: `stagehand-server-darwin-x64`
66+
- Linux: `stagehand-server-linux-x64` or `stagehand-server-linux-arm64`
67+
- Windows: `stagehand-server-win32-x64.exe` or `stagehand-server-win32-arm64.exe`
68+
3. Rename it to match the expected format (remove `-server` from the name):
69+
- `stagehand-darwin-arm64`, `stagehand-linux-x64`, `stagehand-win32-x64.exe`, etc.
70+
4. Place it in `bin/sea/` directory
71+
5. Make it executable (Unix only): `chmod +x bin/sea/stagehand-*`
72+
73+
### Using an environment variable (optional)
74+
75+
Instead of placing the binary in `bin/sea/`, you can point to any binary location:
76+
77+
```sh
78+
$ export STAGEHAND_SEA_BINARY=/path/to/your/stagehand-binary
79+
$ uv run python test_local_mode.py
80+
```
81+
4182
## Adding and running examples
4283

4384
All files in the `examples/` directory are not modified by the generator and can be freely edited or added to.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ exclude = [
136136
".venv",
137137
".nox",
138138
".git",
139+
"hatch_build.py",
139140
]
140141

141142
reportImplicitOverride = true
@@ -154,7 +155,7 @@ show_error_codes = true
154155
#
155156
# We also exclude our `tests` as mypy doesn't always infer
156157
# types correctly and Pyright will still catch any type errors.
157-
exclude = ['src/stagehand/_files.py', '_dev/.*.py', 'tests/.*']
158+
exclude = ['src/stagehand/_files.py', '_dev/.*.py', 'tests/.*', 'hatch_build.py']
158159

159160
strict_equality = true
160161
implicit_reexport = true

scripts/download-binary.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Download the stagehand-server binary for local development.
4+
5+
This script downloads the appropriate binary for your platform from GitHub releases
6+
and places it in bin/sea/ for use during development and testing.
7+
8+
Usage:
9+
python scripts/download-binary.py [--version VERSION]
10+
11+
Examples:
12+
python scripts/download-binary.py
13+
python scripts/download-binary.py --version v3.2.0
14+
"""
15+
16+
import sys
17+
import platform
18+
import argparse
19+
import urllib.request
20+
from pathlib import Path
21+
22+
23+
def get_platform_info() -> tuple[str, str]:
24+
"""Determine platform and architecture."""
25+
system = platform.system().lower()
26+
machine = platform.machine().lower()
27+
28+
if system == "darwin":
29+
plat = "darwin"
30+
elif system == "windows":
31+
plat = "win32"
32+
else:
33+
plat = "linux"
34+
35+
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
36+
return plat, arch
37+
38+
39+
def get_binary_filename(plat: str, arch: str) -> str:
40+
"""Get the expected binary filename for this platform."""
41+
name = f"stagehand-server-{plat}-{arch}"
42+
return name + (".exe" if plat == "win32" else "")
43+
44+
45+
def get_local_filename(plat: str, arch: str) -> str:
46+
"""Get the local filename (what the code expects to find)."""
47+
name = f"stagehand-{plat}-{arch}"
48+
return name + (".exe" if plat == "win32" else "")
49+
50+
51+
def download_binary(version: str) -> None:
52+
"""Download the binary for the current platform."""
53+
plat, arch = get_platform_info()
54+
binary_filename = get_binary_filename(plat, arch)
55+
local_filename = get_local_filename(plat, arch)
56+
57+
# GitHub release URL
58+
repo = "browserbase/stagehand"
59+
tag = version if version.startswith("stagehand-server/v") else f"stagehand-server/{version}"
60+
url = f"https://github.com/{repo}/releases/download/{tag}/{binary_filename}"
61+
62+
# Destination path
63+
repo_root = Path(__file__).parent.parent
64+
dest_dir = repo_root / "bin" / "sea"
65+
dest_dir.mkdir(parents=True, exist_ok=True)
66+
dest_path = dest_dir / local_filename
67+
68+
if dest_path.exists():
69+
print(f"✓ Binary already exists: {dest_path}")
70+
response = input(" Overwrite? [y/N]: ").strip().lower()
71+
if response != "y":
72+
print(" Skipping download.")
73+
return
74+
75+
print(f"📦 Downloading binary for {plat}-{arch}...")
76+
print(f" From: {url}")
77+
print(f" To: {dest_path}")
78+
79+
try:
80+
# Download with progress
81+
def reporthook(block_num, block_size, total_size):
82+
downloaded = block_num * block_size
83+
if total_size > 0:
84+
percent = min(downloaded * 100 / total_size, 100)
85+
mb_downloaded = downloaded / (1024 * 1024)
86+
mb_total = total_size / (1024 * 1024)
87+
print(f"\r Progress: {percent:.1f}% ({mb_downloaded:.1f}/{mb_total:.1f} MB)", end="")
88+
89+
urllib.request.urlretrieve(url, dest_path, reporthook)
90+
print() # New line after progress
91+
92+
# Make executable on Unix
93+
if plat != "win32":
94+
import os
95+
os.chmod(dest_path, 0o755)
96+
97+
size_mb = dest_path.stat().st_size / (1024 * 1024)
98+
print(f"✅ Downloaded successfully: {dest_path} ({size_mb:.1f} MB)")
99+
print(f"\n💡 You can now run: uv run python test_local_mode.py")
100+
101+
except urllib.error.HTTPError as e:
102+
print(f"\n❌ Error: Failed to download binary (HTTP {e.code})")
103+
print(f" URL: {url}")
104+
print(f"\n Available releases at: https://github.com/{repo}/releases")
105+
sys.exit(1)
106+
except Exception as e:
107+
print(f"\n❌ Error: {e}")
108+
sys.exit(1)
109+
110+
111+
def main() -> None:
112+
parser = argparse.ArgumentParser(
113+
description="Download stagehand-server binary for local development",
114+
formatter_class=argparse.RawDescriptionHelpFormatter,
115+
epilog="""
116+
Examples:
117+
python scripts/download-binary.py
118+
python scripts/download-binary.py --version v3.2.0
119+
python scripts/download-binary.py --version stagehand-server/v3.2.0
120+
""",
121+
)
122+
parser.add_argument(
123+
"--version",
124+
default="v3.2.0",
125+
help="Version to download (default: v3.2.0)",
126+
)
127+
128+
args = parser.parse_args()
129+
download_binary(args.version)
130+
131+
132+
if __name__ == "__main__":
133+
main()

0 commit comments

Comments
 (0)