mcpField
D
dotnet-ado-build-test

dotnet-ado-build-test

github.com/wshaddix云与 DevOps8669 次安装35 周前更新
代码仓库

关于 dotnet-ado-build-test

dotnet-ado-build-test 为在 Azure DevOps 中配置 .NET 构建与测试流水线提供模式,面向维护 CI 的开发者。内容涵盖 NuGet 源认证、测试与覆盖率报告,以及 net8.0 和 net9.0 的多目标框架矩阵。

主要功能

  1. 01配置 .NET 构建、测试和打包
  2. 02认证 Azure Artifacts NuGet 源
  3. 03发布 TRX 和 JUnit 测试结果
  4. 04发布 Cobertura 和 JaCoCo 覆盖率
  5. 05设置多 TFM 矩阵

适用场景

  1. 01为 Azure DevOps 项目编写构建测试步骤
  2. 02从 Azure Artifacts 还原 NuGet 包
  3. 03在流水线中发布测试及覆盖率报告
  4. 04跨 net8.0 和 net9.0 执行测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-ado-build-test

安装后在 Claude Code 中输入/dotnet-ado-build-test确认已加载。

详细介绍

dotnet-ado-build-test

Azure DevOps 的 .NET 构建和测试流水线模式:使用 DotNetCoreCLI@2 任务执行构建、测试和打包操作;使用 NuGetAuthenticate@1 通过 Azure Artifacts 源还原 NuGet 包;使用 PublishTestResults@2 发布 TRX 和 JUnit 格式的测试结果;使用 PublishCodeCoverageResults@2 发布 Cobertura 和 JaCoCo 格式的代码覆盖率;以及在 net8.0 和 net9.0 之间采用多 TFM 矩阵策略。

**版本假设:**使用 DotNetCoreCLI@2 任务(当前版本)。使用 UseDotNet@2 安装 SDK。使用 NuGetAuthenticate@1 访问 Azure Artifacts。使用 PublishTestResults@2 和 PublishCodeCoverageResults@2 生成报告。

**范围边界:**此技能负责 Azure DevOps 的 .NET 构建和测试流水线配置。入门 CI 模板(基本构建/测试/打包)由 [skill:dotnet-add-ci] 负责。可组合的流水线模式(模板、多阶段、触发器)见 [skill:dotnet-ado-patterns]。测试策略指南(测试内容、测试架构、质量门禁)由 [skill:dotnet-testing-strategy] 负责。基准测试 CI 工作流由 [skill:dotnet-ci-benchmarking] 负责。

**范围之外:**入门 CI 模板——请参阅 [skill:dotnet-add-ci]。测试架构和策略——请参阅 [skill:dotnet-testing-strategy]。CI 中的基准回归检测——请参阅 [skill:dotnet-ci-benchmarking]。发布和部署——请参阅 [skill:dotnet-ado-publish] 和 [skill:dotnet-ado-unique]。GitHub Actions 构建/测试工作流——请参阅 [skill:dotnet-gha-build-test]。

交叉引用:[skill:dotnet-add-ci] 提供入门构建/测试模板,[skill:dotnet-testing-strategy] 提供测试架构指南,[skill:dotnet-ci-benchmarking] 提供基准测试 CI 集成。


DotNetCoreCLI@2 任务

构建
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'
测试
- 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
打包
- task: DotNetCoreCLI@2
  displayName: 'Pack NuGet packages'
  inputs:
    command: 'pack'
    packagesToPack: 'src/**/*.csproj'
    configuration: 'Release'
    outputDir: '$(Build.ArtifactStagingDirectory)/nupkgs'
    nobuild: true
自定义命令

对于任务不直接支持的命令(例如 dotnet tool install):

- task: DotNetCoreCLI@2
  displayName: 'Install dotnet tools'
  inputs:
    command: 'custom'
    custom: 'tool'
    arguments: 'restore'
安装多个 SDK 版本

为多 TFM 构建安装多个 SDK 版本:

- 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'

