Guide
CI/CD Email Alerts with PowerShell
According to the 2024 State of DevOps Report, teams that get fast feedback on build failures deploy 208x more frequently. This guide shows how to send build notifications, deployment reports, and test failure alerts from GitHub Actions, Azure DevOps, and Jenkins pipelines using PowerShell and Nylas CLI. Works across all major email providers -- no SMTP relay needed.
Written by Aaron de Mello Senior Engineering Manager
Reviewed by Caleb Geene
Why not SMTP from CI/CD?
SMTP relays and provider-specific plugins introduce auth failures, firewall restrictions, and vendor lock-in that break CI/CD email notifications. According to Google's 2024 announcement, "less secure app access" was fully disabled in September 2024, which means SMTP-based pipelines using Gmail credentials stopped working entirely. Nylas CLI sends email through OAuth2 instead -- one binary and one API key across every CI platform.
- SMTP relays require firewall rules -- your runner needs outbound access to port 587 or 465, which many enterprise networks block
- Gmail and Outlook block basic auth -- Google disabled "less secure app access" in September 2024; Microsoft deprecated basic auth for Exchange Online SMTP in January 2023
- Credentials in CI secrets -- SMTP passwords rot, and rotating them means updating every pipeline
- Plugin lock-in -- GitHub's email action, Azure DevOps' built-in notifications, and Jenkins' SMTP plugin each have different configs
Pipeline setup (all platforms)
Nylas CLI installs in under 10 seconds on CI runners via a single shell or PowerShell command, then authenticates headlessly with an API key stored as a CI secret. The install scripts auto-detect the runner's architecture (x86_64 or ARM64) and verify the binary with a SHA-256 checksum before placing it in ~/.config/nylas/bin. See the getting started guide for other install methods.
Linux and macOS runners use the shell install script. Windows runners use the PowerShell install script. After install, authenticate with nylas auth config --api-key using a secret environment variable -- this stores the key locally for subsequent commands in the same job.
# Linux/macOS runners
curl -fsSL https://cli.nylas.com/install.sh | bash
# Windows runners
irm https://cli.nylas.com/install.ps1 | iex
# Authenticate with API key (headless, no browser needed)
nylas auth config --api-key $env:NYLAS_API_KEYGitHub Actions: build failure notification
GitHub Actions can send build failure emails by running Nylas CLI in a conditional pwsh step that only executes when a prior step fails. The workflow installs the CLI, authenticates with an API key from GitHub repository secrets, and sends an email containing the commit SHA, branch name, actor, and a direct link to the failed run. According to GitHub's 2024 usage data, over 75% of Actions workflows run on ubuntu-latest runners, which support both bash and pwsh shells natively.
The if: failure() conditional ensures the notification step only runs when the build step fails. The CLI installs in under 10 seconds on Ubuntu runners, so the overhead is minimal even on failed builds.
# .github/workflows/build-notify.yml
name: Build with Email Notification
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Install Nylas CLI
if: failure()
run: |
curl -fsSL https://cli.nylas.com/install.sh | bash
echo "$HOME/.config/nylas/bin" >> $GITHUB_PATH
- name: Send failure notification
if: failure()
shell: pwsh
env:
NYLAS_API_KEY: ${{ secrets.NYLAS_API_KEY }}
run: |
nylas auth config --api-key $env:NYLAS_API_KEY
$subject = "Build FAILED: ${{ github.repository }} @ ${{ github.sha }}"
$body = @"
Build failed on ${{ github.ref_name }}
Repository: ${{ github.repository }}
Commit: ${{ github.sha }}
Author: ${{ github.actor }}
Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Triggered by push from ${{ github.actor }} at $(Get-Date -Format 'yyyy-MM-dd HH:mm UTC').
"@
nylas email send `
--to "team@company.com" `
--subject $subject `
--body $body `
--yesGitHub Actions: deployment report
A deployment report email gives stakeholders immediate visibility into what shipped, how long the deploy took, and which commits were included. This GitHub Actions workflow uses workflow_dispatch with a choice input for environment selection, records a start timestamp using $GITHUB_OUTPUT, and generates an HTML email with an inline git changelog from the last tag to HEAD. The 2024 State of DevOps Report found that teams with automated deployment notifications resolve incidents 2.5x faster than teams relying on manual status checks.
The workflow requires fetch-depth: 0 on the checkout step so that git log and git describe have full commit history available. PowerShell's here-string syntax (@"..."@) constructs the HTML body with embedded pipeline variables.
# .github/workflows/deploy-report.yml
name: Deploy and Report
on:
workflow_dispatch:
inputs:
environment:
description: "Target environment"
required: true
type: choice
options: [staging, production]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for changelog
- name: Record start time
id: timer
run: echo "start=$(date +%s)" >> $GITHUB_OUTPUT
- name: Deploy to ${{ inputs.environment }}
run: ./scripts/deploy.sh ${{ inputs.environment }}
- name: Install Nylas CLI
run: |
curl -fsSL https://cli.nylas.com/install.sh | bash
echo "$HOME/.config/nylas/bin" >> $GITHUB_PATH
- name: Send deployment report
shell: pwsh
env:
NYLAS_API_KEY: ${{ secrets.NYLAS_API_KEY }}
run: |
nylas auth config --api-key $env:NYLAS_API_KEY
# Calculate deployment duration
$startTime = ${{ steps.timer.outputs.start }}
$endTime = [int](Get-Date -UFormat %s)
$duration = $endTime - $startTime
# Get changelog (commits since last tag)
$changelog = git log --oneline $(git describe --tags --abbrev=0)..HEAD |
ForEach-Object { "<li>$_</li>" }
$html = @"
<html><body style="font-family:system-ui;background:#0f172a;color:#e2e8f0;padding:24px">
<h1 style="color:#4ade80">Deployed to ${{ inputs.environment }}</h1>
<table style="border-collapse:collapse">
<tr><td style="padding:4px 16px 4px 0;color:#94a3b8">Branch</td>
<td>${{ github.ref_name }}</td></tr>
<tr><td style="padding:4px 16px 4px 0;color:#94a3b8">Commit</td>
<td><code>${{ github.sha }}</code></td></tr>
<tr><td style="padding:4px 16px 4px 0;color:#94a3b8">Duration</td>
<td>$($duration)s</td></tr>
</table>
<h2 style="color:#4ade80;margin-top:20px">Changelog</h2>
<ul>$($changelog -join '')</ul>
</body></html>
"@
nylas email send `
--to "engineering@company.com" `
--subject "Deployed to ${{ inputs.environment }}: ${{ github.repository }}" `
--body $html `
--yesAzure DevOps: test failure notification
Azure DevOps pipelines run PowerShell natively through the PowerShell@2 task, which is available on all Microsoft-hosted agent images (Windows, Ubuntu, and macOS). This pipeline installs Nylas CLI on a windows-latest agent, runs .NET tests that output .trx result files, then parses those results and emails a summary of failed test names and error messages. According to Microsoft's Azure DevOps documentation, the condition: failed() expression evaluates to true when any prior step in the job has failed.
The API key is stored in a variable group named nylas-secrets, which can be linked to Azure Key Vault for automatic rotation. The .trx XML format includes the full TestRun.ResultSummary.Counters node with total, passed, and failed counts.
# azure-pipelines.yml
trigger:
branches:
include: [main]
pool:
vmImage: "windows-latest"
variables:
- group: nylas-secrets # Contains NYLAS_API_KEY
steps:
- task: PowerShell@2
displayName: "Install Nylas CLI"
inputs:
targetType: inline
script: irm https://cli.nylas.com/install.ps1 | iex
- task: DotNetCoreCLI@2
displayName: "Run tests"
inputs:
command: test
arguments: '--logger "trx;LogFileName=results.trx"'
continueOnError: true
- task: PowerShell@2
displayName: "Send test failure report"
condition: failed()
inputs:
targetType: inline
script: |
nylas auth config --api-key $(NYLAS_API_KEY)
# Parse .trx test results
$trxFile = Get-ChildItem -Recurse -Filter "results.trx" |
Select-Object -First 1 -ExpandProperty FullName
[xml]$trx = Get-Content $trxFile
$total = $trx.TestRun.ResultSummary.Counters.total
$passed = $trx.TestRun.ResultSummary.Counters.passed
$failed = $trx.TestRun.ResultSummary.Counters.failed
$failures = $trx.TestRun.Results.UnitTestResult |
Where-Object { $_.outcome -eq "Failed" } |
ForEach-Object { "- $($_.testName): $($_.Output.ErrorInfo.Message)" }
$body = @"
Build $(Build.BuildNumber) FAILED on $(Build.SourceBranch)
Test results: $passed/$total passed, $failed failed
Failed tests:
$($failures -join [Environment]::NewLine)
Pipeline: $(System.TeamFoundationCollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)
"@
nylas email send --to "team@company.com" `
--subject "FAILED: $(Build.DefinitionName) #$(Build.BuildNumber)" `
--body $body --yes
env:
NYLAS_API_KEY: $(NYLAS_API_KEY)Jenkins: Jenkinsfile with email alerts
Jenkins Declarative Pipelines call Nylas CLI from pwsh steps defined in post blocks, which run after the main stage completes regardless of outcome. The API key is stored in Jenkins Credentials and injected via the credentials() helper. According to the 2024 Jenkins Community Survey, over 60% of Jenkins installations run on Linux agents -- this Jenkinsfile uses sh for install and pwsh for email composition, which works on any agent with PowerShell Core 7+ installed.
The post { failure { ... } } block sends a notification only when the Test stage fails. The post { success { ... } } block at the pipeline level sends a confirmation when all stages pass. Jenkins injects JOB_NAME, BUILD_NUMBER, GIT_BRANCH, and BUILD_URL as environment variables automatically.
// Jenkinsfile
pipeline {
agent any
environment {
NYLAS_API_KEY = credentials('nylas-api-key')
}
stages {
stage('Setup') {
steps {
sh 'curl -fsSL https://cli.nylas.com/install.sh | bash'
sh 'nylas auth config --api-key $NYLAS_API_KEY'
}
}
stage('Build') {
steps { sh 'make build' }
}
stage('Test') {
steps { sh 'make test' }
post {
failure {
pwsh """
\$body = @"
Build failed in stage: Test
Job: \$env:JOB_NAME
Build: #\$env:BUILD_NUMBER
Branch: \$env:GIT_BRANCH
URL: \$env:BUILD_URL
"@
nylas email send --to 'team@company.com' `
--subject "Jenkins FAILED: \$env:JOB_NAME #\$env:BUILD_NUMBER" `
--body \$body --yes
"""
}
}
}
}
post {
success {
pwsh """
nylas email send --to 'team@company.com' `
--subject "Jenkins PASSED: \$env:JOB_NAME #\$env:BUILD_NUMBER" `
--body "All stages passed. Build: \$env:BUILD_URL" `
--yes
"""
}
}
}Parse test results into email summaries
JUnit XML and Pester NUnit XML are the two most common test result formats across CI/CD pipelines. JUnit XML is the default output for pytest, Go's go test -v (via gotestfmt), and Java's Maven Surefire plugin -- covering an estimated 80% of test suites in GitHub Actions. Pester, PowerShell's native testing framework, outputs NUnit-compatible XML. Both formats store pass/fail counts and error messages in structured XML nodes that PowerShell can parse with [xml] type accelerator.
These two functions extract total count, failure count, and a list of failed test names from each format. Call either function, check if $summary.Failed is greater than zero, and pipe the details into nylas email send for a targeted failure notification.
# Parse JUnit XML (pytest, Go, Java)
function Get-JUnitSummary {
param([string]$Path)
[xml]$xml = Get-Content $Path
$suites = $xml.testsuites.testsuite
$total = ($suites | Measure-Object -Property tests -Sum).Sum
$failures = ($suites | Measure-Object -Property failures -Sum).Sum
$failedTests = $suites.testcase |
Where-Object { $_.failure } |
ForEach-Object { "- $($_.classname).$($_.name)" }
return @{ Total = $total; Failed = $failures; Details = $failedTests -join "`n" }
}
# Parse Pester XML
function Get-PesterSummary {
param([string]$Path)
[xml]$xml = Get-Content $Path
$r = $xml.'test-results'
$failedTests = $r.'test-suite'.results.'test-case' |
Where-Object { $_.result -eq 'Failure' } |
ForEach-Object { "- $($_.name)" }
return @{ Total = $r.total; Failed = $r.failures; Details = $failedTests -join "`n" }
}
# Use in any pipeline
$summary = Get-JUnitSummary -Path "./test-results.xml"
if ($summary.Failed -gt 0) {
nylas email send --to "team@company.com" `
--subject "Test failures: $($summary.Failed)/$($summary.Total)" `
--body $summary.Details --yes
}Smart notification patterns
Sending an email on every failed build creates alert fatigue -- teams that receive more than 50 notifications per day start ignoring them, according to PagerDuty's 2023 State of Digital Operations report. Smart notification patterns reduce noise by filtering duplicates, targeting the commit author instead of the whole team, and escalating only after consecutive failures. These three PowerShell patterns use file-based state tracking (last-build-status.txt and fail-count.txt) to remember previous build outcomes between runs.
Pattern 1 sends email only on the first failure after a passing streak. Pattern 2 uses git log -1 --format='%ae' to extract the commit author's email and notify them directly. Pattern 3 increments a counter and escalates to a lead after 3 or more consecutive failures.
# Pattern 1: Only notify on first failure (not repeated failures)
$lastStatus = if (Test-Path "./last-build-status.txt") {
Get-Content "./last-build-status.txt"
} else { "success" }
$currentStatus = "failure" # from your build step
if ($currentStatus -eq "failure" -and $lastStatus -eq "success") {
nylas email send --to "team@company.com" `
--subject "Build broke! Was passing, now failing." `
--body "First failure after a passing streak. Investigate." --yes
}
$currentStatus | Out-File "./last-build-status.txt"
# Pattern 2: Notify the commit author (not the whole team)
$authorEmail = git log -1 --format='%ae'
nylas email send --to $authorEmail `
--subject "Your commit broke the build" `
--body "Commit $(git log -1 --format='%h'): $(git log -1 --format='%s')" --yes
# Pattern 3: Escalate after 3+ consecutive failures
$failCount = if (Test-Path "./fail-count.txt") {
[int](Get-Content "./fail-count.txt")
} else { 0 }
$failCount++
$failCount | Out-File "./fail-count.txt"
if ($failCount -ge 3) {
nylas email send --to "engineering-lead@company.com" `
--subject "Build broken for $failCount consecutive runs" `
--body "Nobody has fixed the build. Escalating." --yes
}Automated release notes email
Automated release notes eliminate the manual step of writing and distributing changelogs after each release. This PowerShell script reads the current git tag, finds the previous tag with git describe --tags --abbrev=0 HEAD~1, and groups commits between the two tags into features and fixes based on conventional commit prefixes (feat, add, fix). Teams using conventional commits report that automated changelogs reduce release communication time by roughly 30 minutes per release, according to the Conventional Commits project's FAQ.
The script runs as a post-tag CI step or manually via pwsh release-notes-email.ps1. It exits cleanly with code 0 if HEAD is not on a tag, so it won't fail unrelated pipeline runs.
# release-notes-email.ps1 -- Run after git tag
$currentTag = git describe --tags --exact-match 2>$null
$previousTag = git describe --tags --abbrev=0 HEAD~1 2>$null
if (-not $currentTag) { Write-Host "Not on a tag."; exit 0 }
# Group commits by type
$commits = git log --oneline "$previousTag..$currentTag"
$features = $commits | Where-Object { $_ -match "^[a-f0-9]+ (feat|add)" }
$fixes = $commits | Where-Object { $_ -match "^[a-f0-9]+ fix" }
$body = @"
Release $currentTag ($(Get-Date -Format 'yyyy-MM-dd'))
Previous: $previousTag
Features ($($features.Count)):
$($features | ForEach-Object { " $_" } | Out-String)
Fixes ($($fixes.Count)):
$($fixes | ForEach-Object { " $_" } | Out-String)
"@
nylas email send --to "stakeholders@company.com" `
--subject "Release $currentTag published" `
--body $body --yesFrequently asked questions
How do I send email notifications from GitHub Actions?
Install Nylas CLI with curl -fsSL https://cli.nylas.com/install.sh | bash in a setup step. Store your API key as a repository secret. Then call nylas email send from a pwsh step. No SMTP relay or email action plugin needed.
Can I send deployment reports from Azure DevOps pipelines?
Yes. Use a PowerShell@2 task that builds a report from pipeline variables like $(Build.BuildNumber) and $(Build.SourceBranch), then sends it with nylas email send. Store NYLAS_API_KEY in a variable group.
How do I send test failure alerts from CI/CD?
Parse your test results (JUnit XML, Pester, .trx) in a PowerShell step. Extract failing test names and error messages. Send a summary with Nylas CLI. Run this step conditionally: if: failure() in GitHub Actions, condition: failed() in Azure DevOps.
Next steps
- Server monitoring alerts with PowerShell -- disk space, service health, Event Log alerts
- Automated email reports with PowerShell -- CSV attachments, HTML tables, weekly digests
- Send email from PowerShell -- the foundation guide for PowerShell email
- Full command reference -- every flag and subcommand
- GitHub Actions: OIDC security hardening — the supported way to inject short-lived secrets into pipelines
- Azure DevOps: PowerShell task reference — canonical task inputs, error handling, and conditional execution
- GitHub Actions: workflow variables and outputs — how to surface build state into notification payloads