Skip to content

Commit def2b82

Browse files
authored
Add a SKILL.md for getting build results from AzDO (#19228)
* Add a SKILL.md for getting build results from AzDO Copy pasted from dotnet/maui#33325 * Fix Get-BuildErrors script
1 parent 162c811 commit def2b82

File tree

4 files changed

+441
-0
lines changed

4 files changed

+441
-0
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
name: pr-build-status
3+
description: "Retrieve Azure DevOps build information for GitHub Pull Requests, including build IDs, stage status, and failed jobs."
4+
metadata:
5+
author: dotnet-maui
6+
version: "1.0"
7+
compatibility: Requires GitHub CLI (gh) authenticated with access to dotnet/fsharp repository.
8+
---
9+
10+
# PR Build Status Skill
11+
12+
Retrieve Azure DevOps build information for GitHub Pull Requests.
13+
14+
## Tools Required
15+
16+
This skill uses `bash` together with `pwsh` (PowerShell 7+) to run the PowerShell scripts. No file editing or other tools are required.
17+
18+
## When to Use
19+
20+
- User asks about CI/CD status for a PR
21+
- User asks about failed checks or builds
22+
- User asks "what's failing on PR #XXXXX"
23+
- User wants to see test results
24+
25+
## Scripts
26+
27+
All scripts are in `.github/skills/pr-build-status/scripts/`
28+
29+
### 1. Get Build IDs for a PR
30+
```bash
31+
pwsh .github/skills/pr-build-status/scripts/Get-PrBuildIds.ps1 -PrNumber <PR_NUMBER>
32+
```
33+
34+
### 2. Get Build Status
35+
```bash
36+
pwsh .github/skills/pr-build-status/scripts/Get-BuildInfo.ps1 -BuildId <BUILD_ID>
37+
# For failed jobs only:
38+
pwsh .github/skills/pr-build-status/scripts/Get-BuildInfo.ps1 -BuildId <BUILD_ID> -FailedOnly
39+
```
40+
41+
### 3. Get Build Errors and Test Failures
42+
```bash
43+
# Get all errors (build errors + test failures)
44+
pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId <BUILD_ID>
45+
46+
# Get only build/compilation errors
47+
pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId <BUILD_ID> -ErrorsOnly
48+
49+
# Get only test failures
50+
pwsh .github/skills/pr-build-status/scripts/Get-BuildErrors.ps1 -BuildId <BUILD_ID> -TestsOnly
51+
```
52+
53+
## Workflow
54+
55+
1. Get build IDs: `scripts/Get-PrBuildIds.ps1 -PrNumber XXXXX`
56+
2. For each build, get status: `scripts/Get-BuildInfo.ps1 -BuildId YYYYY`
57+
3. For failed builds, get error details: `scripts/Get-BuildErrors.ps1 -BuildId YYYYY`
58+
59+
## Prerequisites
60+
61+
- `gh` (GitHub CLI) - authenticated
62+
- `pwsh` (PowerShell 7+)
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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 "`nSummary: $buildErrors build error(s), $testFailures test failure(s)" -ForegroundColor Cyan
209+
210+
$uniqueResults
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<#
2+
.SYNOPSIS
3+
Retrieves detailed status information for an Azure DevOps build.
4+
5+
.DESCRIPTION
6+
Queries the Azure DevOps build timeline API and returns comprehensive
7+
information about the build including all stages, their status, and
8+
any failed or canceled jobs.
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 FailedOnly
20+
If specified, only returns failed or canceled stages and jobs.
21+
22+
.EXAMPLE
23+
./Get-BuildInfo.ps1 -BuildId 1240455
24+
25+
.EXAMPLE
26+
./Get-BuildInfo.ps1 -BuildId 1240455 -FailedOnly
27+
28+
.EXAMPLE
29+
./Get-BuildInfo.ps1 -BuildId 1240455 -Org "dnceng-public" -Project "public"
30+
31+
.OUTPUTS
32+
Object with BuildId, Status, Result, Stages, and FailedJobs properties.
33+
#>
34+
35+
[CmdletBinding()]
36+
param(
37+
[Parameter(Mandatory = $true, Position = 0)]
38+
[string]$BuildId,
39+
40+
[Parameter(Mandatory = $false)]
41+
[string]$Org = "dnceng-public",
42+
43+
[Parameter(Mandatory = $false)]
44+
[string]$Project = "public",
45+
46+
[Parameter(Mandatory = $false)]
47+
[switch]$FailedOnly
48+
)
49+
50+
$ErrorActionPreference = "Stop"
51+
52+
# Get build info
53+
$buildUrl = "https://dev.azure.com/$Org/$Project/_apis/build/builds/${BuildId}?api-version=7.0"
54+
$timelineUrl = "https://dev.azure.com/$Org/$Project/_apis/build/builds/$BuildId/timeline?api-version=7.0"
55+
56+
try {
57+
$build = Invoke-RestMethod -Uri $buildUrl -Method Get -ContentType "application/json"
58+
$timeline = Invoke-RestMethod -Uri $timelineUrl -Method Get -ContentType "application/json"
59+
}
60+
catch {
61+
Write-Error "Failed to query Azure DevOps API: $_"
62+
exit 1
63+
}
64+
65+
# Extract stages
66+
$stages = $timeline.records | Where-Object { $_.type -eq "Stage" } | ForEach-Object {
67+
[PSCustomObject]@{
68+
Name = $_.name
69+
State = $_.state
70+
Result = $_.result
71+
}
72+
} | Sort-Object -Property { $_.State -eq "completed" }, { $_.State -eq "inProgress" }
73+
74+
# Extract failed/canceled jobs
75+
$failedJobs = $timeline.records |
76+
Where-Object {
77+
($_.type -eq "Stage" -or $_.type -eq "Job") -and
78+
($_.result -eq "failed" -or $_.result -eq "canceled")
79+
} |
80+
ForEach-Object {
81+
[PSCustomObject]@{
82+
Name = $_.name
83+
Type = $_.type
84+
Result = $_.result
85+
}
86+
} | Sort-Object -Property Type, Name
87+
88+
if ($FailedOnly) {
89+
$failedJobs
90+
}
91+
else {
92+
[PSCustomObject]@{
93+
BuildId = $BuildId
94+
BuildNumber = $build.buildNumber
95+
Status = $build.status
96+
Result = $build.result
97+
Pipeline = $build.definition.name
98+
StartTime = $build.startTime
99+
FinishTime = $build.finishTime
100+
Stages = $stages
101+
FailedJobs = $failedJobs
102+
Link = "https://dev.azure.com/$Org/$Project/_build/results?buildId=$BuildId"
103+
}
104+
}

0 commit comments

Comments
 (0)