Qué vas a tener

Un canal de Discord donde aparece un mensaje rojo cuando un workflow falla: repo, rama y enlace al run en GitHub.

No hace falta servidor propio ni bot de Discord. Bastan dos cosas: un Incoming Webhook —una dirección secreta que Discord te da para publicar en un canal sin más permisos— y unas líneas más en tu configuración de GitHub Actions.

Flujo
  1. Falla un job en Actions
  2. Un step hace POST al webhook
  3. Discord muestra el mensaje

Si aún no tienes claro qué es un webhook, lee antes Introducción a webhooks.

Requisitos

  • Un repo en GitHub. No hace falta tener ya workflows: aquí creas el primero
  • Un canal de Discord donde seas administrador, que es quien puede crear webhooks
  • Permiso para añadir secrets en ese repo — o alguien que te lo cree

Crea el webhook en Discord

  1. Abre el canal donde quieres el aviso.
  2. Engranaje del canal → IntegracionesWebhooksNuevo webhook (los nombres pueden variar un poco según el cliente).
  3. Ponle un nombre claro, por ejemplo CI fallos.
  4. Copiar URL del webhook.

Esa URL es un secreto. Quien la tenga puede escribir en el canal. No la pegues en el chat, en un issue ni en el repo.

Prueba el webhook en local

