Aggiornamento automatico delle dipendenze PHP con Dependabot e Renovate

Aggiornamento automatico delle dipendenze PHP con Dependabot e Renovate

A gennaio 2026 ho condotto un audit di sicurezza per un'azienda del settore servizi digitali con circa 30 sviluppatori interni, tre applicazioni Laravel in produzione e un monolite Symfony ereditato da sei anni di sviluppo organico. Il composer.lock del monolite conteneva 187 pacchetti di primo e secondo livello. L'audit ha rivelato che 41 di questi pacchetti avevano almeno una CVE pubblicata negli ultimi dodici mesi, cinque avevano CVE di severity critica (CVSS ≥ 9.0), e il più grave era un pacchetto di image processing fermo a una versione di due anni prima con una vulnerabilità RCE ampiamente exploitata nel wild. Il team non era incompetente: il CTO era consapevole del problema, aveva pianificato una "settimana di pulizia dipendenze" ogni trimestre, e il pattern era sempre lo stesso. Nella settimana pianificata il developer designato lanciava composer outdated, vedeva 80-100 pacchetti potenzialmente aggiornabili, provava ad aggiornarne cinque, si rompeva qualcosa in produzione, tornava indietro, concludeva che "il prossimo trimestre con più tempo a disposizione" e la lista cresceva. Dopo tre trimestri consecutivi con lo stesso risultato, la lista era ingestibile. Il costo reale di quella procrastinazione, calcolato sul breach che avrebbe potuto causare uno degli RCE critici identificati, era nell'ordine delle centinaia di migliaia di euro in caso di incidente: ben oltre lo stipendio annuale del dev-ops senior che avrebbe dovuto presidiare la pipeline.

In sei settimane ho ristrutturato l'intera pipeline di aggiornamento dipendenze per quel cliente e per altre cinque PMI con lo stesso problema strutturale, configurando Renovate come orchestratore degli aggiornamenti Composer, raggruppando le PR per tipo di update (patch, minor, major, security), abilitando l'auto-merge selettivo sulle categorie a basso rischio con i test che fungono da gate, e lasciando solo le update major sotto revisione umana. Il risultato al sesto mese sul cliente dell'incipit: la coda di pacchetti con CVE aperte è scesa da 41 a 2 (entrambi major update in attesa di finestra di regressione), il numero di aggiornamenti applicati senza intervento umano è stato 267, il tempo di reazione medio a una CVE critica pubblicata su Packagist è sceso da "prossima settimana di pulizia" a meno di 24 ore. L'articolo che segue è il distillato operativo di quel lavoro, con le configurazioni reali che installo sul giorno uno e le lezioni apprese sui trappoloni che nessun tutorial online menziona.

Perché Dependabot non basta quasi mai sui progetti PHP seri e perché scelgo Renovate

La scelta fra Dependabot e Renovate è la prima decisione architetturale da prendere, ed è una scelta in cui la documentazione di marketing di GitHub è spesso fuorviante per le esigenze di un progetto Laravel o Symfony di medie dimensioni. Dependabot è uno strumento eccellente per casi d'uso semplici: alcune dipendenze da aggiornare, pattern di raggruppamento banali, pipeline CI/CD leggere. Diventa frustrante molto rapidamente quando la codebase ha 150+ pacchetti Composer, perché il suo motore di raggruppamento è limitato, la sua granularità di scheduling è grossolana, e la sua gestione dei lock file durante il merge di PR concorrenti genera conflitti che qualcuno deve risolvere a mano.

Renovate affronta esattamente quei casi d'uso. Il suo motore di packageRules permette di comporre regole con logica arbitraria - "raggruppa tutte le patch di Symfony in una PR, tutte le patch di Laravel in un'altra, gli aggiornamenti di sicurezza in una terza a priorità massima" - e il suo lockFileMaintenance gestisce i conflitti dei file composer.lock generando rebase automatici quando le PR si intersecano. Il manager Composer di Renovate, documentato ufficialmente sul sito del progetto, estrae dipendenze da composer.json mantenendo allineato composer.lock nella stessa commit, interroga Packagist come datasource primario, e supporta come datasource secondarie git-tags e bitbucket-tags per repository privati. Il supporto per oltre 90 package manager della stessa istanza di Renovate - Composer, npm, Python, Docker, Terraform, GitHub Actions - significa che un singolo strumento copre l'intera catena di dipendenze di un progetto moderno, invece di richiedere un Dependabot per il PHP, un tool separato per i container, uno per le action CI.

