From d665b694c549ee82c3b1af94dacb069a8fa01f9b Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 19 Aug 2026 14:21:19 -0500 Subject: [PATCH] build: create ESRP release template --- eng/ci/extension-release.yml | 7 ++ .../official/jobs/publish-release.yml | 58 +++----------- .../official/jobs/release-pypi-package.yml | 69 ++++++++++++++++ .../official/steps/publish-pypi-esrp.yml | 78 +++++++++++++++++++ 4 files changed, 163 insertions(+), 49 deletions(-) create mode 100644 eng/templates/official/jobs/release-pypi-package.yml create mode 100644 eng/templates/official/steps/publish-pypi-esrp.yml diff --git a/eng/ci/extension-release.yml b/eng/ci/extension-release.yml index d189be8..a8e47dd 100644 --- a/eng/ci/extension-release.yml +++ b/eng/ci/extension-release.yml @@ -10,8 +10,15 @@ resources: type: git name: engineering ref: refs/tags/release + pipelines: + - pipeline: officialBuild + project: internal + source: azure-functions-python-extensions.official + branch: dev + trigger: none variables: + - group: azure-functions-python-release - name: codeql.excludePathPatterns value: deps/,build/ diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml index 59a1a34..13fa5ba 100644 --- a/eng/templates/official/jobs/publish-release.yml +++ b/eng/templates/official/jobs/publish-release.yml @@ -196,52 +196,12 @@ jobs: 2. Merge the PR into worker dev branch condition: always() -- job: "PyPIPackage" - dependsOn: ['FinishWorkerPr'] - displayName: 'PyPI Package' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python Extension release/.x.y.z Artifact' - inputs: - buildType: specific - project: '3f99e810-c336-441f-8892-84983093ad7f' - definition: 798 - specificBuildWithTriggering: true - buildVersionToDownload: latestFromBranch - branchName: refs/heads/dev - targetPath: PythonExtensionArtifact - - task: PipAuthenticate@1 - displayName: 'Pip Authenticate' - inputs: - artifactFeeds: public/upstream-public - onlyAddExtraIndex: false - - task: UsePythonVersion@0 - displayName: 'Use Python 3.11' - inputs: - versionSpec: 3.11 - - pwsh: | - $newLibraryVersion = "$(NewLibraryVersion)" - $pypiToken = "$(PypiToken)" - $extensionName= "$(ExtensionName)" - $extensionDirectory = "$(ExtensionDirectory)" - - # Setup local Python environment - Write-Host "Setup local Python environment" - python -m pip install -U pip - pip install twine - - # Build wheels and project - Write-Host "Build Wheels and Upload to https://upload.pypi.org/project/$extensionName/$newLibraryVersion/" - twine upload --repository-url https://upload.pypi.org/legacy/ --username "__token__" --password "$pypiToken" PythonExtensionArtifact/$extensionName/$extensionName/dist/* - Start-Sleep -Seconds 3 - - # Checking if the new version is uploaded - Write-Host "Check if new version is uploaded" - $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache"} -Method Get -Uri "https://pypi.org/project/$extensionName/$newLibraryVersion/" - - # Return Value - if ($response.StatusCode -ne 200) { - Write-Host "Failed to verify https://pypi.org/project/$extensionName/$newLibraryVersion/" - exit -1 - } - displayName: 'Publish package to pypi.org' +- template: /eng/templates/official/jobs/release-pypi-package.yml@self + parameters: + artifact: + name: $(ExtensionDirectory) + pipeline: officialBuild + packagePath: $(ExtensionDirectory)/$(ExtensionDirectory) + dependsOn: + - FinishWorkerPr + approvers: '$(EsrpManualApprovers)' diff --git a/eng/templates/official/jobs/release-pypi-package.yml b/eng/templates/official/jobs/release-pypi-package.yml new file mode 100644 index 0000000..0784872 --- /dev/null +++ b/eng/templates/official/jobs/release-pypi-package.yml @@ -0,0 +1,69 @@ +parameters: + +# The pipeline artifact to download for publishing. +# artifact: +# name: +# pipeline: +- name: artifact + type: object + +# The path within the downloaded artifact whose dist directory contains the +# Python package distributions. +- name: packagePath + type: string + +# ESRP Release inputs. Runtime macros keep individual aliases out of public +# repositories. +- name: esrpOwners + type: string + default: '$(EsrpOwners)' +- name: esrpApprovers + type: string + default: '$(EsrpApprovers)' + +# Adds a required manual approval step before publishing. +- name: approvers + type: string + +# Allow these jobs to depend on some previously defined jobs. +- name: dependsOn + type: object + default: [] + +jobs: +- job: Approval + timeoutInMinutes: 1440 + pool: server + dependsOn: ${{ parameters.dependsOn }} + steps: + - task: ManualValidation@1 + inputs: + notifyUsers: '' # no notification + approvers: '${{ parameters.approvers }}' + allowApproversToApproveTheirOwnRuns: false + instructions: Approve to publish the Python package to PyPI through ESRP. + +- job: Release + displayName: Release Python package through ESRP + dependsOn: Approval + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + targetPath: $(Pipeline.Workspace) + artifactName: ${{ parameters.artifact.name }} + pipeline: ${{ parameters.artifact.pipeline }} + + steps: + - template: /eng/templates/official/steps/publish-pypi-esrp.yml@self + parameters: + folderLocation: $(Pipeline.Workspace)/${{ parameters.packagePath }}/dist + owners: ${{ parameters.esrpOwners }} + approvers: ${{ parameters.esrpApprovers }} diff --git a/eng/templates/official/steps/publish-pypi-esrp.yml b/eng/templates/official/steps/publish-pypi-esrp.yml new file mode 100644 index 0000000..2e1db45 --- /dev/null +++ b/eng/templates/official/steps/publish-pypi-esrp.yml @@ -0,0 +1,78 @@ +parameters: +- name: folderLocation + type: string +- name: owners + type: string +- name: approvers + type: string +- name: displayName + type: string + default: Publish Python package through ESRP + +steps: +- pwsh: | + $ErrorActionPreference = 'Stop' + + function Assert-ResolvedValue { + param( + [string] $Name, + [string] $Value + ) + + $value = "$Value".Trim() + if (!$value -or $value -match '^\$\([^)]+\)$') { + throw "$Name is empty or unresolved. Configure it in an authorized Azure DevOps variable group." + } + + return $value + } + + $folder = Assert-ResolvedValue 'folderLocation' $env:ESRP_FOLDER_LOCATION + if (!(Test-Path -LiteralPath $folder -PathType Container)) { + throw "ESRP Python package folder does not exist: $folder" + } + + $ownersValue = Assert-ResolvedValue 'owners' $env:ESRP_OWNERS + $approversValue = Assert-ResolvedValue 'approvers' $env:ESRP_APPROVERS + $owners = @($ownersValue -split '[,\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $approvers = @($approversValue -split '[,\r\n]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $aliasPattern = '^[A-Za-z0-9][A-Za-z0-9._-]*@microsoft\.com$' + + foreach ($entry in @($owners; $approvers)) { + if ($entry -notmatch $aliasPattern) { + throw "Owners and approvers must be individual Microsoft accounts in alias@microsoft.com format: $entry" + } + } + + if ($owners.Count -eq 0 -or $approvers.Count -eq 0) { + throw 'At least one owner and one approver are required.' + } + + $overlap = @($owners | Where-Object { $approvers -contains $_ }) + if ($overlap.Count -gt 0) { + throw "Owners and approvers must be different individuals. Found in both lists: $($overlap -join ', ')" + } + displayName: Validate ESRP release configuration + env: + ESRP_FOLDER_LOCATION: ${{ parameters.folderLocation }} + ESRP_OWNERS: ${{ parameters.owners }} + ESRP_APPROVERS: ${{ parameters.approvers }} + +- task: SFP.release-tasks.custom-build-release-task.EsrpRelease@12 + displayName: ${{ parameters.displayName }} + inputs: + connectedservicename: azfunc-internal-esrp-prod + usemanagedidentity: true + keyvaultname: kv-azfunc-esrp-prod + signcertname: esrp-release + clientid: a31bf239-3e15-498e-a335-f6ec0c9d9ccf + intent: PackageDistribution + contenttype: PyPI + contentsource: Folder + folderlocation: ${{ parameters.folderLocation }} + waitforreleasecompletion: true + owners: ${{ parameters.owners }} + approvers: ${{ parameters.approvers }} + serviceendpointurl: https://api.esrp.microsoft.com + mainpublisher: ESRPRELPACMAN + domaintenantid: 33e01921-4d64-4f8c-a055-5bdaffd5e33d