Skip to content

Commit d5d02a1

Browse files
authored
[release/v7.5] Add GitHub Actions annotations for Pester test failures (#26836)
1 parent 9aa54ee commit d5d02a1

3 files changed

Lines changed: 121 additions & 2 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
@@ -1864,6 +1864,69 @@ $stack_trace
18641864

18651865
}
18661866

1867+
function Get-PesterFailureFileInfo
1868+
{
1869+
[CmdletBinding()]
1870+
param (
1871+
[Parameter(Mandatory)]
1872+
[string]$StackTraceString
1873+
)
1874+
1875+
# Parse stack trace to extract file path and line number
1876+
# Common patterns:
1877+
# "at line: 123 in C:\path\to\file.ps1" (Pester 4)
1878+
# "at C:\path\to\file.ps1:123"
1879+
# "at <ScriptBlock>, C:\path\to\file.ps1: line 123"
1880+
# "at 1 | Should -Be 2, /path/to/file.ps1:123" (Pester 5)
1881+
# "at 1 | Should -Be 2, C:\path\to\file.ps1:123" (Pester 5 Windows)
1882+
1883+
$result = @{
1884+
File = $null
1885+
Line = $null
1886+
}
1887+
1888+
if ([string]::IsNullOrWhiteSpace($StackTraceString)) {
1889+
return $result
1890+
}
1891+
1892+
# Try pattern: "at line: 123 in <path>" (Pester 4)
1893+
if ($StackTraceString -match 'at line:\s*(\d+)\s+in\s+(.+?)(?:\r|\n|$)') {
1894+
$result.Line = $matches[1]
1895+
$result.File = $matches[2].Trim()
1896+
return $result
1897+
}
1898+
1899+
# Try pattern: ", <path>:123" (Pester 5 format)
1900+
# This handles both Unix paths (/path/file.ps1:123) and Windows paths (C:\path\file.ps1:123)
1901+
if ($StackTraceString -match ',\s*((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') {
1902+
$result.File = $matches[1].Trim()
1903+
$result.Line = $matches[2]
1904+
return $result
1905+
}
1906+
1907+
# Try pattern: "at <path>:123" (without comma)
1908+
# Handle both absolute Unix and Windows paths
1909+
if ($StackTraceString -match 'at\s+((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):(\d+)(?:\r|\n|$)') {
1910+
$result.File = $matches[1].Trim()
1911+
$result.Line = $matches[2]
1912+
return $result
1913+
}
1914+
1915+
# Try pattern: "<path>: line 123"
1916+
if ($StackTraceString -match '((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):\s*line\s+(\d+)(?:\r|\n|$)') {
1917+
$result.File = $matches[1].Trim()
1918+
$result.Line = $matches[2]
1919+
return $result
1920+
}
1921+
1922+
# Try to extract just the file path if no line number found
1923+
if ($StackTraceString -match '(?:at\s+|in\s+)?((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1)') {
1924+
$result.File = $matches[1].Trim()
1925+
}
1926+
1927+
return $result
1928+
}
1929+
18671930
function Test-XUnitTestResults
18681931
{
18691932
param(

tools/metadata.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
"StableReleaseTag": "v7.4.4",
33
"PreviewReleaseTag": "v7.5.0-preview.3",
44
"ServicingReleaseTag": "v7.0.13",
5-
"ReleaseTag": "v7.5.4",
5+
"ReleaseTag": "v7.4.13",
66
"LTSReleaseTag" : ["v7.4.13"],
7-
"NextReleaseTag": "v7.6.0-preview.6",
7+
"NextReleaseTag": "v7.5.0-preview.4",
88
"LTSRelease": { "PublishToChannels": false, "Package": false },
99
"StableRelease": { "PublishToChannels": false, "Package": false }
1010
}

0 commit comments

Comments
 (0)