C'è un elemento di governance che spesso decide la scelta a favore di Renovate su progetti di PMI strutturate: Renovate è self-hostable e può girare come runner Docker sul tuo server (o come GitHub App Mend Renovate gratuita per repo pubblici e piccoli team privati). Dependabot è legato al provider Git - su GitHub funziona, su GitLab devi usare un'alternativa, su un repo self-hosted non hai scelta. Per un cliente che migra da GitHub a GitLab per ragioni di sovranità del dato in Europa, una pipeline basata su Renovate continua a funzionare identica; una basata su Dependabot richiede di essere riscritta da zero. La stessa logica vale per un cliente che decide di spostare i repository su Gitea interno.

La configurazione Renovate che installo il giorno uno su un monolite PHP in produzione

Il file renovate.json che inserisco come baseline nei progetti cliente è frutto di iterazioni su una dozzina di installazioni e di qualche cicatrice operativa. Il pattern è quello di aprire il minor numero possibile di PR (raggruppando), distinguere chiaramente patch/minor/major, garantire che le vulnerabilità di sicurezza non vengano mai accorpate a update ordinari (e quindi ritardate dalla loro logica di raggruppamento), e abilitare l'auto-merge solo dove la barra di rischio lo giustifica.

// renovate.json - baseline per progetto Laravel/Symfony in produzione
{
    "$schema": "https://docs.renovatebot.com/renovate-schema.json",
    "extends": ["config:recommended"],
    "timezone": "Europe/Rome",
    "schedule": ["after 22:00 every weekday", "every weekend"],
    "labels": ["deps", "renovate"],
    "prHourlyLimit": 4,
    "prConcurrentLimit": 8,
    "lockFileMaintenance": {
        "enabled": true,
        "schedule": ["before 05:00 on monday"]
    },
    "vulnerabilityAlerts": {
        "labels": ["security", "priority-high"],
        "schedule": [],
        "prCreation": "immediate",
        "automerge": false
    },
    "packageRules": [
        {
            "description": "Il vincolo PHP platform non va mai toccato da Renovate",
            "matchPackageNames": ["php"],
            "enabled": false
        },
        {
            "description": "Patch Composer: auto-merge dopo test passati",
            "matchManagers": ["composer"],
            "matchUpdateTypes": ["patch"],
            "groupName": "composer-patch",
            "automerge": true,
            "automergeType": "pr",
            "platformAutomerge": true
        },
        {
            "description": "Minor Composer: PR raggruppata, review manuale",
            "matchManagers": ["composer"],
            "matchUpdateTypes": ["minor"],
            "groupName": "composer-minor",
            "automerge": false,
            "addLabels": ["review-required"]
        },
        {
            "description": "Major Composer: PR separate, mai auto-merge",
            "matchManagers": ["composer"],
            "matchUpdateTypes": ["major"],
            "groupName": null,
            "automerge": false,
            "addLabels": ["major", "regression-test-required"]
        },
        {
            "description": "Framework core: fuori dai gruppi, sempre separato",
            "matchPackageNames": [
                "laravel/framework",
                "symfony/symfony",
                "doctrine/orm"
            ],
            "groupName": null,
            "automerge": false,
            "addLabels": ["framework-core"]
        }
    ],
    "postUpdateOptions": ["composerWithAll"]
}

Quattro scelte meritano un approfondimento. Prima: vulnerabilityAlerts.schedule: [] è una stringa vuota volontaria che significa "nessuna finestra di scheduling". Le PR di sicurezza non aspettano il weekend o la sera, vengono aperte nel momento in cui Renovate le rileva, anche se sono le 14 del giovedì in piena attività. Prima di questa scelta, su uno dei miei clienti avevamo una finestra "only at night" e una CVE critica è rimasta aperta per 22 ore perché Renovate stava aspettando la finestra. Non più. Seconda: platformAutomerge: true delega il merge vero al provider Git (GitHub auto-merge, GitLab auto-merge MR) invece che a Renovate stesso. Questo significa che anche se il runner Renovate non gira in quel momento, non appena i test CI passano il provider fa il merge da solo. Terza: il framework core (Laravel, Symfony, Doctrine) esce deliberatamente dai gruppi e non gode mai di auto-merge. Un upgrade minor del framework può introdurre cambi di comportamento sottili che i test automatici non catturano sempre, e vale la pena che un umano legga il changelog prima di mergere. Quarta: lockFileMaintenance pianificato ogni lunedì alle 05:00 forza un composer update completo per portare avanti tutte le dipendenze transitive anche quando i vincoli in composer.json non sono cambiati - una pulizia settimanale che previene l'accumulo di versioni stantie nel composer.lock.

Stai cercando un Consulente Informatico esperto per mettere in sicurezza la catena di dipendenze dei tuoi progetti PHP e per costruire una pipeline CI/CD che regga un audit NIS2? Nel mio profilo professionale trovi l'esperienza concreta su supply chain security e remediation di vulnerabilità in applicazioni Laravel, Symfony e PHP legacy.

