Skip to content
Open
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
4 changes: 4 additions & 0 deletions .azure-pipelines/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ extends:
inputs:
versionSpec: '3.9'
displayName: 'Use Python'
- task: NodeTool@0
inputs:
versionSpec: '24.x'
displayName: 'Use Node.js'
- script: |
python -m pip install --upgrade pip
pip install -r local-requirements.txt
Expand Down
25 changes: 17 additions & 8 deletions .claude/skills/playwright-roll/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Roll Playwright Python to a new driver version. Walks the upstream

# Rolling Playwright Python

The goal of a roll is to move `driver_version` in `setup.py` to a new release, port every public API change introduced upstream during that interval, and suppress the rest, so that `./scripts/update_api.sh` runs clean and the test suite still passes.
The goal of a roll is to move the driver pin in `DRIVER_SHA` to a new release, port every public API change introduced upstream during that interval, and suppress the rest, so that `./scripts/update_api.sh` runs clean and the test suite still passes.

The previous human-facing summary lives in `../../../ROLLING.md`. This skill is the operational playbook — read it end to end before starting.

Expand All @@ -15,7 +15,7 @@ The Python port is hand-written code in `playwright/_impl/`, plus a generator (`

1. introspects the Python `_impl` classes via `inspect`,
2. emits typed wrapper classes into `playwright/{async,sync}_api/_generated.py`, and
3. diffs the introspected surface against `playwright/driver/package/api.json` (downloaded inside the new driver wheel).
3. diffs the introspected surface against `playwright/driver/package/api.json` (built into the new driver from source).

Anything in `api.json` that is missing or differently typed in `_impl/` causes generation to fail. Three resolutions:

Expand Down Expand Up @@ -52,18 +52,27 @@ There is sometimes no `vX.Y.0` tag for the latest release (the bots cut release
- If `python3-venv` is missing system-wide, use `uv venv env` instead, then `uv pip install --python env/bin/python --upgrade pip`. Don't try to `apt install` — sudo is denied in the harness.
- Always activate the venv before any `pip`, `pytest`, `mypy`, or `pre-commit` invocation.

### 2. Bump the driver and download it
### 2. Bump the driver and build it from source

```sh
# Edit setup.py
driver_version = "<new>" # e.g. "1.59.1"
# Edit DRIVER_SHA (repo root): replace with the microsoft/playwright commit SHA
# for the new release, e.g. the commit that v<new> points at.
# 87bb9ddbd78f329df18c2b24847bc9409240cd07
# Update the "# microsoft/playwright @ v<new>" comment in scripts/build_driver.sh too.

source env/bin/activate
python -m build --wheel # downloads the new driver from cdn.playwright.dev
python -m build --wheel # clones microsoft/playwright @ DRIVER_SHA and builds the driver from source
playwright install chromium # NOT --with-deps; sudo is denied
```

The wheel build prints `Fetching https://cdn.playwright.dev/builds/driver/playwright-<new>-linux.zip` and unpacks the driver under `playwright/driver/package/`. From this point, `playwright/driver/package/api.json` reflects the new release.
The wheel build clones `microsoft/playwright` at the commit in `DRIVER_SHA`
into `driver/playwright-src`, runs `npm ci && npm run build`, and runs upstream's
`utils/build/build-playwright-driver.sh` to produce the per-platform driver
bundles (`driver/playwright-<sha>-*.zip`), then unpacks the driver under
`playwright/driver/package/`. From this point,
`playwright/driver/package/api.json` reflects the new release. This requires
**Node.js, npm, git and bash** on PATH; the first build is slow (full upstream
build + per-platform Node downloads).

### 3. Identify the commit range

Expand Down Expand Up @@ -240,7 +249,7 @@ For each PORT, add one async test and a matching sync test. Conventions:

### 7. Update existing high-touch artifacts

- `setup.py`: already done in step 2.
- `DRIVER_SHA` (and the version comment in `scripts/build_driver.sh`): already done in step 2.
- `README.md`: gets the chromium/firefox/webkit version table updated automatically by `scripts/update_versions.py` (called from `update_api.sh`). Don't edit by hand.
- The "Backport changes" tracking issue on GitHub (filed by `microsoft-playwright-automation`) is the *intent* tracker, but it's frequently out of sync with what's actually been ported. Treat it as a starting point, not the source of truth — the `docs/src/api/` commit walk is authoritative.

Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,43 @@ concurrency:
cancel-in-progress: true

jobs:
build-driver:
name: Build driver
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v6
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Build driver bundles from source
run: bash scripts/build_driver.sh
- name: Upload driver bundles
uses: actions/upload-artifact@v4
with:
name: driver-bundles
path: driver/playwright-*.zip
if-no-files-found: error
# The bundles are already-compressed zips; skip re-compression.
compression-level: 0
retention-days: 1

infra:
name: Lint
needs: build-driver
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Download driver bundles
uses: actions/download-artifact@v4
with:
name: driver-bundles
path: driver/
- name: Install dependencies & browsers
run: |
python -m pip install --upgrade pip
Expand All @@ -43,6 +71,7 @@ jobs:

build:
name: Build
needs: build-driver
timeout-minutes: 45
strategy:
fail-fast: false
Expand Down Expand Up @@ -96,6 +125,11 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Download driver bundles
uses: actions/download-artifact@v4
with:
name: driver-bundles
path: driver/
- name: Install dependencies & browsers
run: |
python -m pip install --upgrade pip
Expand Down Expand Up @@ -125,6 +159,7 @@ jobs:

test-stable:
name: Stable
needs: build-driver
timeout-minutes: 45
strategy:
fail-fast: false
Expand All @@ -143,6 +178,11 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Download driver bundles
uses: actions/download-artifact@v4
with:
name: driver-bundles
path: driver/
- name: Install dependencies & browsers
run: |
python -m pip install --upgrade pip
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Set up Docker QEMU for arm64 docker builds
uses: docker/setup-qemu-action@v4
with:
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/test_docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
Expand All @@ -53,6 +57,10 @@ jobs:
docker exec "${CONTAINER_ID}" pip install -r local-requirements.txt
docker exec "${CONTAINER_ID}" pip install -r requirements.txt
docker exec "${CONTAINER_ID}" pip install -e .
# build.sh (above) already built the driver bundles into driver/ on the
# host. The repo is bind-mounted into the container, so this in-container
# wheel build reuses those bundles (setup.py skips the source build when
# the zip already exists) and therefore needs no Node.js in the image.
docker exec "${CONTAINER_ID}" python -m build --wheel
docker exec "${CONTAINER_ID}" xvfb-run pytest tests/sync/
docker exec "${CONTAINER_ID}" xvfb-run pytest tests/async/
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk
- `scripts/generate_api.py`, `scripts/generate_async_api.py`, `scripts/generate_sync_api.py`, `scripts/documentation_provider.py` — codegen and validation. They diff the Python implementation against the driver's `playwright/driver/package/api.json` and abort if either side is out of sync.
- `scripts/expected_api_mismatch.txt` — explicit allowlist of "documented in JS, not in Python" or "named differently in Python" gaps. Lines that no longer apply must be removed.
- `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror.
- `setup.py` — `driver_version = "X.Y.Z"` is the source of truth for which driver build is downloaded from `cdn.playwright.dev`.
- `DRIVER_SHA` — the single source of truth for which Playwright commit the driver is built from (one line, the 40-char `microsoft/playwright` commit SHA). Read by `setup.py`, `scripts/build_driver.sh`, and CI. The wheel build clones `microsoft/playwright` at this commit and builds the driver from source (via `scripts/build_driver.sh` + upstream's `utils/build/build-playwright-driver.sh`). The SHA is baked into the staged bundle filenames (`driver/playwright-<sha>-<suffix>.zip`), so it doubles as the build cache key.
- `scripts/build_driver.sh` — clones and builds the upstream driver bundles into `driver/`. A portable bash script (shareable with the other language forks) that needs Node.js, npm, git and bash; invoked from `setup.py`'s `bdist_wheel`. Reads the pin from `DRIVER_SHA`; takes no arguments.
- `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs.

## Setup

`CONTRIBUTING.md` has the full sequence. The short version:
`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js, npm, git and bash for the driver build):

```sh
python3 -m venv env && source env/bin/activate
pip install --upgrade pip
pip install -r local-requirements.txt
pip install -e .
python -m build --wheel # downloads the driver listed in setup.py
python -m build --wheel # clones microsoft/playwright @ DRIVER_SHA and builds the driver from source
pre-commit install
```

Expand Down
6 changes: 6 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pip install -r local-requirements.txt

Build and install drivers:

The driver is built from upstream `microsoft/playwright` source, so building a
wheel requires **Node.js (with npm), git and bash** on your PATH. The commit to
build from is pinned in the `DRIVER_SHA` file. The first
`python -m build --wheel` clones that commit and runs its full
build, which is slow.

```sh
pip install -e .
python -m build --wheel
Expand Down
1 change: 1 addition & 0 deletions DRIVER_SHA
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
87bb9ddbd78f329df18c2b24847bc9409240cd07
4 changes: 2 additions & 2 deletions ROLLING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ pip install -r local-requirements.txt
pre-commit install
pip install -e .
```
* change driver version in `setup.py`
* download new driver: `python -m build --wheel`
* change the driver pin in `DRIVER_SHA` (the `microsoft/playwright` commit SHA to build from)
* build the new driver from source: `python -m build --wheel` (clones `microsoft/playwright` at that commit and builds it; requires Node.js, npm, git and bash)
* generate API: `./scripts/update_api.sh`
* commit changes & send PR
* wait for bots to pass & merge the PR
Expand Down
150 changes: 150 additions & 0 deletions scripts/build_driver.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# Copyright (c) Microsoft Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Build the Playwright driver bundles from upstream source.
#
# This script checks out microsoft/playwright at the commit pinned in the
# DRIVER_SHA file (repo root) and runs upstream's
# utils/build/build-playwright-driver.sh. That script cross-builds the
# per-platform bundles, which this script stages into driver/ as
# playwright-<sha>-<suffix>.zip for setup.py to embed into the platform wheels.
#
# The pin is an immutable commit SHA (tags can be moved upstream) and lives in
# the neutral DRIVER_SHA file so setup.py and CI can read it without parsing
# this script. The SHA is baked into the staged bundle filenames, so the
# filename doubles as the build cache key: a roll changes DRIVER_SHA, which
# changes the filenames and invalidates the cache.
#
# A single host builds all platform bundles at once: the upstream script
# downloads the matching Node.js binary for each target, so the host platform
# does not constrain which bundles can be produced.
#
# This is intentionally a shell script (rather than language-specific code) so
# the same build step can be shared across the Playwright language forks.
#
# Usage: scripts/build_driver.sh (reads the pin from DRIVER_SHA; no arguments)

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
DRIVER_DIR="$REPO_ROOT/driver"
SOURCE_DIR="$DRIVER_DIR/playwright-src"
PLAYWRIGHT_REPO="https://github.com/microsoft/playwright"

# The driver pin: an immutable commit in microsoft/playwright.
# microsoft/playwright @ v1.60.0
EXPECTED_SHA="$(tr -d '[:space:]' < "$REPO_ROOT/DRIVER_SHA")"
if [[ -z "$EXPECTED_SHA" ]]; then
echo "DRIVER_SHA is empty or missing at $REPO_ROOT/DRIVER_SHA" >&2
exit 2
fi

# Bundle suffixes produced by utils/build/build-playwright-driver.sh. Keep in
# sync with the "zip_name" values in setup.py.
SUFFIXES=(mac mac-arm64 linux linux-arm64 win32_x64 win32_arm64)

bundles_present() {
local suffix
for suffix in "${SUFFIXES[@]}"; do
[[ -f "$DRIVER_DIR/playwright-$EXPECTED_SHA-$suffix.zip" ]] || return 1
done
return 0
}

require_tools() {
local missing=()
local tool
for tool in git node npm bash; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing+=("$tool")
fi
done
if [[ ${#missing[@]} -gt 0 ]]; then
echo "Building the Playwright driver from source requires the following tools," >&2
echo "which were not found on PATH: ${missing[*]}." >&2
echo "Install Node.js (with npm), git and bash, then retry. On Windows, run the" >&2
echo "build from a bash shell (e.g. Git Bash)." >&2
exit 1
fi
}

checked_out_sha() {
if [[ -d "$SOURCE_DIR/.git" ]]; then
git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || true
fi
}

clone_source() {
# Reuse an existing checkout only if it already points at exactly the pinned

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure why wipe the repo, instead of fetch/checkout the desired commit?

# commit; otherwise wipe it so a stale ref can never leak into the bundles.
if [[ -d "$SOURCE_DIR" ]]; then
[[ "$(checked_out_sha)" == "$EXPECTED_SHA" ]] || rm -rf "$SOURCE_DIR"
fi
if [[ ! -d "$SOURCE_DIR" ]]; then
mkdir -p "$SOURCE_DIR"
echo "Fetching $PLAYWRIGHT_REPO at $EXPECTED_SHA"
# Shallow-fetch a single commit. GitHub allows fetching an arbitrary commit
# by SHA, so a full clone is unnecessary.
git -C "$SOURCE_DIR" init -q
git -C "$SOURCE_DIR" remote add origin "$PLAYWRIGHT_REPO"
git -C "$SOURCE_DIR" fetch --depth 1 origin "$EXPECTED_SHA"
git -C "$SOURCE_DIR" checkout -q FETCH_HEAD
fi
# Make sure we landed on exactly the pinned commit.
if [[ "$(checked_out_sha)" != "$EXPECTED_SHA" ]]; then
echo "Checked out commit '$(checked_out_sha)' but '$EXPECTED_SHA' was requested." >&2
exit 1
fi
}

build_source() {
echo "Installing Playwright dependencies (npm ci)"
(cd "$SOURCE_DIR" && npm ci)
echo "Building Playwright (npm run build)"
(cd "$SOURCE_DIR" && npm run build)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend npm run clean as well for a reused checkout.

echo "Building driver bundles"
(cd "$SOURCE_DIR" && bash utils/build/build-playwright-driver.sh)
}

copy_bundles() {
local output_dir="$SOURCE_DIR/utils/build/output"
# The output dir also holds build intermediates (downloaded Node.js binaries,
# tgz archives, extracted package dirs), so copy only the bundles. Upstream
# names them playwright-<version>-<suffix>.zip; restage each one with the pin
# SHA in the name so the filename doubles as the build cache key.
local suffix matches
for suffix in "${SUFFIXES[@]}"; do
matches=("$output_dir"/playwright-*-"$suffix".zip)
if [[ ! -f "${matches[0]}" ]]; then
echo "Expected driver bundle for '$suffix' was not produced in $output_dir" >&2
exit 1
fi
cp "${matches[0]}" "$DRIVER_DIR/playwright-$EXPECTED_SHA-$suffix.zip"
done
}

# Fast path: the bundles for this exact pin are already staged, so there is
# nothing to (re)build. This keeps repeat invocations cheap and lets consumers
# that only downloaded the prebuilt bundles skip the build entirely (no Node).
if bundles_present; then
echo "Driver bundles for $EXPECTED_SHA already present in $DRIVER_DIR; skipping build."
exit 0
fi

require_tools
clone_source
build_source
copy_bundles
Loading
Loading