Notificações — Telegram e webhook

O Runner envia notificações para cada evento relevante de deploy. Esta página cobre como configurar Telegram e webhook, quais eventos são disparados e como sobrescrever a config global por app.

Pré-requisitos

  • Runner v2.30.10+
  • Acesso a /opt/runner/config.yml no servidor
  • Para Telegram: conta Telegram e acesso ao @BotFather
  • Para webhook: URL HTTPS acessível pelo servidor Runner

Onde fica a configuração

A config de notificações fica na seção notify: do /opt/runner/config.yml. Esta é a config global — vale para todas as apps do servidor.

# /opt/runner/config.yml
notify:
  channel: telegram           # telegram | webhook | both
  telegram_bot_token: "123456:ABC-DEF..."
  telegram_chat_id: "-100123456789"
  webhook_url: "https://hooks.exemplo.com/runner"

  # Classe deploy (iniciado/concluído/falha) — gate 2. default true.
  # false = nenhuma notificação de chat do ciclo de deploy (audit log continua).
  enabled: true

  # Extras (gate 3) — todos opt-in, default false:
  extras:
    batch_summary: false   # resumo "Runner deploy batch — N ok, M failed"
    verbose_errors: false  # 2ª msg com o erro completo numa falha
    health_status: false   # status do health check pós-deploy
    init_log: false        # log do boot do container (⚠️ pode vazar segredos)

  # Alertas dos watchers do cron (v2.118.0+) — classe própria, default true.
  # false = tls-watch, doctor-watch e auto-pause não mandam chat (audit log segue).
  watch_alerts: true

# Fora de notify: intervalo mínimo entre re-alertas do MESMO problema (horas).
watch_realert_hours: 6

Migração (v2.42.x → modelo de classes): as chaves antigas top-level verbose_errors/notify_success foram substituídas. Configs antigos não quebram (chaves desconhecidas são ignoradas), mas não migram sozinhos — enabled cai no default true e os extras em false. Reescreva no formato novo para reativar verbose errors etc.

Modelo de classes e os 3 gates

As notificações são organizadas em classes de evento, e cada classe passa por até 3 gates antes de chegar ao chat. Os gates valem para todos os canais de chat configurados (Telegram, Discord, webhook) de forma uniforme — não há tratativa por canal.

Gate O que é Controla
1 Canal configurado Existe telegram_* / discord_webhook / webhook_url?
2 enabled Liga/desliga a classe deploy (iniciado/concluído/falha)
3 extras.<x> Liga cada evento extra (opt-in)

Policy por classe:

Classe Vai pro chat quando
Deploy (iniciado/concluído/falha) enabled
Batch summary extras.batch_summary (independe do enabled)
Verbose error (2ª msg) enabled e extras.verbose_errors
Health status enabled e extras.health_status
Init log enabled e extras.init_log
Alertas dos watchers (v2.118.0+) watch_alerts (independe do enabled)

Journal sempre-on: o audit log deploy-events.jsonl registra todos os eventos sempre, independentemente dos gates. Os gates só decidem o que vai pro chat — o histórico (runner audit/runner logs) nunca perde evento.

Exemplos:

  • Padrão (enabled: true, extras false): chat recebe iniciado/concluído/falha; sem batch, sem 2ª msg de erro, sem health/init log.
  • Silenciar deploy no chat (enabled: false): nenhum evento de deploy no chat (nem falha) — só o journal registra. Útil quando o batch summary já basta.
  • Só o resumo de batch: enabled: false + extras.batch_summary: true.

Setup Telegram

1. Criar o bot

  1. Abra o Telegram e busque @BotFather
  2. Envie /newbot e siga as instruções
  3. Copie o token (formato: 123456789:ABCdefGHI...)

2. Obter o chat_id

Para DM ou grupo — use o setup interativo:

runner notify setup --channel telegram

O comando valida o token via API do Telegram, detecta o chat_id automaticamente e salva no config.yml.

Para canal — o bot não recebe getUpdates de canais. Obtenha o chat_id manualmente:

curl -s "https://api.telegram.org/bot<TOKEN>/getUpdates"

Envie uma mensagem no grupo ou canal primeiro, depois rode o comando. O chat_id fica em .result[].message.chat.id — negativo para grupos e canais.

3. Configurar manualmente

notify:
  channel: telegram
  telegram_bot_token: "123456789:ABCdefGHI..."
  telegram_chat_id: "-100123456789"

Para canal: adicione o bot como administrador com permissão "Post Messages" antes de usar.

4. Testar

runner notify test

O comando envia uma mensagem de teste ao canal configurado. Se falhar, rode runner notify status para ver o diagnóstico.

