Skip to content

Commit 6e75b91

Browse files
committed
Fix 1.61 build: actually download the patched driver
The published 1.61.0 wheel shipped a VANILLA playwright-core driver: proven by inspecting it (navigator.webdriver=true, Runtime.enable used 10x, no --disable-blink-features in chromiumSwitches, 0 patchright strings, driver version 1.61.1-beta). Patchright's core stealth lives in the DRIVER, so the wheel was detectable (Akamai bot-blocked the ray-ban crawler despite a residential proxy). This corrects the assumption that patchright-python's stealth is 'Python-layer' with a vanilla driver — it is not. Root cause: Playwright-Python 1.61 restructured setup.py — driver_version now reads from a DRIVER_VERSION file, and the bundle is assembled from the vanilla playwright-core npm package via ensure_driver_bundle() (no cdn.playwright.dev url). The existing driver_version/url AST rewrites matched neither and silently no-op'd, so the build fell back to the vanilla driver. - patch_driver_version_file(): pin DRIVER_VERSION to the patched driver version. - patch_ensure_driver_bundle(): rewrite the function to curl our patched driver ZIP from bugbasesecurity/patchright releases (stealth + networkidle baked in). - Workflow: optional patchright_driver_version input; refreshed defaults. The post-build networkidle step (patch_wheels_networkidle.mjs) is now an idempotent safety net — the downloaded driver already carries the exclusion, so it detects it and skips. Verified end-to-end: patcher runs clean on playwright-python v1.61.0; built manylinux wheel bundles the patched driver (39 patchright strings, --disable-blink-features=AutomationControlled in chromiumSwitches, networkidle present); installed wheel -> navigator.webdriver is false on a plain launch()+new_context() with no manual flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f9ca181 commit 6e75b91

2 files changed

Lines changed: 55 additions & 9 deletions

File tree

.github/workflows/build_release.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ on:
44
workflow_dispatch:
55
inputs:
66
playwright_version:
7-
description: 'Playwright-Python version tag (e.g. v1.56.0)'
7+
description: 'Playwright-Python version tag to build from (e.g. v1.61.0)'
88
required: true
9-
default: 'v1.56.0'
9+
default: 'v1.61.0'
1010
patchright_release:
11-
description: 'Patchright release version (e.g. 1.56.0)'
11+
description: 'Patchright-Python release version to publish (e.g. 1.61.0)'
1212
required: true
13-
default: '1.56.0'
13+
default: '1.61.0'
14+
patchright_driver_version:
15+
description: 'Patched driver version to bundle (bugbasesecurity/patchright release, e.g. 1.61.0). Defaults to patchright_release.'
16+
required: false
17+
default: ''
1418

1519
permissions:
1620
contents: write
@@ -22,6 +26,7 @@ jobs:
2226
env:
2327
patchright_release: ${{ github.event.inputs.patchright_release }}
2428
playwright_version: ${{ github.event.inputs.playwright_version }}
29+
patchright_driver_version: ${{ github.event.inputs.patchright_driver_version }}
2530
steps:
2631
- name: Checkout Repository
2732
uses: actions/checkout@v4

patch_python_package.py

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,46 @@
55
import toml
66

77
patchright_version = os.environ.get('patchright_release') or os.environ.get('playwright_version')
8+
patchright_driver_version = os.environ.get('patchright_driver_version') or patchright_version
9+
10+
# The bundled driver must be the PATCHED Patchright driver (stealth lives in the
11+
# driver: navigator.webdriver=false, no Runtime.enable, --disable-blink-features
12+
# =AutomationControlled in chromiumSwitches — the Python-layer patches alone do
13+
# NOT cover these). Our nodejs fork publishes that driver, with the networkidle
14+
# captcha exclusion already baked in, on its GitHub releases.
15+
DRIVER_REPO = "bugbasesecurity/patchright"
816

