From 61e901a0db1a19ddf6b8a7d0e7e4a377cfa3872e Mon Sep 17 00:00:00 2001 From: Iarek Kovtunenko <16271112+iarekk@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:02:16 +0100 Subject: [PATCH 1/2] Split PR pipeline into independent net8 stages; add MI E2E stage on MSALMSIV2 Build (full multi-TFM + unit tests) stays critical path; IntegrationTests, E2ETests (non-MI) and ManagedIdentityE2E run independently (dependsOn: []). MI tests run on the MSALMSIV2 VM pool with its assigned UAMI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- azure-pipelines.yml | 119 ++++++++++++++---- build/template-build-test-project.yaml | 29 +++++ ...mplate-run-managed-identity-e2e-tests.yaml | 41 ++++++ build/template-run-unit-tests.yaml | 76 ++--------- build/template-test-e2e.yaml | 55 ++++++++ build/template-test-integration.yaml | 19 +++ build/template-test-unit.yaml | 16 +++ .../TokenAcquirerTests/TokenAcquirer.cs | 7 +- 8 files changed, 269 insertions(+), 93 deletions(-) create mode 100644 build/template-build-test-project.yaml create mode 100644 build/template-run-managed-identity-e2e-tests.yaml create mode 100644 build/template-test-e2e.yaml create mode 100644 build/template-test-integration.yaml create mode 100644 build/template-test-unit.yaml diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 54faabcd6..28407fb72 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,27 +7,98 @@ pr: include: - master -jobs: -- job: "PR_build" - variables: - # Load the LabAuth client certificate from the local certificate store - # (installed into LocalMachine/My by template-install-dependencies.yaml) - # instead of the msidlabs KeyVault. Microsoft-hosted agents have no managed - # identity that can read the KeyVault, so DefaultAzureCredential would fail. - # The official OneBranch/Wilson pipeline does not set this and keeps using - # the KeyVault source. - UseLabAuthCertFromStore: true - pool: - vmImage: 'windows-2022' - demands: - - msbuild - - visualstudio - steps: - - template: build/template-install-dependencies.yaml - - template: build/template-build.yaml - - template: build/template-run-unit-tests.yaml - parameters: - # Exclude managed-identity E2E tests: they require an MI assigned to the - # host (IMDS), which Microsoft-hosted agents do not have. They still run in - # the official OneBranch pipeline on VM-based agents that have the MI. - e2eTestFilterCriteria: 'Category!=MI_E2E' +stages: + +# Full multi-TFM build + unit tests. The unit test project targets .NET Framework +# (net462/net472) as well as net8/net9/net10, so it needs the complete multi-TFM +# solution build. E2E and managed-identity tests run in their own stages below. +- stage: Build + displayName: 'Build & Unit Tests' + jobs: + - job: PR_build + variables: + # Load the LabAuth client certificate from the local certificate store + # (installed into LocalMachine/My by template-install-dependencies.yaml) + # instead of the msidlabs KeyVault. Microsoft-hosted agents have no managed + # identity that can read the KeyVault, so DefaultAzureCredential would fail. + # The official OneBranch/Wilson pipeline does not set this and keeps using + # the KeyVault source. + UseLabAuthCertFromStore: true + pool: + vmImage: 'windows-2022' + demands: + - msbuild + - visualstudio + steps: + - template: build/template-install-dependencies.yaml + - template: build/template-build.yaml + - template: build/template-test-unit.yaml + +# Integration tests only depend on .NET 8, so build just that project and run them +# in parallel with the full Build stage (dependsOn: [] -> no dependency on Build). +- stage: IntegrationTests + displayName: 'Integration Tests (.NET 8)' + dependsOn: [] + jobs: + - job: IntegrationTests + variables: + UseLabAuthCertFromStore: true + pool: + vmImage: 'windows-2022' + demands: + - msbuild + - visualstudio + steps: + - template: build/template-install-dependencies.yaml + - template: build/template-build-test-project.yaml + parameters: + Project: 'tests/Microsoft.Identity.Web.Test.Integration/Microsoft.Identity.Web.Test.Integration.csproj' + TargetFramework: 'net8.0' + - template: build/template-test-integration.yaml + +# Browser/host E2E tests (excluding managed identity). The E2E projects target +# net8.0 and pull their sample apps in as project references, so build just those +# projects and run in parallel with Build (dependsOn: []). +- stage: E2ETests + displayName: 'E2E Tests (.NET 8)' + dependsOn: [] + jobs: + - job: E2ETests + variables: + UseLabAuthCertFromStore: true + pool: + vmImage: 'windows-2022' + demands: + - msbuild + - visualstudio + steps: + - template: build/template-install-dependencies.yaml + - template: build/template-build-test-project.yaml + parameters: + Project: 'tests/E2E Tests/WebAppUiTests/WebAppUiTests.csproj' + TargetFramework: 'net8.0' + - template: build/template-build-test-project.yaml + parameters: + Project: 'tests/E2E Tests/TokenAcquirerTests/TokenAcquirerTests.csproj' + TargetFramework: 'net8.0' + - template: build/template-test-e2e.yaml + parameters: + # Exclude managed-identity E2E tests: they require an MI assigned to the + # host (IMDS), which Microsoft-hosted agents do not have. They run in the + # ManagedIdentityE2E stage below on the MSALMSIV2 VM pool. + e2eTestFilterCriteria: 'Category!=MI_E2E' + +# Managed-identity E2E tests require a VM with an assigned managed identity (IMDS), +# which Microsoft-hosted agents do not have. They run on the MSALMSIV2 VM pool and +# build only the E2E project (.NET 8), independent of the Build stage. +- stage: ManagedIdentityE2E + displayName: 'Managed Identity E2E (MSALMSIV2)' + dependsOn: [] + jobs: + - job: ManagedIdentityE2E + pool: + name: 'MSALMSIV2' + steps: + - template: build/template-run-managed-identity-e2e-tests.yaml + parameters: + TargetFramework: 'net8.0' diff --git a/build/template-build-test-project.yaml b/build/template-build-test-project.yaml new file mode 100644 index 000000000..477388ec4 --- /dev/null +++ b/build/template-build-test-project.yaml @@ -0,0 +1,29 @@ +# template-build-test-project.yaml +# Restore and build a single test project (and its project references) for one +# target framework. Used by the independent stages that only need .NET 8, so they +# don't wait on the full multi-TFM solution build. + +parameters: +- name: Project + type: string +- name: BuildConfiguration + type: string + default: 'Release' +- name: TargetFramework + type: string + default: 'net8.0' + +steps: + +- task: DotNetCoreCLI@2 + displayName: 'Restore ${{ parameters.Project }}' + inputs: + command: 'restore' + projects: '${{ parameters.Project }}' + +- task: DotNetCoreCLI@2 + displayName: 'Build ${{ parameters.Project }} (${{ parameters.TargetFramework }})' + inputs: + command: 'build' + projects: '${{ parameters.Project }}' + arguments: '--configuration ${{ parameters.BuildConfiguration }} --framework ${{ parameters.TargetFramework }} --no-restore' diff --git a/build/template-run-managed-identity-e2e-tests.yaml b/build/template-run-managed-identity-e2e-tests.yaml new file mode 100644 index 000000000..cfcc16e95 --- /dev/null +++ b/build/template-run-managed-identity-e2e-tests.yaml @@ -0,0 +1,41 @@ +# template-run-managed-identity-e2e-tests.yaml +# Build the TokenAcquirer E2E project (.NET 8 only) and run the managed-identity +# E2E tests (Category=MI_E2E). These require a VM with an assigned managed +# identity (IMDS), so they run on the MSALMSIV2 pool, not Microsoft-hosted agents. +# Mirrors the MSAL.NET template-run-mi-e2e-imds.yaml approach. + +parameters: +- name: BuildConfiguration + type: string + default: 'Release' +- name: TargetFramework + type: string + default: 'net8.0' + +steps: + +- task: UseDotNet@2 + displayName: 'Use .NET SDK 8.0.x' + inputs: + packageType: 'sdk' + version: '8.0.x' + +- template: template-build-test-project.yaml + parameters: + Project: 'tests/E2E Tests/TokenAcquirerTests/TokenAcquirerTests.csproj' + BuildConfiguration: '${{ parameters.BuildConfiguration }}' + TargetFramework: '${{ parameters.TargetFramework }}' + +- task: VSTest@2 + displayName: 'Run managed identity E2E tests' + inputs: + testSelector: 'testAssemblies' + testAssemblyVer2: 'tests\E2E Tests\TokenAcquirerTests\bin\${{ parameters.BuildConfiguration }}\${{ parameters.TargetFramework }}\TokenAcquirerTests.dll' + searchFolder: '$(System.DefaultWorkingDirectory)' + testFiltercriteria: 'Category=MI_E2E' + runTestsInIsolation: true + rerunFailedTests: true + rerunMaxAttempts: '3' + runInParallel: false + failOnMinTestsNotRun: true + minimumExpectedTests: '1' diff --git a/build/template-run-unit-tests.yaml b/build/template-run-unit-tests.yaml index cc7895d7d..c92698c4d 100644 --- a/build/template-run-unit-tests.yaml +++ b/build/template-run-unit-tests.yaml @@ -1,5 +1,9 @@ # template-run-unit-tests.yaml -# Run all unit tests across the IdWeb project +# Aggregator that runs every test suite (unit + integration + E2E) in one job. +# Kept for consumers that want the full suite in a single stage (e.g. the release +# build). The PR pipeline composes the granular templates into separate stages +# instead: template-test-unit.yaml, template-test-integration.yaml and +# template-test-e2e.yaml. parameters: # VSTest filter applied to the E2E test run. Empty by default so every consumer @@ -12,70 +16,10 @@ parameters: steps: -- task: VSTest@2 - displayName: 'Run unit tests' - inputs: - testSelector: 'testAssemblies' - testAssemblyVer2: 'tests\Microsoft.Identity.Web.Test\bin\**\Microsoft.Identity.Web.Test.dll' - searchFolder: '$(System.DefaultWorkingDirectory)' - runInParallel: true - codeCoverageEnabled: true - failOnMinTestsNotRun: true - minimumExpectedTests: '1' - runSettingsFile: 'build\CodeCoverage.runsettings' +- template: template-test-unit.yaml -- task: VSTest@2 - displayName: 'Run integration tests' - inputs: - testSelector: 'testAssemblies' - testAssemblyVer2: 'tests\Microsoft.Identity.Web.Test.Integration\bin\**\Microsoft.Identity.Web.Test.Integration.dll' - searchFolder: '$(System.DefaultWorkingDirectory)' - rerunFailedTests: true - rerunMaxAttempts: '3' - runInParallel: false - codeCoverageEnabled: true - failOnMinTestsNotRun: false - minimumExpectedTests: '1' - runSettingsFile: 'build\CodeCoverage.runsettings' +- template: template-test-integration.yaml -- task: VSTest@2 - displayName: 'Run E2E tests' - inputs: - testSelector: 'testAssemblies' - testAssemblyVer2: | - tests\E2E Tests\WebAppUiTests\bin\**\WebAppUiTests.dll - tests\E2E Tests\TokenAcquirerTests\bin\**\TokenAcquirerTests.dll - tests\E2E Tests\NET 7 tests\IntegrationTests\bin\**\IntegrationTests.dll - searchFolder: '$(System.DefaultWorkingDirectory)' - testFiltercriteria: '${{ parameters.e2eTestFilterCriteria }}' - rerunFailedTests: true - rerunMaxAttempts: '3' - runInParallel: false - codeCoverageEnabled: true - failOnMinTestsNotRun: false - minimumExpectedTests: '1' - runSettingsFile: 'build\CodeCoverage.runsettings' - -- task: PublishBuildArtifacts@1 - displayName: 'Publish traces after test' - inputs: - PathtoPublish: '$(Build.SourcesDirectory)/tests/E2E Tests/PlaywrightTraces/' - ArtifactName: 'traces-after-tests-$(Build.BuildNumber)' - condition: failed() - -# Copy all packages out to staging -- task: CopyFiles@2 - displayName: 'Copy screenshots to staging directory' - inputs: - SourceFolder: '$(Build.SourcesDirectory)/tests/E2E Tests/' - Contents: '**/*screenshotFail.png' - TargetFolder: '$(Build.ArtifactStagingDirectory)\screenshots' - flattenFolders: true - condition: failed() - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Screenshot after test' - inputs: - PathtoPublish: '$(Build.ArtifactStagingDirectory)\screenshots' - ArtifactName: 'ScreenshotFail' - condition: failed() \ No newline at end of file +- template: template-test-e2e.yaml + parameters: + e2eTestFilterCriteria: '${{ parameters.e2eTestFilterCriteria }}' diff --git a/build/template-test-e2e.yaml b/build/template-test-e2e.yaml new file mode 100644 index 000000000..5ebb082c5 --- /dev/null +++ b/build/template-test-e2e.yaml @@ -0,0 +1,55 @@ +# template-test-e2e.yaml +# Run the browser/host E2E tests (Playwright + multi-target). These require the +# full multi-TFM build output, so they run in the build stage. + +parameters: +# VSTest filter applied to the E2E run. Empty by default so the release build runs +# the full suite. The PR build passes 'Category!=MI_E2E' to exclude managed-identity +# tests, which run on the MSALMSIV2 pool in their own stage. +- name: e2eTestFilterCriteria + type: string + default: '' + +steps: + +- task: VSTest@2 + displayName: 'Run E2E tests' + inputs: + testSelector: 'testAssemblies' + testAssemblyVer2: | + tests\E2E Tests\WebAppUiTests\bin\**\WebAppUiTests.dll + tests\E2E Tests\TokenAcquirerTests\bin\**\TokenAcquirerTests.dll + tests\E2E Tests\NET 7 tests\IntegrationTests\bin\**\IntegrationTests.dll + searchFolder: '$(System.DefaultWorkingDirectory)' + testFiltercriteria: '${{ parameters.e2eTestFilterCriteria }}' + rerunFailedTests: true + rerunMaxAttempts: '3' + runInParallel: false + codeCoverageEnabled: true + failOnMinTestsNotRun: false + minimumExpectedTests: '1' + runSettingsFile: 'build\CodeCoverage.runsettings' + +- task: PublishBuildArtifacts@1 + displayName: 'Publish traces after test' + inputs: + PathtoPublish: '$(Build.SourcesDirectory)/tests/E2E Tests/PlaywrightTraces/' + ArtifactName: 'traces-after-tests-$(Build.BuildNumber)' + condition: failed() + +# Copy all packages out to staging +- task: CopyFiles@2 + displayName: 'Copy screenshots to staging directory' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/tests/E2E Tests/' + Contents: '**/*screenshotFail.png' + TargetFolder: '$(Build.ArtifactStagingDirectory)\screenshots' + flattenFolders: true + condition: failed() + +- task: PublishBuildArtifacts@1 + displayName: 'Publish Screenshot after test' + inputs: + PathtoPublish: '$(Build.ArtifactStagingDirectory)\screenshots' + ArtifactName: 'ScreenshotFail' + condition: failed() diff --git a/build/template-test-integration.yaml b/build/template-test-integration.yaml new file mode 100644 index 000000000..3691c5e8b --- /dev/null +++ b/build/template-test-integration.yaml @@ -0,0 +1,19 @@ +# template-test-integration.yaml +# Run the Microsoft.Identity.Web integration tests. Only depend on .NET 8, so a +# consuming stage can build just this project and run in parallel with the build. + +steps: + +- task: VSTest@2 + displayName: 'Run integration tests' + inputs: + testSelector: 'testAssemblies' + testAssemblyVer2: 'tests\Microsoft.Identity.Web.Test.Integration\bin\**\Microsoft.Identity.Web.Test.Integration.dll' + searchFolder: '$(System.DefaultWorkingDirectory)' + rerunFailedTests: true + rerunMaxAttempts: '3' + runInParallel: false + codeCoverageEnabled: true + failOnMinTestsNotRun: false + minimumExpectedTests: '1' + runSettingsFile: 'build\CodeCoverage.runsettings' diff --git a/build/template-test-unit.yaml b/build/template-test-unit.yaml new file mode 100644 index 000000000..2e2cbd407 --- /dev/null +++ b/build/template-test-unit.yaml @@ -0,0 +1,16 @@ +# template-test-unit.yaml +# Run the Microsoft.Identity.Web unit tests. + +steps: + +- task: VSTest@2 + displayName: 'Run unit tests' + inputs: + testSelector: 'testAssemblies' + testAssemblyVer2: 'tests\Microsoft.Identity.Web.Test\bin\**\Microsoft.Identity.Web.Test.dll' + searchFolder: '$(System.DefaultWorkingDirectory)' + runInParallel: true + codeCoverageEnabled: true + failOnMinTestsNotRun: true + minimumExpectedTests: '1' + runSettingsFile: 'build\CodeCoverage.runsettings' diff --git a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs index 3dadbc17b..7baec1ff1 100644 --- a/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs +++ b/tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs @@ -561,9 +561,10 @@ public async Task AcquireTokenWithManagedIdentity_UserAssignedAsync() // Arrange const string scope = "https://vault.azure.net/.default"; const string baseUrl = "https://vault.azure.net"; - // User-assigned managed identity "Msal_Integration_tests" (RG MSAL_MSI, sub c1686c51-b717-4fe0-9af3-24a20a41fb0c). - // Shared/consolidated lab UAMI reused across MSAL and Id.Web E2E tests. - const string clientId = "45344e7d-c562-4be6-868f-18dac789c021"; + // User-assigned managed identity "msiv2uami" (RG MSIV2-Testing-MSALNET, + // sub c1686c51-b717-4fe0-9af3-24a20a41fb0c) assigned to the MSALMSIV2 + // VM pool that runs these MI E2E tests. Shared with MSAL.NET E2E tests. + const string clientId = "6325cd32-9911-41f3-819c-416cdf9104e7"; TokenAcquirerFactoryTesting.ResetTokenAcquirerFactoryInTest(); TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance(); IServiceProvider serviceProvider = tokenAcquirerFactory.Build(); From e3ea4ed10db90ae899d71db28430dffbbf1abe4d Mon Sep 17 00:00:00 2001 From: Iarek Kovtunenko <16271112+iarekk@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:10:47 +0100 Subject: [PATCH 2/2] Install global.json SDK on MSALMSIV2 MI stage to fix restore The MI stage only installed .NET 8, but global.json pins SDK 10.0.x, so dotnet restore failed with 'A compatible .NET SDK was not found'. Add UseDotNet useGlobalJson:true (keeping 8.0.x for the net8.0 test runtime). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/template-run-managed-identity-e2e-tests.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build/template-run-managed-identity-e2e-tests.yaml b/build/template-run-managed-identity-e2e-tests.yaml index cfcc16e95..e2f20c947 100644 --- a/build/template-run-managed-identity-e2e-tests.yaml +++ b/build/template-run-managed-identity-e2e-tests.yaml @@ -14,12 +14,19 @@ parameters: steps: +# The repo global.json pins the .NET SDK (10.0.x). Install it so 'dotnet restore' +# succeeds, plus the .NET 8 runtime the tests execute under. - task: UseDotNet@2 displayName: 'Use .NET SDK 8.0.x' inputs: packageType: 'sdk' version: '8.0.x' +- task: UseDotNet@2 + displayName: 'Use .NET SDK from global.json' + inputs: + useGlobalJson: true + - template: template-build-test-project.yaml parameters: Project: 'tests/E2E Tests/TokenAcquirerTests/TokenAcquirerTests.csproj'