Setup webhook

O Runner faz POST JSON para a URL configurada a cada evento.

O webhook é um canal de notificação como Telegram/Discord — passa pelos mesmos gates (enabled / extras.*) de forma uniforme. Ou seja, com enabled: false o webhook não recebe os eventos do ciclo de deploy (iniciado/concluído/falha); os extras (batch_summary, etc.) só chegam se o extras.<x> correspondente estiver ligado. Para consumir todos os eventos programaticamente sem depender dos gates, leia o audit log deploy-events.jsonl (sempre-on) em vez do webhook.

Configurar

notify:
  channel: webhook
  webhook_url: "https://hooks.exemplo.com/runner"

Payload recebido

{
  "event_type": "deploy_success",
  "project": "meu-app",
  "instance": "production",
  "version": "v1.2.0",
  "previous_version": "v1.1.9",
  "deploy_id": "a3f7c2d1",
  "timestamp": "2026-05-27T14:30:00Z",
  "branch": "main",
  "pr_number": 41,
  "expires_at": "2026-05-29T14:30:00Z",
  "last_commit": {
    "author": "alice",
    "hash": "ee8a4bf",
    "subject": "CONFORMITY front: tokenized footer (#41)"
  },
  "commit_authors": ["alice", "bob"],
  "commits_count": 3,
  "commits": [
    { "author": "alice", "hash": "ee8a4bf", "subject": "fix(api): import nextTick" },
    { "author": "bob", "hash": "9afcdbe", "subject": "fix(api): restore KPIs" }
  ]
}