917
def patch_file(file_path: str, patched_tree: ast.AST) -> None:
1018
with open(file_path, "w") as f:
1119
f.write(ast.unparse(ast.fix_missing_locations(patched_tree)))
1220

21+
def patch_driver_version_file() -> None:
22+
# Playwright-Python 1.61+ reads driver_version from a DRIVER_VERSION file
23+
# instead of a setup.py string constant. Pin it to the patched driver version
24+
# so ensure_driver_bundle() resolves to our published release asset.
25+
driver_version_file = "playwright-python/DRIVER_VERSION"
26+
if os.path.exists(driver_version_file):
27+
with open(driver_version_file, "w") as f:
28+
f.write(f"{patchright_driver_version}\n")
29+
30+
def patch_ensure_driver_bundle(node: ast.FunctionDef) -> None:
31+
# Playwright-Python 1.61+ assembles the bundle from the vanilla playwright-core
32+
# npm package via ensure_driver_bundle(). Replace its body so it downloads our
33+
# patched driver ZIP from the fork's releases instead. (Pre-1.61 used a
34+
# cdn.playwright.dev url constant, handled by the url rewrite below.)
35+
if node.name != "ensure_driver_bundle":
36+
return
37+
node.body = ast.parse(f'''\
38+
destination_path = f"driver/playwright-{{driver_version}}-{{zip_name}}.zip"
39+
if os.path.exists(destination_path):
40+
return
41+
os.makedirs("driver", exist_ok=True)
42+
url = f"https://github.com/{DRIVER_REPO}/releases/download/v{{driver_version}}/playwright-{{driver_version}}-{{zip_name}}.zip"
43+
subprocess.check_call(["curl", "-L", "--fail", "-o", destination_path, url])
44+
if not os.path.exists(destination_path):
45+
raise RuntimeError(f"Driver bundle {{destination_path}} was not downloaded.")
46+
''').body
47+
1348
# Adding _repo_version.py (Might not be intended but fixes the build)
1449
with open("playwright-python/playwright/_repo_version.py", "w") as f:
1550
f.write(f"version = '{patchright_version}'")
@@ -37,24 +72,30 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None:
3772
with open("playwright-python/pyproject.toml", "w") as f:
3873
toml.dump(pyproject_source, f)
3974

75+
# Pin DRIVER_VERSION (Playwright-Python 1.61+ file-based driver version)
76+
patch_driver_version_file()
77+
4078
# Patching setup.py
4179
with open("playwright-python/setup.py") as f:
4280
setup_source = f.read()
4381
setup_tree = ast.parse(setup_source)
4482

4583
for node in ast.walk(setup_tree):
46-
# Modify driver_version
84+
# Redirect the 1.61+ driver bundle download to our patched release asset
85+
if isinstance(node, ast.FunctionDef):
86+
patch_ensure_driver_bundle(node)
87+
88+
# Modify driver_version (pre-1.61 string-constant form; no-op on 1.61+)
4789
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) and isinstance(node.targets[0], ast.Name):
4890
if node.targets[0].id == "driver_version" and node.value.value.startswith("1."):
49-
# node.value.value = node.value.value.split("-")[0]
50-
node.value.value = os.environ.get('patchright_driver_version') or patchright_version
91+
node.value.value = patchright_driver_version
5192

52-
# Modify url
93+
# Modify url (pre-1.61 cdn form; no-op on 1.61+)
5394
if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) and isinstance(node.targets[0], ast.Name):
5495
if node.targets[0].id == "url" and node.value.value == "https://cdn.playwright.dev/builds/driver/":
5596
node.value = ast.JoinedStr(
5697
values=[
57-
ast.Constant(value='https://github.com/bugbasesecurity/patchright/releases/download/v'),
98+
ast.Constant(value=f'https://github.com/{DRIVER_REPO}/releases/download/v'),
5899
ast.FormattedValue(value=ast.Name(id='driver_version', ctx=ast.Load()), conversion=-1),
59100
ast.Constant(value='/')
60101
]

0 commit comments

Comments
 (0)