Skip to content

Commit b827288

Browse files
authored
[release/v7.4] Add GitHub Actions annotations for Pester test failures (#26800)
1 parent e41290f commit b827288

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

.github/actions/test/process-pester-results/process-pester-results.ps1

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ $testIgnoredCount = 0
2424
$testSkippedCount = 0
2525
$testInvalidCount = 0
2626

27+
# Process test results and generate annotations for failures
2728
Get-ChildItem -Path "${TestResultsFolder}/*.xml" -Recurse | ForEach-Object {
2829
$results = [xml] (get-content $_.FullName)
2930

@@ -35,6 +36,61 @@ Get-ChildItem -Path "${TestResultsFolder}/*.xml" -Recurse | ForEach-Object {
3536
$testIgnoredCount += [int]$results.'test-results'.ignored
3637
$testSkippedCount += [int]$results.'test-results'.skipped
3738
$testInvalidCount += [int]$results.'test-results'.invalid
39+
40+
# Generate GitHub Actions annotations for test failures
41+
# Select failed test cases
42+
if ("System.Xml.XmlDocumentXPathExtensions" -as [Type]) {
43+
$failures = [System.Xml.XmlDocumentXPathExtensions]::SelectNodes($results.'test-results', './/test-case[@result = "Failure"]')
44+
}
45+
else {
46+
$failures = $results.SelectNodes('.//test-case[@result = "Failure"]')
47+
}
48+
49+
foreach ($testfail in $failures) {
50+
$description = $testfail.description
51+
$testName = $testfail.name
52+
$message = $testfail.failure.message
53+
$stack_trace = $testfail.failure.'stack-trace'
54+
55+
# Parse stack trace to get file and line info
56+
$fileInfo = Get-PesterFailureFileInfo -StackTraceString $stack_trace
57+
58+
if ($fileInfo.File) {
59+
# Convert absolute path to relative path for GitHub Actions
60+
$filePath = $fileInfo.File
61+
62+
# GitHub Actions expects paths relative to the workspace root
63+
if ($env:GITHUB_WORKSPACE) {
64+
$workspacePath = $env:GITHUB_WORKSPACE
65+
if ($filePath.StartsWith($workspacePath)) {
66+
$filePath = $filePath.Substring($workspacePath.Length).TrimStart('/', '\')
67+
# Normalize to forward slashes for consistency
68+
$filePath = $filePath -replace '\\', '/'
69+
}
70+
}
71+
72+
# Create annotation title
73+
$annotationTitle = "Test Failure: $description / $testName"
74+
75+
# Build the annotation message
76+
$annotationMessage = $message -replace "`n", "%0A" -replace "`r"
77+
78+
# Build and output the workflow command
79+
$workflowCommand = "::error file=$filePath"
80+
if ($fileInfo.Line) {
81+
$workflowCommand += ",line=$($fileInfo.Line)"
82+
}
83+
$workflowCommand += ",title=$annotationTitle::$annotationMessage"
84+
85+
Write-Host $workflowCommand
86+
87+
# Output a link to the test run
88+
if ($env:GITHUB_SERVER_URL -and $env:GITHUB_REPOSITORY -and $env:GITHUB_RUN_ID) {
89+
$logUrl = "$($env:GITHUB_SERVER_URL)/$($env:GITHUB_REPOSITORY)/actions/runs/$($env:GITHUB_RUN_ID)"
90+
Write-Host "Test logs: $logUrl"
91+
}
92+
}
93+
}
3894
}
3995

4096
@"

build.psm1

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,69 @@ $stack_trace
18461846

18471847
}
18481848

1849+
function Get-PesterFailureFileInfo
1850+
{
1851+
[CmdletBinding()]
1852+
param (
1853+
[Parameter(Mandatory)]
1854+
[string]$StackTraceString
1855+
)
1856+
1857+
# Parse stack trace to extract file path and line number
1858+
# Common patterns:
1859+
# "at line: 123 in C:\path\to\file.ps1" (Pester 4)
1860+
# "at C:\path\to\file.ps1:123"
1861+
# "at <ScriptBlock>, C:\path\to\file.ps1: line 123"
1862+
# "at 1 | Should -Be 2, /path/to/file.ps1:123" (Pester 5)
1863+
# "at 1 | Should -Be 2, C:\path\to\file.ps1:123" (Pester 5 Windows)
1864+
1865+
$result = @{
1866+
File = $null
1867+
Line = $null
1868+
}
1869+
1870+
if ([string]::IsNullOrWhiteSpace($StackTraceString)) {
1871+
return $result
1872+
}
1873+
1874+
# Try pattern: "at line: 123 in <path>" (Pester 4)
1875+
if ($StackTraceString -match 'at line:\s*(\d+)\s+in\s+(.+?)(?:\r|\n|$)') {
1876+
$result.Line = $matches[1]
1877+
$result.File = $matches[2].Trim()
1878+
return $result
1879+
}
1880+
1881+
# Try pattern: ", <path>:123" (Pester 5 format)
1882+
# This handles both Unix paths (/path/file.ps1:123) and Windows paths (C:\path\file.ps1:123)
1883+
if ($StackTraceString -match ',\s*((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') {
1884+
$result.File = $matches[1].Trim()
1885+
$result.Line = $matches[2]
1886+
return $result
1887+
}
1888+
1889+
# Try pattern: "at <path>:123" (without comma)
1890+
# Handle both absolute Unix and Windows paths
1891+
if ($StackTraceString -match 'at\s+((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):(\d+)(?:\r|\n|$)') {
1892+
$result.File = $matches[1].Trim()
1893+
$result.Line = $matches[2]
1894+
return $result
1895+
}
1896+
1897+
# Try pattern: "<path>: line 123"
1898+
if ($StackTraceString -match '((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):\s*line\s+(\d+)(?:\r|\n|$)') {
1899+
$result.File = $matches[1].Trim()
1900+
$result.Line = $matches[2]
1901+
return $result
1902+
}
1903+
1904+
# Try to extract just the file path if no line number found
1905+
if ($StackTraceString -match '(?:at\s+|in\s+)?((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1)') {
1906+
$result.File = $matches[1].Trim()
1907+
}
1908+
1909+
return $result
1910+
}
1911+
18491912
function Test-XUnitTestResults
18501913
{
18511914
param(

0 commit comments

Comments
 (0)