Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 44 additions & 28 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ stages:
# Seed from every trusted main merge. PR merge refs are read-only consumers, so the cache must be updated as
# soon as main changes to avoid forcing the entire 204-node graph to miss.
- pwsh: |
& ./eng/common/msbuild.ps1 -ci -warnAsError:$false ./TestFx.slnx `
& ./eng/common/msbuild.ps1 -ci -prepareMachine -warnAsError:$false ./TestFx.slnx `
/restore `
/graph `
/m `
Expand All @@ -160,7 +160,10 @@ stages:
/t:Build `
/v:minimal `
/p:Configuration=$(_BuildConfig) `
/p:FastAcceptanceTest=true `
/p:Publish=false `
"/p:RepoRoot=$(Build.SourcesDirectory)\" `
/p:Test=false `
/p:MSBuildCachePackageEnabled=true `
/p:MSBuildCacheEnabled=true `
/p:MSBuildCacheRemoteCacheIsReadOnly=false `
Expand Down Expand Up @@ -291,6 +294,7 @@ stages:
"-NoProfile",
"-File", "./eng/common/msbuild.ps1",
"-ci",
"-prepareMachine",
"-warnAsError:`$false",
"./TestFx.slnx",
"/restore",
Expand All @@ -301,7 +305,10 @@ stages:
"/t:Build",
"/v:minimal",
"/p:Configuration=$(_BuildConfig)",
"/p:FastAcceptanceTest=true",
"/p:Publish=false",
"/p:RepoRoot=$(Build.SourcesDirectory)\",
"/p:Test=false",
"/p:MSBuildCachePackageEnabled=true",
"/p:MSBuildCacheEnabled=true",
"/p:MSBuildCacheRemoteCacheIsReadOnly=true",
Expand All @@ -321,16 +328,22 @@ stages:

if ($exitCode -ne 0) {
Write-Host "##vso[task.logissue type=warning]MSBuildCache canary failed with exit code $exitCode; continuing with the regular Arcade build."
Write-Host "##vso[task.setvariable variable=MSBuildCacheBuildSucceeded]false"
}
else {
Write-Host "##vso[task.setvariable variable=MSBuildCacheBuildSucceeded]true"
Comment thread
Evangelink marked this conversation as resolved.
Resolved
}

exit 0
displayName: Build solution graph with MSBuildCache
condition: and(succeeded(), or(and(eq(variables['Build.Reason'], 'PullRequest'), ne(variables['System.PullRequest.IsFork'], 'True')), and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))))
continueOnError: true
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

