Skip to content
Open
108 changes: 108 additions & 0 deletions eng/pipelines/common/templates/jobs/publish-packages-job.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#################################################################################
# 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:
# Approval aliases for manual validation before publishing packages
- name: approvalAliases
type: string
default: '[ADO.Net]\\SqlClient Admins'

# Where to publish the packages: 'Internal' or 'Public' feed
- name: publishDestination
type: string

# Boolean value to indicate whether to perform a dry run or actual publish
- name: dryRun
type: boolean

# Boolean value to indicate if the build is a preview release build
- name: isPreview
type: boolean

# Internal feed source URL for publishing packages to Azure DevOps Feed
- name: internalFeedSource
type: string

# Public NuGet source URL for publishing packages to public feed
- name: publicNuGetSource
type: string

# Boolean value to indicate whether to publish symbols
- name: publishSymbols
type: boolean

# Name of the folder containing the packages to be published
- name: packageFolderName
type: string

# NuGet package version to be published
- name: nugetPackageVersion
type: string

# Product name associated with the packages
- 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.

- job: PublishPackages
displayName: "Publish Packages"
variables:
- name: targetDownloadPath
value: "$(Pipeline.Workspace)/release/packages"
dependsOn: AwaitApproval
condition: succeeded()
pool:
vmImage: "ubuntu-latest"
steps:
- task: DownloadPipelineArtifact@2
displayName: "Download Signed Packages"
inputs:
buildType: current
artifactName: ${{ parameters.packageFolderName }}
targetPath: ${{ variables.targetDownloadPath }}
- script: |
echo "NuGet Package Version: ${{ parameters.nugetPackageVersion }}"
echo "Downloaded signed packages to: ${{ variables.targetDownloadPath }}"
displayName: "Echo NuGet Package Version"
# Push to Public NuGet Feed if publishDestination is 'Public'
- ${{ if eq(parameters.publishDestination, 'Public') }}:
- template: ../steps/publish-public-nuget-step.yml
parameters:
dryRun: ${{ parameters.dryRun }}
publicNuGetSource: ${{ parameters.publicNuGetSource }}
packagesGlob: ${{ variables.targetDownloadPath }}/*.nupkg
# Else Push to Internal Feed
- ${{ else }}:
- template: ../steps/publish-internal-feed-step.yml
parameters:
dryRun: ${{ parameters.dryRun }}
internalFeedSource: ${{ parameters.internalFeedSource }}
packagesGlob: ${{ variables.targetDownloadPath }}/*.nupkg
# Publish Symbols if publishSymbols is true and is not a dry run
- ${{ if and(parameters.publishSymbols, not(parameters.dryRun)) }}:
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition on line 103 uses not(parameters.dryRun) which will be evaluated at compile time. However, dryRun is a pipeline parameter that could be set at queue time. The condition should use runtime variable evaluation: not(eq(${{ parameters.dryRun }}, true)).

Suggested change
- ${{ if and(parameters.publishSymbols, not(parameters.dryRun)) }}:
- ${{ if and(eq(parameters.publishSymbols, true), not(eq(parameters.dryRun, true))) }}:

Copilot uses AI. Check for mistakes.
- 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 }}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a required parameter in the publish-symbols-step template. According to the template definition, "referenceType" is a required parameter but it's not being passed in this invocation. Add the referenceType parameter to match the expected interface.

Suggested change
product: ${{ parameters.product }}
product: ${{ parameters.product }}
referenceType: 'branch'

Copilot uses AI. Check for mistakes.
70 changes: 70 additions & 0 deletions eng/pipelines/common/templates/stages/release-stage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#################################################################################
# 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:
# Boolean value to indicate whether to run the release stage
- name: runRelease
type: boolean
default: false

# Where to publish the packages: 'Internal' or 'Public' feed
- name: publishDestination
type: string

# Boolean value to indicate whether to perform a dry run or actual publish
- name: dryRun
type: boolean
default: true

# Approval aliases for manual validation before publishing packages
- name: approvalAliases
type: string

# Internal feed source URL for publishing packages to Azure DevOps Feed
- name: internalFeedSource
type: string

# Public NuGet source URL for publishing packages to public feed
- name: publicNuGetSource
type: string

# Boolean value to indicate whether to publish symbols
- name: publishSymbols
type: boolean
default: false

# Boolean value to indicate if the build is a preview release build
- name: isPreview
type: boolean

# Product name associated with the packages
- name: product
type: string

# NuGet package version to be published
- name: nugetPackageVersion
type: string

# Name of the folder containing the packages to be published
- name: packageFolderName
type: string

stages:
- stage: Release ${{ parameters.product }}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stage name contains a space which may cause issues with Azure DevOps pipeline stage identification. Stage names should use underscores or be quoted. Change to "Release_${{ parameters.product }}" or wrap in quotes.

Suggested change
- stage: Release ${{ parameters.product }}
- stage: Release_${{ parameters.product }}

Copilot uses AI. Check for mistakes.
displayName: "Release (Manual)"
condition: and(succeeded(), eq(variables['Build.Reason'],'Manual'), eq(${{ parameters.runRelease }}, true))
jobs:
- template: ../jobs/publish-packages-job.yml
parameters:
approvalAliases: ${{ parameters.approvalAliases }}
publishDestination: ${{ parameters.publishDestination }}
dryRun: ${{ parameters.dryRun }}
isPreview: ${{ parameters.isPreview }}
internalFeedSource: ${{ parameters.internalFeedSource }}
publicNuGetSource: ${{ parameters.publicNuGetSource }}
publishSymbols: ${{ parameters.publishSymbols }}
packageFolderName: ${{ parameters.packageFolderName }}
nugetPackageVersion: ${{ parameters.nugetPackageVersion }}
product: ${{ parameters.product }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#################################################################################
# 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. #
#################################################################################

# Template to publish NuGet packages to an internal Azure DevOps feed

parameters:
# Boolean value to indicate whether to perform a dry run or actual publish
- name: dryRun
type: boolean
default: true

# Internal feed source URL for publishing packages to Azure DevOps Feed
- name: internalFeedSource
type: string

# Glob pattern to identify packages to be published
- name: packagesGlob
type: string

steps:
- script: |
pwsh ./tools/scripts/publishPackagesToAzDOFeed.ps1 `
-dryRun ${{ parameters.dryRun }} `
-internalFeedSource '${{ parameters.internalFeedSource }}' `
-packagesGlob '${{ parameters.packagesGlob }}'
displayName: "Publish to Internal Feed"
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#################################################################################
# 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. #
#################################################################################

# Template to publish NuGet packages to a public NuGet feed

parameters:
# Boolean value to indicate whether to perform a dry run or actual publish
- name: dryRun
type: boolean
default: true

# Public NuGet source URL for publishing packages to public feed
- name: publicNuGetSource
type: string

# Service connection name for authenticating to NuGet.org
- name: nugetServiceConnection
type: string
default: "ADO Nuget Org Connection"

# Glob pattern to identify packages to be published
- name: packagesGlob
type: string

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 }}"
Loading
Loading