1+ <#
2+ . SYNOPSIS
3+ Retrieves build errors and test failures from an Azure DevOps build.
4+
5+ . DESCRIPTION
6+ Queries the Azure DevOps build timeline to find failed jobs and tasks,
7+ then extracts build errors (MSBuild errors, compilation failures) and
8+ test failures with their details.
9+
10+ . PARAMETER BuildId
11+ The Azure DevOps build ID.
12+
13+ . PARAMETER Org
14+ The Azure DevOps organization. Defaults to 'dnceng-public'.
15+
16+ . PARAMETER Project
17+ The Azure DevOps project. Defaults to 'public'.
18+
19+ . PARAMETER TestsOnly
20+ If specified, only returns test results (no build errors).
21+
22+ . PARAMETER ErrorsOnly
23+ If specified, only returns build errors (no test results).
24+
25+ . PARAMETER JobFilter
26+ Optional filter to match job/task names (supports wildcards).
27+
28+ . EXAMPLE
29+ ./Get-BuildErrors.ps1 -BuildId 1240456
30+
31+ . EXAMPLE
32+ ./Get-BuildErrors.ps1 -BuildId 1240456 -ErrorsOnly
33+
34+ . EXAMPLE
35+ ./Get-BuildErrors.ps1 -BuildId 1240456 -TestsOnly -JobFilter "*SafeArea*"
36+
37+ . OUTPUTS
38+ Objects with Type (BuildError/TestFailure), Source, Message, and Details properties.
39+ #>
40+
41+ [CmdletBinding ()]
42+ param (
43+ [Parameter (Mandatory = $true , Position = 0 )]
44+ [string ]$BuildId ,
45+
46+ [Parameter (Mandatory = $false )]
47+ [string ]$Org = " dnceng-public" ,
48+
49+ [Parameter (Mandatory = $false )]
50+ [string ]$Project = " public" ,
51+
52+ [Parameter (Mandatory = $false )]
53+ [switch ]$TestsOnly ,
54+
55+ [Parameter (Mandatory = $false )]
56+ [switch ]$ErrorsOnly ,
57+
58+ [Parameter (Mandatory = $false )]
59+ [string ]$JobFilter
60+ )
61+
62+ $ErrorActionPreference = " Stop"
63+
64+ # Get build timeline
65+ $timelineUrl = " https://dev.azure.com/$Org /$Project /_apis/build/builds/${BuildId} /timeline?api-version=7.0"
66+
67+ try {
68+ $timeline = Invoke-RestMethod - Uri $timelineUrl - Method Get - ContentType " application/json"
69+ }
70+ catch {
71+ Write-Error " Failed to query Azure DevOps timeline API: $_ "
72+ exit 1
73+ }
74+
75+ $allResults = @ ()
76+
77+ # --- SECTION 1: Find Build Errors from Failed Tasks ---
78+ if (-not $TestsOnly ) {
79+ $failedTasks = $timeline.records | Where-Object {
80+ $_.type -eq " Task" -and
81+ $_.result -eq " failed" -and
82+ $_.log.url -and
83+ (-not $JobFilter -or $_.name -like $JobFilter )
84+ }
85+
86+ foreach ($task in $failedTasks ) {
87+ Write-Host " Analyzing failed task: $ ( $task.name ) " - ForegroundColor Red
88+
89+ try {
90+ $log = Invoke-RestMethod - Uri $task.log.url - Method Get
91+ $lines = $log -split " `r ?`n "
92+
93+ # Find MSBuild errors and ##[error] markers
94+ $errorLines = $lines | Where-Object {
95+ $_ -match " : error [A-Z]+\d*:" -or # MSBuild errors (CS1234, MT1234, etc.)
96+ $_ -match " : Error :" -or # Xamarin.Shared.Sdk errors
97+ $_ -match " ##\[error\]" # Azure DevOps error markers
98+ }
99+
100+ foreach ($errorLine in $errorLines ) {
101+ # Clean up the line
102+ $cleanLine = $errorLine -replace " ^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*" , " "
103+ $cleanLine = $cleanLine -replace " ##\[error\]" , " "
104+
105+ # Skip generic "exited with code" errors - we want the actual error
106+ if ($cleanLine -match " exited with code" ) {
107+ continue
108+ }
109+
110+ $allResults += [PSCustomObject ]@ {
111+ Type = " BuildError"
112+ Source = $task.name
113+ Message = $cleanLine.Trim ()
114+ Details = " "
115+ }
116+ }
117+ }
118+ catch {
119+ Write-Warning " Failed to fetch log for task $ ( $task.name ) : $_ "
120+ }
121+ }
122+ }
123+
124+ # --- SECTION 2: Find Test Failures from Jobs ---
125+ if (-not $ErrorsOnly ) {
126+ $jobs = $timeline.records | Where-Object {
127+ $_.type -eq " Job" -and
128+ $_.log.url -and
129+ $_.state -eq " completed" -and
130+ $_.result -eq " failed" -and
131+ (-not $JobFilter -or $_.name -like $JobFilter )
132+ }
133+
134+ foreach ($job in $jobs ) {
135+ Write-Host " Analyzing job for test failures: $ ( $job.name ) " - ForegroundColor Yellow
136+
137+ try {
138+ $logContent = Invoke-RestMethod - Uri $job.log.url - Method Get
139+ $lines = $logContent -split " `r ?`n "
140+
141+ # Find test result lines: "failed <TestName> (duration)"
142+ # Format: ESC[31mfailedESC[m TestName ESC[90m(duration)ESC[m
143+ # Note: \x1b is the hex escape for the ESC character (0x1B)
144+ for ($i = 0 ; $i -lt $lines.Count ; $i ++ ) {
145+ # Match ANSI-colored format - the reset code ESC[m comes immediately after "failed"
146+ if ($lines [$i ] -match ' ^\d{4}-\d{2}-\d{2}.*\x1b\[31mfailed\x1b\[m\s+(.+?)\s+\x1b\[90m\(([^)]+)\)\x1b\[m$' ) {
147+ $testName = $matches [1 ]
148+ $duration = $matches [2 ]
149+ $errorMessage = " "
150+ $stackTrace = " "
151+
152+ # Look ahead for error message and stack trace
153+ for ($j = $i + 1 ; $j -lt $lines.Count ; $j ++ ) {
154+ $line = $lines [$j ]
155+ $cleanLine = $line -replace " ^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*" , " "
156+
157+ if ($cleanLine -match " ^\s*Error Message:" ) {
158+ for ($k = $j + 1 ; $k -lt [Math ]::Min($j + 10 , $lines.Count ); $k ++ ) {
159+ $msgLine = $lines [$k ] -replace " ^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*" , " "
160+ if ($msgLine -match " ^\s*Stack Trace:" -or [string ]::IsNullOrWhiteSpace($msgLine )) {
161+ break
162+ }
163+ $errorMessage += $msgLine.Trim () + " "
164+ }
165+ }
166+
167+ if ($cleanLine -match " ^\s*Stack Trace:" ) {
168+ for ($k = $j + 1 ; $k -lt [Math ]::Min($j + 5 , $lines.Count ); $k ++ ) {
169+ $stLine = $lines [$k ] -replace " ^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s*" , " "
170+ if ($stLine -match " at .+ in .+:line \d+" ) {
171+ $stackTrace = $stLine.Trim ()
172+ break
173+ }
174+ }
175+ break
176+ }
177+
178+ # Stop if we hit the next test (plain or ANSI-colored format)
179+ if ($cleanLine -match ' (?:\x1b\[\d+m)?(passed|failed|skipped)(?:\x1b\[m)?\s+\S+' ) {
180+ break
181+ }
182+ }
183+
184+ $allResults += [PSCustomObject ]@ {
185+ Type = " TestFailure"
186+ Source = $job.name
187+ Message = $testName
188+ Details = if ($errorMessage ) { " $errorMessage `n $stackTrace " .Trim() } else { $stackTrace }
189+ }
190+ }
191+ }
192+ }
193+ catch {
194+ Write-Warning " Failed to fetch log for job $ ( $job.name ) : $_ "
195+ }
196+ }
197+ }
198+
199+ # Remove duplicate errors (same message from same source)
200+ $uniqueResults = $allResults | Group-Object - Property Type, Source, Message | ForEach-Object {
201+ $_.Group | Select-Object - First 1
202+ }
203+
204+ # Summary
205+ $buildErrors = ($uniqueResults | Where-Object { $_.Type -eq " BuildError" }).Count
206+ $testFailures = ($uniqueResults | Where-Object { $_.Type -eq " TestFailure" }).Count
207+
208+ Write-Host " `n Summary: $buildErrors build error(s), $testFailures test failure(s)" - ForegroundColor Cyan
209+
210+ $uniqueResults
0 commit comments