dotnet-ado-publish

SKILL.md

dotnet-ado-publish

Publishing pipelines for .NET projects in Azure DevOps: NuGet package push to Azure Artifacts and nuget.org, container image build and push to Azure Container Registry (ACR) using Docker@2, artifact staging with PublishBuildArtifacts@1 and PublishPipelineArtifact@1, and pipeline artifacts for multi-stage release pipelines.

Version assumptions: DotNetCoreCLI@2 for pack/push operations. Docker@2 for container image builds. NuGetCommand@2 for NuGet push to external feeds. PublishPipelineArtifact@1 (preferred over PublishBuildArtifacts@1).

Scope

  • NuGet package push to Azure Artifacts and nuget.org
  • Container image build and push to ACR using Docker@2
  • Artifact staging with PublishPipelineArtifact@1
  • Pipeline artifacts for multi-stage release pipelines

Out of scope

  • Container image authoring (Dockerfile, base image selection) -- see [skill:dotnet-containers]
  • Native AOT MSBuild configuration -- see [skill:dotnet-native-aot]
  • CLI release pipelines -- see [skill:dotnet-cli-release-pipeline]
  • Starter CI templates -- see [skill:dotnet-add-ci]
  • GitHub Actions publishing -- see [skill:dotnet-gha-publish]
  • ADO-unique features (environments, service connections) -- see [skill:dotnet-ado-unique]

Cross-references: [skill:dotnet-containers] for container image authoring and SDK container properties, [skill:dotnet-native-aot] for AOT publish configuration in CI, [skill:dotnet-cli-release-pipeline] for CLI-specific release automation, [skill:dotnet-add-ci] for starter publish templates.


NuGet Push to Azure Artifacts

Push with DotNetCoreCLI@2


trigger:
  tags:
    include:
      - 'v*'

stages:
  - stage: Pack
    jobs:
      - job: PackJob
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: UseDotNet@2
            inputs:
              packageType: 'sdk'
              version: '8.0.x'

          - task: DotNetCoreCLI@2
            displayName: 'Pack'
            inputs:
              command: 'pack'
              packagesToPack: 'src/**/*.csproj'
              configuration: 'Release'
              outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'
              versioningScheme: 'byEnvVar'
              versionEnvVar: 'PACKAGE_VERSION'
            env:
              PACKAGE_VERSION: $(Build.SourceBranchName)

          - task: PublishPipelineArtifact@1
            displayName: 'Upload NuGet packages'
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/nupkgs'
              artifactName: 'nupkgs'

  - stage: PushToFeed
    dependsOn: Pack
    jobs:
      - job: PushJob
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - download: current
            artifact: nupkgs

          - task: NuGetAuthenticate@1
            displayName: 'Authenticate NuGet'

          - task: DotNetCoreCLI@2
            displayName: 'Push to Azure Artifacts'
            inputs:
              command: 'push'
              packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
              nuGetFeedType: 'internal'
              publishVstsFeed: 'MyProject/MyFeed'

```bash

### Version from Git Tag

Extract the version from the triggering Git tag using a script step. `Build.SourceBranch` is a runtime variable, so use
a script to parse it rather than compile-time template expressions:

```yaml

steps:
  - script: |
      set -euo pipefail
      if [[ "$(Build.SourceBranch)" == refs/tags/v* ]]; then
        VERSION="${BUILD_SOURCEBRANCH#refs/tags/v}"
      else
        VERSION="0.0.0-ci.$(Build.BuildId)"
      fi
      echo "##vso[task.setvariable variable=packageVersion]$VERSION"
    displayName: 'Extract version from tag'

  - task: DotNetCoreCLI@2
    displayName: 'Pack'
    inputs:
      command: 'pack'
      packagesToPack: 'src/**/*.csproj'
      configuration: 'Release'
      outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'
      arguments: '-p:Version=$(packageVersion)'

```csharp

---

## NuGet Push to nuget.org

### Push with `NuGetCommand@2`

For pushing to external NuGet feeds (nuget.org), use a service connection:

```yaml

- task: NuGetCommand@2
  displayName: 'Push to nuget.org'
  inputs:
    command: 'push'
    packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
    nuGetFeedType: 'external'
    publishFeedCredentials: 'NuGetOrgServiceConnection'

```bash

The service connection stores the nuget.org API key securely. Create it in Project Settings > Service Connections >
NuGet.

### Conditional Push (Stable vs Pre-Release)

```yaml

- task: NuGetCommand@2
  displayName: 'Push to nuget.org (stable only)'
  condition: and(succeeded(), not(contains(variables['packageVersion'], '-')))
  inputs:
    command: 'push'
    packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
    nuGetFeedType: 'external'
    publishFeedCredentials: 'NuGetOrgServiceConnection'

- task: DotNetCoreCLI@2
  displayName: 'Push to Azure Artifacts (all versions)'
  inputs:
    command: 'push'
    packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: 'MyProject/MyFeed'

```bash

Pre-release versions (containing `-` like `1.2.3-preview.1`) go only to Azure Artifacts; stable versions go to both
feeds.

### Skip Duplicate Packages

```yaml