每次调用 UseDotNet@2 都会将 SDK 版本添加到 PATH。最后安装的版本会成为默认版本,但通过 --framework 进行目标框架指定时,所有版本均可用。


使用 Azure Artifacts 源还原 NuGet 包

使用 NuGetAuthenticate@1 进行源身份验证
steps:
  - task: NuGetAuthenticate@1
    displayName: 'Authenticate NuGet feeds'

  - task: DotNetCoreCLI@2
    displayName: 'Restore'
    inputs:
      command: 'restore'
      projects: 'MyApp.sln'
      feedsToUse: 'config'
      nugetConfigPath: 'nuget.config'

NuGetAuthenticate@1 任务会为 nuget.config 中引用的所有 Azure Artifacts 源配置凭据。无需提供显式 PAT 或 API 密钥——该任务会使用流水线的身份。

直接选择源

对于不使用 nuget.config 的简单配置,可直接在还原任务中选择源:

- task: DotNetCoreCLI@2
  displayName: 'Restore with Azure Artifacts'
  inputs:
    command: 'restore'
    projects: 'MyApp.sln'
    feedsToUse: 'select'
    vstsFeed: 'MyProject/MyFeed'
    includeNuGetOrg: true
上游源

Azure Artifacts 源可以将 nuget.org 代理为上游源。配置完成后,只需引用一个源即可访问私有包和公共 NuGet 包:


<?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>

启用源上的上游源后,nuget.org 包会缓存到 Azure Artifacts 源中,从而为所有包提供单一的已通过身份验证的源。

跨组织访问源

对于不同 Azure DevOps 组织中的源,请使用服务连接:

- 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'

发布测试结果

使用 TRX 格式的 PublishTestResults@2
- 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'

关键决策:

  • 在测试任务上设置 continueOnError: true,可确保即使测试失败,发布步骤也始终运行
  • 在发布任务上设置 condition: always(),可使其不受前序步骤结果影响而运行
  • mergeTestResults: true 会将多个测试项目的结果合并为一次测试运行
  • testRunTitle 可在 Azure DevOps 的 Test 选项卡中提供描述性名称
JUnit 格式

某些第三方测试框架会输出 JUnit XML。请使用 JUnit 格式:

- task: PublishTestResults@2
  displayName: 'Publish JUnit results'
  condition: always()
  inputs:
    testResultsFormat: 'JUnit'
    testResultsFiles: '**/junit-results.xml'
    mergeTestResults: true
附带附件的测试结果

将屏幕截图或日志附加到测试结果,以便调试失败的测试:

- 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

代码覆盖率

PublishCodeCoverageResults@2 与 Cobertura
- 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'

PublishCodeCoverageResults@2 任务(v2)会在 Azure DevOps 的“生成摘要”选项卡中自动生成 HTML 覆盖率报告,无需使用 reportgenerator。

使用 ReportGenerator 生成详细覆盖率报告

如需生成超出内置渲染功能的自定义覆盖率报告:

- 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'
覆盖率阈值

通过在脚本步骤中解析 Cobertura XML 来强制执行最低覆盖率要求:

- 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'

多 TFM 矩阵策略

跨 TFM 和操作系统的矩阵构建
jobs:
  - job: Test
    strategy:
      matrix:
        Linux_net80:
          vmImage: 'ubuntu-latest'
          tfm: 'net8.0'
          dotnetVersion: '8.0.x'
        Linux_net90:
          vmImage: 'ubuntu-latest'
          tfm: 'net9.0'
          dotnetVersion: '9.0.x'
        Windows_net80:
          vmImage: 'windows-latest'
          tfm: 'net8.0'
          dotnetVersion: '8.0.x'
        Windows_net90:
          vmImage: 'windows-latest'
          tfm: 'net9.0'
          dotnetVersion: '9.0.x'
    pool:
      vmImage: $(vmImage)
    steps:
      - task: UseDotNet@2
        displayName: 'Install .NET $(dotnetVersion)'
        inputs:
          packageType: 'sdk'
          version: $(dotnetVersion)

      - task: DotNetCoreCLI@2
        displayName: 'Test $(tfm) on $(vmImage)'
        inputs:
          command: 'test'
          projects: '**/*Tests.csproj'
          arguments: >-
            -c Release
            --framework $(tfm)
            --logger "trx;LogFileName=$(tfm)-results.trx"
            --results-directory $(Common.TestResultsDirectory)
        continueOnError: true

      - task: PublishTestResults@2
        displayName: 'Publish $(tfm) results'
        condition: always()
        inputs:
          testResultsFormat: 'VSTest'
          testResultsFiles: '$(Common.TestResultsDirectory)/**/*.trx'
          testRunTitle: '$(tfm) on $(vmImage)'