Il gate di auto-merge: test, coverage e la barra di rischio calibrata sul team

L'auto-merge delle patch è la scelta che fa la differenza fra una pipeline "ben configurata ma inutilizzata" e una pipeline che mantiene davvero il composer.lock sano. Il motivo è banale: nessun team apre 40 PR al mese manualmente per integrarle, e se ogni patch richiede review di un senior la cadenza si abbassa a 3-5 merge a settimana, che su 150 pacchetti non sono sufficienti a tenere il passo con Packagist. L'auto-merge porta quella cadenza a 40+ merge a settimana senza occupare tempo umano, a patto che il gate dei test sia solido.

Il gate che impongo come prerequisito per abilitare l'auto-merge è questo: la pipeline CI deve includere esecuzione della suite di unit test, almeno un livello di feature/integration test che copra i flussi critici (login, checkout, API endpoint principali), un check di coverage minimo (tipicamente 70% sul codice non-legacy, meno sulle parti ereditate) e un check di static analysis (PHPStan livello 5 o superiore, oppure Psalm livello 3). Se tutti questi check passano per una patch update, la patch è auto-mergiata. Se uno qualunque fallisce, la PR resta aperta con il flag needs-attention e un human si mette al computer.

La struttura .github/workflows/ci.yml che configuro in parallelo è questa, pensata per chiudere il cerchio con l'auto-merge di Renovate:

# .github/workflows/ci.yml - gate per auto-merge Renovate
name: CI
on:
    pull_request:
    push:
        branches: [main]
jobs:
    tests:
        runs-on: ubuntu-latest
        services:
            mysql:
                image: mysql:8.0
                env:
                    MYSQL_ROOT_PASSWORD: root
                    MYSQL_DATABASE: testing
                ports: ["3306:3306"]
        steps:
            - uses: actions/checkout@v4
            - uses: shivammathur/setup-php@v2
              with:
                  php-version: "8.2"
                  coverage: xdebug
                  tools: composer:v2
            - run: composer install --prefer-dist --no-interaction
            # unit + feature test con coverage
            - run: |
                  vendor/bin/pest \
                      --coverage \
                      --min=70 \
                      --coverage-clover=coverage.xml
            # static analysis come seconda barriera
            - run: vendor/bin/phpstan analyse --level=5 --memory-limit=1G
            # audit esplicito delle CVE su composer.lock
            - run: composer audit --locked --abandoned=report

L'ultima riga è quella che raramente trovo già presente nei progetti cliente e che aggiungo sempre: composer audit --locked legge la lista di advisory pubblicate su Packagist e fa fallire il job se una qualunque dipendenza installata ha una CVE aperta. Questo comando è disponibile di default da Composer 2.4 in poi e non richiede servizi esterni. Combinato con Renovate, chiude il loop: se Renovate apre una PR che aggiorna una dipendenza vulnerabile, il job CI verifica che la versione aggiornata non abbia più CVE aperte e passa; se per qualche motivo una CVE nuova viene pubblicata fra l'apertura della PR e il merge, composer audit la rileva e blocca il merge finché qualcuno non valuta la situazione.

Sul cliente dell'incipit, nei primi tre mesi di operatività della pipeline abbiamo avuto un totale di 289 PR aperte da Renovate, 267 mergiate automaticamente dopo i test verdi, 18 lasciate in attesa di review per fallimento di uno dei gate (tipicamente test di feature che toccavano comportamento cambiato in minor update, non bug nel pacchetto aggiornato), e 4 rollback fatti a mano entro 24 ore dal merge perché la patch aveva introdotto una regressione che i test non catturavano. Il tasso di rollback del 1,5% è in linea con quello che misuro su altri clienti e, in assoluto, è drasticamente inferiore al tasso di incidenti causati dalle dipendenze stantie prima dell'adozione della pipeline.

Le CVE di supply chain e perché il raggruppamento separato è non negoziabile

Il punto in cui la configurazione di cui sopra si rivela strategica è il trattamento separato dei vulnerabilityAlerts dagli altri update. Se raggruppi un fix di sicurezza con un aggiornamento minor di un'altra libreria, il merge di quel gruppo dipende dal superamento dei test di entrambe le modifiche - e se il test dell'altra libreria fallisce, stai posticipando la patch di sicurezza per un problema completamente scorrelato. Questo è esattamente il tipo di "ritardo silenzioso" che la compromissione della supply chain Composer tramite typosquatting e dependency confusion sfrutta: l'attaccante sa che fra la pubblicazione di un fix e il suo deployment effettivo ci sono sempre giorni o settimane di finestra utile.

