A composite GitHub Action that posts Adaptive Cards to Microsoft Teams via Incoming Webhooks.
Office 365 Connectors Deprecation: Microsoft has announced that Office 365 Connectors, including Incoming Webhooks, are being retired. While this action continues to work with existing webhooks, Microsoft recommends migrating to Workflows (Power Automate) for new integrations. See Microsoft's retirement announcement for timeline and migration guidance.
- Sends rich Adaptive Card notifications to Microsoft Teams channels
- Status styling with colors and emoji (success/failure/warning)
- Displays repository, actor, and optional environment information
- Optional collapsible commit history section
- UTF-8-safe emoji handling (constructed from Unicode code points)
- PowerShell 7 with no external dependencies
- Works on Linux, Windows, and macOS runners
Minimal usage (sends on success and failure):
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Teams notification
if: ${{ always() }}
uses: marcus-hooper/send-teams-notification@v1
with:
job_status: ${{ job.status }}
webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }}Note: The
if: ${{ always() }}condition ensures the notification step runs regardless of whether previous steps succeeded or failed. Without this, the notification would be skipped when the job failsβwhich is usually when you most want to be notified.
With environment and custom title:
- name: Send Teams notification
if: ${{ always() }}
uses: marcus-hooper/send-teams-notification@v1
with:
job_status: ${{ job.status }}
environment: production
card_title: "π Deployment"
webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }}With commit messages (collapsible section in card):
- name: Get recent commits
id: commits
run: |
COMMITS=$(git log --pretty=format:'{"title":"%h","value":"[%s](https://github.com/${{ github.repository }}/commit/%H)"}' -3 | jq -s '.')
echo "json=$COMMITS" >> $GITHUB_OUTPUT
- name: Send Teams notification
if: ${{ always() }}
uses: marcus-hooper/send-teams-notification@v1
with:
job_status: ${{ job.status }}
commit_messages: ${{ steps.commits.outputs.json }}
webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }}Note: The git log format above may produce invalid JSON if commit messages contain double quotes or backslashes. For repositories with complex commit messages, consider using
jqto properly escape the values, or limit to commit SHAs only.
Here's a complete deployment workflow with Teams notification:
name: Deploy and Notify
on:
push:
branches: [main]
jobs:
deploy:
name: Deploy Application
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 5
- name: Deploy to production
run: |
# Your deployment steps here
echo "Deploying application..."
- name: Get recent commits
id: commits
run: |
COMMITS=$(git log --pretty=format:'{"title":"%h","value":"[%s](https://github.com/${{ github.repository }}/commit/%H)"}' -3 | jq -s '.')
echo "json=$COMMITS" >> $GITHUB_OUTPUT
- name: Send Teams notification
if: ${{ always() }}
uses: marcus-hooper/send-teams-notification@v1
with:
job_status: ${{ job.status }}
environment: ${{ github.environment }}
card_title: "π Production Deployment"
commit_messages: ${{ steps.commits.outputs.json }}
webhook_url: ${{ secrets.TEAMS_WEBHOOK_URL }}The action sends an Adaptive Card to Teams with the following structure:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β π Production Deployment β
βββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
Success β
β β
β Repository owner/repo β
β Environment production β
β Actor username β
β β
β βΆ Recent Commits (tap to expand) β
β abc1234 Fix authentication bug β
β def5678 Add new feature β
β ghi9012 Update dependencies β
β β
β [View Run] β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
Status Styling:
| Status | Color | Emoji |
|---|---|---|
| Success | Green (#107C10) | β |
| Failure | Red (#D13438) | β |
| Cancelled/Other | Yellow (#F2C744) |
| Input | Required | Default | Description |
|---|---|---|---|
job_status |
Yes | Status of the job: success, failure, or cancelled |
|
webhook_url |
Yes | Teams Incoming Webhook URL (store in secrets) | |
commit_messages |
No | [] |
JSON array for FactSet, e.g. [{"title":"SHA","value":"Message"}] |
environment |
No | Deployment environment label | |
card_title |
No | π GitHub Deployment |
Title for the card |
repository |
No | github.repository |
Repository name to display |
actor |
No | github.actor |
Actor name to display |
run_id |
No | github.run_id |
Workflow run ID for the "View Run" link |
| Output | Description |
|---|---|
sent |
Whether a POST to Teams was attempted |
payload_bytes |
Size of the JSON payload in bytes |
run_url |
Link to the workflow run |
- Any GitHub Actions runner (GitHub-hosted or self-hosted) with PowerShell 7 available
- Microsoft Teams Incoming Webhook connector configured for the target channel
| Limitation | Details |
|---|---|
| Teams webhook only | Does not support Bot Framework or Graph API delivery |
| No retry logic | Single attempt to send; fails immediately on HTTP error |
| Card size limit | Teams limits Adaptive Cards to ~28KB; large commit lists may be truncated |
| Webhook rate limits | Teams may throttle frequent webhook calls |
| No message updates | Cannot update or delete sent cards (webhook limitation) |
| Encoding sensitivity | Requires UTF-8 without BOM; emoji via code points to avoid YAML issues |
| Error | Cause | Solution |
|---|---|---|
| 400/BadRequest | Teams rejects malformed or oversized cards | Keep payload < ~28 KB |
| Nothing appears in Teams | Webhook URL invalid or connector disabled | Verify the webhook URL and that the channel has the Incoming Webhook connector |
| Emoji or characters look wrong | Encoding issues | Ensure your YAML files are UTF-8 without BOM |
| No commits section | Invalid JSON | Ensure commit_messages is valid JSON (quote properly and avoid YAML mangling) |
| 403/Forbidden | Webhook URL expired or revoked | Generate a new webhook URL in Teams |
| Connector not available | Channel permissions restrict connectors | Ask a Teams admin to enable connectors for the channel or team |
- Check workflow logs - Expand the "Send Teams notification" step for detailed output
- Verify webhook URL - Test the webhook URL manually with a simple curl/Invoke-RestMethod call
- Check payload size - The
payload_bytesoutput shows the JSON size; keep it under 28KB - Validate commit JSON - Add a step to echo
${{ steps.commits.outputs.json }}to verify format - Test locally - Run the PowerShell script directly with environment variables set (see Development section)
- Builds an Adaptive Card 1.5 with status styling and optional collapsible commit list
- Sends via Teams Incoming Webhook as an attachment payload
- Derives repo/actor from GitHub context if not provided
- PowerShell 7+
- Pester 5.x (for tests)
- PSScriptAnalyzer (for linting)
# Run Pester tests
Invoke-Pester ./tests -Output Detailed
# Run tests with coverage
$config = New-PesterConfiguration
$config.Run.Path = './tests'
$config.Output.Verbosity = 'Detailed'
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @('./scripts/send-teams.ps1', './scripts/Send-TeamsNotification.psm1')
Invoke-Pester -Configuration $config# Install PSScriptAnalyzer if needed
Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser
# Run linter
Invoke-ScriptAnalyzer -Path ./scripts -Recurse -Settings PSGalleryFor end-to-end testing with a real Teams channel:
$env:INPUT_WEBHOOK_URL = 'https://outlook.office.com/webhook/...'
$env:INPUT_JOB_STATUS = 'success'
$env:INPUT_REPOSITORY = 'owner/repo'
$env:INPUT_ACTOR = 'username'
$env:INPUT_RUN_ID = '123456789'
./scripts/send-teams.ps1send-teams-notification/
βββ action.yml # GitHub Action definition (composite action)
βββ scripts/
β βββ send-teams.ps1 # Entry point script
β βββ Send-TeamsNotification.psm1 # PowerShell module with testable functions
βββ tests/
β βββ Send-TeamsNotification.Tests.ps1 # Pester unit tests
βββ .github/
β βββ dependabot.yml # Dependabot configuration
β βββ labels.yml # Repository label definitions
β βββ PULL_REQUEST_TEMPLATE.md
β βββ ISSUE_TEMPLATE/
β β βββ bug_report.yml # Bug report form
β β βββ feature_request.yml # Feature request form
β β βββ config.yml # Issue template chooser config
β βββ workflows/
β βββ ci.yml # CI workflow (lint, test, coverage)
β βββ codeql.yml # CodeQL security analysis
β βββ dependabot-auto-merge.yml # Auto-merge Dependabot PRs
β βββ labels.yml # Label synchronization
β βββ release.yml # Major version tag updates
β βββ schedule.yml # Weekly health check
β βββ scorecard.yml # OpenSSF Scorecard analysis
β βββ security.yml # Security scanning
β βββ validate.yml # Action validation
βββ README.md # This file
βββ CHANGELOG.md # Version history
βββ CONTRIBUTING.md # Contribution guidelines
βββ SECURITY.md # Security policy
βββ LICENSE # MIT License
βββ .gitignore # Git ignore patterns
Contributions are welcome! Please see CONTRIBUTING.md for detailed guidelines.
Quick start:
- Check existing issues or open a new one
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Make your changes and add tests if applicable
- Ensure CI passes (lint and test)
- Submit a pull request
See the issue templates for bug reports and feature requests.
- Store
webhook_urlin GitHub Secrets (e.g.,TEAMS_WEBHOOK_URL) - Do not echo or log the webhook URL
- Review branch protections for workflows that can send notifications
See SECURITY.md for security policy and reporting vulnerabilities.
- deployment-notification-o365 - Email notifications via Microsoft Graph API
- Teams Incoming Webhooks - Teams connector setup guide
- Adaptive Cards Designer - Visual card designer tool
- Adaptive Cards Schema - Card element reference
See CHANGELOG.md for version history.
MIT License - see LICENSE for details.