在单个作业中为多 TFM 安装多个 SDK

在一个作业中运行所有 TFM(而非使用矩阵)时,请安装所有必需的 SDK:

steps:
  - 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'

  - task: DotNetCoreCLI@2
    displayName: 'Test all TFMs'
    inputs:
      command: 'test'
      projects: '**/*Tests.csproj'
      arguments: '-c Release'

如果未安装匹配的 SDK,dotnet test 就无法针对该 TFM 进行构建,并会因 NETSDK1045 而失败。

基于模板的可复用矩阵
# templates/jobs/matrix-test.yml
parameters:
  - name: configurations
    type: object
    default:
      - tfm: 'net8.0'
        dotnetVersion: '8.0.x'
      - tfm: 'net9.0'
        dotnetVersion: '9.0.x'

jobs:
  - ${{ each config in parameters.configurations }}:
    - job: Test_${{ replace(config.tfm, '.', '_') }}
      displayName: 'Test ${{ config.tfm }}'
      pool:
        vmImage: 'ubuntu-latest'
      steps:
        - task: UseDotNet@2
          inputs:
            packageType: 'sdk'
            version: ${{ config.dotnetVersion }}

        - task: DotNetCoreCLI@2
          displayName: 'Test ${{ config.tfm }}'
          inputs:
            command: 'test'
            projects: '**/*Tests.csproj'
            arguments: '-c Release --framework ${{ config.tfm }}'

Agent 注意事项

  1. 在多行 script: 步骤中使用 set -euo pipefail —— Linux 上的 ADO script: 任务默认使用 set -e,但不会设置 pipefail 或 nounset;如果不设置 pipefail,管道命令中的失败会被悄然忽略。
  2. 在测试任务上使用 continueOnError: true,不要用于结果发布任务 —— 测试任务不能在结果发布前导致流水线失败,但结果发布任务应反映实际测试结果。
  3. 为多 TFM 构建安装所有必需的 SDK 版本 —— 如果 dotnet test 没有匹配的 SDK,就会产生 NETSDK1045;请为每个必需版本添加一个 UseDotNet@2 步骤。
  4. NuGetAuthenticate@1 必须置于还原步骤之前 —— 身份验证令牌会在任务执行时注入 Agent 的 NuGet 配置;如果在身份验证前执行还原,就会因 401 而失败。
  5. 复杂的源配置应使用 feedsToUse: 'config' 和 nuget.config —— feedsToUse: 'select' 仅支持一个 Azure Artifacts 源;多源场景需要 nuget.config 文件。
  6. 收集覆盖率需要使用 --collect:"XPlat Code Coverage" —— 默认的 dotnet test 不会生成覆盖率文件;XPlat Code Coverage 收集器内置于 .NET SDK 中。
  7. PublishCodeCoverageResults@2 需要 Cobertura XML —— 将 TRX 或其他格式传给覆盖率发布任务不会产生输出;请确保收集器输出 Cobertura 格式。
  8. ADO 矩阵语法与 GHA 不同 —— ADO 使用带键值对的命名矩阵条目,而非数组;每个条目都必须定义作业中使用的所有变量名。
  9. 切勿在流水线 YAML 中硬编码凭据 —— 请使用关联到 Azure Key Vault 的变量组或流水线级机密变量;硬编码的机密信息会留存在仓库历史记录中。

评论

支持 Markdown