Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions eng/ci/extension-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
58 changes: 9 additions & 49 deletions eng/templates/official/jobs/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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)'
69 changes: 69 additions & 0 deletions eng/templates/official/jobs/release-pypi-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
parameters:

# The pipeline artifact to download for publishing.
# artifact:
# name: <name>
# pipeline: <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 }}
78 changes: 78 additions & 0 deletions eng/templates/official/steps/publish-pypi-esrp.yml
Original file line number Diff line number Diff line change
@@ -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
Loading