From 12c8aa10a927ab8a0da14db57ef124067cdf7563 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 13 Oct 2026 15:41:22 -0700 Subject: [PATCH] devops: publish to PyPI from GitHub Actions with trusted publishing --- .azure-pipelines/publish.yml | 116 --------------------- .claude/skills/playwright-release/SKILL.md | 5 +- .github/workflows/publish.yml | 83 +++++++++++++++ 3 files changed, 87 insertions(+), 117 deletions(-) delete mode 100644 .azure-pipelines/publish.yml create mode 100644 .github/workflows/publish.yml diff --git a/.azure-pipelines/publish.yml b/.azure-pipelines/publish.yml deleted file mode 100644 index 522be3739..000000000 --- a/.azure-pipelines/publish.yml +++ /dev/null @@ -1,116 +0,0 @@ -pr: none - -trigger: - tags: - include: - - '*' - -resources: - repositories: - - repository: 1esPipelines - type: git - name: 1ESPipelineTemplates/1ESPipelineTemplates - ref: refs/tags/release - -extends: - template: v1/1ES.Official.PipelineTemplate.yml@1esPipelines - parameters: - pool: - name: DevDivPlaywrightAzurePipelinesUbuntu2204 - os: linux - sdl: - sourceAnalysisPool: - name: DevDivPlaywrightAzurePipelinesWindows2022 - # The image must be windows-based due to restrictions of the SDL tools. See: https://aka.ms/AAo6v8e - # In the case of a windows build, this can be the same as the above pool image. - os: windows - suppression: - suppressionFile: $(Build.SourcesDirectory)\.azure-pipelines\guardian\SDL\.gdnsuppress - stages: - - stage: Stage - jobs: - - job: Build - templateContext: - outputs: - - output: pipelineArtifact - 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' - # 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: - 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 --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 - inputs: - - input: pipelineArtifact - artifactName: esrp-build - targetPath: $(Build.ArtifactStagingDirectory)/esrp-build - steps: - - checkout: none - - task: EsrpRelease@11 - inputs: - connectedservicename: 'Playwright-ESRP-PME' - usemanagedidentity: true - keyvaultname: 'playwright-esrp-pme' - signcertname: 'ESRP-Release-Sign' - clientid: '13434a40-7de4-4c23-81a3-d843dc81c2c5' - intent: 'PackageDistribution' - contenttype: 'PyPi' - # Keeping it commented out as a workaround for: - # https://portal.microsofticm.com/imp/v3/incidents/incident/499972482/summary - # contentsource: 'folder' - folderlocation: '$(Build.ArtifactStagingDirectory)/esrp-build' - waitforreleasecompletion: true - owners: 'yurys@microsoft.com' - approvers: 'yurys@microsoft.com' - serviceendpointurl: 'https://api.esrp.microsoft.com' - mainpublisher: 'Playwright' - domaintenantid: '975f013f-7f24-47e8-a7d3-abc4752bf346' - displayName: 'ESRP Release to PIP' diff --git a/.claude/skills/playwright-release/SKILL.md b/.claude/skills/playwright-release/SKILL.md index 96c2e8239..df8562fcc 100644 --- a/.claude/skills/playwright-release/SKILL.md +++ b/.claude/skills/playwright-release/SKILL.md @@ -154,4 +154,7 @@ gh release list --repo microsoft/playwright-python --limit 3 # v1.xx.0 shows a Give the user the URL of the draft. Your work ends here. Do not publish the release: never run `gh release create` without `--draft`, and never run -`gh release edit --draft=false`. Publishing creates the tag. +`gh release edit --draft=false`. Publishing creates the tag and starts +`.github/workflows/publish.yml`, which builds the wheels and uploads them to PyPI +after a reviewer approves the `pypi` environment. Never run that workflow, and +never approve the environment. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..649668c2a --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,83 @@ +name: Publish + +# Builds the wheels of a release tag and uploads them to PyPI. Uses PyPI trusted +# publishing: PyPI trusts this workflow file in the "pypi" environment, so no +# PyPI token is stored. Runs when a release is published. To publish an existing +# tag, run it by hand with the tag name. +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish, for example v1.64.0" + required: true + +permissions: + contents: read + +jobs: + build: + name: Build wheels + runs-on: ubuntu-latest + env: + TAG: ${{ inputs.tag || github.event.release.tag_name }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: refs/tags/${{ env.TAG }} + # setuptools-scm reads the version from the tag. + fetch-depth: 0 + - name: Check the tag + run: | + if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Can only publish a release tag (vX.Y.Z), got: $TAG" + exit 1 + fi + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - name: Install dependencies + run: python -m pip install "$(grep '^build==' local-requirements.txt)" + - name: Build wheels + run: | + for wheel in $(python setup.py --list-wheels); do + PLAYWRIGHT_TARGET_WHEEL=$wheel python -m build --wheel --outdir dist + done + - name: Check the wheels + run: | + ls -l dist + test "$(ls dist/*.whl | wc -l)" -eq "$(python setup.py --list-wheels | wc -l)" + for file in dist/*; do + if [[ "$(basename "$file")" != "playwright-${TAG#v}-"*.whl ]]; then + echo "Unexpected file for $TAG: $file" + exit 1 + fi + done + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: wheels + path: dist/ + if-no-files-found: error + + publish: + name: Upload to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/playwright/ + permissions: + # Lets the job get the GitHub OIDC token that PyPI trusted publishing needs. + id-token: write + steps: + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: wheels + path: dist/ + - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2