Guide

Send Email Notifications from CI/CD Pipelines

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 with Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP -- no SMTP relay needed.

By Pouya Sanooei

Why not SMTP from CI/CD?

Most CI/CD email notification setups rely on SMTP relays or provider plugins. Both have problems:

  • 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 is deprecating basic auth for SMTP
  • 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

Nylas CLI sends email through OAuth2 via any provider. One binary, one API key, every CI platform.

Pipeline setup (all platforms)

Install Nylas CLI and authenticate using an API key. See the PowerShell email guide for full install details.

# 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

This workflow sends an email when a build fails on main. The email includes the commit SHA, branch, actor, and a direct link to the failed run.

# .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/.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

After a successful deployment, send an HTML report with environment details, duration, and a git changelog.

# .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/.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 support PowerShell natively with PowerShell@2 tasks. This pipeline sends a notification on test failure with parsed .trx results.

# 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 pipelines call Nylas CLI from PowerShell steps. Store the API key in Jenkins Credentials.

// 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

Different test frameworks output different formats. Here are PowerShell parsers for JUnit XML and Pester results.

# 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

Don't spam your team on every build. Target notifications to the right people at the right time.

# 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

After tagging a release, generate notes from git history and email them to stakeholders.

# 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