Notify CI failure #45410
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Notify CI failure | |
| # Fires after another workflow completes and pings chat when a run on a | |
| # protected branch fails. This is the outbound side of recommendation #2: a | |
| # solo-maintainer safety net so a red `main` (which auto-deploys to production) | |
| # is noticed immediately rather than on the next manual check. | |
| # | |
| # Delivery uses plain `curl` to Slack / Discord incoming webhooks stored as | |
| # repository secrets (SLACK_WEBHOOK_URL / DISCORD_WEBHOOK_URL). Set either, both, | |
| # or neither — a missing secret simply skips that channel. No external actions are | |
| # used, so the pinned-action allowlist (scripts/github-action-pins.mjs) is not | |
| # involved. See docs/webhooks.md. | |
| on: | |
| workflow_run: | |
| workflows: | |
| - CI | |
| - SAST | |
| - Secret Scan | |
| - PR Policy | |
| - Live domain monitor | |
| - Live drift check | |
| - Eval Canary | |
| - Ingestion Autopilot | |
| - Docker image build | |
| types: | |
| - completed | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: notify-ci-failure-${{ github.event.workflow_run.id }} | |
| cancel-in-progress: false | |
| jobs: | |
| notify: | |
| name: Notify on failure | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| # Only alert on genuine failures of runs against protected branches. | |
| # The head_repository guard restricts notifications to runs that originated | |
| # in THIS repo (push, schedule, workflow_dispatch). A fork PR's triggering | |
| # run also fires workflow_run with repo secrets in scope, and a fork could | |
| # name its branch `main`/`release/...` to forge or spam alerts — the guard | |
| # blocks that while preserving the scheduled production monitors (which run | |
| # on `schedule`, so an `event == 'push'` filter would wrongly drop them). | |
| if: >- | |
| github.event.workflow_run.conclusion == 'failure' && | |
| github.event.workflow_run.head_repository.full_name == github.repository && | |
| (github.event.workflow_run.head_branch == 'main' || | |
| startsWith(github.event.workflow_run.head_branch, 'release/')) | |
| steps: | |
| - name: Post failure notification | |
| env: | |
| SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| WORKFLOW_NAME: ${{ github.event.workflow_run.name }} | |
| HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| RUN_URL: ${{ github.event.workflow_run.html_url }} | |
| RUN_EVENT: ${{ github.event.workflow_run.event }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| message="🔴 *CI failure:* ${WORKFLOW_NAME} failed on \`${HEAD_BRANCH}\` (${REPO}, ${RUN_EVENT}) | |
| ${RUN_URL}" | |
| if [ -z "${SLACK_WEBHOOK_URL:-}" ] && [ -z "${DISCORD_WEBHOOK_URL:-}" ]; then | |
| echo "No SLACK_WEBHOOK_URL or DISCORD_WEBHOOK_URL secret set; nothing to notify." | |
| exit 0 | |
| fi | |
| json_escape() { | |
| # Escape a string for embedding in JSON via a here-doc-free jq-less approach. | |
| python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))' | |
| } | |
| payload_text=$(printf '%s' "$message" | json_escape) | |
| if [ -n "${SLACK_WEBHOOK_URL:-}" ]; then | |
| echo "Posting to Slack..." | |
| curl --fail --silent --show-error --max-time 15 \ | |
| -X POST -H 'Content-Type: application/json' \ | |
| --data "{\"text\": ${payload_text}}" \ | |
| "$SLACK_WEBHOOK_URL" || echo "Slack post failed (non-fatal)." | |
| fi | |
| if [ -n "${DISCORD_WEBHOOK_URL:-}" ]; then | |
| echo "Posting to Discord..." | |
| curl --fail --silent --show-error --max-time 15 \ | |
| -X POST -H 'Content-Type: application/json' \ | |
| --data "{\"content\": ${payload_text}}" \ | |
| "$DISCORD_WEBHOOK_URL" || echo "Discord post failed (non-fatal)." | |
| fi |