Skip to content

v3.19.4

v3.19.4 #32

Workflow file for this run

# This workflow is triggered when a GitHub release is created.
# It can also be run manually to re-publish to PyPI in case it failed for some reason.
name: Publish PyPI
on:
workflow_dispatch:
inputs: {}
release:
types: [published]
jobs:
build_wheels:
name: build wheels (${{ matrix.binary_name }})
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
binary_name: stagehand-server-v3-linux-x64
output_path: src/stagehand/_sea/stagehand-linux-x64
wheel_platform_tag: manylinux2014_x86_64
- os: macos-latest
binary_name: stagehand-server-v3-darwin-arm64
output_path: src/stagehand/_sea/stagehand-darwin-arm64
wheel_platform_tag: macosx_11_0_arm64
- os: macos-15-intel
binary_name: stagehand-server-v3-darwin-x64
output_path: src/stagehand/_sea/stagehand-darwin-x64
wheel_platform_tag: macosx_10_9_x86_64
- os: windows-latest
binary_name: stagehand-server-v3-win32-x64.exe
output_path: src/stagehand/_sea/stagehand-win32-x64.exe
wheel_platform_tag: win_amd64
runs-on: ${{ matrix.os }}
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.9.13"
- name: Resolve latest stagehand/server-v3 release
id: stagehand-server-release
uses: actions/github-script@v6
with:
github-token: ${{ secrets.STAGEHAND_SOURCE_TOKEN || github.token }}
script: |
const { data } = await github.rest.repos.listReleases({
owner: 'browserbase',
repo: 'stagehand',
per_page: 100,
});
const parseStableTag = (tag) => {
if (typeof tag !== 'string') return null;
const match = /^stagehand-server-v3\/v(\d+)\.(\d+)\.(\d+)$/.exec(tag);
if (!match) return null;
return match.slice(1).map(Number);
};
let best = null;
for (const release of data) {
if (release.draft || release.prerelease) continue;
const version = parseStableTag(release.tag_name);
if (!version) continue;
if (!best) {
best = { release, version };
continue;
}
const isGreater =
version[0] > best.version[0] ||
(version[0] === best.version[0] && version[1] > best.version[1]) ||
(version[0] === best.version[0] && version[1] === best.version[1] && version[2] > best.version[2]);
if (isGreater) {
best = { release, version };
}
}
if (!best) {
core.setFailed('No stable stagehand-server-v3/vX.Y.Z release found in browserbase/stagehand');
return;
}
core.info(`Using stagehand/server-v3 release tag: ${best.release.tag_name}`);
core.setOutput('tag', best.release.tag_name);
core.setOutput('id', String(best.release.id));
- name: Download stagehand/server SEA binary (from GitHub Release assets)
env:
GH_TOKEN: ${{ secrets.STAGEHAND_SOURCE_TOKEN || github.token }}
RELEASE_TAG: ${{ steps.stagehand-server-release.outputs.tag }}
RELEASE_ID: ${{ steps.stagehand-server-release.outputs.id }}
ASSET_NAME: ${{ matrix.binary_name }}
OUTPUT_PATH: ${{ matrix.output_path }}
shell: bash
run: |
set -euo pipefail
# Ensure we only ship the binary for this runner's OS/arch.
python - <<'PY'
from pathlib import Path
sea_dir = Path("src/stagehand/_sea")
sea_dir.mkdir(parents=True, exist_ok=True)
for p in sea_dir.glob("stagehand-*"):
p.unlink(missing_ok=True)
for p in sea_dir.glob("*.exe"):
p.unlink(missing_ok=True)
PY
echo "Downloading ${ASSET_NAME} from browserbase/stagehand@${RELEASE_TAG}"
url="$(
curl -fsSL \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/browserbase/stagehand/releases/${RELEASE_ID}" \
| python -c 'import json,sys; d=json.load(sys.stdin); a=next((x for x in d.get("assets",[]) if x.get("name")==sys.argv[1]), None); print(a.get("url","") if a else "")' \
"${ASSET_NAME}"
)"
if [ -z "${url}" ]; then
echo "Release asset not found: ${ASSET_NAME} (tag=${RELEASE_TAG})" >&2
exit 1
fi
mkdir -p "$(dirname "${OUTPUT_PATH}")"
curl -fsSL \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/octet-stream" \
"${url}" \
-o "${OUTPUT_PATH}"
chmod +x "${OUTPUT_PATH}" 2>/dev/null || true
rm -f src/stagehand/_sea/.keep || true
- name: Build wheel
env:
STAGEHAND_WHEEL_TAG: py3-none-${{ matrix.wheel_platform_tag }}
run: uv build --wheel
- name: Log SEA contents
shell: bash
run: |
echo "Contents of src/stagehand/_sea/"
ls -al src/stagehand/_sea || true
python - <<'PY'
import pathlib, zipfile, collections, sys
had_error = False
for wheel in sorted(pathlib.Path("dist").glob("*.whl")):
print(f"Contents of {wheel.name} entries matching stagehand/_sea")
with zipfile.ZipFile(wheel, "r") as zf:
names = [info.filename for info in zf.infolist()]
counts = collections.Counter(names)
dups = sorted([name for name, n in counts.items() if n > 1])
for info in zf.infolist():
if "stagehand/_sea/" in info.filename:
print(info.filename)
if dups:
had_error = True
print("ERROR: duplicate zip entries detected:")
for name in dups:
print(f" {counts[name]}x {name}")
if had_error:
sys.exit(1)
PY
- name: Upload wheel artifact
uses: actions/upload-artifact@v4
with:
name: wheel-${{ matrix.binary_name }}
path: dist/*.whl
retention-days: 7
build_sdist:
name: build sdist
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.9.13"
- name: Build sdist
run: uv build --sdist
- name: Upload sdist artifact
uses: actions/upload-artifact@v4
with:
name: sdist
path: dist/*.tar.gz
retention-days: 7
publish:
name: publish
needs: [build_wheels, build_sdist]
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
version: "0.9.13"
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Flatten dist directory
shell: bash
run: |
set -euo pipefail
mkdir -p dist_out
find dist -type f \( -name "*.whl" -o -name "*.tar.gz" \) -print0 | while IFS= read -r -d '' f; do
cp -f "$f" dist_out/
done
ls -la dist_out
- name: Publish to PyPI
env:
PYPI_TOKEN: ${{ secrets.STAGEHAND_PYPI_TOKEN || secrets.PYPI_TOKEN }}
run: |
set -euo pipefail
uv publish --token="$PYPI_TOKEN" dist_out/*