trigger: batch: true branches: include: - main paths: exclude: - .github/** - .gitignore - .devcontainer/* - docs/* - .vscode/* - .markdownlint-cli2.jsonc - .markdownlintignore - CODE_OF_CONDUCT.md - CONTRIBUTING.md - README.md - SECURITY.md - azure-pipelines-official.yml - azure-pipelines-codeql.yml # The main-branch CI trigger runs the Windows Release tests to publish coverage. The nightly run still exercises the # full authoritative pipeline. schedules: - cron: 0 3 * * * displayName: Nightly full validation branches: include: - main always: true # Branch(es) that trigger(s) build(s) on PR pr: branches: include: - "*" paths: exclude: - .github/** - .gitignore - .devcontainer/* - docs/* - .vscode/* - .markdownlint-cli2.jsonc - .markdownlintignore - CODE_OF_CONDUCT.md - CONTRIBUTING.md - README.md - SECURITY.md - azure-pipelines-official.yml - azure-pipelines-codeql.yml - "**/*.xlf" parameters: # This option should be used with caution. This is useful for unblocking circular deps issue with testanywhere - name: SkipTests displayName: "Skip tests" type: boolean default: False variables: # Cannot use key:value syntax in root defined variables - name: _TeamName value: MSTest - name: _RunAsInternal value: False - name: _RunAsPublic value: True # Microsoft.Testing.Platform report/dump arguments for the Windows Debug unit-test command that uses # --test-modules. That mode bypasses test/Directory.Build.targets, so the command must restore these options # explicitly. The full Release runs evaluate the test projects and receive the same options from that targets file. # --report-trx / --report-trx-filename "{asm}_{tfm}.trx" — TRX per (assembly x TFM). # --report-ctrf / --report-ctrf-filename "{asm}_{tfm}.ctrf.json" — CTRF JSON per (assembly x TFM). # --report-junit / --report-junit-filename "{asm}_{tfm}.xml" — JUnit XML per (assembly x TFM). # --report-azdo — surfaces failed tests in the AzDO UI. # --report-azdo-summary — Markdown job summary uploaded to the build's Extensions tab. # --report-azdo-slow-test-history 30 — query the pipeline's own test history (last 30 days) and lower the # per-test 'still running' threshold for historically fast tests. # --report-azdo-slow-test-history-min-sample 10 / --report-azdo-slow-test-history-multiplier 3.0 — exercise # the slow-test-history sub-options and their 'requires report-azdo-slow-test-history' validation. Values # match the extension defaults, so they don't change which tests are flagged; they only keep the option # surface covered. # --report-azdo-flaky-history 30 — query test history (last 30 days) and annotate reported failures with # flakiness context. Annotation only: it does NOT demote failures to warnings (that is the separate # report-azdo-demote-known-flaky option, intentionally left off so a genuine regression is never hidden). # --hangdump --hangdump-timeout 15m — dump a module that hangs past 15 minutes. # The report-azdo history options (slow-test-history, flaky-history) authenticate via the SYSTEM_ACCESSTOKEN # secret. It is mapped onto the public Test/TestDebug steps below; fork PR builds don't receive the # secret so the history service silently no-ops there, while trusted-branch builds exercise it end-to-end. # Live publishing is dogfooded only on the macOS main/nightly leg when the job receives an OAuth token. A # unique per-job/attempt run name lets the post-test step verify the extension-created runs; creation or # publication failures retain the TRX fallback. Long parallel solution runs on both Windows and Linux left # the shared-run owner test host waiting for peers until its hang timeout, so those jobs stay on TRX. # Placeholders ({asm}/{tfm}) are resolved by Microsoft.Testing.Platform.Services.ArtifactNamingHelper. - name: _DebugUnitTestReportArgs value: '--report-trx --report-trx-filename "{asm}_{tfm}.trx" --report-ctrf --report-ctrf-filename "{asm}_{tfm}.ctrf.json" --report-azdo --report-azdo-summary --report-azdo-slow-test-history 30 --report-azdo-slow-test-history-min-sample 10 --report-azdo-slow-test-history-multiplier 3.0 --report-azdo-flaky-history 30 --report-junit --report-junit-filename "{asm}_{tfm}.xml" --hangdump --hangdump-timeout 15m' stages: - stage: build displayName: Build jobs: # Lightweight job that classifies the PR diff into product, samples, infrastructure-only, # and XML-documentation-only # scopes and exposes them as output variables. Downstream jobs use them to skip work that # the diff does not touch (so e.g. a samples-only Dependabot PR does not spin up # the full Windows/Linux/macOS product matrix, and a product-only PR does not run # the samples build). # # Fail-safe behaviour: # - Non-PR triggers (manual queue, scheduled, etc.) leave SYSTEM_PULLREQUEST_TARGETBRANCH # empty, so we emit "true" for both change flags and "false" for the optimized scopes. # - If the diff is empty (e.g. force-push to the same SHA), we also run everything. # - Downstream conditions treat missing product/sample outputs as changed and missing # optimized-scope outputs as false, so classification uncertainty runs full validation. - job: DetectChanges displayName: Detect changed paths pool: name: NetCore-Public demands: ImageOverride -equals build.ubuntu.2204.amd64.open steps: - checkout: self fetchDepth: 0 clean: false - pwsh: ./eng/validate-affected-tests.ps1 displayName: Validate affected-test rollout - bash: | set -euo pipefail target="${SYSTEM_PULLREQUEST_TARGETBRANCH:-}" # ADO exposes the target branch as a full ref (e.g. "refs/heads/main"); strip the # "refs/heads/" prefix so it can be combined with the "origin/" remote-tracking prefix. target="${target#refs/heads/}" hasProduct=true hasSamples=true infrastructureOnly=false xmlDocsOnly=false if [[ -z "${target}" ]]; then echo "Not a PR build (SYSTEM_PULLREQUEST_TARGETBRANCH is empty); running all jobs." else echo "PR target branch: ${target}" git fetch --no-tags --depth=200 origin "${target}" 2>/dev/null \ || git fetch --no-tags origin "${target}" base="$(git merge-base "origin/${target}" HEAD)" echo "Diff range: ${base}..HEAD" files="$(git diff --name-only "${base}" HEAD)" echo "Changed files:" echo "${files}" if [[ -z "${files}" ]]; then echo "No files changed in diff; running all jobs." else # Never let a PR classify itself with code from its own branch. The trusted target-branch # classifier contains a narrow infrastructure allowlist; missing, broken, unknown, and empty # classifications all retain the full-validation defaults above. buildClassifier="${AGENT_TEMPDIRECTORY}/classify-build-change.ps1" classificationSucceeded=false if ! git show "origin/${target}:eng/classify-build-change.ps1" > "${buildClassifier}"; then echo "##vso[task.logissue type=warning]The trusted target branch does not contain the build change classifier; using full validation." elif ! pwsh -NoProfile -File "${buildClassifier}" -SelfTest; then echo "##vso[task.logissue type=warning]The trusted build change classifier failed its self-tests; using full validation." elif ! classification="$(pwsh -NoProfile -File "${buildClassifier}" -Base "${base}" -Head HEAD -Repository "${BUILD_SOURCESDIRECTORY}")"; then echo "##vso[task.logissue type=warning]Build change classification did not run; using full validation." else classification_pattern='^hasProductChanges=(true|false);hasSamplesChanges=(true|false);infrastructureOnly=(true|false)$' if [[ "${classification}" =~ ${classification_pattern} ]]; then hasProduct="${BASH_REMATCH[1]}" hasSamples="${BASH_REMATCH[2]}" infrastructureOnly="${BASH_REMATCH[3]}" classificationSucceeded=true else echo "##vso[task.logissue type=warning]Build change classification returned an unexpected value; using full validation." fi fi # Never let a PR classify itself with code from its own branch. Extract the classifier from the # trusted target branch and fail closed when it is missing, broken, or returns an unexpected value. classifier="${AGENT_TEMPDIRECTORY}/classify-xml-doc-change.ps1" xmlDocsOnly=false if [[ "${classificationSucceeded}" != "true" ]]; then echo "Skipping XML documentation classification because build change classification was not trusted." elif ! git show "origin/${target}:eng/classify-xml-doc-change.ps1" > "${classifier}"; then echo "##vso[task.logissue type=warning]The trusted target branch does not contain the XML documentation classifier; using full validation." elif ! pwsh -NoProfile -File "${classifier}" -SelfTest; then echo "##vso[task.logissue type=warning]The trusted XML documentation classifier failed its self-tests; using full validation." elif ! xmlDocsOnly="$(pwsh -NoProfile -File "${classifier}" -Base "${base}" -Head HEAD -Repository "${BUILD_SOURCESDIRECTORY}")"; then echo "##vso[task.logissue type=warning]XML documentation change classification did not run; using full validation." xmlDocsOnly=false fi if [[ "${xmlDocsOnly}" != "true" && "${xmlDocsOnly}" != "false" ]]; then echo "##vso[task.logissue type=warning]XML documentation change classification returned an unexpected value; using full validation." xmlDocsOnly=false fi fi fi echo "hasProductChanges=${hasProduct}" echo "hasSamplesChanges=${hasSamples}" echo "infrastructureOnly=${infrastructureOnly}" echo "xmlDocsOnly=${xmlDocsOnly}" echo "##vso[task.setvariable variable=hasProductChanges;isOutput=true]${hasProduct}" echo "##vso[task.setvariable variable=hasSamplesChanges;isOutput=true]${hasSamples}" echo "##vso[task.setvariable variable=infrastructureOnly;isOutput=true]${infrastructureOnly}" echo "##vso[task.setvariable variable=xmlDocsOnly;isOutput=true]${xmlDocsOnly}" name: detect displayName: Classify changed files - template: /eng/common/templates/jobs/jobs.yml parameters: enableMicrobuild: true enablePublishTestResults: false artifacts: publish: artifacts: name: 'Artifacts_$(Agent.JobName)_$(_BuildConfig)' logs: name: 'Logs_Build_$(Agent.JobName)_$(_BuildConfig)_Attempt$(System.JobAttempt)' manifests: true enableTelemetry: true jobs: - job: Windows dependsOn: DetectChanges # Release normally runs full validation and coverage for every non-infrastructure PR, but # XML-documentation-only PRs use it only for compiler and package validation. Debug remains # skippable for samples-only and XML-doc-only PRs. condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.infrastructureOnly'], 'true'), or(eq(variables._BuildConfig, 'Release'), and(ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false')))) # Main replays tests in instrumented collection batches before the normal full/coverage run. # Keep the ordinary PR budget unchanged. ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: timeoutInMinutes: 240 ${{ else }}: timeoutInMinutes: 90 pool: name: NetCore-Public demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open variables: - template: /eng/pipelines/variables/test-env-vars.yml - name: _XmlDocsOnly value: $[ dependencies.DetectChanges.outputs['detect.xmlDocsOnly'] ] strategy: matrix: Release: _BuildConfig: Release Debug: _BuildConfig: Debug steps: - template: /eng/pipelines/steps/install-windows-prereqs.yml - task: PowerShell@2 displayName: 'Enable local dumps' inputs: targetType: 'inline' script: | New-Item -Path $(Build.SourcesDirectory)\artifacts\CrashDumps -ItemType Directory -Force New-Item -Path "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" -Force | Out-Null New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" -Name "DumpFolder" -Value "$(Build.SourcesDirectory)\artifacts\CrashDumps" -PropertyType ExpandString -Force New-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps" -Name "DumpCount" -Value 10 -PropertyType DWord -Force - template: /eng/pipelines/steps/configure-azdo-live-test-publishing.yml parameters: enabled: false - script: eng\common\CIBuild.cmd -configuration $(_BuildConfig) -prepareMachine /p:Publish=false /p:Test=false /p:FastAcceptanceTest=true name: Build displayName: Build - ${{ if eq(parameters.SkipTests, False) }}: - template: /eng/pipelines/steps/test-windows-configuration-tests.yml parameters: enableAffectedTests: true ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: affectedTestsMode: collect ${{ else }}: affectedTestsMode: run # artifacts/TestResults/$(_BuildConfig) only exists once a test step has written into it, and # PublishBuildArtifacts@1 hard-fails when PathtoPublish does not exist. A job that dies before the # tests run therefore reported one real problem as two failed steps, the way the official build did # in https://dev.azure.com/dnceng/internal/_build/results?buildId=3057151, where an Azure Artifacts # outage skipped Build and Test and this step then failed with "Not found PathtoPublish". Probe for # the folder so the publish skips when there is nothing to publish. When the tests did write results # they are published exactly as before, including on a failed run, which is why this uses always(). - pwsh: | $testResults = Join-Path '$(Build.SourcesDirectory)' 'artifacts/TestResults/$(_BuildConfig)' $hasTestResults = 'false' if (Test-Path -LiteralPath $testResults -PathType Container) { $hasTestResults = 'true' } Write-Host "Test results under '$testResults': $hasTestResults" Write-Host "##vso[task.setvariable variable=HasTestResults]$hasTestResults" displayName: 'Check for Test Results folders' condition: always() - task: PublishBuildArtifacts@1 displayName: 'Publish Test Results folders' inputs: PathtoPublish: '$(Build.SourcesDirectory)/artifacts/TestResults/$(_BuildConfig)' ArtifactName: TestResults_Windows_$(_BuildConfig)_Attempt$(System.JobAttempt) condition: and(always(), ne(variables._XmlDocsOnly, 'true'), eq(variables.HasTestResults, 'true')) # The integration suite only creates artifacts/tmp/$(_BuildConfig)/testsuite once it runs, and # CopyFiles@2 hard-fails when SourceFolder does not exist. A Test failure early enough to skip the # suite therefore reported one real problem as three failed steps: Test, Copy binlogs, and Publish # integration tests binlogs all failed in # https://dev.azure.com/dnceng/internal/_build/results?buildId=3045544. Probe for the binlogs so # both steps skip when there are none. When the suite did produce binlogs they are copied and # published exactly as before, including on a failed run, which is why these use always(). - pwsh: | $testsuite = Join-Path '$(Build.SourcesDirectory)' 'artifacts/tmp/$(_BuildConfig)/testsuite' $hasBinlogs = 'false' if (Test-Path -LiteralPath $testsuite -PathType Container) { $binlog = Get-ChildItem -LiteralPath $testsuite -Filter '*.binlog' -Recurse -File -ErrorAction SilentlyContinue | Select-Object -First 1 if ($null -ne $binlog) { $hasBinlogs = 'true' } } Write-Host "Integration tests binlogs under '$testsuite': $hasBinlogs" Write-Host "##vso[task.setvariable variable=HasIntegrationTestBinlogs]$hasBinlogs" displayName: 'Check for integration tests binlogs' condition: and(always(), eq(variables._BuildConfig, 'Release')) - task: CopyFiles@2 displayName: 'Copy binlogs' inputs: SourceFolder: '$(Build.SourcesDirectory)/artifacts/tmp/$(_BuildConfig)/testsuite' Contents: | **/*.binlog TargetFolder: '$(Build.ArtifactStagingDirectory)/binlogs' condition: and(always(), eq(variables._BuildConfig, 'Release'), eq(variables.HasIntegrationTestBinlogs, 'true')) - task: PublishBuildArtifacts@1 displayName: 'Publish integration tests binlogs' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)/binlogs' ArtifactName: Integration_Tests_Windows_Binlogs_$(_BuildConfig) condition: and(always(), eq(variables._BuildConfig, 'Release'), eq(variables.HasIntegrationTestBinlogs, 'true')) # "Enable local dumps" creates artifacts/CrashDumps before the build, but an earlier failure can skip # that setup step. PublishBuildArtifacts@1 then hard-fails because PathtoPublish does not exist, adding # another red step on top of the real failure. Probe for the folder so the publish skips when the setup # step did not create it. The probe runs on every outcome so the variable is set whichever way the job # goes; the publish still only runs on a failed run and publishes the folder exactly as before. - pwsh: | $crashDumps = Join-Path '$(Build.SourcesDirectory)' 'artifacts/CrashDumps' $hasCrashDumps = 'false' if (Test-Path -LiteralPath $crashDumps -PathType Container) { $hasCrashDumps = 'true' } Write-Host "Crash dumps under '$crashDumps': $hasCrashDumps" Write-Host "##vso[task.setvariable variable=HasCrashDumps]$hasCrashDumps" displayName: 'Check for local dumps' condition: always() - task: PublishBuildArtifacts@1 displayName: 'Publish local dumps' inputs: PathtoPublish: '$(Build.SourcesDirectory)/artifacts/CrashDumps' ArtifactName: TestResults_Windows_$(_BuildConfig) condition: and(failed(), eq(variables.HasCrashDumps, 'true')) # Real UWP and packaged WinUI activation require more than a Windows image label. Keep this as a # separate blocking job: its strict preflight verifies the VS/UWP SDKs and interactive machine state # before packing and running the application-model acceptance tests. If this image stops satisfying # those requirements, move the job to a purpose-built pool rather than weakening or skipping tests. - job: WindowsAppModel displayName: Windows application-model acceptance dependsOn: DetectChanges condition: and(succeeded(), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), or(ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false'), ne(dependencies.DetectChanges.outputs['detect.hasSamplesChanges'], 'false'))) timeoutInMinutes: 120 pool: name: NetCore-Public demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open variables: - template: /eng/pipelines/variables/test-env-vars.yml - name: _BuildConfig value: Release - name: Microsoft_Testing_TestInfrastructure_TempDirectory_Cleanup value: 0 steps: - checkout: self clean: true - template: /eng/pipelines/steps/install-windows-prereqs.yml - template: /eng/pipelines/steps/configure-azdo-live-test-publishing.yml parameters: enabled: false - template: /eng/pipelines/steps/test-windows-app-model.yml - job: Linux dependsOn: DetectChanges condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false')) timeoutInMinutes: 90 pool: name: NetCore-Public demands: ImageOverride -equals build.ubuntu.2204.amd64.open variables: - template: /eng/pipelines/variables/test-env-vars.yml strategy: matrix: Release: _BuildConfig: Release steps: - template: /eng/pipelines/steps/configure-azdo-live-test-publishing.yml parameters: enabled: false - script: eng/common/cibuild.sh -configuration $(_BuildConfig) -prepareMachine /p:Test=false /p:Publish=false /p:NonWindowsBuild=true /p:FastAcceptanceTest=true displayName: Build - ${{ if eq(parameters.SkipTests, False) }}: - template: /eng/pipelines/steps/test-non-windows.yml parameters: osName: Linux - job: MacOS dependsOn: DetectChanges # Hosted macOS agents have substantially longer and less predictable queue times than the public # Linux and Windows pools. Keep macOS coverage on main CI, nightly, and manually queued builds, but # exclude it from PR validation so agent availability does not delay the required PR status. condition: and(succeededOrFailed(), ne(variables['Build.Reason'], 'PullRequest'), ne(dependencies.DetectChanges.outputs['detect.xmlDocsOnly'], 'true'), ne(dependencies.DetectChanges.outputs['detect.hasProductChanges'], 'false')) timeoutInMinutes: 90 pool: name: Azure Pipelines vmImage: macos-latest os: macOS variables: - template: /eng/pipelines/variables/test-env-vars.yml strategy: matrix: Release: _BuildConfig: Release steps: - template: /eng/pipelines/steps/configure-azdo-live-test-publishing.yml parameters: enabled: true - script: eng/common/cibuild.sh -configuration $(_BuildConfig) -prepareMachine /p:Test=false /p:Publish=false /p:NonWindowsBuild=true /p:FastAcceptanceTest=true displayName: Build - ${{ if eq(parameters.SkipTests, False) }}: - template: /eng/pipelines/steps/test-non-windows.yml parameters: osName: MacOS - template: /eng/common/templates/jobs/jobs.yml parameters: enableMicrobuild: true artifacts: publish: logs: name: 'Logs_Build_$(Agent.JobName)_$(_BuildConfig)_Attempt$(System.JobAttempt)' manifests: true enableTelemetry: true jobs: - job: WindowsSamples dependsOn: DetectChanges condition: and(succeededOrFailed(), ne(dependencies.DetectChanges.outputs['detect.hasSamplesChanges'], 'false')) timeoutInMinutes: 90 pool: name: NetCore-Public demands: ImageOverride -equals windows.vs2026preview.scout.amd64.open strategy: matrix: Release: _BuildConfig: Release steps: - task: PowerShell@2 displayName: 'Install Windows SDK' inputs: targetType: filePath filePath: './eng/install-windows-sdk.ps1' failOnStderr: true showWarnings: true - ${{ if eq(parameters.SkipTests, False) }}: - task: PowerShell@2 displayName: 'Preflight Windows sample test agent' inputs: targetType: filePath filePath: './eng/pipelines/test-windows-app-model-preflight.ps1' arguments: '-Mode Preflight -RemoveStaleTestPackages' failOnStderr: true showWarnings: true - task: PowerShell@2 displayName: 'Build Samples' inputs: targetType: filePath filePath: './eng/build-samples.ps1' arguments: '-Configuration $(_BuildConfig) -TreatWarningsAsErrors -BinaryLogDirectory "$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)"' failOnStderr: false - ${{ if eq(parameters.SkipTests, False) }}: - task: PowerShell@2 displayName: 'Test Samples' inputs: targetType: filePath filePath: './eng/test-samples.ps1' arguments: '-Configuration $(_BuildConfig) -BinaryLogDirectory "$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)"' failOnStderr: false