- task: DotNetCoreCLI@2
  displayName: 'Push (skip duplicates)'
  inputs:
    command: 'push'
    packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
    nuGetFeedType: 'internal'
    publishVstsFeed: 'MyProject/MyFeed'
  continueOnError: true # Azure Artifacts returns 409 for duplicates

```text

Azure Artifacts returns HTTP 409 for duplicate package versions. Use `continueOnError: true` for idempotent pipeline
reruns, or configure the feed to allow overwriting pre-release versions in Feed Settings.

---

## Container Image Build and Push to ACR

### `Docker@2` Task

Build and push a container image to Azure Container Registry. See [skill:dotnet-containers] for Dockerfile authoring
guidance:

```yaml

stages:
  - stage: BuildContainer
    jobs:
      - job: DockerBuild
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: Docker@2
            displayName: 'Login to ACR'
            inputs:
              command: 'login'
              containerRegistry: 'MyACRServiceConnection'

          - task: Docker@2
            displayName: 'Build and push'
            inputs:
              command: 'buildAndPush'
              repository: 'myapp'
              containerRegistry: 'MyACRServiceConnection'
              dockerfile: 'src/MyApp/Dockerfile'
              buildContext: '.'
              tags: |
                $(Build.BuildId)
                latest

```text

### Tagging Strategy

```yaml

- task: Docker@2
  displayName: 'Build and push with semver tags'
  inputs:
    command: 'buildAndPush'
    repository: 'myapp'
    containerRegistry: 'MyACRServiceConnection'
    dockerfile: 'src/MyApp/Dockerfile'
    buildContext: '.'
    tags: |
      $(packageVersion)
      $(Build.SourceVersion)
      latest

```text

Use semantic version tags for release images and commit SHA tags for traceability. The `latest` tag should only be
applied to stable releases.

### SDK Container Publish (Dockerfile-Free)

Use .NET SDK container publish for projects without a Dockerfile. See [skill:dotnet-containers] for `PublishContainer`
MSBuild configuration:

```yaml

- task: Docker@2
  displayName: 'Login to ACR'
  inputs:
    command: 'login'
    containerRegistry: 'MyACRServiceConnection'

- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: '8.0.x'

- script: |
    dotnet publish src/MyApp/MyApp.csproj \
      -c Release \
      -p:PublishProfile=DefaultContainer \
      -p:ContainerRegistry=$(ACR_LOGIN_SERVER) \
      -p:ContainerRepository=myapp \
      -p:ContainerImageTags='"$(packageVersion);latest"'
  displayName: 'Publish container via SDK'
  env:
    ACR_LOGIN_SERVER: $(acrLoginServer)

```text

### Native AOT Container Publish

Publish a Native AOT binary as a container image. AOT configuration is owned by [skill:dotnet-native-aot]; this shows
the CI pipeline step only:

```yaml

- script: |
    dotnet publish src/MyApp/MyApp.csproj \
      -c Release \
      -r linux-x64 \
      -p:PublishAot=true \
      -p:PublishProfile=DefaultContainer \
      -p:ContainerRegistry=$(ACR_LOGIN_SERVER) \
      -p:ContainerRepository=myapp \
      -p:ContainerBaseImage=mcr.microsoft.com/dotnet/runtime-deps:8.0-noble-chiseled \
      -p:ContainerImageTags='"$(packageVersion)"'
  displayName: 'Publish AOT container'

```text

The `runtime-deps` base image is sufficient for AOT binaries since they include the runtime. See
[skill:dotnet-native-aot] for AOT MSBuild properties and [skill:dotnet-containers] for base image selection.

---

## Artifact Staging

### `PublishPipelineArtifact@1` (Recommended)

Pipeline artifacts are the modern replacement for build artifacts, offering faster upload/download and deduplication:

```yaml

steps:
  - task: DotNetCoreCLI@2
    displayName: 'Publish app'
    inputs:
      command: 'publish'
      projects: 'src/MyApp/MyApp.csproj'
      arguments: '-c Release -o $(Build.ArtifactStagingDirectory)/app'

  - task: PublishPipelineArtifact@1
    displayName: 'Upload app artifact'
    inputs:
      targetPath: '$(Build.ArtifactStagingDirectory)/app'
      artifactName: 'app'

  - task: PublishPipelineArtifact@1
    displayName: 'Upload NuGet packages'
    inputs:
      targetPath: '$(Build.ArtifactStagingDirectory)/nupkgs'
      artifactName: 'nupkgs'

```text

### `PublishBuildArtifacts@1` (Legacy)

Use only when integrating with classic release pipelines that require build artifacts:

```yaml

- task: PublishBuildArtifacts@1
  displayName: 'Upload build artifact (legacy)'
  inputs:
    pathToPublish: '$(Build.ArtifactStagingDirectory)/app'
    artifactName: 'app'
    publishLocation: 'Container'

```text

### Downloading Artifacts in Downstream Stages

```yaml