Campos opcionais (v2.30.12+):

  • branch — branch que originou o deploy
  • pr_number — PR detectado (squash trailer (#N), merge commit, ou runner add --pr N)
  • expires_at — TTL do PR preview (ISO 8601), quando aplicável
  • last_commit — autor, hash e subject do commit real mais novo do deploy
  • commit_authors — autores únicos do range (multi-commit)
  • commits_count — total de commits reais reconhecidos no deploy
  • commits — lista dos commits reais do range (newest-first, capada), com os auto-commits chore: update encrypted secrets filtrados
  • runner_config (v2.175.3+) — linhas "configuração atualizada pelo runner: …" dos auto-commits do range, quando o deploy também tem commits reais. Ausente quando vazio.

Para eventos de falha, o payload inclui também "error": "<mensagem>".

Usar Telegram e webhook simultaneamente

notify:
  channel: both
  telegram_bot_token: "123456789:ABCdefGHI..."
  telegram_chat_id: "-100123456789"
  webhook_url: "https://hooks.exemplo.com/runner"

Eventos disparados

event_type Quando O que inclui
deploy_started Início do deploy (antes do fetch) projeto, instância, versão, branch
deploy_success Deploy termina com healthcheck OK projeto, instância, versão, duração — chat conforme enabled (audit log sempre)
deploy_failure Deploy falha ou healthcheck não passa projeto, instância, versão, erro extraído
rollback_started Rollback automático iniciado projeto, instância, versão alvo
rollback_success Rollback automático concluído projeto, instância, versão revertida
health_check_failed Healthcheck falhou após N retries projeto, instância, versão, modo
health_skipped healthcheck.mode: skip_check ativo (warn) projeto, instância
canary_weight_changed Peso ajustado no scheduler projeto, instância, peso anterior, peso novo
canary_promoted Canary chegou a 100% projeto, instância, versão promovida
instance_destroyed runner destroy <instance> projeto, instância
pr_expired runner cleanup --expired destruiu PR vencido (v2.30.12+) projeto, instância, pr_number
manifest_repo_divergiu O repositório mudou o manifesto de uma app com manifesto fixado no servidor (2.178.0+) projeto, os nomes dos campos em details, manifest
manifesto_fixado_adulterado A raiz <app>/.deploy.yml não é o manifesto fixado: o cron pulou a app (2.178.0+) projeto, error, manifest
manifesto_fixado_ilegivel O pin.json da app não pôde ser lido: o cron pulou a app (2.178.0+) projeto, error (código e caminho do arquivo), manifest

Os três eventos do manifesto fixado saem do cron e só na transição: na primeira vez que a situação aparece, não a cada ciclo. São alertas de watcher: vão para o chat conforme watch_alerts, não conforme enabled. O audit log registra sempre.

Eles trazem o campo estruturado manifest, além do texto em details/error:

"manifest": {"codigo": "manifest_repo_divergiu", "commit": "d4e5f6", "campos": ["healthcheck"]}

codigo é o event_type. commit e campos (os nomes das chaves de topo que mudaram, nunca os valores) só existem no manifest_repo_divergiu. Ver `runner manifest`.

Commits reais na notificação de deploy

A lista de commits aparece na mensagem de início (🚀 Deploy iniciado) — pra você ver o que está sendo deployado e diagnosticar um possível erro antes de a conclusão chegar. Quando o deploy reconhece vários commits, lista os commits reais do range (📝 Commits:, até 5, com trailer … +N mais). Os auto-commits internos chore: update encrypted secrets (que o Runner cria ao persistir .runner/secrets.enc) são filtrados — você vê o que de fato foi deployado, não o auto-commit. Os mesmos campos vão pro payload do webhook (commits, commits_count, commit_authors) e pro audit log, em todos os eventos.

Deploy por `dist`: a lista do builder (v2.175.3+)

Na dist só existe o commit de snapshot deploy: vX.Y.Z. A partir do builder 0.5.3, o corpo desse commit traz a lista do que entrou na release, antes do trailer de carimbo:

deploy: v2.385.0

Commits:
- a1b2c3d feat(painel): filtro por instância (alice)
- d4e5f6a fix(auth): corrige refresh token expirado (bob)
- ... e mais 12 commits

Builder-Carimbo: v=...

O Runner lê essa lista e ela vira o conteúdo da notificação: a manchete, os itens (📝 Commits:) e os autores (👥 Por:). O "e mais N" entra na contagem.

👥 Por: `alice, bob` (14 commits)
📝 Commits:
  • `a1b2c3d` — `feat(painel): filtro por instância`
  • `d4e5f6a` — `fix(auth): corrige refresh token expirado`
  … +12 mais

A lista só é lida quando o commit tem o carimbo do builder. Um commit de dist de release antigo, sem a lista, aparece como sempre: 📝 Commit: <hash> — deploy: v2.384.0.

Configuração alterada pelo próprio Runner (v2.175.3+)

Quando o Runner commita no repo da app (o bundle de secrets depois de um runner env set, runner secrets push ou deploy, e o .deploy.yml no bootstrap, no manifest via CCS ou no migrate-source), o subject continua o mesmo de sempre (chore: update encrypted secrets). O corpo do commit passa a dizer o que mudou, só pelo NOME:

chore: update encrypted secrets

adicionada: SMTP_HOST
alterada: SMTP_PASSWORD
removida: OLD_TOKEN
.deploy.yml alterado: healthcheck, port
  • Só nomes de chave e campos de topo do .deploy.yml. O valor nunca vai para o commit, o log ou a notificação. Nada é decifrado: a comparação é feita contra a versão que já está no repo, pelo texto cifrado de cada chave.
  • O detalhe por chave existe só no esquema v2 de secrets, em que as chaves são explícitas no arquivo. Num bundle legado, o corpo traz só a linha secrets atualizados (bundle legado: o detalhe por chave aparece depois de migrar para v2 com runner secrets migrate), e a notificação mostra secrets atualizados (bundle legado).
  • O nome só aparece se seguir [A-Za-z_][A-Za-z0-9_]* e tiver até 64 caracteres.
  • Ficam sem corpo o re-key da ckey (chore: reset encrypted secrets (ckey re-key)), porque a ckey nova não abre o bundle anterior, e a remoção do bundle do repo.

Na notificação, esse corpo vira uma linha:

configuração atualizada pelo runner: secrets (adicionada: SMTP_HOST; alterada: SMTP_PASSWORD), .deploy.yml (healthcheck, port)
  • Deploy só com o auto-commit (por exemplo, env set seguido de deploy): a linha aparece no lugar do commit, em 📝 Commit:.
  • Deploy com commits reais e auto-commit: o auto-commit não entra na contagem nem em Por:. A linha aparece separada, com ⚙️.
  • A linha mostra até 10 nomes e depois "e mais N chaves". Sai uma linha de configuração por mensagem; as demais viram (+N).

Tamanho da mensagem (v2.175.3+)

👥 Por: mostra até 5 autores e depois "e mais N". A mensagem montada tem orçamento de 4000 caracteres (o Telegram corta em 4096; o Discord usa 2000). Se passar, o corte é feito numa quebra de linha, nunca no meio de um trecho entre crases, e a última linha vira ….

  • Auto-commit de runner anterior à 2.175.3 (sem corpo) continua filtrado.

Template Telegram/Discord (v2.43.2+)

O header lidera com a ação; a identidade é textual (sem badge de cor de ambiente). Projeto: só aparece quando project_group está setado no .deploy.yml.

Início (traz domínio, branch e os commits):

🚀 Deploy iniciado

Deploy: loja-admin-front
Projeto: Loja
🌐 Instance: production
🔗 admin.exemplo.com.br
🏷️  fc4086f ← ca73d54
🌿 Branch: feature/prod-deploy · PR #41
🆔 6bc09611
👥 Por: equipe-dev (2 commits)
📝 Commits:
  • fc4086f — merge: refund type frontend + label mapping wallet.
  • d832c1d — fix(auth): pre-flight JWT expiry + abort-all 401 cascade

Conclusão / Falha (enxutas e auto-contidas, com duração; falha traz o erro):

✅ Deploy concluído

Deploy: loja-admin-front
Projeto: Loja
🌐 Instance: production
🏷️  fc4086f
🆔 dca185c6
⏱️ 22s

Linha 🌿 Branch:: mostra sempre a branch do deploy; anexa · PR #N quando um PR é detectado (campo pr_number ou trailer (#N) no subject do commit). Não há mais o ⚠️ direct push — quando não há PR, mostra só a branch.

Para PR deploys com TTL ativo, aparece uma linha ⏰ Auto-destroy em Xh no início.

Verbose errors (falha de deploy)

Com extras.verbose_errors: true (opt-in, default false), numa falha o Runner envia uma segunda mensagem com o log completo do erro:

  1. Resumo com o erro extraído (até 800 chars)
  2. Log completo do erro (até 3800 chars)

As duas mensagens são correlacionadas pelo mesmo deploy_id. Exige a classe deploy ligada (enabled: true).

notify:
  enabled: true
  extras:
    verbose_errors: true

Alertas dos watchers — tls-watch, doctor-watch e auto-pause (v2.118.0+)

Além do ciclo de deploy, o cron (runner fetch --deploy) roda watchers que olham a saúde do próprio servidor e avisam pelo mesmo canal do deploy. Não são eventos de deploy — por isso têm classe própria e não seguem enabled.

Watcher O que observa Quando avisa
tls-watch (v2.117.0+) Todo route file do Traefik com certResolver: o domínio serve o TRAEFIK DEFAULT CERT? Sem registro DNS (NXDOMAIN): alerta e não retenta — o Traefik nunca vai emitir. DNS ok mas cert padrão: reescreve o route file (# tls-retry: <ts>) pra forçar o reload e o Traefik retentar o ACME; avisa a cada tentativa (mínimo 60 min entre elas). Quando o certificado sai, manda "✅ Certificado ACME emitido" e encerra a pendência
doctor-watch Checks do runner doctor sobre o state (runner doctor --watch) Check que passou a falhar; "state OK novamente" na recuperação
auto-pause do fetch (#128) Apps cujo git fetch falha em sequência Ao pausar (⏸️ "App auto-pausada") e ao retomar sozinho

Exemplo de mensagem (tls-watch, DNS ausente):

🩺 Alerta de saúde do runner

Deploy: runner
🌐 Instance: tls-watch
🆔 e5623a98

app.exemplo.com.br: sem certificado ACME (Traefik no certificado padrão) porque o DNS
não resolve (NXDOMAIN). O Traefik NÃO retenta sozinho — crie o registro A; o runner
força a retentativa no próximo ciclo assim que o DNS existir.
Arquivo: /opt/traefik/dynamic/usuario_app-front__production.yml

O que fazer com ele: o texto já diz — criar o registro DNS, ou, se a app foi descontinuada, runner unregister <app> (o route file some e o alerta para).

Ligar e desligar

Uma chave, default ligado:

# /opt/runner/config.yml
notify:
  watch_alerts: true     # false = silencia os três watchers no chat
runner settings set notify.watch_alerts false   # desliga
runner settings set notify.watch_alerts true    # liga de volta
runner settings get notify.watch_alerts

Desligado, o alerta continua no audit log (deploy-events.jsonl) e no log do cron (WARN [tls-watch] … em /var/log/runner-cron.log) — só não vai pro chat. enabled: false (classe deploy) não silencia os watchers; é preciso desligar watch_alerts explicitamente.

Com que frequência repete

O mesmo problema não repete a cada ciclo de 5 min. O runner guarda o estado (<state_dir>/.tls-watch-state.json, .doctor-watch-state.json) e só re-alerta depois de watch_realert_hours (top-level do config.yml, default 6):

runner settings set watch_realert_hours 12   # re-alerta a cada 12 h
runner settings set watch_realert_hours 0    # alerta em todo ciclo (barulhento)

Vale pro tls-watch e pro doctor-watch. Domínio que sai dos route files (app desregistrada, router removido) sai do estado sozinho.

Confirmar que está funcionando

grep -n "tls-watch" /var/log/runner-cron.log | tail          # o que o cron viu
cat /opt/runner/state/.tls-watch-state.json                    # pendências e alerted_at
curl -k -sS -o /dev/null -v --max-time 4 \
  --resolve <dominio>:443:127.0.0.1 https://<dominio>/ 2>&1 | grep issuer   # cert atual

Se o alerta chegou no chat só com o cabeçalho (sem o parágrafo do domínio), o binário é anterior à 2.117.1 — runner self-update.

Canais nomeados (config.yml)

Além da config global de deploy (seção anterior), o config.yml pode declarar canais nomeados em notify.channels:. Cada app referencia esses canais pelo nome — as credenciais ficam só aqui, nunca no .deploy.yml do repo.

# /opt/runner/config.yml
notify:
  # ... config global (channel/enabled/extras) ...
  channels:
    time_telegram:
      type: telegram
      bot_token: "123456:ABC-DEF..."
      chat_id: "-100123456789"

    ops_email:
      type: smtp
      host: smtp.exemplo.com
      port: 587
      username: [email protected]
      password: "{SMTP_PASSWORD}"
      from: "[email protected]"
      to: "[email protected]"

    meu_webhook:
      type: webhook
      base_headers:
        Authorization: "Basic {BASE64_CREDS}"

Tipos de canal suportados: telegram, webhook, smtp (e-mail).

Notificações por app (.deploy.yml)

Cada app pode declarar, no próprio .deploy.yml, uma lista de subscrições que referenciam canais nomeados do config.yml. É aditivo à config global — não substitui as notificações de classe deploy do servidor.

# .deploy.yml da app
project: meu-app
image: caddy:2-alpine
port: 80

notify:
  - on: [deploy_success, deploy_failure]
    when: "{{::instance_name}} == production"
    channel: time_telegram
    template: "{{::repository_name}} v{{::deploy_version}} — {{::deploy_result_status}}"

  - on: [deploy_rollback]
    channel: meu_webhook
    request:
      method: POST
      url: "https://hooks.exemplo.com/{{::TOKEN}}"
      body: '{"msg":"{{::repository_name}} rollback"}'

Campos:

Campo Descrição
on: Eventos: deploy_started, deploy_success, deploy_failure, deploy_rollback
when: Condição opcional {{::var}} == valor / != valor. Sem when, dispara sempre
channel: Nome de um canal em notify.channels: do config.yml
template: Corpo da mensagem (canais telegram/smtp)
subject: Assunto do e-mail (opcional, smtp)
request: method/url/headers/body (canal webhook)

template, subject: e os campos de request: usam a mesma sintaxe {{::var}} de secrets:. Além do contexto do deploy (repository_name, deploy_result_status, deploy_version, deploy_error, instance_name, instance_domain, event, deploy_id, project e, em v2.135.0+, container_id, container_name, previous_container_id, previous_container_name — #177), dá pra referenciar variáveis do environment:/secrets: do próprio app — resolvidas do bundle criptografado no momento do deploy. Valores reais de secret nunca ficam gravados no manifesto.

Uma subscrição com canal inexistente, mal configurado ou que falhe no envio é apenas logada — nunca bloqueia o deploy (fire-and-forget).

Isso é útil quando apps de projetos diferentes precisam notificar chats/e-mails distintos no mesmo servidor.

Comandos disponíveis

# Setup interativo
runner notify setup --channel telegram
runner notify setup --channel webhook
runner notify setup --channel both

# Enviar mensagem de teste
runner notify test

# Ver status dos canais configurados
runner notify status

O que pode dar errado

Telegram não envia após configuração manual

O chat_id de grupos e canais é negativo (começa com -100...). Valores positivos são de DMs. Verifique o sinal.

Teste direto:

curl -s "https://api.telegram.org/bot<TOKEN>/sendMessage?chat_id=<CHAT_ID>&text=teste"

A resposta inclui "ok": true se funcionou, ou "description": "..." com o erro.

Bot em grupo não recebe mensagens

Por padrão, bots em grupos só recebem mensagens diretas (/comando). Para receber todas as mensagens e detectar o chat_id:

  1. No @BotFather: /mybots > seu bot > Bot Settings > Group Privacy > Turn off
  2. Remova e readicione o bot ao grupo

Webhook retorna erro de SSL

O Runner exige HTTPS com certificado válido. Certificados auto-assinados não são aceitos. Use um domínio com certificado Let's Encrypt ou similar.

App pausada não notifica

O evento app_paused é disparado apenas no momento da pausa. Se a app já estava pausada quando o Runner iniciou, não há notificação retroativa.

By Borlot.com.br on 27/05/2026