Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 51 additions & 40 deletions .github/workflows/publish-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,7 @@ name: Publish PyPI

on:
workflow_dispatch:
inputs:
stagehand_tag:
description: "Stagehand repo git ref to build SEA binaries from (e.g. @browserbasehq/stagehand@3.0.6)"
required: true
type: string
inputs: {}

release:
types: [published]
Expand Down Expand Up @@ -49,39 +45,38 @@ jobs:
with:
version: "0.9.13"

- name: Checkout stagehand (server source)
uses: actions/checkout@v4
- name: Resolve latest stagehand/server release
id: stagehand-server-release
uses: actions/github-script@v6
with:
repository: browserbase/stagehand
ref: ${{ inputs.stagehand_tag || vars.STAGEHAND_TAG }}
path: _stagehand
fetch-depth: 1
# If browserbase/stagehand is private, set STAGEHAND_SOURCE_TOKEN (PAT) in this repo.
token: ${{ secrets.STAGEHAND_SOURCE_TOKEN || github.token }}

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "23"
cache: "pnpm"
cache-dependency-path: _stagehand/pnpm-lock.yaml

- name: Build SEA server binary (from source)
github-token: ${{ secrets.STAGEHAND_SOURCE_TOKEN || github.token }}
script: |
const { data } = await github.rest.repos.listReleases({
owner: 'browserbase',
repo: 'stagehand',
per_page: 100,
});
const release = data.find(r => typeof r.tag_name === 'string' && r.tag_name.startsWith('stagehand-server/v'));
if (!release) {
core.setFailed('No stagehand-server/v* release found in browserbase/stagehand');
return;
}
core.info(`Using stagehand/server release tag: ${release.tag_name}`);
core.setOutput('tag', release.tag_name);
core.setOutput('id', String(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

if [[ -z "${{ inputs.stagehand_tag }}" && -z "${{ vars.STAGEHAND_TAG }}" ]]; then
echo "Missing stagehand ref: set repo variable STAGEHAND_TAG or provide workflow input stagehand_tag." >&2
exit 1
fi

# Ensure we only ship the binary built for this runner's OS/arch.
# Ensure we only ship the binary for this runner's OS/arch.
python - <<'PY'
from pathlib import Path
sea_dir = Path("src/stagehand/_sea")
Expand All @@ -92,15 +87,31 @@ jobs:
p.unlink(missing_ok=True)
PY

pushd _stagehand >/dev/null
pnpm install --frozen-lockfile
CI=true pnpm --filter @browserbasehq/stagehand-server build:binary
popd >/dev/null
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}"

cp "_stagehand/packages/server/dist/sea/${{ matrix.binary_name }}" "${{ matrix.output_path }}"
chmod +x "${{ matrix.output_path }}" 2>/dev/null || true
chmod +x "${OUTPUT_PATH}" 2>/dev/null || true
rm -f src/stagehand/_sea/.keep || true
git add -f src/stagehand/_sea/*

- name: Build wheel
env:
Expand Down
24 changes: 24 additions & 0 deletions RELEASE_WORKFLOWS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Release + publish workflow

This repo publishes the `stagehand` Python package to PyPI when a **GitHub Release** is published. The release is currently initiated manually via the `release-please` CLI.

## Chronological flow (step-by-step)

1. Run `pnpx release-please release-pr` (local machine).
- Opens/updates a Release PR to `main` with version + `CHANGELOG.md` updates.
2. Merge the Release PR into `main`.
3. Run `pnpx release-please github-release` (local machine).
- Publishes the GitHub Release + git tag.
4. Wait for GitHub Actions to publish to PyPI (automatic).
- Trigger: GitHub Release `published` event runs `.github/workflows/publish-pypi.yml`.
- Builds platform wheels that embed the Stagehand server binary (downloaded from the latest `stagehand-server/v*` GitHub Release in `browserbase/stagehand`), then publishes to PyPI.

## Important implementation notes

- **Server binary bundling into wheels**
- `.github/workflows/publish-pypi.yml` downloads the prebuilt Stagehand server SEA binary from the latest `stagehand-server/v*` GitHub Release in `browserbase/stagehand`, then places it into `src/stagehand/_sea/*` before running `uv build --wheel`.
- **Stagehand server version selection (current behavior)**
- `publish-pypi.yml` resolves the latest GitHub Release tag matching `stagehand-server/v*` from `browserbase/stagehand` and downloads the matching `stagehand-server-<platform>` release asset for each wheel build.
- **Secrets**
- PyPI publish uses `secrets.STAGEHAND_PYPI_TOKEN || secrets.PYPI_TOKEN`.
- `.github/workflows/release-doctor.yml` runs `bin/check-release-environment` on qualifying PRs and fails if `PYPI_TOKEN` is missing.
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading