Perplessità e una certa turbolenza, qualche giorno fa, nella comunità JavaScript.
Create-react-app in errore; ng update in errore; serverless (aws) in errore.
E così centinaia di altri progetti aventi come dipendenza una piccola libreria chiamata is-promise, che vanta milioni di download settimanali, stando alle statistiche di npmjs.org.
Alla cronaca, passano 3 ore di buio di sabato 25 aprile, dal momento in cui è stata resa pubblica una versione “rotta” della libreria al momento della risoluzione. 3 ore in cui centinaia di progetti popolari sono risultati inutilizzabili.
Il dibattito che è seguito, non nuovo peraltro, è sulla opportunità o meno di utilizzare nei propri progetti pacchetti composti da una linea di codice o poco più. Ma la polemica sul numero di linee di codice è perlopiù sterile e la lasciamo ai puristi.
Piuttosto, cerchiamo di capire cosa portare a casa da questo episodio, rinforzando alcuni concetti sulla gestione delle librerie npm.
Come abbiamo visto sopra, lo sgambetto è dietro l’angolo. Una dipendenza rotta et voilà! Anche i player dai quali non ti aspetteresti l’inciampo facile (create-react-app -> Facebook, angular -> Google, serverless -> AWS), cadono su una libreria da una linea di codice.
Noi che siamo piccini, cosa possiamo fare per tutelare i rilasci che includono un package.json? Dobbiamo avere il terrore dell’npm install nella nostra pipeline di deploy?
Proviamo a scegliere una strategia.
- Dependency pinning, ovvero fissare nella pietra le versioni delle librerie che usiamo, specificando nel json l’esatta versione da installare, in modo che in sviluppo, in staging, in produzione, in locale, ovunque, sia in uso la stessa identica versione. Tutto risolto? Non esattamente, perché ci tocca aggiornare a manina i pacchetti con security fixes importanti. Se ci ricordiamo di farlo.
(Tip! “Non aggiorno nulla e sono sereno” è una strada percorribile, ma non consigliata. Provate a lanciare npm audit in uno dei vostri progetti e consultare l’output delle vulnerabilità).
- File di lock e range. Utilizzare un range di versioni affidandoci al versionamento semantico (semver.org), e confidando nel fatto che solo il salto di major possa rompere la retrocompatibilità. Nel mondo reale, tuttavia, non è sempre detto che tutte le minor e le patch siano innocue e l’illusione degli aggiornamenti automatici di sicurezza svanisce presto. In aiuto, arriva il file di lock, che altri non è che un dependency pinning mascherato dietro un range di versioni. I file di lock vengono generati da npm o yarn e rappresentano la fotografia dell’albero delle dipendenze, che viene ricostruito esattamente come descritto, anche in presenza di range di versioni nelle specifiche del package.json, così da avere la certezza di avere le stesse versioni in ogni ambiente. Tutto risolto? Non esattamente, perché ci tocca aggiornare a manina i pacchetti con security fixes importanti e ci tocca spiegare a tutti gli sviluppatori che se dovessero cancellare il file di lock e ricostruirlo li obbligherete a verificare tutte le librerie aggiornate oppure a fare dietrofront… insomma, una serie di spiacevoli situazioni e perdite di tempo.
- Strumenti di automazione nella gestione delle dipendenze. L’uso di tool quali renovate e greenkeeper ci aiuta a gestire le dipendenze, rilevando in automatico la presenza di aggiornamenti e avvisandoci tramite pull requests che è disponibile un aggiornamento di una libreria tra le nostre dipendenze. Possiamo automatizzare la creazione di una branch apposita, lanciare i test con la nuova versione e in caso di esito positivo fare un merge nella master con la nuova versione nel json. Possiamo decidere di non fare l’aggiornamento e farlo in seguito; possiamo configurare lo strumento in modo che esegua in automatico gli aggiornamenti di sicurezza e gestisca eventuali conflitti nel package.json tra le varie branch. Naturalmente, va gestito, configurato appositamente e controllato ogni volta che una pull request ci avvisa della presenza di una nuova versione di qualche libreria.
In ogni caso, la verifica degli aggiornamenti dovrebbe essere fatta a scadenza regolare non troppo lontana nel tempo. Questo perché è solitamente più facile controllare aggiornamenti vicini tra loro per poche librerie piuttosto che aggiornamenti lontani, che prevedono più modifiche rispetto alla versione in uso, per più librerie.
Come sviluppatori, siamo abituati ad utilizzare codice di altri sviluppatori. Lo facciamo tutti i giorni perché è inutile reinventare la ruota. Nel migliore dei casi, verifichiamo che tale codice sia largamente utilizzato, recentemente aggiornato e sia provvisto di test e documentazione appropriata. E lo facciamo perché siamo consapevoli che, in cambio di velocità e rodaggio collettivo, stiamo delegando parte del controllo della nostra applicazione a qualcun altro.
Test e strategia di aggiornamento sono cruciali per non lasciare le cose al caso e limitare le sorprese.