Skip to content

Notify CI failure #45410

Notify CI failure

Notify CI failure #45410

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