Antes de Actions, confirma que la URL escribe en el canal. Sustituye todo el valor entre comillas por la URL que te dio Discord (empieza por https://discord.com/api/webhooks/ — no la pegues encima del prefijo).

Lenguaje: bash
WEBHOOK_URL="pega-aqui-tu-url-completa"
curl -sS -o /dev/null -w "%{http_code}\
" -X POST "$WEBHOOK_URL" \
  -H "Content-Type: application/json" \
  -d "{\"content\":\"Prueba desde la integración\"}"

Esperado: 204 —que en Discord significa «recibido, sin nada que devolver»— y el mensaje visible en el canal.

A partir de aquí desaparecen las pestañas de sistema: los workflows corren en la máquina de GitHub, que es Linux, así que los comandos son siempre los mismos aunque tú estés en Windows.

Guárdala en GitHub Secrets

  1. En el repo: SettingsSecrets and variablesActions.
  2. New repository secret.
  3. Nombre exacto: DISCORD_WEBHOOK_URL.
  4. Valor: la URL que copiaste. Guarda.

Si el secret tiene otro nombre, el workflow de abajo no lo verá.

Añade el workflow

Crea .github/workflows/discord-on-failure.yml (el nombre del archivo da igual) con esto:

Lenguaje: yaml

name: Aviso Discord si falla CI

on:
  workflow_dispatch:

jobs:
  # Lo dejamos fallando a propósito para probar el aviso.
  # Luego lo cambias por tu job real.
  fail-on-purpose:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Forzar fallo (solo para probar)
        run: exit 1

  notify-discord:
    needs: fail-on-purpose
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Discord
        env:
          DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
        run: |
          if [ -z "$DISCORD_WEBHOOK_URL" ]; then
            echo "Falta el secret DISCORD_WEBHOOK_URL."
            exit 1
          fi
          payload=$(jq -n \
            --arg repo "${{ github.repository }}" \
            --arg branch "${{ github.ref_name }}" \
            --arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
            '{
              embeds: [{
                title: "GitHub Actions falló",
                description: ("**" + $repo + "** · rama `" + $branch + "`"),
                url: $url,
                color: 15158332
              }]
            }')
          curl -sS -X POST \
            -H "Content-Type: application/json" \
            -d "$payload" \
            "$DISCORD_WEBHOOK_URL"

Lo que merece atención de ese bloque:

PiezaPara qué
needs: + if: failure()La pareja que hace el truco: needs dice a qué job espera y if: failure() que solo se ejecute si aquel falló. Sin esto avisaría siempre
DISCORD_WEBHOOK_URLSaca la URL de GitHub Secrets. Nunca se escribe en el archivo: el YAML se ve en el repo
jq + curlArman el mensaje y lo envían. Vienen instalados en la máquina de GitHub, no hay que añadir nada
color: 15158332El rojo de la barra lateral del mensaje, en decimal. Cámbialo si prefieres otro

Pruébalo

  1. Crea el archivo en el repo: Add file → Create new file → ruta `.github/workflows/discord-on-failure.yml` → pega el YAML → Commit (o súbelo con git).
  2. En Actions, abre Aviso Discord si falla CI → Run workflow.
  3. Espera a que fail-on-purpose falle (es normal) y a que notify-discord termine en verde.
  4. Mira el canal de Discord: deberías ver el embed rojo con repo, rama y enlace.

Adáptalo a tu CI

Cuando el aviso de prueba te funcione, deja de usar fail-on-purpose. Abajo tienes YAML listo para pegar: solo adapta el comando de tu job (o los nombres) y el secret sigue siendo el mismo.

Un job (lo habitual)

Cambia npm test por tu comando real. Puedes pegar este bloque entero:

Lenguaje: yaml

name: Aviso Discord si falla CI

on:
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  notify-discord:
    needs: test
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Discord
        env:
          DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
        run: |
          if [ -z "$DISCORD_WEBHOOK_URL" ]; then
            echo "Falta el secret DISCORD_WEBHOOK_URL."
            exit 1
          fi
          payload=$(jq -n \
            --arg repo "${{ github.repository }}" \
            --arg branch "${{ github.ref_name }}" \
            --arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
            '{
              embeds: [{
                title: "GitHub Actions falló",
                description: ("**" + $repo + "** · rama `" + $branch + "`"),
                url: $url,
                color: 15158332
              }]
            }')
          curl -sS -X POST \
            -H "Content-Type: application/json" \
            -d "$payload" \
            "$DISCORD_WEBHOOK_URL"

needs: test = espera a test.
if: failure() = solo corre si test falló.
Si test pasa en verde, Discord no recibe nada.

Varios jobs

Igual, pero el aviso espera a varios jobs. Discord avisa si falla cualquiera:

Lenguaje: yaml

name: Aviso Discord si falla CI

on:
  workflow_dispatch:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

  notify-discord:
    needs: [test, build, lint]
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Discord
        env:
          DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
        run: |
          if [ -z "$DISCORD_WEBHOOK_URL" ]; then
            echo "Falta el secret DISCORD_WEBHOOK_URL."
            exit 1
          fi
          payload=$(jq -n \
            --arg repo "${{ github.repository }}" \
            --arg branch "${{ github.ref_name }}" \
            --arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
            '{
              embeds: [{
                title: "GitHub Actions falló",
                description: ("**" + $repo + "** · rama `" + $branch + "`"),
                url: $url,
                color: 15158332
              }]
            }')
          curl -sS -X POST \
            -H "Content-Type: application/json" \
            -d "$payload" \
            "$DISCORD_WEBHOOK_URL"

Cuándo se dispara

No es otro flujo: es el bloque on: del mismo archivo.

Qué pones en on:Efecto
Solo workflow_dispatchLo lanzas a mano (como en la prueba)
Solo push a mainCorre solo al hacer push a main
Los dosPuedes probar a mano y recibir avisos reales en main

Los ejemplos de “un job” / “varios jobs” ya llevan los dos. Si aún usas el YAML de prueba (solo manual), añade el push cuando el mensaje te convenza:

Lenguaje: yaml

on:
  workflow_dispatch:
  push:
    branches: [main]

Seguridad

  • El secret debe vivir solo en GitHub Secrets (o en el almacén de secretos de tu org).
  • Este flujo no verifica firmas de GitHub: Discord confía en quien conozca la URL.
  • No dispares el aviso en cada step ruidoso; un mensaje por fallo de job suele bastar. Discord puede responder 429 si spameas.

Errores habituales

Qué vesCausa habitualQué hacer
notify-discord ni apareceEl job anterior no falló, o no está en needsMira el grafo del run; if: failure() solo actúa tras un fallo
El step falla con URL rara / curl exit ≠ 0Secret mal pegado (espacios, comillas, URL a medias)Reescribe el secret entero; no lo “arregles” a ojo en el log
404 desde DiscordWebhook borrado o URL viejaCrea otro webhook y actualiza el secret
401 / sin permisoMenos habitual en Incoming Webhooks; URL incompleta a veces se comporta raroRegenera la URL
Todo verde en Actions y silencio en DiscordMiras otro canal, o el job avisado no fallóConfirma el canal del webhook y el grafo del run
Mensaje duplicadoReejecutaste el workflowNormal aquí; no hay deduplicación

Alternativas

Email de GitHub
Llega, pero se pierde entre notificaciones. Menos útil en el canal del equipo.
Webhook de GitHub con sufijo /github en Discord
Eventos del repo (push, PR, etc.); no es el mismo control que «falló este job de Actions».
Bot de Telegram + Actions
Mismo aviso de CI hacia un chat, con token y chat_id.
Slack Incoming Webhook
Mismo patrón (POST con una URL secreta), otro destino.

Recursos