diff --git a/eng/pipelines/common/templates/jobs/approval-job.yml b/eng/pipelines/common/templates/jobs/approval-job.yml new file mode 100644 index 0000000000..051a3875ff --- /dev/null +++ b/eng/pipelines/common/templates/jobs/approval-job.yml @@ -0,0 +1,47 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +parameters: + - name: approvalAliases + type: string + default: '[ADO.Net]\\SqlClient Admins' + + - name: publishDestination + type: string + + - name: dryRun + type: boolean + + - name: isPreview + type: boolean + + - name: publishSymbols + type: boolean + + - name: nugetPackageVersion + type: string + + - name: product + type: string + +jobs: +- job: AwaitApproval + displayName: 'Await Release Approval' + pool: server + steps: + - task: ManualValidation@0 + displayName: 'Manual Approval' + timeoutInMinutes: 4320 # 3 days + inputs: + notifyUsers: ${{ parameters.approvalAliases }} + instructions: | + Release Checklist: + * Destination: ${{ parameters.publishDestination }} + * Preview build: ${{ parameters.isPreview }} + * Dry run: ${{ parameters.dryRun }} + * Symbols: ${{ parameters.publishSymbols }} + * NuGet package version: ${{ parameters.nugetPackageVersion }} + * Product: ${{ parameters.product }} + Approve to continue or Reject to abort. diff --git a/eng/pipelines/common/templates/jobs/publish-packages-job.yml b/eng/pipelines/common/templates/jobs/publish-packages-job.yml new file mode 100644 index 0000000000..d36932a6d6 --- /dev/null +++ b/eng/pipelines/common/templates/jobs/publish-packages-job.yml @@ -0,0 +1,65 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +parameters: + - name: publishDestination + type: string + + - name: dryRun + type: boolean + + - name: internalFeedSource + type: string + + - name: publicNuGetSource + type: string + + - name: publishSymbols + type: boolean + + - name: packageFolderName + type: string + + - name: nugetPackageVersion + type: string + + - name: product + type: string + +jobs: +- job: PublishPackages + displayName: 'Publish Packages' + dependsOn: AwaitApproval + condition: succeeded() + pool: + vmImage: 'ubuntu-latest' + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Signed Packages' + inputs: + buildType: current + artifactName: ${{ parameters.packageFolderName }} + targetPath: $(Pipeline.Workspace)/release/packages + - script: | + echo "NuGet Package Version: ${{ parameters.nugetPackageVersion }}" + displayName: 'Echo NuGet Package Version' + - ${{ if ne(parameters.publishDestination, 'Public') }}: + - template: ../steps/publish-internal-feed-step.yml + parameters: + dryRun: ${{ parameters.dryRun }} + internalFeedSource: ${{ parameters.internalFeedSource }} + packagesGlob: $(System.DefaultWorkingDirectory)/_dotnet-sqlclient-Official/drop_buildMDS_build_signed_package/*.nupkg + - ${{ if eq(parameters.publishDestination, 'Public') }}: + - template: ../steps/publish-public-nuget-step.yml + parameters: + dryRun: ${{ parameters.dryRun }} + publicNuGetSource: ${{ parameters.publicNuGetSource }} + packagesGlob: $(System.DefaultWorkingDirectory)/_dotnet-sqlclient-Official/drop_buildMDS_build_signed_package/*.nupkg + - ${{ if and(parameters.publishSymbols, ne(parameters.dryRun, true)) }}: + - template: ../steps/publish-symbols-step.yml + parameters: + publishSymbols: ${{ parameters.publishSymbols }} + symbolsArtifactName: ${{ parameters.product }}_symbols_$(System.TeamProject)_$(Build.Repository.Name)_$(Build.SourceBranchName)_${{ parameters.nugetPackageVersion }}_$(System.TimelineId) + product: ${{ parameters.product }} diff --git a/eng/pipelines/common/templates/stages/release-stage.yml b/eng/pipelines/common/templates/stages/release-stage.yml new file mode 100644 index 0000000000..2fa4d891a6 --- /dev/null +++ b/eng/pipelines/common/templates/stages/release-stage.yml @@ -0,0 +1,66 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +parameters: + - name: runRelease + type: boolean + default: false + + - name: publishDestination + type: string + + - name: dryRun + type: boolean + default: false + + - name: approvalAliases + type: string + + - name: internalFeedSource + type: string + + - name: publicNuGetSource + type: string + + - name: publishSymbols + type: boolean + default: false + + - name: isPreview + type: boolean + + - name: product + type: string + + - name: nugetPackageVersion + type: string + + - name: packageFolderName + type: string + +stages: +- stage: releaseMDS + displayName: 'Release (Manual)' + condition: and(succeeded(), eq(variables['Build.Reason'],'Manual'), eq(${{ parameters.runRelease }}, true)) + jobs: + - template: ../jobs/approval-job.yml + parameters: + approvalAliases: ${{ parameters.approvalAliases }} + publishDestination: ${{ parameters.publishDestination }} + dryRun: ${{ parameters.dryRun }} + isPreview: ${{ parameters.isPreview }} + publishSymbols: ${{ parameters.publishSymbols }} + nugetPackageVersion: ${{ parameters.nugetPackageVersion }} + product: ${{ parameters.product }} + - template: ../jobs/publish-packages-job.yml + parameters: + publishDestination: ${{ parameters.publishDestination }} + dryRun: ${{ parameters.dryRun }} + internalFeedSource: ${{ parameters.internalFeedSource }} + publicNuGetSource: ${{ parameters.publicNuGetSource }} + publishSymbols: ${{ parameters.publishSymbols }} + packageFolderName: ${{ parameters.packageFolderName }} + nugetPackageVersion: ${{ parameters.nugetPackageVersion }} + product: ${{ parameters.product }} diff --git a/eng/pipelines/common/templates/steps/publish-internal-feed-step.yml b/eng/pipelines/common/templates/steps/publish-internal-feed-step.yml new file mode 100644 index 0000000000..8ebfe9f74a --- /dev/null +++ b/eng/pipelines/common/templates/steps/publish-internal-feed-step.yml @@ -0,0 +1,46 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +parameters: + - name: dryRun + type: boolean + + - name: internalFeedSource + type: string + + - name: packagesGlob + type: string + default: '$(System.DefaultWorkingDirectory)/_dotnet-sqlclient-Official/drop_buildMDS_build_signed_package/*.nupkg' + +steps: +- script: | + set -e + SRC='${{ parameters.internalFeedSource }}' + if [ -z "$SRC" ]; then + echo "Internal feed source parameter not set." + exit 1 + fi + if [ "${{ parameters.dryRun }}" = "true" ]; then + echo "[DRY RUN] Listing packages targeted for push to: ${{ parameters.publicNuGetSource }}" + echo "Using glob pattern: ${{ parameters.packagesGlob }}" + # Derive directory and filename pattern from the glob for a precise find (handles nested patterns minimally) + glob='${{ parameters.packagesGlob }}' + dir="${glob%/*}" + name="${glob##*/}" + echo "Resolved directory: $dir" + echo "Filename pattern: $name" + if [ -d "$dir" ]; then + echo "Matched files:" || true + # Print all matched files to identify what would be pushed + find "$dir" -type f -name "$name" -print || true + else + echo "Directory does not exist yet: $dir" + fi + fi + for f in $(find "${{ parameters.packagesGlob }}" -name "*.nupkg"); do + echo "Push $f" + dotnet nuget push --source "$SRC" --api-key az "$f" + done + displayName: 'Publish to Internal Feed' diff --git a/eng/pipelines/common/templates/steps/publish-public-nuget-step.yml b/eng/pipelines/common/templates/steps/publish-public-nuget-step.yml new file mode 100644 index 0000000000..8c827ec93f --- /dev/null +++ b/eng/pipelines/common/templates/steps/publish-public-nuget-step.yml @@ -0,0 +1,52 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +parameters: + - name: dryRun + type: boolean + + - name: publicNuGetSource + type: string + + - name: nugetServiceConnection + type: string + default: 'ADO Nuget Org Connection' + + - name: packagesGlob + type: string + default: '$(System.DefaultWorkingDirectory)/_dotnet-sqlclient-Official/drop_buildMDS_build_signed_package/*.nupkg' + +steps: +- task: NuGetToolInstaller@1 + displayName: 'Install Latest Nuget' + inputs: + checkLatest: true +- script: | + echo "[DRY RUN] Listing packages targeted for push to: ${{ parameters.publicNuGetSource }}" + echo "Using glob pattern: ${{ parameters.packagesGlob }}" + # Derive directory and filename pattern from the glob for a precise find (handles nested patterns minimally) + glob='${{ parameters.packagesGlob }}' + dir="${glob%/*}" + name="${glob##*/}" + echo "Resolved directory: $dir" + echo "Filename pattern: $name" + if [ -d "$dir" ]; then + echo "Matched files:" || true + # Print all matched files to identify what would be pushed + find "$dir" -type f -name "$name" -print || true + else + echo "Directory does not exist yet: $dir" + fi + displayName: 'Dry Run - List Packages' + condition: and(succeeded(), eq(${{ parameters.dryRun }}, true)) + +- task: NuGetCommand@2 + displayName: 'Push to Nuget.org' + condition: and(succeeded(), eq(${{ parameters.dryRun }}, false)) + inputs: + command: push + packagesToPush: '${{ parameters.packagesGlob }}' + nuGetFeedType: external + publishFeedCredentials: '${{ parameters.nugetServiceConnection }}' diff --git a/eng/pipelines/common/templates/steps/publish-symbols-step.yml b/eng/pipelines/common/templates/steps/publish-symbols-step.yml index 5f8d2e6a7d..d8eae8837a 100644 --- a/eng/pipelines/common/templates/steps/publish-symbols-step.yml +++ b/eng/pipelines/common/templates/steps/publish-symbols-step.yml @@ -11,26 +11,27 @@ parameters: default: 'SqlClientDrivers' - name: publishSymbols - type: string + type: boolean + default: false - name: symbolsVersion type: string - default: '$(NuGetPackageVersion)' + default: '$(NugetPackageVersion)' - name: symbolServer type: string default: '$(SymbolServer)' - + - name: symbolTokenUri type: string default: '$(SymbolTokenUri)' - + - name: symbolsArtifactName type: string - + - name: publishToServers type: object - default: + default: internal: true public: true @@ -45,15 +46,20 @@ parameters: values: - MDS - MSS + - AKV + + - name: azureSubscription + type: string + default: 'Symbols publishing Workload Identity federation service-ADO.Net' steps: - powershell: 'Write-Host "##vso[task.setvariable variable=ArtifactServices.Symbol.AccountName;]${{parameters.SymAccount}}"' displayName: 'Update Symbol.AccountName with ${{parameters.SymAccount}}' - condition: and(succeeded(), ${{ eq(parameters.publishSymbols, 'true') }}) + condition: and(succeeded(), eq(parameters.publishSymbols, true)) -- ${{ if eq(parameters.product, 'MDS') }}: +- ${{ if and(eq(parameters.publishSymbols, true), eq(parameters.product, 'MDS')) }}: - task: PublishSymbols@2 - displayName: 'Upload symbols to ${{parameters.SymAccount }} org' + displayName: 'Upload MDS symbols to ${{parameters.SymAccount }} org' inputs: SymbolsFolder: '$(Build.SourcesDirectory)\artifacts\${{parameters.referenceType }}\bin' SearchPattern: | @@ -67,64 +73,46 @@ steps: SymbolsVersion: ${{parameters.symbolsVersion }} SymbolsArtifactName: ${{parameters.symbolsArtifactName }} Pat: $(System.AccessToken) - condition: and(succeeded(), ${{ eq(parameters.publishSymbols, 'true') }}) - -- task: AzureCLI@2 - displayName: 'Publish symbols' - condition: and(succeeded(), ${{ eq(parameters.publishSymbols, 'true') }}) - inputs: - azureSubscription: 'Symbols publishing Workload Identity federation service-ADO.Net' - scriptType: ps - scriptLocation: inlineScript - inlineScript: | - $publishToInternalServer = "${{parameters.publishToServers.internal }}".ToLower() - $publishToPublicServer = "${{parameters.publishToServers.public }}".ToLower() - - echo "Publishing request name: ${{parameters.symbolsArtifactName }}" - echo "Publish to internal server: $publishToInternalServer" - echo "Publish to public server: $publishToPublicServer" - - $symbolServer = "${{parameters.symbolServer }}" - $tokenUri = "${{parameters.symbolTokenUri }}" - # Registered project name in the symbol publishing pipeline: https://portal.microsofticm.com/imp/v3/incidents/incident/520844254/summary - $projectName = "Microsoft.Data.SqlClient.SNI" - - # Get the access token for the symbol publishing service - $symbolPublishingToken = az account get-access-token --resource $tokenUri --query accessToken -o tsv - - echo "> 1.Symbol publishing token acquired." - - echo "Registering the request name ..." - $requestName = "${{parameters.symbolsArtifactName }}" - $requestNameRegistrationBody = "{'requestName': '$requestName'}" - Invoke-RestMethod -Method POST -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" -Body $requestNameRegistrationBody - - echo "> 2.Registration of request name succeeded." - - echo "Publishing the symbols ..." - $publishSymbolsBody = "{'publishToInternalServer': $publishToInternalServer, 'publishToPublicServer': $publishToPublicServer}" - echo "Publishing symbols request body: $publishSymbolsBody" - Invoke-RestMethod -Method POST -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests/$requestName" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" -Body $publishSymbolsBody - - echo "> 3.Request to publish symbols succeeded." - - # The following REST calls are used to check publishing status. - echo "> 4.Checking the status of the request ..." - - Invoke-RestMethod -Method GET -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests/$requestName" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" - - echo "Use below tables to interpret the values of xxxServerStatus and xxxServerResult fields from the response." - - echo "PublishingStatus" - echo "-----------------" - echo "0 NotRequested; The request has not been requested to publish." - echo "1 Submitted; The request is submitted to be published" - echo "2 Processing; The request is still being processed" - echo "3 Completed; The request has been completed processing. It can be failed or successful. Check PublishingResult to get more details" - - echo "PublishingResult" - echo "-----------------" - echo "0 Pending; The request has not completed or has not been requested." - echo "1 Succeeded; The request has published successfully" - echo "2 Failed; The request has failed to publish" - echo "3 Cancelled; The request was cancelled" + condition: and(succeeded(), eq(parameters.publishSymbols, true)) + - task: AzureCLI@2 + displayName: 'Publish MDS symbols' + condition: and(succeeded(), eq(parameters.publishSymbols, true)) + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + scriptType: ps + scriptLocation: inlineScript + inlineScript: | + $publishToInternalServer = "${{parameters.publishToServers.internal }}".ToLower() + $publishToPublicServer = "${{parameters.publishToServers.public }}".ToLower() + + echo "Publishing request name: ${{parameters.symbolsArtifactName }}" + echo "Publish to internal server: $publishToInternalServer" + echo "Publish to public server: $publishToPublicServer" + + $symbolServer = "${{parameters.symbolServer }}" + $tokenUri = "${{parameters.symbolTokenUri }}" + $projectName = "Microsoft.Data.SqlClient.SNI" + + $symbolPublishingToken = az account get-access-token --resource $tokenUri --query accessToken -o tsv + + echo "> 1.Symbol publishing token acquired." + + echo "Registering the request name ..." + $requestName = "${{parameters.symbolsArtifactName }}" + $requestNameRegistrationBody = "{'requestName': '$requestName'}" + Invoke-RestMethod -Method POST -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" -Body $requestNameRegistrationBody + + echo "> 2.Registration of request name succeeded." + + echo "Publishing the symbols ..." + $publishSymbolsBody = "{'publishToInternalServer': $publishToInternalServer, 'publishToPublicServer': $publishToPublicServer}" + echo "Publishing symbols request body: $publishSymbolsBody" + Invoke-RestMethod -Method POST -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests/$requestName" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" -Body $publishSymbolsBody + + echo "> 3.Request to publish symbols succeeded." + + echo "> 4.Checking the status of the request ..." + Invoke-RestMethod -Method GET -Uri "https://$symbolServer.trafficmanager.net/projects/$projectName/requests/$requestName" -Headers @{ Authorization = "Bearer $symbolPublishingToken" } -ContentType "application/json" + + echo "PublishingStatus"; echo "-----------------"; echo "0 NotRequested"; echo "1 Submitted"; echo "2 Processing"; echo "3 Completed" + echo "PublishingResult"; echo "-----------------"; echo "0 Pending"; echo "1 Succeeded"; echo "2 Failed"; echo "3 Cancelled" diff --git a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml index e104b99193..ba33cdf50d 100644 --- a/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-signing-pipeline.yml @@ -68,6 +68,45 @@ parameters: # parameters are shown up in ADO UI in a build queue time type: string default: 90 +# Manual Release Parameters +# Release stage runs ONLY when build is manually queued AND runRelease = true. +- name: runRelease + displayName: 'Run manual release stage' + type: boolean + default: false + +- name: publishDestination + displayName: 'Publish destination' + type: string + default: Internal + values: + - Internal + - Public + +- name: dryRun + displayName: 'Dry run (no publish)' + type: boolean + default: false + +- name: internalFeedSource + displayName: 'Internal feed source URL' + type: string + default: '' + +- name: publicNuGetSource + displayName: 'Public NuGet source URL' + type: string + default: 'https://api.nuget.org/v3/index.json' + +- name: product + displayName: 'Product code (MDS|MSS|AKV)' + type: string + default: 'MDS' + values: + - MDS + - MSS + - AKV + variables: - template: /eng/pipelines/libraries/variables.yml@self - name: packageFolderName @@ -174,3 +213,18 @@ extends: # artifact: $(packageFolderName) # patterns: '**/*.nupkg' # displayName: 'Download NuGet Package' + + # Manual Release Stage (templated) + - template: eng/pipelines/common/templates/stages/release-stage.yml@self + parameters: + runRelease: ${{ parameters.runRelease }} + publishDestination: ${{ parameters.publishDestination }} + dryRun: ${{ parameters.dryRun }} + approvalAliases: '[ADO.Net]\\SqlClient Admins' + internalFeedSource: ${{ parameters.internalFeedSource }} + publicNuGetSource: ${{ parameters.publicNuGetSource }} + publishSymbols: ${{ parameters.publishSymbols }} + isPreview: ${{ parameters.isPreview }} + packageFolderName: $(packageFolderName) + nugetPackageVersion: $(NugetPackageVersion) + product: ${{ parameters.product }}