Nel mio approccio, una patch di sicurezza viaggia sempre in una PR dedicata, con label security priority-high, e viene processata dal CI con la priorità più alta possibile. Se il cliente ha un canale Slack o Telegram di alerting, la notifica arriva in tempo reale, e il processo operativo è che qualcuno debba mettere gli occhi su quella PR entro un'ora. L'auto-merge è intenzionalmente disabilitato per le vulnerabilityAlerts - non perché non mi fidi della pipeline di test, ma perché un fix di sicurezza arriva spesso con piccoli breaking change inattesi (un'API deprecata, un parametro con default diverso) che valgono la pena di essere letti da un senior prima di andare in produzione. Questo è coerente con il pattern operativo descritto in dettaglio nella checklist di hardening Laravel/Symfony in 14 giorni per PMI che si preparano a NIS2: la sicurezza è un processo che richiede presenza umana nei punti critici, non un pilota automatico.

Un secondo livello che aggiungo su progetti che gestiscono dati sensibili è l'integrazione di vulnerabilityAlerts di Renovate con Dependabot di GitHub, che consulta il Security Advisory Database ufficiale di GitHub. I due feed di advisory sono complementari: Packagist pubblica prevalentemente gli advisory del PHP Security Advisories Database (FriendsOfPHP), mentre GitHub raccoglie segnalazioni anche da GHSA e NVD. Avere entrambi attivi significa che una CVE pubblicata prima da Dependabot Security Updates può aprire una PR indipendente che Renovate poi riconosce e rispetta. Il costo operativo è trascurabile (qualche notifica duplicata, che il team filtra facilmente), il beneficio di copertura è significativo.

I cinque errori che ho visto fare ai team prima di chiamarmi

Il primo errore è abilitare Renovate senza aver stabilito la CI dei test. L'auto-merge delle patch senza test equivale a deploy automatici alla cieca - è peggio di non avere Renovate, perché ti dà una falsa sensazione di sicurezza mentre accumuli regressioni non rilevate. La regola che impongo prima di qualunque installazione di Renovate è: test suite stabile con tempo di esecuzione sotto i 15 minuti, static analysis configurata, composer audit attivo. Se uno di questi manca, prima lo costruisco e poi installo Renovate.

Il secondo errore è non distinguere fra framework core e dipendenze utility. Un minor update di guzzlehttp/guzzle e un minor update di laravel/framework sono due bestie completamente diverse: il primo è quasi sempre sicuro da auto-mergere con test verdi, il secondo può introdurre cambi semantici in service provider, contract interfaces, configurazione del container IoC, migrazioni deprecate. I miei packageRules separano sempre i framework core con una regola dedicata che toglie l'auto-merge e impone review manuale, anche per le patch - scelta conservativa ma ripagata da zero incidenti in produzione su quei pacchetti in due anni.

Il terzo errore è lasciare prConcurrentLimit troppo alto. I progetti senza limite ricevono 30 PR contemporaneamente il primo giorno e il team abbandona Renovate per frustrazione. Il valore 8 che uso come default genera un flusso gestibile: se ci sono 80 pacchetti da aggiornare, Renovate ne propone 8 alla volta, aspetta che vengano processate, poi apre le successive. L'esperienza del team è "ho sempre qualche PR da guardare", non "sono annegato di PR".

Il quarto errore è non configurare lockFileMaintenance. Senza manutenzione periodica del composer.lock, le dipendenze transitive invecchiano silenziosamente anche quando i vincoli di primo livello in composer.json sembrano aggiornati. lockFileMaintenance settimanale forza una riesecuzione di composer update che propaga al lock tutte le versioni più recenti compatibili con i vincoli, mantenendo sano l'intero albero.

Il quinto errore è non versionare renovate.json con il codice e non trattarlo come codice di prima classe. Le regole di auto-merge, i raggruppamenti, i label, la strategia su major update sono decisioni architetturali che vanno discusse in code review, documentate negli ADR, e riconsiderate quando il team cresce o l'applicazione cambia natura. Il renovate.json non è una configurazione statica da "installa e dimentica": è un contratto operativo che evolve col progetto. Se la tua azienda ha una pipeline PHP con dipendenze Composer che non vengono aggiornate regolarmente, oppure ha subito un audit di sicurezza che ha rivelato CVE aperte su pacchetti stantii e stai cercando come uscirne senza entrare nel ciclo "settimana di pulizia → rottura → rollback → stallo", contattami per una consulenza: in due settimane configuro Renovate calibrato sulle tue caratteristiche specifiche (dimensione codebase, maturità della CI, livello di rischio tollerato), costruisco o rafforzo il gate di auto-merge, e ti consegno un piano di rientro sulle CVE già aperte con priorità calibrata su severity e exploitability reale.

Ultima modifica: