From 7b32e393e8d5a49c5c528e4b1516d4d9f8cc0108 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 3 Aug 2026 14:38:10 -0500 Subject: [PATCH 1/2] build: migrate to ESRP publishing --- eng/ci/extension-release.yml | 69 +++- eng/ci/official-build.yml | 1 + .../official/jobs/publish-release.yml | 247 -------------- .../official/release/build-artifacts.yml | 54 +++ .../official/release/bump-version.yml | 123 +++++++ .../official/release/publish-release.yml | 309 ++++++++++++++++++ 6 files changed, 555 insertions(+), 248 deletions(-) delete mode 100644 eng/templates/official/jobs/publish-release.yml create mode 100644 eng/templates/official/release/build-artifacts.yml create mode 100644 eng/templates/official/release/bump-version.yml create mode 100644 eng/templates/official/release/publish-release.yml diff --git a/eng/ci/extension-release.yml b/eng/ci/extension-release.yml index d189be8..05f3e52 100644 --- a/eng/ci/extension-release.yml +++ b/eng/ci/extension-release.yml @@ -1,5 +1,23 @@ pr: none +parameters: + - name: ExtensionName + displayName: 'Extension project to release' + type: string + default: 'azurefunctions-extensions-base' + values: + - azurefunctions-extensions-base + - azurefunctions-extensions-bindings-blob + - azurefunctions-extensions-bindings-cosmosdb + - azurefunctions-extensions-bindings-eventhub + - azurefunctions-extensions-bindings-servicebus + - azurefunctions-extensions-connectors + - azurefunctions-extensions-http-fastapi + - name: NewLibraryVersion + displayName: 'Extension version to release (for example, 1.2.0)' + type: string + default: '' + resources: repositories: - repository: 1es @@ -14,6 +32,27 @@ resources: variables: - name: codeql.excludePathPatterns value: deps/,build/ + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-base') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/base' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-bindings-blob') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/bindings/blob' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-bindings-cosmosdb') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/bindings/cosmosdb' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-bindings-eventhub') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/bindings/eventhub' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-bindings-servicebus') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/bindings/servicebus' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-connectors') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/connectors' + - ${{ if eq(parameters.ExtensionName, 'azurefunctions-extensions-http-fastapi') }}: + - name: ExtensionDirectory + value: 'azurefunctions/extensions/http/fastapi' extends: template: v1/1ES.Official.PipelineTemplate.yml@1es @@ -24,6 +63,34 @@ extends: os: windows stages: + - stage: BumpVersion + displayName: 'Bump Extension Version' + dependsOn: [] + jobs: + - template: /eng/templates/official/release/bump-version.yml@self + parameters: + extensionVersion: ${{ parameters.NewLibraryVersion }} + extensionName: ${{ parameters.ExtensionName }} + extensionDirectory: ${{ variables.ExtensionDirectory }} + createPullRequest: true + baseBranch: dev + + - stage: Build + displayName: 'Build Extension Artifacts' + dependsOn: BumpVersion + jobs: + - template: /eng/templates/official/release/build-artifacts.yml@self + parameters: + extensionVersion: ${{ parameters.NewLibraryVersion }} + extensionName: ${{ parameters.ExtensionName }} + artifactName: ${{ parameters.ExtensionName }} + - stage: Release + displayName: 'Release Extension' + dependsOn: Build jobs: - - template: /eng/templates/official/jobs/publish-release.yml@self + - template: /eng/templates/official/release/publish-release.yml@self + parameters: + extensionVersion: ${{ parameters.NewLibraryVersion }} + extensionName: ${{ parameters.ExtensionName }} + artifactName: ${{ parameters.ExtensionName }} diff --git a/eng/ci/official-build.yml b/eng/ci/official-build.yml index ec0c832..670f635 100644 --- a/eng/ci/official-build.yml +++ b/eng/ci/official-build.yml @@ -4,6 +4,7 @@ trigger: include: - dev - release/* + - '*/release/*' # CI only, does not trigger on PRs. pr: none diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml deleted file mode 100644 index 59a1a34..0000000 --- a/eng/templates/official/jobs/publish-release.yml +++ /dev/null @@ -1,247 +0,0 @@ -jobs: - -- job: "CreateReleaseBranch" - displayName: 'Create Release Branch' - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - steps: - - pwsh: | - $githubToken = "$(GithubPat)" - $newLibraryVersion = "$(NewLibraryVersion)" - $extensionName= "$(ExtensionName)" - $extensionDirectory = "$(ExtensionDirectory)" - - if($newLibraryVersion -match '(\d)+.(\d)+.*') { - # Create GitHub credential - git config --global user.name "AzureFunctionsPython" - git config --global user.email "azfunc@microsoft.com" - - # Heading to Artifact Repository - Write-Host "Operating based on $stagingDirectory/azure-functions-python-extensions" - git checkout -b "$extensionName/release/$newLibraryVersion" - - # Change __init__.py version - Write-Host "Change version number in $extensionDirectory __init__.py to $newLibraryVersion" - ((Get-Content $extensionName/$extensionDirectory/__init__.py) -replace "__version__ = '(\d)+.(\d)+.*'", "__version__ = '$newLibraryVersion'" -join "`n") + "`n" | Set-Content -NoNewline $extensionName/$extensionDirectory/__init__.py - git add $extensionName/$extensionDirectory/__init__.py - git commit -m "build: update $extensionName Version to $newLibraryVersion" - - # Create release branch release/X.Y.Z - Write-Host "Creating release branch release/$newLibraryVersion" - git push --repo="https://$githubToken@github.com/Azure/azure-functions-python-extensions.git" - - } else { - Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.5.0)" - exit -1 - } - displayName: 'Push release/x.y.z' - -- job: "CheckReleaseBranch" - dependsOn: ['CreateReleaseBranch'] - displayName: '(Manual) Check Release Branch' - pool: server - steps: - - task: ManualValidation@1 - displayName: '(Optional) Modify release/x.y.z branch' - inputs: - notifyUsers: '' # No email notifications sent - instructions: | - 1. Check if the https://github.com/Azure/azure-functions-python-extensions/tree/release/$(NewLibraryVersion) passes all unit test. - 2. If not, modify the release/$(NewLibraryVersion) branch. - 3. Ensure release/$(NewLibraryVersion) branch contains all necessary changes. - -- job: "CreateReleaseTag" - dependsOn: ['CheckReleaseBranch'] - displayName: 'Create Release Tag' - steps: - - pwsh: | - $githubToken = "$(GithubPat)" - $newLibraryVersion = "$(NewLibraryVersion)" - $githubToken = "$(GithubPat)" - $extensionName= "$(ExtensionName)" - $extensionDirectory = "$(ExtensionDirectory)" - - if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { - # Create GitHub credential - git config --global user.name "AzureFunctionsPython" - git config --global user.email "azfunc@microsoft.com" - - # Clone Repository - git clone https://$githubToken@github.com/Azure/azure-functions-python-extensions - Write-Host "Cloned azure-functions-python-extensions into local" - Set-Location "azure-functions-python-extensions" - git checkout "origin/$extensionName/release/$newLibraryVersion" - - # Create release tag X.Y.Z - Write-Host "Creating release tag $newLibraryVersion" - git tag -a "$extensionName/$newLibraryVersion" -m "$newLibraryVersion" - - # Push tag to remote - git push origin $extensionName/$newLibraryVersion - - } else { - Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.5.0)" - exit -1 - } - displayName: 'Tag and push x.y.z' - - pwsh: | - $githubUser = "$(GithubUser)" - $githubToken = "$(GithubPat)" - $newLibraryVersion = "$(NewLibraryVersion)" - $extensionName= "$(ExtensionName)" - $extensionDirectory = "$(ExtensionDirectory)" - - if($newLibraryVersion -match '(\d)+.(\d)+.(\d)+') { - # Create GitHub credential - $credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) - - # Create Release Note - Write-Host "Creating release note in GitHub" - $body = (@{tag_name="$newLibraryVersion";name="$extensionName $newLibraryVersion";body="- Fill in Release Note Here";draft=$true} | ConvertTo-Json -Compress) - $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-python-extensions/releases" - - # Return Value - if ($response.StatusCode -ne 201) { - Write-Host "Failed to create release note in GitHub" - exit -1 - } - - $draftUrl = $response | ConvertFrom-Json | Select -expand url - Write-Host "Release draft created in $draftUrl" - } else { - Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.1.8)" - exit -1 - } - displayName: 'Create GitHub release draft' - -- job: "CheckGitHubRelease" - dependsOn: ['CreateReleaseTag'] - displayName: '(Manual) Check GitHub release note' - pool: server - steps: - - task: ManualValidation@1 - displayName: 'Write GitHub release note' - inputs: - notifyUsers: '' - instructions: 'Please head to https://github.com/Azure/azure-functions-python-extensions/releases to finish the release note' - -- job: "CreateWorkerPR" - dependsOn: ['CheckGitHubRelease'] - displayName: 'Create PR in Worker Repo' - steps: - - pwsh: | - $githubUser = "$(GithubUser)" - $githubToken = "$(GithubPat)" - $newLibraryVersion = "$(NewLibraryVersion)" - $extensionName= "$(ExtensionName)" - $extensionDirectory = "$(ExtensionDirectory)" - $newBranch = "extensions/$(ExtensionName)/$newLibraryVersion" - - if($newLibraryVersion -match '(\d)+.(\d)+.*') { - # Create GitHub credential - git config --global user.name "AzureFunctionsPython" - git config --global user.email "azfunc@microsoft.com" - - # Create GitHub credential - $credential = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) - - # Clone Repository - git clone https://$githubToken@github.com/Azure/azure-functions-python-worker - Write-Host "Cloned azure-functions-python-worker into local and checkout $newBranch branch" - Set-Location "azure-functions-python-worker" - git checkout -b $newBranch "origin/dev" - - # Modify Extension $extensionName Version in pyproject.toml - Write-Host "Replacing Extension $extensionName version in worker's pyproject.toml" - ((Get-Content workers/pyproject.toml) -replace '"$(ExtensionName)==(\d)+.(\d)+.*"','"$(ExtensionName)==$(NewLibraryVersion)"' -join "`n") + "`n" | Set-Content -NoNewline workers/pyproject.toml - - # Commit Python Version - Write-Host "Pushing $newBranch to azure-functions-python-worker repo" - git add workers/pyproject.toml - git commit -m "build: update $extensionName version to $newLibraryVersion" - git push origin $newBranch - - # Create PR - Write-Host "Creating PR draft in GitHub" - $body = (@{head="$newBranch";base="dev";body="Python Extension [$extensionName] Version [$newLibraryVersion](https://github.com/Azure/azure-functions-python-extensions/releases/tag/$extensionName/$newLibraryVersion)";draft=$true;maintainer_can_modify=$true;title="build: update $extensionName version to $newLibraryVersion"} | ConvertTo-Json -Compress) - $response = Invoke-WebRequest -Headers @{"Cache-Control"="no-cache";"Content-Type"="application/json";"Authorization"="Basic $credential";"Accept"="application/vnd.github.v3+json"} -Method Post -Body "$body" -Uri "https://api.github.com/repos/Azure/azure-functions-python-worker/pulls" - - # Return Value - if ($response.StatusCode -ne 201) { - Write-Host "Failed to create PR in Azure Functions Python Worker" - exit -1 - } - - $draftUrl = $response | ConvertFrom-Json | Select -expand url - Write-Host "PR draft created in $draftUrl" - } else { - Write-Host "NewLibraryVersion $newLibraryVersion is malformed (example: 1.1.8)" - exit -1 - } - displayName: 'Create PR in Worker Repo' - -- job: "FinishWorkerPr" - dependsOn: ['CreateWorkerPR'] - displayName: '(Manual) Finish Worker PR' - pool: server - steps: - - task: ManualValidation@1 - displayName: 'Wait For Python Worker Build' - inputs: - notifyUsers: '' - instructions: | - 1. Please wait and check if all goes green in the https://github.com/Azure/azure-functions-python-worker/pulls - 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' diff --git a/eng/templates/official/release/build-artifacts.yml b/eng/templates/official/release/build-artifacts.yml new file mode 100644 index 0000000..f10496f --- /dev/null +++ b/eng/templates/official/release/build-artifacts.yml @@ -0,0 +1,54 @@ +parameters: + - name: extensionVersion + type: string + - name: extensionName + type: string + displayName: 'PyPI project and repository directory name' + - name: artifactName + type: string + displayName: 'Pipeline artifact name produced by this job' + - name: pythonVersion + type: string + default: '3.11' + - name: githubOrg + type: string + default: 'Azure' + - name: githubRepo + type: string + default: 'azure-functions-python-extensions' + +jobs: + - job: Build + displayName: 'Build ${{ parameters.extensionName }}' + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/${{ parameters.extensionName }} + artifactName: ${{ parameters.artifactName }} + steps: + - checkout: none + - bash: | + git clone --branch "${{ parameters.extensionName }}/release/${{ parameters.extensionVersion }}" --single-branch \ + "https://$(GithubUser):$(GithubPat)@github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}.git" . + displayName: 'Checkout extension release branch' + - task: UsePythonVersion@0 + inputs: + versionSpec: '${{ parameters.pythonVersion }}' + - task: PipAuthenticate@1 + displayName: 'Pip Authenticate' + inputs: + artifactFeeds: public/upstream-public + onlyAddExtraIndex: false + - bash: | + python --version + python -m pip install -U pip + python -m pip install build twine + cd "${{ parameters.extensionName }}" + python -m build + python -m twine check dist/* + displayName: 'Build ${{ parameters.extensionName }}' \ No newline at end of file diff --git a/eng/templates/official/release/bump-version.yml b/eng/templates/official/release/bump-version.yml new file mode 100644 index 0000000..0ed965e --- /dev/null +++ b/eng/templates/official/release/bump-version.yml @@ -0,0 +1,123 @@ +parameters: + - name: extensionVersion + type: string + - name: extensionName + type: string + displayName: 'PyPI project and repository directory name' + - name: extensionDirectory + type: string + displayName: 'Package path under the extension project directory' + - name: versionPattern + type: string + default: "__version__ = ['\"]([0-9])+\\.([0-9])+.*['\"]" + displayName: 'Regex pattern used to locate the version assignment' + - name: versionReplacement + type: string + default: "__version__ = '{{VERSION}}'" + displayName: 'Replacement pattern; {{VERSION}} is replaced by the release version' + - name: createPullRequest + type: boolean + default: true + - name: baseBranch + type: string + default: 'dev' + - name: githubOrg + type: string + default: 'Azure' + - name: githubRepo + type: string + default: 'azure-functions-python-extensions' + +jobs: + - job: CreateReleaseBranch + displayName: 'Create Release Branch' + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + steps: + - pwsh: | + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $extensionVersion = "${{ parameters.extensionVersion }}" + $extensionName = "${{ parameters.extensionName }}" + + # Accept stable versions and common PEP 440 or SemVer prerelease forms. + $versionValidation = '^\d+\.\d+\.\d+((a|b|rc)\d+|\.dev\d+|\.post\d+|-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$' + if ($extensionVersion -notmatch $versionValidation) { + Write-Host "ExtensionVersion '$extensionVersion' is malformed. Expected major.minor.patch with an optional prerelease suffix." + exit 1 + } + + $releaseBranch = "$extensionName/release/$extensionVersion" + git check-ref-format --branch $releaseBranch 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Host "The derived branch name '$releaseBranch' is not a valid Git ref name." + exit 1 + } + + git config --global user.name "AzureFunctions" + git config --global user.email "azurefunctions@microsoft.com" + $credential = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) + + git checkout -b "$releaseBranch" + + $versionFilePath = Join-Path $extensionName "${{ parameters.extensionDirectory }}/__init__.py" + $versionPattern = $env:VERSION_PATTERN + $versionReplacementTemplate = $env:VERSION_REPLACEMENT + $versionReplacement = $versionReplacementTemplate -replace '\{\{VERSION\}\}', $extensionVersion + + Write-Host "Changing version in $versionFilePath to $extensionVersion" + ((Get-Content $versionFilePath) -replace $versionPattern, $versionReplacement -join "`n") + "`n" | + Set-Content -NoNewline $versionFilePath + git add $versionFilePath + git commit -m "build: update $extensionName version to $extensionVersion" + + git remote set-url origin "https://${githubUser}:${githubToken}@github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}.git" + git push --set-upstream origin "$releaseBranch" + + if ([System.Convert]::ToBoolean("${{ parameters.createPullRequest }}")) { + $prBody = @{ + head = "$releaseBranch" + base = "${{ parameters.baseBranch }}" + title = "build: update $extensionName version to $extensionVersion" + body = "Release $extensionName $extensionVersion" + draft = $true + maintainer_can_modify = $true + } | ConvertTo-Json -Compress + + $headers = @{ + "Authorization" = "Basic $credential" + "Content-Type" = "application/json" + "Accept" = "application/vnd.github.v3+json" + "User-Agent" = "AzureDevOpsPipeline" + } + $response = Invoke-WebRequest -Headers $headers -Method Post -Body $prBody ` + -Uri "https://api.github.com/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/pulls" + + if ($response.StatusCode -ne 201) { + Write-Host "##[warning]Failed to create release branch PR. HTTP $($response.StatusCode)" + } else { + $draftUrl = ($response.Content | ConvertFrom-Json).html_url + Write-Host "Release branch PR created: $draftUrl" + } + } + displayName: 'Push extension release branch' + env: + VERSION_PATTERN: ${{ parameters.versionPattern }} + VERSION_REPLACEMENT: ${{ parameters.versionReplacement }} + + - job: CheckReleaseBranch + dependsOn: CreateReleaseBranch + displayName: '(Manual) Check Release Branch' + pool: server + steps: + - task: ManualValidation@1 + displayName: '(Optional) Modify release branch' + inputs: + notifyUsers: '' + instructions: | + 1. Check that https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/tree/${{ parameters.extensionName }}/release/${{ parameters.extensionVersion }} builds and passes all unit tests. + 2. Modify the release branch if needed. + 3. Ensure the branch contains all changes required for the release. \ No newline at end of file diff --git a/eng/templates/official/release/publish-release.yml b/eng/templates/official/release/publish-release.yml new file mode 100644 index 0000000..6749f4a --- /dev/null +++ b/eng/templates/official/release/publish-release.yml @@ -0,0 +1,309 @@ +parameters: + - name: extensionVersion + type: string + - name: extensionName + type: string + displayName: 'PyPI project and repository directory name' + - name: artifactName + type: string + displayName: 'Pipeline artifact produced by the build stage' + - name: githubOrg + type: string + default: 'Azure' + - name: githubRepo + type: string + default: 'azure-functions-python-extensions' + - name: esrpMainPublisher + type: string + default: 'azure-functions' + displayName: 'ESRP onboarding publisher name' + +jobs: + - job: CreateReleaseTag + displayName: 'Create Release Tag' + steps: + - pwsh: | + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $extensionVersion = "${{ parameters.extensionVersion }}" + $extensionName = "${{ parameters.extensionName }}" + $releaseBranch = "$extensionName/release/$extensionVersion" + $releaseTag = "$extensionName/$extensionVersion" + + git config --global user.name "AzureFunctions" + git config --global user.email "azurefunctions@microsoft.com" + git clone "https://${githubUser}:${githubToken}@github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}" + Set-Location "${{ parameters.githubRepo }}" + git checkout "origin/$releaseBranch" + + Write-Host "Creating release tag $releaseTag" + git tag -a "$releaseTag" -m "$extensionVersion" + git push origin "$releaseTag" + displayName: 'Tag and push extension version' + + - download: current + artifact: ${{ parameters.artifactName }} + displayName: 'Download build artifacts' + + - pwsh: | + $ErrorActionPreference = 'Stop' + $ProgressPreference = 'SilentlyContinue' + + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $extensionVersion = "${{ parameters.extensionVersion }}" + $extensionName = "${{ parameters.extensionName }}" + $releaseTag = "$extensionName/$extensionVersion" + + $credential = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) + $apiHeaders = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Basic $credential" + "X-GitHub-Api-Version" = "2022-11-28" + "Content-Type" = "application/json" + } + + $distFolder = Join-Path '$(Pipeline.Workspace)' (Join-Path "${{ parameters.artifactName }}" 'dist') + if (-not (Test-Path -LiteralPath $distFolder)) { + throw "Distribution folder not found: $distFolder" + } + + $assetsToUpload = @( + Get-ChildItem -LiteralPath $distFolder -File | + Where-Object { $_.Name -like '*.whl' -or $_.Name -like '*.tar.gz' } | + Sort-Object Name + ) + if ($assetsToUpload.Count -eq 0) { + throw "No .whl or .tar.gz files found in $distFolder" + } + + $body = @{ + tag_name = "$releaseTag" + name = "$extensionName $extensionVersion" + body = "- Fill in Release Note Here" + draft = $true + } | ConvertTo-Json -Compress + $createResponse = Invoke-WebRequest -Headers $apiHeaders -Method Post -Body $body ` + -Uri "https://api.github.com/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases" + if ($createResponse.StatusCode -ne 201) { + throw "Failed to create release. HTTP $($createResponse.StatusCode)" + } + + $release = $createResponse.Content | ConvertFrom-Json + $uploadUrlBase = $release.upload_url -replace '\{\?name,label\}$', '' + $uploadHeaders = @{ + "Accept" = "application/vnd.github+json" + "Authorization" = "Basic $credential" + "X-GitHub-Api-Version" = "2022-11-28" + } + + try { + foreach ($asset in $assetsToUpload) { + $assetUploadUrl = '{0}?name={1}' -f $uploadUrlBase, [System.Uri]::EscapeDataString($asset.Name) + Write-Host "Uploading $($asset.Name)..." + try { + $uploadResponse = Invoke-WebRequest -Headers $uploadHeaders -Method Post ` + -Uri $assetUploadUrl -ContentType 'application/octet-stream' -InFile $asset.FullName + } catch { + $status = $null + if ($_.Exception.Response) { + $status = [int]$_.Exception.Response.StatusCode + } + if ($status -eq 422) { + throw "Asset '$($asset.Name)' already exists on release $releaseTag." + } + throw "Upload failed for $($asset.Name): HTTP $status - $($_.Exception.Message)" + } + + if ($uploadResponse.StatusCode -ne 201) { + throw "Upload failed for $($asset.Name). HTTP $($uploadResponse.StatusCode)" + } + } + } catch { + $uploadError = $_ + if ($release -and $release.id) { + $releaseDeleteUri = "https://api.github.com/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases/$($release.id)" + try { + Invoke-WebRequest -Headers $apiHeaders -Method Delete -Uri $releaseDeleteUri | Out-Null + } catch { + Write-Host "##[warning]Failed to delete incomplete draft release $($release.id)." + } + } + throw $uploadError + } + + Write-Host "Draft release created with assets: $($release.html_url)" + displayName: 'Create GitHub release draft and attach assets' + + - job: CheckGitHubRelease + dependsOn: CreateReleaseTag + displayName: '(Manual) Check GitHub release note' + pool: server + steps: + - task: ManualValidation@1 + displayName: 'Write GitHub release note' + inputs: + notifyUsers: '' + instructions: 'Please finish the release note at https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases' + + - job: CreateWorkerPR + dependsOn: CheckGitHubRelease + displayName: 'Create PR in Worker Repo' + steps: + - pwsh: | + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $extensionVersion = "${{ parameters.extensionVersion }}" + $extensionName = "${{ parameters.extensionName }}" + $newBranch = "extensions/$extensionName/$extensionVersion" + + git config --global user.name "AzureFunctions" + git config --global user.email "azurefunctions@microsoft.com" + $credential = [System.Convert]::ToBase64String( + [System.Text.Encoding]::ASCII.GetBytes("${githubUser}:${githubToken}")) + + git clone "https://${githubUser}:${githubToken}@github.com/Azure/azure-functions-python-worker" + Set-Location 'azure-functions-python-worker' + git checkout -b "$newBranch" 'origin/dev' + + $escapedExtensionName = [regex]::Escape($extensionName) + $dependencyPattern = '"' + $escapedExtensionName + '==[^";]+' + $dependencyReplacement = '"' + $extensionName + '==' + $extensionVersion + $pyprojectContent = Get-Content 'workers/pyproject.toml' -Raw + $updatedContent = $pyprojectContent -replace $dependencyPattern, $dependencyReplacement + if ($updatedContent -eq $pyprojectContent) { + throw "Could not find an exact $extensionName dependency to update in workers/pyproject.toml" + } + $updatedContent | Set-Content -NoNewline 'workers/pyproject.toml' + + git add 'workers/pyproject.toml' + git commit -m "build: update $extensionName version to $extensionVersion" + git push origin $newBranch + + $prBody = @{ + head = "$newBranch" + base = 'dev' + body = "Python Extension [$extensionName] Version [$extensionVersion](https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases/tag/$extensionName/$extensionVersion)" + draft = $true + maintainer_can_modify = $true + title = "build: update $extensionName version to $extensionVersion" + } | ConvertTo-Json -Compress + $headers = @{ + "Authorization" = "Basic $credential" + "Content-Type" = "application/json" + "Accept" = "application/vnd.github.v3+json" + "User-Agent" = "AzureDevOpsPipeline" + } + $response = Invoke-WebRequest -Headers $headers -Method Post -Body $prBody ` + -Uri 'https://api.github.com/repos/Azure/azure-functions-python-worker/pulls' + if ($response.StatusCode -ne 201) { + throw "Failed to create PR in Azure Functions Python Worker. HTTP $($response.StatusCode)" + } + + $draftUrl = ($response.Content | ConvertFrom-Json).html_url + Write-Host "Worker PR draft created: $draftUrl" + displayName: 'Create PR in Worker Repo' + + - job: FinishWorkerPr + dependsOn: CreateWorkerPR + displayName: '(Manual) Finish Worker PR' + pool: server + steps: + - task: ManualValidation@1 + displayName: 'Wait For Python Worker Build' + inputs: + notifyUsers: '' + instructions: | + 1. Wait for all checks to pass at https://github.com/Azure/azure-functions-python-worker/pulls. + 2. Merge the PR into the worker dev branch. + + - job: ESRPRelease + dependsOn: FinishWorkerPr + displayName: 'Publish package to PyPI with ESRP' + templateContext: + type: releaseJob + isProduction: true + inputs: + - input: pipelineArtifact + artifactName: ${{ parameters.artifactName }} + targetPath: $(Pipeline.Workspace)/${{ parameters.artifactName }} + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + steps: + - checkout: none + - pwsh: | + $ErrorActionPreference = 'Stop' + $distFolder = '$(Pipeline.Workspace)/${{ parameters.artifactName }}/dist' + $extensionName = '${{ parameters.extensionName }}' + $extensionVersion = '${{ parameters.extensionVersion }}' + + if (-not (Test-Path -LiteralPath $distFolder -PathType Container)) { + throw "Distribution folder not found: $distFolder" + } + + $wheels = @(Get-ChildItem -LiteralPath $distFolder -File -Filter '*.whl') + $sourceDistributions = @(Get-ChildItem -LiteralPath $distFolder -File -Filter '*.tar.gz') + $unsupportedFiles = @( + Get-ChildItem -LiteralPath $distFolder -File | + Where-Object { $_.Name -notlike '*.whl' -and $_.Name -notlike '*.tar.gz' } + ) + + if ($wheels.Count -ne 1 -or $sourceDistributions.Count -ne 1) { + throw "Expected one wheel and one source distribution in $distFolder; found $($wheels.Count) wheel(s) and $($sourceDistributions.Count) source distribution(s)." + } + if ($unsupportedFiles.Count -ne 0) { + throw "Unexpected files found in the ESRP package folder: $($unsupportedFiles.Name -join ', ')" + } + + Add-Type -AssemblyName System.IO.Compression.FileSystem + $archive = [System.IO.Compression.ZipFile]::OpenRead($wheels[0].FullName) + try { + $metadataEntry = $archive.Entries | + Where-Object { $_.FullName -like '*.dist-info/METADATA' } | + Select-Object -First 1 + if (-not $metadataEntry) { + throw "Wheel metadata was not found in $($wheels[0].Name)." + } + + $reader = [System.IO.StreamReader]::new($metadataEntry.Open()) + try { + $metadata = $reader.ReadToEnd() + } finally { + $reader.Dispose() + } + } finally { + $archive.Dispose() + } + + $packageName = [regex]::Match($metadata, '(?m)^Name:\s*(.+)\r?$').Groups[1].Value.Trim() + $packageVersion = [regex]::Match($metadata, '(?m)^Version:\s*(.+)\r?$').Groups[1].Value.Trim() + if ($packageName -ne $extensionName -or $packageVersion -ne $extensionVersion) { + throw "Artifact metadata mismatch. Expected $extensionName $extensionVersion; found $packageName $packageVersion." + } + + Write-Host "Validated ESRP release contents for $packageName $packageVersion:" + Write-Host " - $($wheels[0].Name)" + Write-Host " - $($sourceDistributions[0].Name)" + displayName: 'Validate ESRP package contents' + + - task: SFP.release-tasks.custom-build-release-task.EsrpRelease@9 + displayName: 'ESRP Release ${{ parameters.extensionName }}' + inputs: + connectedservicename: 'azfunc-internal-esrp-prod' + usemanagedidentity: true + keyvaultname: 'kv-azfunc-esrp-prod' + signcertname: 'esrp-signing' + clientid: 'a31bf239-3e15-498e-a335-f6ec0c9d9ccf' + intent: 'PackageDistribution' + contenttype: 'PyPi' + contentsource: 'Folder' + folderlocation: '$(Pipeline.Workspace)/${{ parameters.artifactName }}/dist' + waitforreleasecompletion: true + owners: $(Build.RequestedForEmail) + approvers: $(Build.RequestedForEmail) + serviceendpointurl: 'https://api.esrp.microsoft.com' + mainpublisher: '${{ parameters.esrpMainPublisher }}' + domaintenantid: '33e01921-4d64-4f8c-a055-5bdaffd5e33d' \ No newline at end of file From aa99c5b98714eff80d91633e659b6599ca52315a Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Tue, 4 Aug 2026 14:27:11 -0500 Subject: [PATCH 2/2] fix syntax error --- eng/templates/official/release/publish-release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/templates/official/release/publish-release.yml b/eng/templates/official/release/publish-release.yml index 6749f4a..8c6a751 100644 --- a/eng/templates/official/release/publish-release.yml +++ b/eng/templates/official/release/publish-release.yml @@ -284,7 +284,7 @@ jobs: throw "Artifact metadata mismatch. Expected $extensionName $extensionVersion; found $packageName $packageVersion." } - Write-Host "Validated ESRP release contents for $packageName $packageVersion:" + Write-Host "Validated ESRP release contents for $packageName ${packageVersion}:" Write-Host " - $($wheels[0].Name)" Write-Host " - $($sourceDistributions[0].Name)" displayName: 'Validate ESRP package contents'