When a pipeline fails, the default notification says "Build Failed" with a link. You want to know why it failed without clicking through. Here's how to capture and include error details in notifications.
Capturing Error Output
Store Task Output in Variables
- task: PowerShell@2
name: RunTests
displayName: 'Run Tests'
continueOnError: true
inputs:
targetType: 'inline'
script: |
try {
$output = npm test 2>&1
Write-Host $output
echo "##vso[task.setvariable variable=TestOutput;isOutput=true]$($output -join "`n")"
echo "##vso[task.setvariable variable=TestResult;isOutput=true]Success"
}
catch {
$errorMsg = $_.Exception.Message
Write-Host "##vso[task.logissue type=error]$errorMsg"
echo "##vso[task.setvariable variable=TestOutput;isOutput=true]$errorMsg"
echo "##vso[task.setvariable variable=TestResult;isOutput=true]Failed"
exit 1
}
Capture Build Logs
- task: PowerShell@2
displayName: 'Capture Build Errors'
condition: failed()
inputs:
targetType: 'inline'
script: |
$org = "$(System.CollectionUri)".TrimEnd('/')
$project = "$(System.TeamProject)"
$buildId = "$(Build.BuildId)"
$pat = "$(System.AccessToken)"
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$headers = @{ Authorization = "Basic $base64Auth" }
# Get timeline (contains all task results)
$timelineUrl = "$org/$project/_apis/build/builds/$buildId/timeline?api-version=7.0"
$timeline = Invoke-RestMethod -Uri $timelineUrl -Headers $headers
# Find failed tasks
$failedTasks = $timeline.records |
Where-Object { $_.result -eq "failed" -and $_.type -eq "Task" } |
Select-Object name, @{N='issues';E={$_.issues | ForEach-Object { $_.message }}}
$errorSummary = $failedTasks | ForEach-Object {
"Task: $($_.name)`n$($_.issues -join "`n")"
}
echo "##vso[task.setvariable variable=ErrorSummary]$($errorSummary -join "`n`n")"
Sending Detailed Failure Notifications
- task: PowerShell@2
displayName: 'Send Failure Email'
condition: failed()
inputs:
targetType: 'inline'
script: |
$errorDetails = "$(ErrorSummary)"
if ([string]::IsNullOrEmpty($errorDetails)) {
$errorDetails = "No specific error captured. Check build logs."
}
$htmlBody = @"
<html>
<body style="font-family: Arial, sans-serif;">
<h2 style="color: #dc3545;">Pipeline Failed</h2>
<table style="border-collapse: collapse; margin: 20px 0;">
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Pipeline</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$(Build.DefinitionName)</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Build</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$(Build.BuildNumber)</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Branch</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$(Build.SourceBranchName)</td>
</tr>
<tr>
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Triggered By</strong></td>
<td style="padding: 8px; border: 1px solid #ddd;">$(Build.RequestedFor)</td>
</tr>
</table>
<h3>Error Details</h3>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto;">
$($errorDetails)
</pre>
<p>
<a href="$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
style="background: #0078d4; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
View Build Logs
</a>
</p>
</body>
</html>
"@
$body = @{
personalizations = @(
@{
to = @(@{ email = "$(FailureNotificationEmail)" })
subject = "FAILED: $(Build.DefinitionName) #$(Build.BuildNumber)"
}
)
from = @{ email = "[email protected]"; name = "Azure DevOps" }
content = @(@{ type = "text/html"; value = $htmlBody })
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri "https://api.sendgrid.com/v3/mail/send" `
-Method Post `
-Headers @{ "Authorization" = "Bearer $(SendGridApiKey)" } `
-ContentType "application/json" `
-Body $body
Including Test Results
- task: PublishTestResults@2
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/test-results.xml'
- task: PowerShell@2
displayName: 'Get Test Summary'
inputs:
targetType: 'inline'
script: |
$org = "$(System.CollectionUri)".TrimEnd('/')
$project = "$(System.TeamProject)"
$buildId = "$(Build.BuildId)"
$pat = "$(System.AccessToken)"
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$headers = @{ Authorization = "Basic $base64Auth" }
# Get test runs
$runsUrl = "$org/$project/_apis/test/runs?buildId=$buildId&api-version=7.0"
$runs = Invoke-RestMethod -Uri $runsUrl -Headers $headers
$summary = foreach ($run in $runs.value) {
$resultsUrl = "$org/$project/_apis/test/runs/$($run.id)/results?api-version=7.0"
$results = Invoke-RestMethod -Uri $resultsUrl -Headers $headers
$failed = $results.value | Where-Object { $_.outcome -eq "Failed" }
if ($failed) {
foreach ($test in $failed) {
"FAILED: $($test.testCaseTitle)`n$($test.errorMessage)"
}
}
}
echo "##vso[task.setvariable variable=FailedTests]$($summary -join "`n`n")"
Slack/Teams Alternative
For faster notification, use webhooks:
Microsoft Teams
- task: PowerShell@2
condition: failed()
inputs:
targetType: 'inline'
script: |
$webhookUrl = "$(TeamsWebhookUrl)"
$body = @{
"@type" = "MessageCard"
"@context" = "http://schema.org/extensions"
themeColor = "dc3545"
summary = "Build Failed"
sections = @(
@{
activityTitle = "Pipeline Failed: $(Build.DefinitionName)"
facts = @(
@{ name = "Build"; value = "$(Build.BuildNumber)" }
@{ name = "Branch"; value = "$(Build.SourceBranchName)" }
@{ name = "Error"; value = "$(ErrorSummary)" }
)
markdown = $true
}
)
potentialAction = @(
@{
"@type" = "OpenUri"
name = "View Build"
targets = @(
@{
os = "default"
uri = "$(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
}
)
}
)
} | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $webhookUrl -Method Post -Body $body -ContentType "application/json"
Notification Template
Store in repo for consistency:
<!-- templates/failure-email.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }
.header { background: #dc3545; color: white; padding: 20px; }
.content { padding: 20px; }
.error-box { background: #f8d7da; border: 1px solid #f5c6cb; padding: 15px; border-radius: 5px; }
.button { background: #0078d4; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; }
table { width: 100%; border-collapse: collapse; margin: 15px 0; }
td { padding: 8px; border: 1px solid #ddd; }
</style>
</head>
<body>
<div class="header">
<h2>{{PipelineName}} - Build Failed</h2>
</div>
<div class="content">
<table>
<tr><td><strong>Build</strong></td><td>{{BuildNumber}}</td></tr>
<tr><td><strong>Branch</strong></td><td>{{Branch}}</td></tr>
<tr><td><strong>Triggered By</strong></td><td>{{RequestedFor}}</td></tr>
</table>
<h3>Error Summary</h3>
<div class="error-box">
<pre>{{ErrorDetails}}</pre>
</div>
<p><a href="{{BuildUrl}}" class="button">View Build Logs</a></p>
</div>
</body>
</html>
Need help improving your DevOps workflows? Get in touch - we help teams build better CI/CD processes.