dotnet-ado-build-test
.NET build and test pipeline patterns for Azure DevOps: DotNetCoreCLI@2 task for build, test, and pack operations,
NuGet restore with Azure Artifacts feeds using NuGetAuthenticate@1, test result publishing with PublishTestResults@2
for TRX and JUnit formats, code coverage with PublishCodeCoverageResults@2 for Cobertura and JaCoCo formats, and
multi-TFM matrix strategy across net8.0 and net9.0.
Version assumptions: DotNetCoreCLI@2 task (current). UseDotNet@2 for SDK installation. NuGetAuthenticate@1 for
Azure Artifacts. PublishTestResults@2 and PublishCodeCoverageResults@2 for reporting.
Scope
- DotNetCoreCLI@2 task for build, test, pack, and custom commands
- NuGet restore with Azure Artifacts feeds (NuGetAuthenticate@1)
- Test result publishing with PublishTestResults@2 (TRX, JUnit)
- Code coverage with PublishCodeCoverageResults@2 (Cobertura)
- Multi-TFM matrix strategy across TFMs and operating systems
Out of scope
- Starter CI templates -- see [skill:dotnet-add-ci]
- Test architecture and strategy -- see [skill:dotnet-testing-strategy]
- Benchmark regression detection in CI -- see [skill:dotnet-ci-benchmarking]
- Publishing and deployment -- see [skill:dotnet-ado-publish] and [skill:dotnet-ado-unique]
- GitHub Actions build/test workflows -- see [skill:dotnet-gha-build-test]
Cross-references: [skill:dotnet-add-ci] for starter build/test templates, [skill:dotnet-testing-strategy] for test
architecture guidance, [skill:dotnet-ci-benchmarking] for benchmark CI integration.
DotNetCoreCLI@2 Task
Build
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: 'MyApp.sln'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: 'MyApp.sln'
arguments: '-c Release --no-restore'
```bash
### Test
```yaml
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >-
-c Release --logger "trx;LogFileName=test-results.trx" --results-directory
$(Build.ArtifactStagingDirectory)/test-results
```csharp
### Pack
```yaml
- task: DotNetCoreCLI@2
displayName: 'Pack NuGet packages'
inputs:
command: 'pack'
packagesToPack: 'src/**/*.csproj'
configuration: 'Release'
outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'
nobuild: true
```csharp
### Custom Command
For commands not directly supported by the task (e.g., `dotnet tool install`):
```yaml
- task: DotNetCoreCLI@2
displayName: 'Install dotnet tools'
inputs:
command: 'custom'
custom: 'tool'
arguments: 'restore'
```bash
### Multi-Version SDK Install
Install multiple SDK versions for multi-TFM builds:
```yaml
- task: UseDotNet@2
displayName: 'Install .NET 8'
inputs:
packageType: 'sdk'
version: '8.0.x'
- task: UseDotNet@2
displayName: 'Install .NET 9'
inputs:
packageType: 'sdk'
version: '9.0.x'
```text
Each `UseDotNet@2` invocation adds the SDK version to PATH. The last installed version becomes the default, but all
versions are available via `--framework` targeting.
---
## NuGet Restore with Azure Artifacts Feeds
### `NuGetAuthenticate@1` for Feed Authentication
```yaml
steps:
- task: NuGetAuthenticate@1
displayName: 'Authenticate NuGet feeds'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: 'MyApp.sln'
feedsToUse: 'config'
nugetConfigPath: 'nuget.config'
```bash
The `NuGetAuthenticate@1` task configures credentials for all Azure Artifacts feeds referenced in `nuget.config`. No
explicit PAT or API key is needed -- the task uses the pipeline's identity.
### Selecting Feeds Directly
For simple setups without a `nuget.config`, select feeds directly in the restore task:
```yaml
- task: DotNetCoreCLI@2
displayName: 'Restore with Azure Artifacts'
inputs:
command: 'restore'
projects: 'MyApp.sln'
feedsToUse: 'select'
vstsFeed: 'MyProject/MyFeed'
includeNuGetOrg: true
```text
### Upstream Sources
Azure Artifacts feeds can proxy nuget.org as an upstream source. When configured, a single feed reference provides
access to both private packages and public NuGet packages:
```xml
<!-- nuget.config with Azure Artifacts upstream -->
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<add key="MyFeed" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />
</packageSources>
</configuration>
```json
With upstream sources enabled on the feed, nuget.org packages are cached in the Azure Artifacts feed, providing a single
authenticated source for all packages.
### Cross-Organization Feed Access
For feeds in different Azure DevOps organizations, use a service connection:
```yaml
- task: NuGetAuthenticate@1
displayName: 'Authenticate external feed'
inputs:
nuGetServiceConnections: 'ExternalOrgFeedConnection'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: 'MyApp.sln'
feedsToUse: 'config'
nugetConfigPath: 'nuget.config'
```bash
---
## Test Result Publishing
### `PublishTestResults@2` with TRX Format
```yaml
- task: DotNetCoreCLI@2
displayName: 'Run tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >-
-c Release --logger "trx;LogFileName=results.trx" --results-directory $(Common.TestResultsDirectory)
continueOnError: true
- task: PublishTestResults@2
displayName: 'Publish test results'
condition: always()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '$(Common.TestResultsDirectory)/**/*.trx'
mergeTestResults: true
testRunTitle: '.NET Unit Tests'
```text
**Key decisions:**
- `continueOnError: true` on the test task ensures the publish step always runs, even on test failures
- `condition: always()` on the publish task runs regardless of previous step outcome
- `mergeTestResults: true` combines results from multiple test projects into a single test run
- `testRunTitle` provides a descriptive name in the Azure DevOps Test tab
### JUnit Format
Some third-party test frameworks output JUnit XML. Use the `JUnit` format:
```yaml
- task: PublishTestResults@2
displayName: 'Publish JUnit results'
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/junit-results.xml'
mergeTestResults: true
```xml
### Test Results with Attachments
Attach screenshots or logs to test results for debugging failed tests:
```yaml
- task: DotNetCoreCLI@2
displayName: 'Run tests with attachments'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >-
-c Release --logger "trx;LogFileName=results.trx" --results-directory $(Common.TestResultsDirectory)
--collect:"XPlat Code Coverage"
continueOnError: true
- task: PublishTestResults@2
displayName: 'Publish test results'
condition: always()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '$(Common.TestResultsDirectory)/**/*.trx'
mergeTestResults: true
testRunTitle: '.NET Tests'
publishRunAttachments: true
```text
---
## Code Coverage
### `PublishCodeCoverageResults@2` with Cobertura
```yaml
- task: DotNetCoreCLI@2
displayName: 'Test with coverage'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >-
-c Release --collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)/coverage
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/coverage/**/coverage.cobertura.xml'
```xml
The `PublishCodeCoverageResults@2` task (v2) auto-generates HTML coverage reports in the Azure DevOps Build Summary tab
without requiring `reportgenerator`.
### Coverage with ReportGenerator for Detailed Reports
For custom coverage reports beyond the built-in rendering:
```yaml
- task: DotNetCoreCLI@2
displayName: 'Test with coverage'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: >-
-c Release --collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)/coverage
- script: |
set -euo pipefail
dotnet tool install -g dotnet-reportgenerator-globaltool
reportgenerator \
-reports:$(Agent.TempDirectory)/coverage/**/coverage.cobertura.xml \
-targetdir:$(Build.ArtifactStagingDirectory)/coverage-report \
-reporttypes:HtmlInline_AzurePipelines\;Cobertura
displayName: 'Generate coverage report'
- task: PublishCodeCoverageResults@2
displayName: 'Publish coverage'
inputs:
summaryFileLocation: '$(Build.ArtifactStagingDirectory)/coverage-report/Cobertura.xml'
- task: PublishPipelineArtifact@1
displayName: 'Upload coverage report'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/coverage-report'
artifactName: 'coverage-report'
```text
### Coverage Thresholds
Enforce minimum coverage by parsing the Cobertura XML in a script step:
```yaml
- script: |
set -euo pipefail
COVERAGE_FILE=$(find $(Agent.TempDirectory)/coverage -name 'coverage.cobertura.xml' | head -1)
COVERAGE=$(python3 -c "
import xml.etree.ElementTree as ET
tree = ET.parse('$COVERAGE_FILE')
print(float(tree.getroot().attrib['line-rate']) * 100)
")
echo "Line coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "##vso[task.logissue type=error]Coverage ${COVERAGE}% is below 80% threshold"
exit 1
fi
displayName: 'Enforce coverage threshold'
```text
---
## Multi-TFM Matrix Strategy
## Detailed Examples
See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.
1---2name: dotnet-ado-build-test3category: operations4subcategory: ci-cd5description: Configures .NET build/test in Azure DevOps. DotNetCoreCLI task, Artifacts, test results.6license: MIT7targets: ['*']8tags: [testing, dotnet, skill]9version: '0.0.1'10author: 'dotnet-agent-harness'11invocable: true12claudecode:13 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']14codexcli:15 short-description: '.NET skill guidance for testing tasks'16opencode:17 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']18copilot: {}19geminicli: {}20antigravity: {}21---2223# dotnet-ado-build-test2425.NET build and test pipeline patterns for Azure DevOps: `DotNetCoreCLI@2` task for build, test, and pack operations,26NuGet restore with Azure Artifacts feeds using `NuGetAuthenticate@1`, test result publishing with `PublishTestResults@2`27for TRX and JUnit formats, code coverage with `PublishCodeCoverageResults@2` for Cobertura and JaCoCo formats, and28multi-TFM matrix strategy across net8.0 and net9.0.2930**Version assumptions:** `DotNetCoreCLI@2` task (current). `UseDotNet@2` for SDK installation. `NuGetAuthenticate@1` for31Azure Artifacts. `PublishTestResults@2` and `PublishCodeCoverageResults@2` for reporting.3233## Scope3435- DotNetCoreCLI@2 task for build, test, pack, and custom commands36- NuGet restore with Azure Artifacts feeds (NuGetAuthenticate@1)37- Test result publishing with PublishTestResults@2 (TRX, JUnit)38- Code coverage with PublishCodeCoverageResults@2 (Cobertura)39- Multi-TFM matrix strategy across TFMs and operating systems4041## Out of scope4243- Starter CI templates -- see [skill:dotnet-add-ci]44- Test architecture and strategy -- see [skill:dotnet-testing-strategy]45- Benchmark regression detection in CI -- see [skill:dotnet-ci-benchmarking]46- Publishing and deployment -- see [skill:dotnet-ado-publish] and [skill:dotnet-ado-unique]47- GitHub Actions build/test workflows -- see [skill:dotnet-gha-build-test]4849Cross-references: [skill:dotnet-add-ci] for starter build/test templates, [skill:dotnet-testing-strategy] for test50architecture guidance, [skill:dotnet-ci-benchmarking] for benchmark CI integration.5152---5354## `DotNetCoreCLI@2` Task5556### Build5758````yaml5960steps:61 - task: UseDotNet@262 displayName: 'Install .NET SDK'63 inputs:64 packageType: 'sdk'65 version: '8.0.x'6667 - task: DotNetCoreCLI@268 displayName: 'Restore'69 inputs:70 command: 'restore'71 projects: 'MyApp.sln'7273 - task: DotNetCoreCLI@274 displayName: 'Build'75 inputs:76 command: 'build'77 projects: 'MyApp.sln'78 arguments: '-c Release --no-restore'7980```bash8182### Test8384```yaml8586- task: DotNetCoreCLI@287 displayName: 'Run tests'88 inputs:89 command: 'test'90 projects: '**/*Tests.csproj'91 arguments: >-92 -c Release --logger "trx;LogFileName=test-results.trx" --results-directory93 $(Build.ArtifactStagingDirectory)/test-results9495```csharp9697### Pack9899```yaml100101- task: DotNetCoreCLI@2102 displayName: 'Pack NuGet packages'103 inputs:104 command: 'pack'105 packagesToPack: 'src/**/*.csproj'106 configuration: 'Release'107 outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'108 nobuild: true109110```csharp111112### Custom Command113114For commands not directly supported by the task (e.g., `dotnet tool install`):115116```yaml117118- task: DotNetCoreCLI@2119 displayName: 'Install dotnet tools'120 inputs:121 command: 'custom'122 custom: 'tool'123 arguments: 'restore'124125```bash126127### Multi-Version SDK Install128129Install multiple SDK versions for multi-TFM builds:130131```yaml132133- task: UseDotNet@2134 displayName: 'Install .NET 8'135 inputs:136 packageType: 'sdk'137 version: '8.0.x'138139- task: UseDotNet@2140 displayName: 'Install .NET 9'141 inputs:142 packageType: 'sdk'143 version: '9.0.x'144145```text146147Each `UseDotNet@2` invocation adds the SDK version to PATH. The last installed version becomes the default, but all148versions are available via `--framework` targeting.149150---151152## NuGet Restore with Azure Artifacts Feeds153154### `NuGetAuthenticate@1` for Feed Authentication155156```yaml157158steps:159 - task: NuGetAuthenticate@1160 displayName: 'Authenticate NuGet feeds'161162 - task: DotNetCoreCLI@2163 displayName: 'Restore'164 inputs:165 command: 'restore'166 projects: 'MyApp.sln'167 feedsToUse: 'config'168 nugetConfigPath: 'nuget.config'169170```bash171172The `NuGetAuthenticate@1` task configures credentials for all Azure Artifacts feeds referenced in `nuget.config`. No173explicit PAT or API key is needed -- the task uses the pipeline's identity.174175### Selecting Feeds Directly176177For simple setups without a `nuget.config`, select feeds directly in the restore task:178179```yaml180181- task: DotNetCoreCLI@2182 displayName: 'Restore with Azure Artifacts'183 inputs:184 command: 'restore'185 projects: 'MyApp.sln'186 feedsToUse: 'select'187 vstsFeed: 'MyProject/MyFeed'188 includeNuGetOrg: true189190```text191192### Upstream Sources193194Azure Artifacts feeds can proxy nuget.org as an upstream source. When configured, a single feed reference provides195access to both private packages and public NuGet packages:196197```xml198199<!-- nuget.config with Azure Artifacts upstream -->200<?xml version="1.0" encoding="utf-8"?>201<configuration>202 <packageSources>203 <clear />204 <add key="MyFeed" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />205 </packageSources>206</configuration>207208```json209210With upstream sources enabled on the feed, nuget.org packages are cached in the Azure Artifacts feed, providing a single211authenticated source for all packages.212213### Cross-Organization Feed Access214215For feeds in different Azure DevOps organizations, use a service connection:216217```yaml218219- task: NuGetAuthenticate@1220 displayName: 'Authenticate external feed'221 inputs:222 nuGetServiceConnections: 'ExternalOrgFeedConnection'223224- task: DotNetCoreCLI@2225 displayName: 'Restore'226 inputs:227 command: 'restore'228 projects: 'MyApp.sln'229 feedsToUse: 'config'230 nugetConfigPath: 'nuget.config'231232```bash233234---235236## Test Result Publishing237238### `PublishTestResults@2` with TRX Format239240```yaml241242- task: DotNetCoreCLI@2243 displayName: 'Run tests'244 inputs:245 command: 'test'246 projects: '**/*Tests.csproj'247 arguments: >-248 -c Release --logger "trx;LogFileName=results.trx" --results-directory $(Common.TestResultsDirectory)249 continueOnError: true250251- task: PublishTestResults@2252 displayName: 'Publish test results'253 condition: always()254 inputs:255 testResultsFormat: 'VSTest'256 testResultsFiles: '$(Common.TestResultsDirectory)/**/*.trx'257 mergeTestResults: true258 testRunTitle: '.NET Unit Tests'259260```text261262**Key decisions:**263264- `continueOnError: true` on the test task ensures the publish step always runs, even on test failures265- `condition: always()` on the publish task runs regardless of previous step outcome266- `mergeTestResults: true` combines results from multiple test projects into a single test run267- `testRunTitle` provides a descriptive name in the Azure DevOps Test tab268269### JUnit Format270271Some third-party test frameworks output JUnit XML. Use the `JUnit` format:272273```yaml274275- task: PublishTestResults@2276 displayName: 'Publish JUnit results'277 condition: always()278 inputs:279 testResultsFormat: 'JUnit'280 testResultsFiles: '**/junit-results.xml'281 mergeTestResults: true282283```xml284285### Test Results with Attachments286287Attach screenshots or logs to test results for debugging failed tests:288289```yaml290291- task: DotNetCoreCLI@2292 displayName: 'Run tests with attachments'293 inputs:294 command: 'test'295 projects: '**/*Tests.csproj'296 arguments: >-297 -c Release --logger "trx;LogFileName=results.trx" --results-directory $(Common.TestResultsDirectory)298 --collect:"XPlat Code Coverage"299 continueOnError: true300301- task: PublishTestResults@2302 displayName: 'Publish test results'303 condition: always()304 inputs:305 testResultsFormat: 'VSTest'306 testResultsFiles: '$(Common.TestResultsDirectory)/**/*.trx'307 mergeTestResults: true308 testRunTitle: '.NET Tests'309 publishRunAttachments: true310311```text312313---314315## Code Coverage316317### `PublishCodeCoverageResults@2` with Cobertura318319```yaml320321- task: DotNetCoreCLI@2322 displayName: 'Test with coverage'323 inputs:324 command: 'test'325 projects: '**/*Tests.csproj'326 arguments: >-327 -c Release --collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)/coverage328329- task: PublishCodeCoverageResults@2330 displayName: 'Publish code coverage'331 inputs:332 summaryFileLocation: '$(Agent.TempDirectory)/coverage/**/coverage.cobertura.xml'333334```xml335336The `PublishCodeCoverageResults@2` task (v2) auto-generates HTML coverage reports in the Azure DevOps Build Summary tab337without requiring `reportgenerator`.338339### Coverage with ReportGenerator for Detailed Reports340341For custom coverage reports beyond the built-in rendering:342343```yaml344345- task: DotNetCoreCLI@2346 displayName: 'Test with coverage'347 inputs:348 command: 'test'349 projects: '**/*Tests.csproj'350 arguments: >-351 -c Release --collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)/coverage352353- script: |354 set -euo pipefail355 dotnet tool install -g dotnet-reportgenerator-globaltool356 reportgenerator \357 -reports:$(Agent.TempDirectory)/coverage/**/coverage.cobertura.xml \358 -targetdir:$(Build.ArtifactStagingDirectory)/coverage-report \359 -reporttypes:HtmlInline_AzurePipelines\;Cobertura360 displayName: 'Generate coverage report'361362- task: PublishCodeCoverageResults@2363 displayName: 'Publish coverage'364 inputs:365 summaryFileLocation: '$(Build.ArtifactStagingDirectory)/coverage-report/Cobertura.xml'366367- task: PublishPipelineArtifact@1368 displayName: 'Upload coverage report'369 inputs:370 targetPath: '$(Build.ArtifactStagingDirectory)/coverage-report'371 artifactName: 'coverage-report'372373```text374375### Coverage Thresholds376377Enforce minimum coverage by parsing the Cobertura XML in a script step:378379```yaml380381- script: |382 set -euo pipefail383 COVERAGE_FILE=$(find $(Agent.TempDirectory)/coverage -name 'coverage.cobertura.xml' | head -1)384 COVERAGE=$(python3 -c "385 import xml.etree.ElementTree as ET386 tree = ET.parse('$COVERAGE_FILE')387 print(float(tree.getroot().attrib['line-rate']) * 100)388 ")389 echo "Line coverage: ${COVERAGE}%"390 if (( $(echo "$COVERAGE < 80" | bc -l) )); then391 echo "##vso[task.logissue type=error]Coverage ${COVERAGE}% is below 80% threshold"392 exit 1393 fi394 displayName: 'Enforce coverage threshold'395396```text397398---399400## Multi-TFM Matrix Strategy401402## Detailed Examples403404See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.