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='/') ]