# Keep the cache canary independent from the authoritative Arcade build. Publish only the project-level
# diagnostics; CacheClient.log and the OAuth-bearing canary environment stay outside published artifacts.
# Publish only the project-level diagnostics; CacheClient.log and the OAuth-bearing cache environment stay
# outside published artifacts. A successful cache build supplies the outputs consumed by the test steps.
# On failure, remove its partial outputs before running the regular Arcade build as a fallback.
- pwsh: |
$cacheLogSource = "$(Agent.TempDirectory)\MSBuildCache"
$cacheLogDestination = "$(Build.SourcesDirectory)\artifacts\log\$(_BuildConfig)\MSBuildCache"
Expand All @@ -347,44 +360,46 @@ stages:
Write-Host "##vso[task.logissue type=warning]Failed to preserve MSBuildCache diagnostics. $($_.Exception.Message)"
}
finally {
$cleanupErrors = @()
if ("$(MSBuildCacheBuildSucceeded)" -ne "true") {
$cleanupErrors = @()

$artifactsDirectory = "$(Build.SourcesDirectory)\artifacts"
if (Test-Path $artifactsDirectory) {
Get-ChildItem $artifactsDirectory -Force |
Where-Object Name -NotIn @("log", "msbuild-cache", "toolset") |
ForEach-Object {
$path = $_.FullName
try {
Remove-Item $path -Recurse -Force -ErrorAction Stop
}
catch {
$cleanupErrors += "${path}: $($_.Exception.Message)"
}
}
}

$artifactsDirectory = "$(Build.SourcesDirectory)\artifacts"
if (Test-Path $artifactsDirectory) {
Get-ChildItem $artifactsDirectory -Force |
Where-Object Name -NotIn @("log", "msbuild-cache", "toolset") |
@(
$cacheLogSource,
"$(Build.SourcesDirectory)\src\Package\MSTest.Sdk\Sdk\Sdk.props",
"$(Build.SourcesDirectory)\src\Package\MSTest.Sdk\Sdk\Runner\Runner.targets"
) |
Where-Object { Test-Path $_ } |
ForEach-Object {
$path = $_.FullName
$path = $_
try {
Remove-Item $path -Recurse -Force -ErrorAction Stop
}
catch {
$cleanupErrors += "${path}: $($_.Exception.Message)"
}
}
}

@(
$cacheLogSource,
"$(Build.SourcesDirectory)\src\Package\MSTest.Sdk\Sdk\Sdk.props",
"$(Build.SourcesDirectory)\src\Package\MSTest.Sdk\Sdk\Runner\Runner.targets"
) |
Where-Object { Test-Path $_ } |
ForEach-Object {
$path = $_
try {
Remove-Item $path -Recurse -Force -ErrorAction Stop
}
catch {
$cleanupErrors += "${path}: $($_.Exception.Message)"
}
if ($cleanupErrors) {
Write-Host "##vso[task.logissue type=warning]Failed to clean some MSBuildCache outputs before the fallback build:`n$($cleanupErrors -join "`n")"
}
Comment thread
Evangelink marked this conversation as resolved.
Resolved

if ($cleanupErrors) {
throw "Failed to clean MSBuildCache canary outputs:`n$($cleanupErrors -join "`n")"
}
}
displayName: Preserve cache diagnostics and clean canary outputs
displayName: Preserve cache diagnostics and clean failed outputs
condition: and(always(), or(and(eq(variables['Build.Reason'], 'PullRequest'), ne(variables['System.PullRequest.IsFork'], 'True')), and(ne(variables['Build.Reason'], 'PullRequest'), eq(variables['Build.SourceBranch'], 'refs/heads/main'))))

- task: PowerShell@2
Expand All @@ -407,6 +422,7 @@ stages:
/p:MSBuildCacheEnabled=false
name: Build
displayName: Build
condition: and(succeeded(), ne(variables['MSBuildCacheBuildSucceeded'], 'true'))
Comment thread
Evangelink marked this conversation as resolved.
Resolved

- ${{ if eq(parameters.SkipTests, False) }}:

Expand Down
6 changes: 3 additions & 3 deletions docs/dev-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,16 @@ For more information about all the different options available, supply the argum

### MSBuildCache

The Windows PR pipeline experimentally runs [MSBuildCache](https://github.com/microsoft/MSBuildCache) before the regular Arcade build. The cache-aware build uses Arcade's `eng/common/msbuild.ps1` launcher to invoke the solution directly because Arcade's outer `Build.proj` discovers projects dynamically and cannot expose the repository's static project graph to the cache plugin. PR builds consume the immutable Azure Pipeline cache read-only, and fork PRs skip this step because they do not receive the required token scope.
The Windows PR pipeline experimentally runs [MSBuildCache](https://github.com/microsoft/MSBuildCache). The cache-aware build uses Arcade's `eng/common/msbuild.ps1` launcher to invoke the solution directly because Arcade's outer `Build.proj` discovers projects dynamically and cannot expose the repository's static project graph to the cache plugin. PR builds consume the immutable Azure Pipeline cache read-only, and fork PRs skip this step because they do not receive the required token scope. When the cache build succeeds, its outputs are used by the remaining test steps and the regular Arcade build is skipped. When it fails, the pipeline preserves the cache diagnostics, removes partial outputs, and runs the regular Arcade build as a fallback.

Every merge to `main` that touches product build inputs runs a dedicated, batched seed stage for both Debug and Release. This is required: the cache fingerprints project inputs, so entries become stale whenever shared build inputs such as `global.json`, `eng/Versions.props`, or Arcade change. This stage is the only remote cache publisher; PR, manual, and nightly canary builds consume the cache read-only so they cannot race to publish immutable entries. Debug and Release use separate cache universes because configuration-independent projects can otherwise race while the two configurations publish in parallel.

The cache's detached-process exclusions use fully rooted paths (for example, `$(WinDir)\**`). A drive-relative pattern such as `\Windows\**` does not match the absolute file-access paths reported by MSBuild and causes otherwise successful cache builds to fail after compilation. Cache builds also pass `-warnAsError:$false` to Arcade's launcher because MSBuildCache intentionally warns about allowlisted detached telemetry accesses. The regular Arcade build still treats warnings as errors and remains authoritative.
The cache's detached-process exclusions use fully rooted paths (for example, `$(WinDir)\**`). A drive-relative pattern such as `\Windows\**` does not match the absolute file-access paths reported by MSBuild and causes otherwise successful cache builds to fail after compilation. Cache builds also pass `-warnAsError:$false` to Arcade's launcher because MSBuildCache intentionally warns about allowlisted detached telemetry accesses. The fallback Arcade build still treats warnings as errors.

MSBuildCache currently requires Windows, the Visual Studio version pinned in `global.json`, Git on `PATH`, and a clean repository. It does not support incremental developer builds. To validate the local cache from a clean checkout, run:

```powershell
eng\common\msbuild.ps1 -msbuildEngine vs -warnAsError:$false TestFx.slnx /restore /graph /m /reportfileaccesses /t:Build /p:Configuration=Release /p:MSBuildCachePackageEnabled=true /p:MSBuildCacheEnabled=true /p:MSBuildCacheLogDirectory=artifacts\log\Release\MSBuildCache\Plugin /bl:artifacts\log\Release\MSBuildCache\Build.binlog
eng\common\msbuild.ps1 -msbuildEngine vs -warnAsError:$false TestFx.slnx /restore /graph /m /reportfileaccesses /t:Build /p:Configuration=Release /p:FastAcceptanceTest=true /p:Publish=false /p:Test=false /p:MSBuildCachePackageEnabled=true /p:MSBuildCacheEnabled=true /p:MSBuildCacheLogDirectory=artifacts\log\Release\MSBuildCache\Plugin /bl:artifacts\log\Release\MSBuildCache\Build.binlog
```

Use `MSTest.slnf` or `Microsoft.Testing.Platform.slnf` in place of `TestFx.slnx` to validate a filtered graph. Delete `artifacts\msbuild-cache` to clear the local content cache. Change the prefix of `MSBuildCacheCacheUniverse` in `Directory.Build.props`, or pass `/p:MSBuildCacheCacheUniverse=<new-value>` for one invocation, to invalidate cache entries. The default universe includes `$(Configuration)`, so use the same configuration when populating and consuming a cache.
Expand Down
Loading