Le notifiche Microsoft Teams inviate da GitLab CI/CD tramite “Incoming Webhook” hanno smesso di comparire, mentre la pipeline continua a risultare “success”. In questa guida trovi una diagnosi completa e soluzioni pratiche per ripristinare l’invio, migliorare l’osservabilità e prevenire future interruzioni.
Scenario e sintomi
Un canale di Microsoft Teams riceveva regolarmente messaggi generati da uno script Python eseguito in una pipeline GitLab CI/CD. All’improvviso, i messaggi non compaiono più nel canale, ma le pipeline continuano a chiudersi con stato “success”. L’ipotesi iniziale è un malfunzionamento del connettore Incoming Webhook di Teams o un problema di payload.
Come funziona davvero l’Incoming Webhook di Teams
L’Incoming Webhook è un endpoint univoco associato a un canale. Quando invii un HTTP POST con Content-Type: application/json
, Teams tenta di rendere il contenuto come MessageCard (schema Office 365 Connector) o, in molti tenant, come Adaptive Card (versioni supportate dal tenant). Se lo schema è errato, se mancano campi obbligatori o se l’URL è stato invalidato, la richiesta può terminare con codici 4xx
/5xx
oppure — se il tuo script non gestisce bene le risposte — “passare in silenzio” senza alcuna notifica visibile nel canale.
Checklist rapida: individuare la causa in pochi minuti
Passo | Cosa verificare / fare | Perché è utile |
---|---|---|
Controllare lo stato di Teams | Verifica nell’area di amministrazione Microsoft 365 > Service Status eventuali interruzioni o degradi. | Un problema lato servizio può bloccare i connettori a livello globale o regionale. |
Verificare impostazioni Team/Canale | In Teams > Gestisci team > App, assicurati che i connettori siano consentiti e che “Incoming Webhook” sia installato sul canale giusto (attenzione a canali privati/condivisi). | Policy o modifiche ai permessi possono interrompere la consegna senza toccare la pipeline. |
Testare il webhook a mano | Invia un JSON di esempio con curl o Postman allo stesso URL; osserva il codice HTTP e l’eventuale messaggio (410 Gone = URL scaduto). | Isola rapidamente se il problema è nello script o nell’endpoint di Teams. |
Ricreare il webhook | Rimuovi il connettore dal canale, creane uno nuovo e aggiorna l’URL nella pipeline (in una variabile protetta). | Token revocati o corrotti sono una causa frequente: rigenerare risolve. |
Validare il payload | Controlla che lo script generi una MessageCard o Adaptive Card valida (campi obbligatori, sintassi, dimensione). | Teams scarta payload non conformi, spesso con 400 Bad Request . |
Abilitare log & retry | Logga sempre codice/headers/estratto del body di risposta e implementa retry con backoff esponenziale e jitter. | Rivela errori transitori (rete, throttling, 429) e velocizza la diagnosi. |
Valutare alternative | Se i problemi persistono: orchestrare via Azure Logic Apps o Power Automate, o inviare messaggi a Teams con Microsoft Graph. | Più osservabilità, best practice di sicurezza e resilienza enterprise. |
Cause tipiche (e come riconoscerle)
- URL scaduto o revocato: la risposta è spesso
410 Gone
. Succede se il connettore è stato rimosso/ricreato, o se il canale è stato archiviato o cambiato di stato. - Connettori disattivati da policy: i tenant possono disabilitare i connettori; potresti vedere
403 Forbidden
o generici400
. - Payload non valido: campi sbagliati (es.
@type
mancante), JSON malformato, uso di schema Adaptive Card non supportato. Tipico400 Bad Request
. - Throttling: troppi messaggi in burst possono restituire
429 Too Many Requests
o errori intermittenti. Raggruppa messaggi o dilaziona l’invio. - Payload troppo grande: limiti di dimensione (nell’ordine delle decine di KB). Riduci il numero di campi/facts o invia messaggi modulari.
- Proxy/Firewall: runner GitLab dietro proxy non configurato o ispezione TLS può causare errori di handshake (
SSL
) o timeouts. - Header errati:
Content-Type
sbagliato o charset non riconosciuto può produrre415 Unsupported Media Type
. - Canali speciali: differenze tra canali standard, privati o condivisi possono incidere sulla disponibilità del connettore.
Diagnostica veloce: test manuale con curl
Usa lo stesso URL che la pipeline sta usando; esportalo come variabile d’ambiente in locale o in una job temporanea.
export TEAMSWEBHOOKURL="https://outlook.office.com/webhook/..."
cat > payload.json << 'JSON'
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Test Webhook",
"themeColor": "0076D7",
"title": "Test dal terminale",
"text": "Questa è una notifica di prova."
}
JSON
curl -i
-H "Content-Type: application/json"
-d @payload.json
"$TEAMSWEBHOOKURL"
Interpreta la risposta:
HTTP | Significato | Azione consigliata |
---|---|---|
200 OK / 1 | Messaggio accettato. | Se in pipeline non arriva, è un problema nello script o nelle variabili d’ambiente. |
400 Bad Request | Payload non valido. | Convalida schema, riduci dimensione, controlla caratteri speciali/UTF‑8. |
403 Forbidden | Blocco a livello di policy/tenant. | Controlla le impostazioni App/Connettori in Teams. |
410 Gone | URL revocato o connettore rimosso. | Ricrea l’Incoming Webhook e aggiorna l’URL in CI/CD. |
415 Unsupported Media Type | Content-Type errato. | Usa application/json puro. |
429 Too Many Requests | Throttling per rate limit. | Retry con back‑off, raggruppa messaggi, aggiungi ritardi. |
5xx | Errore temporaneo lato servizio. | Retry + circuito aperto se persiste, verifica stato servizio. |
Payload: esempi validi (MessageCard e Adaptive Card)
MessageCard (massima compatibilità)
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Build GitLab",
"themeColor": "36C5F0",
"title": "✅ Build completata",
"sections": [
{
"activityTitle": "Progetto: my-app",
"facts": [
{"name": "Pipeline", "value": "#1245"},
{"name": "Branch", "value": "main"},
{"name": "Durata", "value": "3m 12s"}
],
"markdown": true
}
],
"potentialAction": [
{
"@type": "OpenUri",
"name": "Apri pipeline",
"targets": [{"os": "default", "uri": "https://gitlab.example/pipeline/1245"}]
}
]
}
Adaptive Card (se supportata dal tuo tenant)
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{"type": "TextBlock", "size": "Large", "weight": "Bolder", "text": "Build GitLab: ✅ SUCCESS"},
{"type": "FactSet", "facts": [
{"title": "Pipeline", "value": "#1245"},
{"title": "Branch", "value": "main"},
{"title": "Durata", "value": "3m 12s"}
]}
],
"actions": [
{"type": "Action.OpenUrl", "title": "Apri pipeline", "url": "https://gitlab.example/pipeline/1245"}
]
}
}
]
}
Nota: se ottieni 400
, prova prima con MessageCard; è la via più robusta tra tenant e configurazioni diverse. Mantieni il payload compatto e valida il JSON (virgolette, virgole, caratteri speciali).
Implementazione robusta in Python (requests)
Integra error handling, timeouts, retry con back‑off ed evidenziazione degli status code. Salva un estratto della risposta per la diagnosi.
import json, os, time, random, logging
import requests
WEBHOOKURL = os.environ.get("TEAMSWEBHOOK_URL")
TIMEOUT = (3.05, 10) # connect, read
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
def posttoteams(payload: dict, max_retries: int = 5) -> None:
if not WEBHOOK_URL:
raise RuntimeError("TEAMSWEBHOOKURL non impostata")
```
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
attempt = 0
while True:
attempt += 1
try:
resp = requests.post(
WEBHOOK_URL,
data=data,
headers={"Content-Type": "application/json"},
timeout=TIMEOUT,
)
status = resp.status_code
body_preview = (resp.text or "").strip()[:500]
logging.info("Teams responded %s: %r", status, body_preview)
if status == 200:
return
if status in (429, 502, 503, 504):
# retryable
if attempt <= max_retries:
backoff = min(2 ** attempt, 60) + random.uniform(0, 1)
logging.warning("Retryable status %s. Retry in %.1fs", status, backoff)
time.sleep(backoff)
continue
# non-retryable or out of retries
raise RuntimeError(f"Teams webhook error {status}: {body_preview}")
except requests.RequestException as e:
if attempt <= max_retries:
backoff = min(2 ** attempt, 60) + random.uniform(0, 1)
logging.warning("Network error: %s. Retry in %.1fs", e, backoff)
time.sleep(backoff)
continue
raise
```
Esempio di invio di una MessageCard minimale
if name == "main":
card = {
"@type": "MessageCard",
"@context": "[https://schema.org/extensions](https://schema.org/extensions)",
"summary": "Notifica CI",
"themeColor": "36C5F0",
"title": "🔔 Notifica di test",
"text": "Questa è una prova inviata da GitLab CI."
}
posttoteams(card)
.gitlab-ci.yml: variabili, segreti e job di notifica
Conserva l’URL del webhook in una variabile protetta (masked) a livello di progetto/ambiente. Esempio di job di notifica che invia un payload MessageCard con dettagli di pipeline:
stages:
- build
- test
- notify
variables:
PYTHONUNBUFFERED: "1"
notify:teams:
stage: notify
image: python:3.12-slim
rules:
- if: $CIPIPELINESOURCE == "push"
script:
- pip install --no-cache-dir requests
- |
python - <<'PY'
import os, json, requests
url = os.environ.get("TEAMSWEBHOOKURL")
card = {
"@type": "MessageCard",
"@context": "[https://schema.org/extensions](https://schema.org/extensions)",
"summary": "Build GitLab",
"themeColor": "36C5F0",
"title": f"Pipeline {os.environ.get('CIPIPELINESTATUS', '').upper()}",
"sections": [{
"facts": [
{"name": "Pipeline", "value": f"#{os.environ.get('CIPIPELINEIID')}"},
{"name": "Project", "value": os.environ.get('CIPROJECTPATH')},
{"name": "Branch", "value": os.environ.get('CICOMMITBRANCH')},
{"name": "Commit", "value": os.environ.get('CICOMMITSHORT_SHA')}
],
"markdown": true
}],
"potentialAction": [{
"@type": "OpenUri",
"name": "Apri pipeline",
"targets": [{"os": "default", "uri": os.environ.get('CIPIPELINEURL')}]
}]
}
r = requests.post(url, headers={"Content-Type":"application/json"}, data=json.dumps(card).encode("utf-8"), timeout=15)
print("Status:", r.status_code, "Body:", r.text[:500])
if r.status_code != 200:
raise SystemExit(f"Teams webhook failed: {r.status_code}")
PY
allow_failure: false
after_script:
- echo "Notifica terminata."
Verifiche di configurazione su Teams
- Consenso dei connettori abilitato: nelle impostazioni del Team assicurati che la voce relativa all’uso dei connettori sia attiva. Se il tenant usa policy centralizzate per app di terze parti, verifica che “Incoming Webhook” non sia bloccata.
- Connettore installato sul canale corretto: se il canale è stato rinominato, spostato, reso privato o archiviato, l’endpoint potrebbe non funzionare più.
- Permessi del proprietario: solo proprietari del team/canale possono aggiungere o rimuovere il connettore; evita che modifiche indesiderate invalidino l’URL.
Hardening e pratiche consigliate
- Versioning degli URL: tieni il webhook in una variabile CI/CD e ruotalo in modo controllato. Usa feature flags per passare da un URL all’altro senza toccare il codice.
- Riduzione del rumore: invia un solo messaggio “SUCCESS” per pipeline, raggruppa eventi minori in un’unica card o limita le notifiche alle pipeline su main/release.
- Rate limits: inserisci sleep fra messaggi in burst e implementa retry con back‑off (vedi esempio Python).
- Osservabilità: logga sempre status code, estratto del body e tempi di risposta; in caso di errore, allega alla pipeline un artefatto testo con la richiesta e la risposta (sanitizzate).
- Proxy & reti: se usi un proxy aziendale, imposta
HTTPSPROXY
/NOPROXY
sul runner. Verifica che l’ispezione TLS non alteri il certificato. - Dimensione del payload: mantieni il JSON snello; evita liste/array lunghissimi. In caso di log estesi, carica i dettagli altrove e inserisci solo un link nella card.
Playbook di risoluzione (dettaglio passo-passo)
- Sanity check del servizio: controlla se ci sono annunci di degrado su Teams. Se sì, sospendi i tentativi fino al ripristino.
- Conferma policy e app: verifica che il connettore “Incoming Webhook” sia consentito dal tuo tenant e che il canale lo abbia installato.
- Test a mano dell’URL: invia il payload MessageCard di esempio con
curl
. Se non ottieni200
, il problema è nell’endpoint o nel payload. - Ruota l’URL: rimuovi e ricrea il connettore; aggiorna la variabile
TEAMSWEBHOOKURL
. Esegui un nuovo test. - Valida lo schema: prova prima MessageCard. Se serve Adaptive Card, usa versioni minori (es. 1.2/1.3) se il tenant non accetta 1.4/1.5.
- Controlla headers:
Content-Type: application/json
senza charset superflui. Niente BOM UTF‑8. - Log & retry: integra la funzione di retry con back‑off; registra status/body per ogni tentativo.
- Gestisci rate limit: se invii molti messaggi, usa un buffer per combinarli in una sola card o dilaziona con ritardi casuali (jitter).
- Automatizza un health check: schedula un job nightly che invii un “ping” al canale; allarmi se ricevi errori ≠
200
.
Template “health check” riutilizzabile
healthcheck:teams:
stage: notify
image: curlimages/curl:8.8.0
rules:
- if: $CIPIPELINESOURCE == "schedule"
script:
- |
cat > payload.json << 'JSON'
{
"@type": "MessageCard",
"@context": "https://schema.org/extensions",
"summary": "Teams Webhook Health",
"themeColor": "6A5ACD",
"title": "🔎 Health check",
"text": "Ping automatico dal CI. Se non vedi questo messaggio, il webhook potrebbe essere rotto."
}
JSON
- |
set -e
HTTPCODE=$(curl -s -o resp.txt -w "%{httpcode}" \
-H "Content-Type: application/json" \
-d @payload.json "$TEAMSWEBHOOKURL")
echo "HTTP: $HTTP_CODE"
head -c 500 resp.txt || true
test "$HTTP_CODE" = "200"
allow_failure: false
artifacts:
when: always
paths: [resp.txt]
expire_in: 1 week
Confronto: Incoming Webhook vs Graph API vs Logic Apps
Opzione | Pro | Contro | Quando sceglierla |
---|---|---|---|
Incoming Webhook | Velocissimo da attivare, nessun token OAuth, semplice JSON. | Osservabilità limitata, schema talvolta restrittivo, rate limits. | Notifiche semplici e veloci, team piccoli/medi. |
Microsoft Graph (app) | API ufficiale messaggi canale, auditing migliore, sicurezza enterprise. | Richiede registrazione app, gestione token/permessi, complessità. | Organizzazioni con requisiti di sicurezza, volumi elevati, flussi complessi. |
Logic Apps / Power Automate | Retry, monitoraggio, orchestrazione, connettori multipli. | Costi, latenza aggiuntiva, complessità di gestione. | Pipeline mission‑critical, bisogno di resilienza e governance. |
FAQ pratiche
“La pipeline dice success, ma non vedo messaggi in Teams”
Probabilmente lo script non sta validando il codice HTTP. Fallisci il job se il codice ≠ 200
e stampa un estratto della risposta. Aggiungi artefatti con i log.
“Ho cambiato nome al canale, il webhook è ancora valido?”
Di solito sì, ma se il canale è stato convertito (privato/condiviso), archiviato o se il connettore è stato rimosso, l’URL può invalidarsi. In caso di dubbio: ricrea.
“Posso inviare testo semplice senza card?”
Il supporto al puro {"text": "..."}
non è garantito in tutti i tenant. Per massima compatibilità invia una MessageCard minimale.
“Ricevo 429”
Stai inviando troppo velocemente. Combina messaggi (p.es. un riepilogo per job) o inserisci ritardi e back‑off esponenziale con jitter.
“Errore SSL dal runner”
Se c’è ispezione TLS, importa il certificato della CA aziendale nel container/runner o escludi l’endpoint dalle ispezioni, rispettando la policy di sicurezza.
Lista di controllo pronta all’uso
- ✅ Verificato stato servizio Teams.
- ✅ Confermato che i connettori e “Incoming Webhook” sono abilitati sul canale.
- ✅ Test manuale con
curl
→200 OK
. - ✅ Payload MessageCard valido e compatto.
- ✅ Rotazione dell’URL e aggiornamento variabili CI/CD.
- ✅ Logging dettagliato di richiesta/risposta e fail fast su
≠200
. - ✅ Retry con back‑off + gestione rate limit.
- ✅ Health check periodico e alerting.
Conclusioni
Quasi tutte le interruzioni delle notifiche Microsoft Teams via Incoming Webhook si risolvono verificando tre aree: endpoint (URL ancora valido e policy che lo consenta), payload (schema e dimensione) e integrazione (script con log, gestione errori e retry). Partendo dal test manuale con curl
, proseguendo con la rigenerazione del connettore e con la validazione del JSON, puoi ripristinare rapidamente il flusso. Consolidando poi logging, health check e rate limiting, trasformi un’integrazione fragile in una pipeline affidabile e osservabile.
Risposta e soluzioni proposte (riepilogo operativo)
Passo | Cosa verificare / fare | Perché è utile |
---|---|---|
Controllare lo stato del servizio Teams | Verifica eventuali interruzioni sullo stato del servizio Microsoft 365. | Un degrado lato Microsoft può bloccare i connettori. |
Verificare le impostazioni del team / canale | Abilita “Consenti ai connettori di inviare messaggi al canale”. | Policy o permessi possono interrompere la consegna. |
Testare il webhook manualmente | Invia un payload JSON di esempio; osserva il codice HTTP (410 Gone = URL scaduto). | Isola il problema (webhook vs script GitLab). |
Ricreare il webhook | Rimuovi e rigenera il connettore; aggiorna l’URL nella pipeline. | Rimedia a token invalidati o corrotti. |
Validare il payload | Usa MessageCard o Adaptive Card conforme allo schema. | Teams scarta payload non validi. |
Log e retry | Registra status/body e implementa retry con back‑off. | Gestisci errori transitori e throttling. |
Alternative | Azure Logic Apps, Power Automate, o invio via Microsoft Graph. | Più resilienza e governance. |
Suggerimenti aggiuntivi
- Versioning degli URL: mantieni i segreti nelle variabili CI/CD per rotazioni senza commit.
- Rate limits: concatena messaggi in una sola card o inserisci ritardi per burst intensi.
- Monitoraggio proattivo: aggiungi un health check e allerta se la risposta ≠
200 OK
.
Con queste verifiche dovresti individuare se la causa è il servizio Teams, i permessi del canale, l’URL del connettore o il payload e ripristinare rapidamente la ricezione delle notifiche nel canale.
—
Hai casi particolari (canali privati, multi‑tenant, runner dietro proxy, payload molto grandi)? Adatta i template qui sopra: usa MessageCard minimale per sbloccare, poi aggiungi gradualmente dettagli per non superare i limiti e mantenere l’aderenza allo schema.