diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml index fbd916e46..522be3739 100644 --- a/.azure-pipelines/publish.yml +++ b/.azure-pipelines/publish.yml @@ -36,25 +36,55 @@ extends: path: $(Build.ArtifactStagingDirectory)/esrp-build artifact: esrp-build steps: + - bash: | + if [[ ! "$CURRENT_BRANCH" =~ ^v1\..* ]]; then + echo "Can only publish from a release tag (v1.*)." + echo "Unexpected ref name: $CURRENT_BRANCH" + exit 1 + fi + env: + CURRENT_BRANCH: ${{ variables['Build.SourceBranchName'] }} + displayName: 'Check the ref is a release tag' + # Allow manual runs on any branch to exercise the build without publishing. + condition: ne(variables['Build.Reason'], 'Manual') - task: UsePythonVersion@0 inputs: versionSpec: '3.10' displayName: 'Use Python' - - task: NodeTool@0 + # Resolve every pip install (including the isolated build environments + # that `python -m build` and `pip install -e .` create) through the + # DevDiv_PublicPackages Azure Artifacts feed instead of pypi.org, as + # required by SFI-ES4.2.4. The task exports an authenticated PIP_INDEX_URL. + - task: PipAuthenticate@1 inputs: - versionSpec: '24.x' - displayName: 'Use Node.js' + artifactFeeds: DevDiv/DevDiv_PublicPackages + displayName: 'Authenticate pip to DevDiv_PublicPackages feed' + - task: UseNode@1 + inputs: + version: '24.x' + displayName: 'Install Node.js' + # scripts/build_driver.py fetches playwright-core with `npm pack`, which + # picks up the registry and credentials from this .npmrc. + - script: echo "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/" >> .npmrc + displayName: 'Point npm registry at DevDiv_PublicPackages feed' + - task: npmAuthenticate@0 + inputs: + workingFile: .npmrc + displayName: 'Authenticate npm to DevDiv_PublicPackages feed' - script: | - python -m pip install --upgrade pip - pip install -r local-requirements.txt - pip install -r requirements.txt - pip install -e . + python -m pip install --upgrade pip --disable-pip-version-check + pip install -r local-requirements.txt --disable-pip-version-check + pip install -r requirements.txt --disable-pip-version-check + pip install -e . --disable-pip-version-check for wheel in $(python setup.py --list-wheels); do PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir $(Build.ArtifactStagingDirectory)/esrp-build done displayName: 'Install & Build' - job: Publish dependsOn: Build + # Only publish from release tags; manual runs on a branch stop after Build, + # which lets the build be exercised without publishing. + condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v1.')) templateContext: type: releaseJob isProduction: true diff --git a/.gitignore b/.gitignore index 8424e9bfc..76cb0329a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ utils/docker/dist/ Pipfile Pipfile.lock .venv/ + +# Written by the release pipeline (npmAuthenticate@0); holds feed credentials. +.npmrc diff --git a/CLAUDE.md b/CLAUDE.md index e96c9c3a9..81a1adc4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,13 +15,13 @@ Python bindings for [Playwright](https://playwright.dev). The Python client talk - `tests/async/`, `tests/sync/` — pytest suites. Most new tests are added to the async file with a sync mirror. - `DRIVER_VERSION` — the single source of truth for which Playwright release the driver is assembled from (one line, the `playwright-core` npm version, e.g. `1.61.0`, no `v` prefix). Read by `setup.py`, `scripts/build_driver.py`, and CI. The wheel build downloads `playwright-core` at this version from npm plus the matching Node.js binary and assembles the per-platform bundles — no source build. The version is baked into the staged bundle filenames (`driver/playwright--.zip`), so it doubles as the build cache key. - `NODE_VERSION` — the Node.js version bundled with the driver (one line, e.g. `24.16.0`). Maintained at roll time by `scripts/update_node_version.py` (latest LTS, mirroring upstream's `utils/build/update-playwright-node.mjs`). -- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Pure Python stdlib (no Node/npm/git); invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six). +- `scripts/build_driver.py` — assembles the per-platform driver bundles into `driver/` by downloading the `playwright-core` npm package (`DRIVER_VERSION`) and the official Node.js binaries (`NODE_VERSION`). Fetches `playwright-core` with `npm pack` (needs Node.js/npm on PATH; honours a root `.npmrc`) and the Node.js binaries over plain HTTP; invoked from `setup.py`'s `bdist_wheel` with the target platform's suffix (no arg builds all six). - `api.json` is **not** shipped in the bundle and is never written into the driver — `scripts/update_api.sh` generates it from a nearby `microsoft/playwright` checkout (`$PW_SRC_DIR`) into a temp file and passes it to codegen via `PW_API_JSON` (read by `scripts/documentation_provider.py`). Needed only when regenerating the API, never at runtime. - `ROLLING.md`, `CONTRIBUTING.md` — human-facing setup and roll docs. ## Setup -`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js, npm, git and bash for the driver build): +`CONTRIBUTING.md` has the full sequence. The short version (needs Node.js and npm for the driver build): ```sh python3 -m venv env && source env/bin/activate diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93baff37a..41d883dbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,8 +23,8 @@ Build and install drivers: The driver is assembled from published artifacts — the `playwright-core` npm package (version pinned in `DRIVER_VERSION`) and the official Node.js binary -(pinned in `NODE_VERSION`). Building a wheel just downloads them; no Node/npm/git -toolchain is required. +(pinned in `NODE_VERSION`). Building a wheel downloads them with `npm pack` and +plain HTTP, so Node.js/npm must be installed; no git or source build is needed. ```sh pip install -e . diff --git a/ROLLING.md b/ROLLING.md index 78faf556b..63222c38c 100644 --- a/ROLLING.md +++ b/ROLLING.md @@ -12,7 +12,7 @@ pre-commit install pip install -e . ``` * change the driver pin in `DRIVER_VERSION` (the `playwright-core` npm version, e.g. `1.61.0`) and refresh `NODE_VERSION`: `python scripts/update_node_version.py` -* download the new driver: `python -m build --wheel` (fetches `playwright-core` from npm + the matching Node.js binary and assembles the bundle; no source build). Set `npm_config_registry` to use a different npm registry. +* download the new driver: `python -m build --wheel` (fetches `playwright-core` via `npm pack` + the matching Node.js binary and assembles the bundle; no source build). * generate API (needs a nearby `microsoft/playwright` checkout at `v`): `PW_SRC_DIR=../playwright ./scripts/update_api.sh` * commit changes & send PR * wait for bots to pass & merge the PR diff --git a/scripts/build_driver.py b/scripts/build_driver.py index 28714c8af..0583bca74 100755 --- a/scripts/build_driver.py +++ b/scripts/build_driver.py @@ -27,23 +27,24 @@ LICENSE - the Node.js license package/** - the playwright-core npm package -Unlike the old source build this needs no Node.js, npm, git or bash — only the -Python standard library. +``playwright-core`` is fetched with ``npm pack`` (run from the repository root, so +a root-level ``.npmrc`` -- e.g. the one the release pipeline writes to point at +the internal Azure Artifacts feed -- and its credentials are honoured). The +Node.js binaries are downloaded directly from nodejs.org. Apart from Node.js/npm +this needs only the Python standard library. Usage:: scripts/build_driver.py # assemble every platform bundle scripts/build_driver.py # assemble a single bundle, e.g. mac-arm64 -Set ``npm_config_registry`` to download ``playwright-core`` from an -alternative npm registry. - ``setup.py`` invokes the single-suffix form so a wheel build only downloads the one Node.js binary it needs. """ import os import shutil +import subprocess import sys import tarfile import tempfile @@ -56,9 +57,6 @@ REPO_ROOT = Path(__file__).resolve().parent.parent DRIVER_DIR = REPO_ROOT / "driver" -NPM_REGISTRY = os.environ.get( - "npm_config_registry", "https://registry.npmjs.org" -).rstrip("/") NODEJS_DIST = "https://nodejs.org/dist" @@ -136,9 +134,25 @@ def _extract_zip_file(archive: zipfile.ZipFile, name: str, destination: Path) -> def fetch_playwright_core(version: str, work_dir: Path) -> Path: """Download playwright-core@ and extract its package/ tree once.""" - url = f"{NPM_REGISTRY}/playwright-core/-/playwright-core-{version}.tgz" + npm = "npm.cmd" if sys.platform == "win32" else "npm" + spec = f"playwright-core@{version}" + # npm is run from the repository root so that a root-level .npmrc (registry + # and credentials) is honoured. `npm pack` writes -.tgz. + print(f"Downloading {spec} with npm pack", flush=True) + try: + subprocess.check_call( + [npm, "pack", spec, "--pack-destination", str(work_dir)], + cwd=REPO_ROOT, + ) + except FileNotFoundError: + raise SystemExit( + "npm was not found on PATH; Node.js/npm are required to assemble the driver." + ) + except subprocess.CalledProcessError as error: + raise SystemExit(f"npm pack {spec} failed with exit code {error.returncode}") tgz = work_dir / f"playwright-core-{version}.tgz" - download(url, tgz) + if not tgz.is_file(): + raise SystemExit(f"npm pack did not produce {tgz}") with tarfile.open(tgz, "r:gz") as tar: # npm tarballs nest every file under a top-level "package/" directory, # which is exactly the bundle layout we want. @@ -148,7 +162,7 @@ def fetch_playwright_core(version: str, work_dir: Path) -> Path: if m.name == "package" or m.name.startswith("package/") ] if not members: - raise SystemExit(f"No package/ entries found in {url}") + raise SystemExit(f"No package/ entries found in {tgz.name}") _extract_members(tar, work_dir, members) tgz.unlink() return work_dir / "package"