From f510a0fe64921c1d6143baacbe53e455d193632c Mon Sep 17 00:00:00 2001 From: monadoid Date: Fri, 16 Jan 2026 14:14:43 -0700 Subject: [PATCH 1/3] fix: CI was pulling the entire stagehand repo and re-creating the node sea binary. Now it just pulls the binary. docs: Added docs --- .github/workflows/publish-pypi.yml | 90 +++++++++++++++++------------- RELEASE_WORKFLOWS.md | 24 ++++++++ 2 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 RELEASE_WORKFLOWS.md diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 2095212..771ad27 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -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] @@ -49,39 +45,37 @@ 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 }} + 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") @@ -92,15 +86,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/tags/${RELEASE_TAG}" \ + | 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: diff --git a/RELEASE_WORKFLOWS.md b/RELEASE_WORKFLOWS.md new file mode 100644 index 0000000..1dfb78a --- /dev/null +++ b/RELEASE_WORKFLOWS.md @@ -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-` 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. From 0b80321b2b4de7d6f7a1d1c12d3faa84b1556621 Mon Sep 17 00:00:00 2001 From: monadoid Date: Fri, 16 Jan 2026 14:17:56 -0700 Subject: [PATCH 2/3] lint fix --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 420bb05..a7f085f 100644 --- a/uv.lock +++ b/uv.lock @@ -1359,7 +1359,7 @@ wheels = [ [[package]] name = "stagehand" -version = "3.4.6" +version = "3.4.7" source = { editable = "." } dependencies = [ { name = "anyio" }, From 0cd451cef6e31f931b35362f40991734db1f715d Mon Sep 17 00:00:00 2001 From: monadoid Date: Fri, 16 Jan 2026 14:28:04 -0700 Subject: [PATCH 3/3] Use release id instead of release tag --- .github/workflows/publish-pypi.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 771ad27..d77d01a 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -69,6 +69,7 @@ jobs: 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 @@ -92,7 +93,7 @@ jobs: curl -fsSL \ -H "Authorization: Bearer ${GH_TOKEN}" \ -H "Accept: application/vnd.github+json" \ - "https://api.github.com/repos/browserbase/stagehand/releases/tags/${RELEASE_TAG}" \ + "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}" )"