Qué vas a tener

Un mensaje en Telegram (chat privado o grupo) cuando un job de Actions falla: repo, rama y enlace al run.

No hace falta servidor propio. Bastan un bot de Telegram, dos secretos guardados en GitHub y unas líneas más en tu configuración de GitHub Actions.

Flujo
  1. Falla un job en Actions
  2. Un step llama a sendMessage
  3. Telegram muestra el aviso

Si aún no tienes claro el bot/token/chat_id, lee Avisos con un bot de Telegram.

Requisitos

  • Un repo en GitHub. No hace falta tener ya workflows: aquí creas el primero
  • Cuenta de Telegram — el bot se crea en el propio chat, en un par de minutos
  • Permiso para añadir secrets en ese repo, o alguien que te los cree

Crea el bot

  1. En Telegram, abre @BotFather (se abre en una pestaña nueva).
  2. /newbot → nombre y un username que acabe en bot.
  3. Copia el token.
  4. BotFather te da un enlace t.me/…: ábrelo, pulsa Start (o escribe hola). Si usas un grupo, añade el bot y dale permiso de enviar; el id del grupo suele ser negativo.

Obtén el chat_id

Pega el token de BotFather donde dice pega-aqui-tu-token. Elige tu sistema en el bloque.

Lenguaje: bash
TOKEN="pega-aqui-tu-token"
curl -sS "https://api.telegram.org/bot${TOKEN}/getUpdates"

Busca un trozo como este (tus números serán distintos):

Lenguaje: json

"chat": {
  "id": 123456789,
  "type": "private"
}

Ese "id" es el chat_id. Si result viene vacío, manda otro mensaje al bot y reintenta. En PowerShell, con $r de arriba, también puedes mirar $r.result.message.chat.id (o el primer elemento de result si viene como lista).

Puedes comprobar el destino antes de tocar Actions (sustituye también 123456789 por tu chat_id):

Lenguaje: bash
TOKEN="pega-aqui-tu-token"
CHAT_ID="123456789"
curl -sS -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
  -H "Content-Type: application/json" \
  -d "{\"chat_id\":${CHAT_ID},\"text\":\"Prueba desde la integración\"}"

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árdalos en GitHub Secrets

En el repo: SettingsSecrets and variablesActions:

NombreValor
TELEGRAM_BOT_TOKENToken de BotFather
TELEGRAM_CHAT_IDEl chat_id que sacaste

Si los nombres no coinciden, el workflow de abajo no los verá.

Añade el workflow

Crea .github/workflows/telegram-on-failure.yml:

Lenguaje: yaml

name: Aviso Telegram 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-telegram:
    needs: fail-on-purpose
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Telegram
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: |
          if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
            echo "Faltan TELEGRAM_BOT_TOKEN o TELEGRAM_CHAT_ID."
            exit 1
          fi
          text=$(printf 'GitHub Actions falló\n%s · rama %s\n%s/%s/actions/runs/%s' \
            "${{ github.repository }}" \
            "${{ github.ref_name }}" \
            "${{ github.server_url }}" \
            "${{ github.repository }}" \
            "${{ github.run_id }}")
          payload=$(jq -n \
            --arg chat "$TELEGRAM_CHAT_ID" \
            --arg text "$text" \
            '{chat_id: $chat, text: $text}')
          curl -sS -X POST \
            "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
            -H "Content-Type: application/json" \
            -d "$payload"

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
Los dos secretosToken y destino. Nunca se escriben en el archivo: el YAML se ve en el repo
jq + curlArman el mensaje y llaman a sendMessage. Vienen instalados en la máquina de GitHub, no hay que añadir nada

Pruébalo

  1. Crea el archivo en el repo: Add file → Create new file → ruta `.github/workflows/telegram-on-failure.yml` → pega el YAML → Commit (o súbelo con git).
  2. En Actions, abre Aviso Telegram si falla CI → Run workflow.
  3. Espera a que fail-on-purpose falle (es normal) y a que notify-telegram termine en verde.
  4. Mira Telegram: mensaje 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: adapta el comando del job; los secrets siguen siendo los mismos.

Un job (lo habitual)

Lenguaje: yaml

name: Aviso Telegram si falla CI

on:
  workflow_dispatch:
  push:
    branches: [main]

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

  notify-telegram:
    needs: test
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Telegram
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: |
          if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
            echo "Faltan TELEGRAM_BOT_TOKEN o TELEGRAM_CHAT_ID."
            exit 1
          fi
          text=$(printf 'GitHub Actions falló\n%s · rama %s\n%s/%s/actions/runs/%s' \
            "${{ github.repository }}" \
            "${{ github.ref_name }}" \
            "${{ github.server_url }}" \
            "${{ github.repository }}" \
            "${{ github.run_id }}")
          payload=$(jq -n \
            --arg chat "$TELEGRAM_CHAT_ID" \
            --arg text "$text" \
            '{chat_id: $chat, text: $text}')
          curl -sS -X POST \
            "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
            -H "Content-Type: application/json" \
            -d "$payload"

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

Varios jobs

El aviso espera a varios jobs y salta si falla cualquiera:

Lenguaje: yaml

name: Aviso Telegram 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-telegram:
    needs: [test, build, lint]
    if: failure()
    runs-on: ubuntu-latest
    steps:
      - name: Avisar en Telegram
        env:
          TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
          TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
        run: |
          if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
            echo "Faltan TELEGRAM_BOT_TOKEN o TELEGRAM_CHAT_ID."
            exit 1
          fi
          text=$(printf 'GitHub Actions falló\n%s · rama %s\n%s/%s/actions/runs/%s' \
            "${{ github.repository }}" \
            "${{ github.ref_name }}" \
            "${{ github.server_url }}" \
            "${{ github.repository }}" \
            "${{ github.run_id }}")
          payload=$(jq -n \
            --arg chat "$TELEGRAM_CHAT_ID" \
            --arg text "$text" \
            '{chat_id: $chat, text: $text}')
          curl -sS -X POST \
            "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
            -H "Content-Type: application/json" \
            -d "$payload"

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.

Texto plano

Los ejemplos usan texto plano (sin parse_mode). Es lo más seguro con nombres de repo/rama que llevan _ o /. Si más adelante usas HTML o MarkdownV2, escapa lo que venga de GitHub.

Seguridad

  • Un bot dedicado a CI reduce el radio de daño.
  • Este flujo no verifica firmas de GitHub: quien tenga el token puede escribir al chat_id.
  • Evita spamear: un mensaje por fallo de job suele bastar.

Errores habituales

Qué vesCausa habitualQué hacer
Sin chat_id en getUpdatesNo diste Start al botAbre el chat, Start, reintenta
UnauthorizedToken mal pegadoReescribe el secret
chat not foundchat_id de otro chat o botVuelve a sacar el id con getUpdates
notify-telegram no correEl job anterior no fallóRevisa needs + if: failure()
Actions en verde y silencio en TelegramMiras otro chat, o el job avisado no fallóConfirma chat_id y el grafo del run
Mensaje duplicadoReejecutaste el workflowNormal aquí; no hay deduplicación
400 de TelegramJSON mal formado o parse_mode sin escaparUsa el bloque del ejemplo con jq y texto plano

Alternativas

Incoming Webhook de otro chat
Mismo aviso de CI, otro destino (p. ej. Discord o Slack).
Email de GitHub
Cero setup; más fácil de ignorar.
Grupo de Telegram vs chat privado
Grupo para el equipo; privado para avisos solo tuyos.

Recursos