
Un workflow che dice "success" senza prove che abbia fatto qualcosa è solo speranza ben formattata.
Lo status code 0 significa una cosa sola: il processo è arrivato in fondo senza sollevare un'eccezione. Non significa che abbia cambiato lo stato del mondo. Non significa che il file sia stato scritto, che la pagina sia stata generata, che l'utente vedrà qualcosa di diverso. Significa solo che il programma non si è schiantato. È una sfumatura che ho deliberatamente ignorato, dopodiché è venuta a presentarmi il conto.
L'incidente
Il setup è semplice. The Techno-Socratic Method è la mia pubblicazione settimanale, ospitata su Substack ma ridistribuita sul mio sito. Un workflow GitHub Actions schedulato (scheduled-build.yml) gira ogni sabato mattina, controlla l'RSS di Substack, e se trova un nuovo articolo lo pubblica.
Il controllo era banale: prendere la data di oggi, guardare se compariva nella pubDate dell'ultimo item RSS. Se sì, deploya. Se no, salta.
Il bug? Nel formato data.
Substack restituisce pubDate in RFC822: Sat, 09 May 2026 06:00:00 GMT. Il mio script costruiva la data di oggi in ISO 8601: 2026-05-09. Due stringhe che descrivono lo stesso giorno e che non si incontrano mai. Il grep non matchava mai. Il workflow saltava sempre il deploy, in silenzio, usciva con 0, GitHub Actions verde.
Per settimane non me ne sono accorto. Pubblicavo gli articoli a mano il giorno stesso, con un push, e il sito si aggiornava per altre ragioni (il push, non il cron). Il workflow schedulato faceva il suo giro e tornava a casa a mani vuote, dicendo che era andato tutto bene.
Sabato 9 maggio è stato il primo cron a saltare ad essere notato. Non il primo a saltare. Il primo a essere notato.
Il fix è una riga: forzare la stessa locale per costruire la stringa del giorno corrente, in modo che parli lo stesso linguaggio di pubDate.
LC_ALL=C date -u +"%d %b %Y"
Da 2026-05-09 a 09 May 2026. Stesso giorno, formato compatibile con quello che Substack mi dà. Il grep ha cominciato a matchare. Il sabato dopo il cron ha deployato davvero.
Non era la prima volta
Qui c'è una confessione da fare. Il bug era lì dalla prima versione di quel workflow. Mesi. Tutti i deploy precedenti erano passati per fortuna, perché qualcuno (io) aveva pushato a mano nel giorno giusto. Il cron schedulato non aveva mai prodotto un singolo deploy che valesse qualcosa. Aveva prodotto solo log verdi.
Il sito TSM è la mia palestra personale. Lì sperimento, lì rompo cose senza svegliare un cliente alle tre di notte. È sano avere un posto così. È anche il motivo per cui un bug che in un sistema cliente avrebbe causato un incidente serio è rimasto lì per settimane: nessuno guardava davvero, e i log dicevano che andava tutto bene.
Quando ho fatto i conti a posteriori, la stima migliore è "probabilmente la maggioranza dei cron precedenti era già rotta". Non sono andato a contare le run una a una, perché non è quello il punto. Il punto è il pattern: il sistema sapeva di non aver fatto niente, e me lo stava dicendo nel modo più chiaro che poteva, restituendo 0. Ero io che leggevo 0 come "ha funzionato" invece di "non si è schiantato".
Il pattern, oltre il mio cron
Il problema non è il mio cron. Il problema è che ovunque, nei sistemi che incontro tutti i giorni, c'è una distanza fra il segnale che il sistema produce e il fatto che vorremmo che quel segnale rappresentasse. Definisco "sintetico" il segnale che il sistema produce per dichiarare di aver lavorato, e "osservabile" il segnale che dimostra che il lavoro è stato fatto davvero.
| Sistema | Segnale sintetico | Segnale osservabile |
|---|---|---|
| Cron CI | Workflow run "success" | Il file deployato esiste nel bucket |
| Monitoring | Dashboard verde | L'utente in produzione vede la cosa |
| Alert | "PagerDuty configurato" | Qualcuno è stato svegliato di notte negli ultimi tre mesi |
| Healthcheck | HTTP 200 | L'app dietro al load balancer risponde, non solo il load balancer |
| Backup | Job di backup completato | Restore eseguito e validato negli ultimi trenta giorni |
| Test suite | "All green" | Il test sarebbe rosso se cancello una riga di logica reale |
Ognuna di queste righe è un incidente in agguato che ho visto, o che ho contribuito a costruire. Il workflow "success" è quello che ho appena raccontato. La dashboard verde è il cliente che paga per CloudWatch e Grafana e non guarda mai i grafici, finché un alert non scatta tre ore più tardi perché la metrica era stale da una settimana. Il PagerDuty configurato è quello che nessuno ha mai testato con un finto incidente, e la prima volta che dovrebbe partire non parte, perché il routing era stato cambiato sei mesi prima. L'healthcheck a 200 è il classico: l'endpoint risponde 200 perché il load balancer è vivo, ma l'app dietro è morta da ore. Il backup completato è il backup che nessuno ha mai provato a ripristinare, e il giorno in cui serve davvero si scopre che è corrotto, o che il restore richiede tre ore che nessuno aveva previsto. La test suite all green è la suite che passa anche se cancelli a caso una funzione, perché i test sono accoppiati alla forma del codice e non al suo comportamento.
Lo stesso pattern è ovunque, anche fuori dal software. Il mio Xiaomi lavapavimenti, quando il serbatoio dell'acqua è vuoto, mi dice "serbatoio vuoto o non installato". Un OR diagnostico pigro. La cella di carico distingue benissimo i due stati (il serbatoio installato pesa qualcosa anche da vuoto, quello assente pesa zero), ma il firmware decide che non vale la pena fare lo switch. Il sistema sa, ma non si impegna a dirti la verità. È un cugino del cron che dice success: il sistema sa cosa è successo e sceglie di non distinguere. La pigrizia diagnostica è una scelta del produttore, non un vincolo.
Cosa faccio adesso
Non c'è una rivoluzione qui. C'è un'abitudine da cambiare nel modo in cui leggo i segnali.
- Verifica un side-effect osservabile, non lo status code. Se un cron deploya una pagina, il test del cron è "la pagina esiste e ha il contenuto giusto", non "il workflow ha terminato".
- Il lavoro di un cron è cambiare lo stato del mondo. Testa quel cambio, non l'esecuzione del workflow. Vale per le pipeline di pubblicazione, per i job di sync, per le scadenze dei certificati. Il segno che è andato tutto bene non è che il job è arrivato in fondo, è che il mondo è diverso come dovrebbe.
- Per pipeline schedulate critiche, aggiungi un sotto-test che fallisce se l'output atteso non c'è nella finestra prevista. Se sabato mattina il bucket S3 non ha un file nuovo, qualcosa deve diventare rosso. Non si può aspettare di scoprirlo lunedì leggendo l'analytics.
- I test sul workflow CI sono workflow CI essi stessi. Meta-monitoring leggero, sì. Turtles all the way down, sì. Meglio una tartaruga in più che una pipeline che vive di speranza.
- La fortuna non è un piano di rilascio. Tutto quello che funziona "perché qualcuno ha pushato a mano" è una copertura per modo di dire, e il giorno in cui quel qualcuno è in ferie il bug viene allo scoperto.
Niente di tutto questo richiede tooling nuovo. Richiede solo di smettere di confondere "il processo è finito" con "il lavoro è stato fatto".
Il filo comune
In un altro appunto ho già scritto che un backup che non è mai stato testato non è un backup, è una speranza. È lo stesso pattern. Cambia solo l'oggetto: lì il backup, qui il cron schedulato, in altri sistemi la dashboard, l'alert, il test. La forma è identica. Il sistema produce un segnale di buon esito che non è stato verificato sul piano dei fatti, e nessuno se ne accorge finché un evento esterno (un cliente che si lamenta, un dato che manca, un sito che è giù) non costringe a guardare.
Non è incompetenza. È il modo naturale in cui i sistemi crescono. Aggiungi un check, controlla che il processo termini, vai avanti. La differenza fra "termina" e "produce il risultato che ti aspetti" è una sfumatura che richiede tempo di pensiero, e il tempo di pensiero è la cosa che manca sempre per prima.
Se hai una pipeline schedulata che dice "success" da mesi e non hai mai verificato un side-effect concreto, c'è una buona probabilità che ti stia mentendo. Non perché sia rotta nel modo evidente, ma perché ti sta dicendo "sono arrivata in fondo" e tu lo stai leggendo come "ho fatto il mio lavoro". Sono due frasi diverse.
Se vuoi capire dove i tuoi sistemi ti stanno raccontando una versione comoda della realtà... parliamone . Ti dico cosa controllare per primo, e cosa puoi smettere di guardare.

In sintesi (TL;DR)
- Lo status code di un workflow racconta che il processo è finito, non che il lavoro sia stato fatto.
- Ogni sistema ha un modo di mentire sul successo: dashboard verdi con metriche stale, healthcheck che pingano il load balancer e non l'app, backup che non si ripristinano mai, alert che non sparano perché la condizione è scritta male.
- Il rimedio è verificare un side-effect osservabile, non un segnale sintetico: il file deployato esiste, l'utente vede la pagina, qualcuno è stato svegliato.
- L'ho imparato sul mio blog, in un sabato di maggio, dopo settimane di workflow rotti che riportavano success.