stages:
  - stage: Build
    jobs:
      - job: BuildJob
        steps:
          - script: dotnet publish -c Release -o $(Build.ArtifactStagingDirectory)/app
          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/app'
              artifactName: 'app'

  - stage: Deploy
    dependsOn: Build
    jobs:
      - deployment: DeployJob
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: app
                - script: echo "Deploying from $(Pipeline.Workspace)/app"

```text

The `download: current` keyword downloads artifacts from the current pipeline run. Use `download: pipelineName` for
artifacts from a different pipeline.

---

## Pipeline Artifacts for Release Pipelines

### Multi-Stage Release with Artifact Promotion

```yaml

trigger:
  tags:
    include:
      - 'v*'

stages:
  - stage: Build
    jobs:
      - job: BuildAndPack
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - task: UseDotNet@2
            inputs:
              packageType: 'sdk'
              version: '8.0.x'

          - task: DotNetCoreCLI@2
            displayName: 'Build'
            inputs:
              command: 'build'
              projects: 'MyApp.sln'
              arguments: '-c Release'

          - task: DotNetCoreCLI@2
            displayName: 'Publish'
            inputs:
              command: 'publish'
              projects: 'src/MyApp/MyApp.csproj'
              arguments: '-c Release -o $(Build.ArtifactStagingDirectory)/app'

          - task: DotNetCoreCLI@2
            displayName: 'Pack'
            inputs:
              command: 'pack'
              packagesToPack: 'src/MyLibrary/MyLibrary.csproj'
              configuration: 'Release'
              outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'

          - task: PublishPipelineArtifact@1
            displayName: 'Upload app'
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/app'
              artifactName: 'app'

          - task: PublishPipelineArtifact@1
            displayName: 'Upload packages'
            inputs:
              targetPath: '$(Build.ArtifactStagingDirectory)/nupkgs'
              artifactName: 'nupkgs'

  - stage: DeployStaging
    dependsOn: Build
    jobs:
      - deployment: DeployStaging
        environment: 'staging'
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: app
                - script: echo "Deploying to staging from $(Pipeline.Workspace)/app"

  - stage: PublishPackages
    dependsOn: DeployStaging
    jobs:
      - job: PushPackages
        pool:
          vmImage: 'ubuntu-latest'
        steps:
          - download: current
            artifact: nupkgs

          - task: NuGetAuthenticate@1

          - task: NuGetCommand@2
            displayName: 'Push to nuget.org'
            inputs:
              command: 'push'
              packagesToPush: '$(Pipeline.Workspace)/nupkgs/*.nupkg'
              nuGetFeedType: 'external'
              publishFeedCredentials: 'NuGetOrgServiceConnection'

  - stage: DeployProduction
    dependsOn: DeployStaging
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:
      - deployment: DeployProduction
        environment: 'production'
        pool:
          vmImage: 'ubuntu-latest'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: current
                  artifact: app
                - script: echo "Deploying to production from $(Pipeline.Workspace)/app"

```text

### Cross-Pipeline Artifact Consumption

Consume artifacts from a different pipeline (e.g., a shared build pipeline):

```yaml

resources:
  pipelines:
    - pipeline: buildPipeline
      source: 'MyApp-Build'
      trigger:
        branches:
          include:
            - main

stages:
  - stage: Deploy
    jobs:
      - deployment: DeployFromBuild
        environment: 'staging'
        strategy:
          runOnce:
            deploy:
              steps:
                - download: buildPipeline
                  artifact: app
                - script: echo "Deploying from $(Pipeline.Workspace)/buildPipeline/app"

```text

---

## Agent Gotchas

1. **Use `PublishPipelineArtifact@1` over `PublishBuildArtifacts@1`** -- pipeline artifacts are faster, support
   deduplication, and work with multi-stage YAML pipelines; build artifacts are legacy and required only for classic
   release pipelines.
2. **Azure Artifacts returns 409 for duplicate package versions** -- use `continueOnError: true` for idempotent reruns,
   or handle duplicates in feed settings by allowing pre-release version overwrites.
3. **`NuGetCommand@2` with `external` feed type requires a service connection** -- do not hardcode API keys in pipeline
   YAML; create a NuGet service connection in Project Settings that stores the key securely.
4. **SDK container publish requires Docker on the agent** -- `dotnet publish` with `PublishProfile=DefaultContainer`
   needs Docker; hosted `ubuntu-latest` agents include Docker, but self-hosted agents may not.
5. **AOT publish requires matching RID** -- `dotnet publish -r linux-x64` must match the agent OS; do not use
   `-r win-x64` on a Linux agent.
6. **`download: current` uses `$(Pipeline.Workspace)` not `$(Build.ArtifactStagingDirectory)`** -- artifacts downloaded
   in deployment jobs are at `$(Pipeline.Workspace)/artifactName`, not the staging directory.
7. **Never hardcode registry credentials in pipeline YAML** -- use Docker service connections for ACR/DockerHub
   authentication; service connections store credentials securely and rotate independently.
8. **Tag triggers require explicit `tags.include` in the trigger section** -- tags are not included by default CI
   triggers; add `tags: include: ['v*']` to trigger on version tags.
Weekly Installs
1
First Seen
11 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1