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

VerifiedCLI 3.1.1 · Gmail, Outlook · last tested April 11, 2026

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_KEY

GitHub 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 `
              --yes

GitHub 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 `
              --yes

Azure 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 --yes

Frequently 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