From 6e75b91bbd888803992c7ccf436a33517fa775d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 16:03:19 +0530 Subject: [PATCH] Fix 1.61 build: actually download the patched driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/build_release.yml | 13 +++++--- patch_python_package.py | 51 ++++++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_release.yml b/.github/workflows/build_release.yml index 0e308e7..c39f6ea 100644 --- a/.github/workflows/build_release.yml +++ b/.github/workflows/build_release.yml @@ -4,13 +4,17 @@ on: workflow_dispatch: inputs: playwright_version: - description: 'Playwright-Python version tag (e.g. v1.56.0)' + description: 'Playwright-Python version tag to build from (e.g. v1.61.0)' required: true - default: 'v1.56.0' + default: 'v1.61.0' patchright_release: - description: 'Patchright release version (e.g. 1.56.0)' + description: 'Patchright-Python release version to publish (e.g. 1.61.0)' required: true - default: '1.56.0' + default: '1.61.0' + patchright_driver_version: + description: 'Patched driver version to bundle (bugbasesecurity/patchright release, e.g. 1.61.0). Defaults to patchright_release.' + required: false + default: '' permissions: contents: write @@ -22,6 +26,7 @@ jobs: env: patchright_release: ${{ github.event.inputs.patchright_release }} playwright_version: ${{ github.event.inputs.playwright_version }} + patchright_driver_version: ${{ github.event.inputs.patchright_driver_version }} steps: - name: Checkout Repository uses: actions/checkout@v4 diff --git a/patch_python_package.py b/patch_python_package.py index 7093e05..2ee0b54 100644 --- a/patch_python_package.py +++ b/patch_python_package.py @@ -5,11 +5,46 @@ import toml patchright_version = os.environ.get('patchright_release') or os.environ.get('playwright_version') +patchright_driver_version = os.environ.get('patchright_driver_version') or patchright_version + +# The bundled driver must be the PATCHED Patchright driver (stealth lives in the +# driver: navigator.webdriver=false, no Runtime.enable, --disable-blink-features +# =AutomationControlled in chromiumSwitches — the Python-layer patches alone do +# NOT cover these). Our nodejs fork publishes that driver, with the networkidle +# captcha exclusion already baked in, on its GitHub releases. +DRIVER_REPO = "bugbasesecurity/patchright" def patch_file(file_path: str, patched_tree: ast.AST) -> None: with open(file_path, "w") as f: f.write(ast.unparse(ast.fix_missing_locations(patched_tree))) +def patch_driver_version_file() -> None: + # Playwright-Python 1.61+ reads driver_version from a DRIVER_VERSION file + # instead of a setup.py string constant. Pin it to the patched driver version + # so ensure_driver_bundle() resolves to our published release asset. + driver_version_file = "playwright-python/DRIVER_VERSION" + if os.path.exists(driver_version_file): + with open(driver_version_file, "w") as f: + f.write(f"{patchright_driver_version}\n") + +def patch_ensure_driver_bundle(node: ast.FunctionDef) -> None: + # Playwright-Python 1.61+ assembles the bundle from the vanilla playwright-core + # npm package via ensure_driver_bundle(). Replace its body so it downloads our + # patched driver ZIP from the fork's releases instead. (Pre-1.61 used a + # cdn.playwright.dev url constant, handled by the url rewrite below.) + if node.name != "ensure_driver_bundle": + return + node.body = ast.parse(f'''\ +destination_path = f"driver/playwright-{{driver_version}}-{{zip_name}}.zip" +if os.path.exists(destination_path): + return +os.makedirs("driver", exist_ok=True) +url = f"https://github.com/{DRIVER_REPO}/releases/download/v{{driver_version}}/playwright-{{driver_version}}-{{zip_name}}.zip" +subprocess.check_call(["curl", "-L", "--fail", "-o", destination_path, url]) +if not os.path.exists(destination_path): + raise RuntimeError(f"Driver bundle {{destination_path}} was not downloaded.") +''').body + # Adding _repo_version.py (Might not be intended but fixes the build) with open("playwright-python/playwright/_repo_version.py", "w") as f: f.write(f"version = '{patchright_version}'") @@ -37,24 +72,30 @@ def patch_file(file_path: str, patched_tree: ast.AST) -> None: with open("playwright-python/pyproject.toml", "w") as f: toml.dump(pyproject_source, f) +# Pin DRIVER_VERSION (Playwright-Python 1.61+ file-based driver version) +patch_driver_version_file() + # Patching setup.py with open("playwright-python/setup.py") as f: setup_source = f.read() setup_tree = ast.parse(setup_source) for node in ast.walk(setup_tree): - # Modify driver_version + # Redirect the 1.61+ driver bundle download to our patched release asset + if isinstance(node, ast.FunctionDef): + patch_ensure_driver_bundle(node) + + # Modify driver_version (pre-1.61 string-constant form; no-op on 1.61+) if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) and isinstance(node.targets[0], ast.Name): if node.targets[0].id == "driver_version" and node.value.value.startswith("1."): - # node.value.value = node.value.value.split("-")[0] - node.value.value = os.environ.get('patchright_driver_version') or patchright_version + node.value.value = patchright_driver_version - # Modify url + # Modify url (pre-1.61 cdn form; no-op on 1.61+) if isinstance(node, ast.Assign) and isinstance(node.value, ast.Constant) and isinstance(node.targets[0], ast.Name): if node.targets[0].id == "url" and node.value.value == "https://cdn.playwright.dev/builds/driver/": node.value = ast.JoinedStr( values=[ - ast.Constant(value='https://github.com/bugbasesecurity/patchright/releases/download/v'), + ast.Constant(value=f'https://github.com/{DRIVER_REPO}/releases/download/v'), ast.FormattedValue(value=ast.Name(id='driver_version', ctx=ast.Load()), conversion=-1), ast.Constant(value='/') ]