{"id":31304,"date":"2019-10-31T21:40:34","date_gmt":"2019-10-31T18:40:34","guid":{"rendered":"https:\/\/prohoster.info\/blog\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\/"},"modified":"2019-10-31T21:40:34","modified_gmt":"2019-10-31T18:40:34","slug":"evolyutsiya-ci-v-komande-mobilnoj-razrabotki","status":"publish","type":"post","link":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","title":{"rendered":"L'evoluzione del CI nel team di sviluppo mobile","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Oggi, la maggior parte dei prodotti software \u00e8 sviluppata in team. Le condizioni per il successo dello sviluppo di team possono essere rappresentate in uno schema semplice.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/b283cfd7772d8f479be0754ebbe51b19.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDopo aver scritto il codice, devi assicurarti che esso:<\/p>\n<ol>\n<li>Funziona.<\/li>\n<li>Non rompe nulla, compreso il codice che hanno scritto i tuoi colleghi.<\/li>\n<\/ol>\n<p>\nSe entrambe le condizioni sono soddisfatte, sei sulla buona strada per il successo. Per controllare facilmente queste condizioni e non deviare da un percorso vantaggioso, \u00e8 stato inventato il Continuous Integration.<\/p>\n<p>Il CI \u00e8 un processo di lavoro in cui integri il tuo codice nel codice comune del prodotto il pi\u00f9 spesso possibile. E non solo integri, ma controlli anche costantemente che tutto funzioni. Poich\u00e9 \u00e8 necessario controllare molto e frequentemente, vale la pena considerare l'automazione. \u00c8 possibile verificare tutto manualmente, ma non conviene, ecco perch\u00e9.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<ul>\n<li><strong>Le persone sono costose<\/strong>. Il costo di un\u2019ora di lavoro di qualsiasi programmatore \u00e8 maggiore del costo orario di qualsiasi server.<\/li>\n<li><strong>Le persone sbagliano<\/strong>. Pertanto, possono verificarsi situazioni in cui esegui test su un ramo sbagliato o raccogli un commit errato per i tester.<\/li>\n<li><strong>Le persone sono pigre<\/strong>. Periodicamente, quando finisco qualche compito, mi viene in mente: \"Ma cosa devo controllare? Ho scritto due righe \u2014 certo funziona tutto!\" Penso che anche a qualcuno di voi venga in mente qualche pensiero del genere. Ma \u00e8 sempre necessario controllare.<\/li>\n<\/ul>\n<p>\nCome \u00e8 stata implementata e sviluppata l'integrazione continua nel team di sviluppo mobile di Avito, come siamo passati da 0 a 450 compilazioni al giorno, e cosa raccolgono le macchine di build per 200 ore al giorno, lo racconta Nikolai Nesterov (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/users\/nnesterov\/\" class=\"user_link\">nnesterov<\/a><\/noindex>) \u2014 partecipante a tutti i cambiamenti evolutivi del CI\/CD dell'applicazione Android.<\/p>\n<p>La storia \u00e8 basata sull'esempio del team Android, ma la maggior parte degli approcci \u00e8 applicabile anche su iOS.<\/p>\n<p><center><div class=\"youtube-placeholder\" data-id=\"lz8MNATTUCU\" onclick=\"loadVideo(this)\">\r\n        <img decoding=\"async\" src=\"https:\/\/img.youtube.com\/vi\/lz8MNATTUCU\/hqdefault.jpg\" alt=\"Guarda il video\" loading=\"lazy\" width=\"480\" height=\"360\" style=\"width:100%;height:auto;\">\r\n        <div class=\"play-button\"><\/div>\r\n    <\/div><\/center><br \/>\nTanto tempo fa, nel team Android di Avito lavorava una sola persona. Per definizione, non aveva bisogno di nulla del Continuous Integration: non c'era nessuno con cui integrarsi.<\/p>\n<p>Ma l'applicazione cresceva, c'erano sempre pi\u00f9 nuovi compiti, di conseguenza, il team crescevano. A un certo punto, \u00e8 arrivato il momento di organizzare in modo pi\u00f9 formale il processo di integrazione del codice. \u00c8 stato deciso di utilizzare il Git flow.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/f433effc5ab5a59de3d6bd20a7a87057.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nLa concezione del Git flow \u00e8 nota: nel progetto c'\u00e8 un ramo comune chiamato develop, e per ogni nuova funzionalit\u00e0 gli sviluppatori creano un ramo separato, ci committano, lo spingono, e quando vogliono unire il loro codice nel ramo develop, aprono una pull request. Per lo scambio di conoscenze e la discussione dei metodi, abbiamo introdotto la code review, il che significa che i colleghi devono controllare e approvare il codice reciproco.<\/p>\n<h2>Verifiche<\/h2>\n<p>\nGuardare il codice con gli occhi \u00e8 fantastico, ma non basta. Per questo vengono introdotte verifiche automatiche.<\/p>\n<ul>\n<li>Prima di tutto controlliamo <strong>la build ARC<\/strong>.<\/li>\n<li>Molti <strong>test Junit<\/strong>.<\/li>\n<li><strong>Calcoliamo la code coverage<\/strong>, visto che avviamo i test.<\/li>\n<\/ul>\n<p>\nPer capire come avviare queste verifiche, guardiamo il processo di sviluppo in Avito.<\/p>\n<p>Schematicamente si pu\u00f2 rappresentare cos\u00ec:<\/p>\n<ul>\n<li>Lo sviluppatore scrive codice sul proprio laptop. Pu\u00f2 avviare le verifiche di integrazione proprio qui \u2014 o tramite hooks di commit, oppure semplicemente eseguendo le verifiche in background.<\/li>\n<li>Dopo che lo sviluppatore ha spinto il codice, apre una pull request. Affinch\u00e9 il suo codice venga unito nel ramo develop, deve passare la code review e ottenere il numero necessario di approvazioni. \u00c8 possibile attivare le verifiche e le build qui: fino a quando tutte le build non saranno riuscite, la pull request non pu\u00f2 essere unita.<\/li>\n<li>Dopo che la pull request \u00e8 stata unita e il codice \u00e8 entrato in develop, si pu\u00f2 scegliere un momento conveniente: ad esempio, di notte, quando tutti i server sono liberi, e far girare le verifiche il pi\u00f9 possibile.<\/li>\n<\/ul>\n<p>\nA nessuno \u00e8 piaciuto avviare le verifiche sul proprio laptop. Quando lo sviluppatore ha terminato la funzionalit\u00e0, vuole caricarla il prima possibile e aprire la pull request. Se in quel momento vengono avviate delle verifiche lunghe, non \u00e8 solo poco piacevole, ma rallenta anche lo sviluppo: mentre il laptop fa delle verifiche, non \u00e8 possibile lavorare normalmente.<\/p>\n<p>Avviare le verifiche di notte ci \u00e8 piaciuto molto, perch\u00e9 ci sono molti server e tempo, si pu\u00f2 sbizzarrirsi. Ma, sfortunatamente, quando il codice della funzionalit\u00e0 \u00e8 stato inserito in develop, lo sviluppatore ha gi\u00e0 molta meno motivazione per correggere gli errori trovati dal CI. Di tanto in tanto mi sorprendevo a pensare, mentre guardavo il report mattutino con tutti gli errori trovati, che li avrei sistemati pi\u00f9 tardi, perch\u00e9 ora c'era un fantastico nuovo compito in Jira che volevo iniziare a fare.<\/p>\n<p>Se le verifiche bloccano la pull request, allora c'\u00e8 abbastanza motivazione, perch\u00e9 finch\u00e9 le build non diventano verdi, il codice non entrer\u00e0 in develop, e quindi il compito non sar\u00e0 completato.<\/p>\n<p>Alla fine abbiamo scelto questa strategia: di notte eseguiamo il massimo numero possibile di controlli, mentre i pi\u00f9 critici e, soprattutto, i pi\u00f9 veloci, li lanciamo sulla pull request. Ma non ci fermiamo qui: parallelamente ottimizziamo la velocit\u00e0 di esecuzione dei controlli in modo da trasferirli dalla modalit\u00e0 notturna ai controlli sulla pull request.<\/p>\n<p>A quel tempo tutte le nostre build venivano eseguite piuttosto velocemente, quindi abbiamo semplicemente incluso come blocker per la pull request la build ARK, i test Junit e il calcolo della copertura del codice. L'abbiamo inclusa, abbiamo riflettuto e abbiamo rinunciato alla copertura del codice perch\u00e9 ritenuto non necessaria.<\/p>\n<p><strong><em>Per configurare il CI di base ci sono voluti due giorni (qui e oltre, la stima temporale \u00e8 indicativa, utile per il contesto). <\/em><\/strong><\/p>\n<p>Dopo ci\u00f2 abbiamo iniziato a riflettere: stiamo controllando correttamente? Avviamo correttamente le build sulla pull request?<\/p>\n<p>Eseguivamo la build sull'ultimo commit del ramo da cui \u00e8 stata aperta la pull request. Ma i controlli su quel commit possono mostrare solo che il codice scritto dallo sviluppatore funziona. Ma non dimostrano che non ha rotto nulla. In realt\u00e0, \u00e8 necessario controllare lo stato del ramo develop dopo che la funzionalit\u00e0 \u00e8 stata integrata.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/c1a179b68b02c04031177f9bf51a81ed.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPer questo abbiamo scritto un semplice script bash <strong>premerge.sh:<\/strong><\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\nset -e\n\ngit fetch origin develop\n\ngit merge origin\/develop<\/code><\/pre>\n<p>\nQui vengono semplicemente recuperate tutte le modifiche pi\u00f9 recenti da develop e integrate nel ramo attuale. Abbiamo aggiunto lo script premerge.sh come primo passo di tutte le build e abbiamo iniziato a controllare esattamente ci\u00f2 che vogliamo, cio\u00e8 <strong>l'integrazione<\/strong>.<\/p>\n<p><strong><em>Per la localizzazione dei problemi, la ricerca di soluzioni e la scrittura di questo script ci sono voluti tre giorni.<\/em><\/strong><\/p>\n<p>L'applicazione si \u00e8 evoluta, i compiti sono aumentati, il team \u00e8 cresciuto e premerge.sh ha iniziato a darci problemi. Cambiamenti conflittuali entravano in develop, causando errori nelle build.<\/p>\n<p>Ecco un esempio di come accade:<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/6762d9ce4e431549455c49f2317a8f20.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDue sviluppatori iniziano contemporaneamente a lavorare sulle funzionalit\u00e0 A e B. Lo sviluppatore della funzionalit\u00e0 A scopre una funzione non utilizzata nel progetto <code>answer()<\/code> e, da buon boy scout, la elimina. Nel frattempo, lo sviluppatore della funzionalit\u00e0 B nella sua branch aggiunge una nuova chiamata a quella funzione.<\/p>\n<p>Gli sviluppatori concludono il lavoro e aprono simultaneamente la pull request. Vengono avviate le build, premerge.sh controlla entrambe le pull request rispetto allo stato aggiornato di develop: tutti i controlli sono verdi. Dopo ci\u00f2 viene unita la pull request della funzionalit\u00e0 A, viene unita la pull request della funzionalit\u00e0 B... Boom! Develop si rompe, perch\u00e9 nel codice di develop c'\u00e8 una chiamata a una funzione inesistente.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/0261dcc9f7f2e5e018ab136081b4679b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nQuando develop non si compila, questo <strong>disastro locale<\/strong>. Tutto il team non riesce a raccogliere nulla e a consegnarlo per i test.<\/p>\n<p>\u00c8 capitato che mi occupassi soprattutto di compiti infrastrutturali: analisi, rete, database. In altre parole, ero io a scrivere quelle funzioni e classi che utilizzano gli altri sviluppatori. Per questo motivo, mi trovavo spesso in situazioni simili. C'\u00e8 stato anche un periodo in cui avevo un'immagine del genere appesa.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/0eb1aadd05b33ce995027ab52d5c1e53.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPoich\u00e9 non ci soddisfaceva, abbiamo iniziato a esplorare opzioni su come prevenire questo.<\/p>\n<h2>Come non rompere develop<\/h2>\n<p>\nPrima opzione: <strong>ricompilare tutte le pull request quando si aggiorna develop. <\/strong>Se nel nostro esempio la pull request con la funzionalit\u00e0 A entra per prima in develop, la pull request della funzionalit\u00e0 B verr\u00e0 ricompilata e, quindi, i controlli non passeranno a causa di un errore di compilazione.<\/p>\n<p>Per capire quanto tempo ci vorr\u00e0, consideriamo un esempio con due PR. Apriamo due PR: due build, due avvii di controlli. Dopo che il primo PR \u00e8 stato fuso in develop, il secondo deve essere ricompilato. In totale, per due PR servono tre avvi di controlli: 2 + 1 = 3.<\/p>\n<p>In linea di massima, va bene. Ma abbiamo esaminato le statistiche, e una situazione tipica nel nostro team era avere 10 PR aperti, e quindi il numero di controlli \u00e8 la somma della progressione: 10 + 9 +\u2026 + 1 = 55. Quindi, per accettare 10 PR, bisogna ricompilare 55 volte. E questo in una situazione ideale, in cui tutti i controlli passano al primo tentativo, e nessuno apre ulteriori pull request mentre stiamo gestendo questo gruppo di dieci.<\/p>\n<p>Immaginate di essere uno sviluppatore che deve premere per primo il pulsante \"merge\", perch\u00e9 se lo fa un vicino, dovr\u00e0 aspettare che tutte le build vengano eseguite di nuovo... No, cos\u00ec non va, questo rallenter\u00e0 seriamente lo sviluppo.<\/p>\n<p>Secondo possibile modo: <strong>compilare la pull request dopo la revisione del codice. <\/strong>Cio\u00e8, apri la pull request, ottieni il numero necessario di approvazioni dai colleghi, correggi ci\u00f2 che serve, dopodich\u00e9 avvii le build. Se hanno successo, la pull request viene fusa in develop. In questo caso, non ci sono ulteriori riavvii, ma il feedback diventa molto pi\u00f9 lento. Io, come sviluppatore, aprendo una pull request, voglio immediatamente vedere se si compila. Ad esempio, se un test fallisce, deve essere riparato rapidamente. Nel caso di una build ritardata, il feedback rallenta, e di conseguenza, anche lo sviluppo. Questo non ci soddisfaceva.<\/p>\n<p>Alla fine, \u00e8 rimasta solo la terza opzione \u2014 <strong>fare da s\u00e9<\/strong>. Tutto il nostro codice e tutti i nostri sorgenti sono conservati nel repository sul server Bitbucket. Di conseguenza, abbiamo dovuto sviluppare un plugin per Bitbucket.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/5e4b1f770ea1a3449b616c3101640294.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nQuesto plugin ridefinisce il meccanismo di fusione delle pull request. Inizia in modo standard: viene aperta una PR, vengono avviati tutti i build, si svolge la revisione del codice. Ma dopo che la revisione del codice \u00e8 stata superata e lo sviluppatore decide di fare clic su \u00abmerge\u00bb, il plugin controlla rispetto a quale stato di develop sono stati eseguiti i controlli. Se, dopo i build, develop \u00e8 stato aggiornato, il plugin non permetter\u00e0 di unire tale pull request nel ramo principale. Riavvier\u00e0 semplicemente i build rispetto al fresh develop.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/0edee880e1f2d286a0c64033103ac136.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nNel nostro esempio di modifiche in conflitto, tali build non passeranno a causa di un errore di compilazione. Di conseguenza, lo sviluppatore della funzionalit\u00e0 B dovr\u00e0 correggere il codice e riavviare le verifiche, quindi il plugin applicher\u00e0 automaticamente la pull request.<\/p>\n<p>Prima dell'implementazione di questo plugin, in media avevamo 2,7 esecuzioni di verifica per ogni pull request. Con il plugin sono diventate 3,6 esecuzioni. Questo ci ha soddisfatti.<\/p>\n<p>\u00c8 importante notare che questo plugin ha un difetto: riavvia la build solo una volta. Quindi rimane comunque un piccolo intervallo attraverso il quale possono entrare in develop modifiche in conflitto. Ma la probabilit\u00e0 di ci\u00f2 \u00e8 bassa e abbiamo accettato questo compromesso tra numero di esecuzioni e probabilit\u00e0 di malfunzionamenti. In due anni \u00e8 successo solo una volta, quindi probabilmente ne \u00e8 valsa la pena.<\/p>\n<p><strong><em>Ci sono volute due settimane per scrivere la prima versione del plugin per Bitbucket. <\/em><\/strong><\/p>\n<h3>Nuove verifiche<\/h3>\n<p>\nNel frattempo, il nostro team continuava a crescere. Venivano aggiunte nuove verifiche.<\/p>\n<p>Abbiamo pensato: perch\u00e9 correggere gli errori se possiamo prevenirli? E quindi abbiamo implementato <strong>statica del codice<\/strong>. Abbiamo iniziato con lint, che fa parte dell'Android SDK. Ma in quel momento non sapeva lavorare affatto con il codice Kotlin, e noi avevamo gi\u00e0 il 75% dell'app scritto in Kotlin. Quindi a lint si sono aggiunti i <strong>controlli di Android Studio.<\/strong><\/p>\n<p>Per fare ci\u00f2, abbiamo dovuto ingegnerizzarci molto: prendere Android Studio, impacchettarla in Docker e farla partire sul CI con un monitor virtuale, in modo che pensasse di essere eseguita su un vero laptop. Ma funzionava.<\/p>\n<p>In questo periodo abbiamo anche iniziato a scrivere molti <strong>test di strumentazione<\/strong> e abbiamo implementato <strong>test di screenshot<\/strong>. Questo \u00e8 quando viene generato uno screenshot di riferimento per una piccola vista separata, e il test consiste nel catturare uno screenshot dalla vista e confrontarlo pixel per pixel con quello di riferimento. Se ci sono differenze, significa che c'\u00e8 un problema nel layout o qualcosa non va negli stili.<\/p>\n<p>Ma i test di strumentazione e i test di screenshot devono essere eseguiti sui dispositivi: su emulatori o su dispositivi reali. Considerando che ci sono molti test e devono essere eseguiti frequentemente, serve un'intera farm. Creare la propria farm \u00e8 troppo dispendioso, quindi abbiamo trovato una soluzione pronta: Firebase Test Lab.<\/p>\n<h3>Firebase Test Lab<\/h3>\n<p>\n\u00c8 stato scelto perch\u00e9 Firebase \u00e8 un prodotto Google, quindi dovrebbe essere affidabile e difficilmente morir\u00e0 mai. I prezzi sono accessibili: 5$ all'ora per l'uso di un dispositivo reale, 1$ all'ora per l'emulatore.<\/p>\n<p><strong><em>Per implementare Firebase Test Lab nel nostro CI ci sono volute circa tre settimane.<\/em><\/strong><\/p>\n<p>Ma il team continuava a crescere, e Firebase, sfortunatamente, iniziava a deluderci. A quel tempo non aveva alcun SLA. A volte Firebase richiedeva di attendere finch\u00e9 non erano disponibili un numero sufficiente di dispositivi per i test, e non iniziava a eseguirli immediatamente, come avremmo voluto. L'attesa in coda poteva durare anche fino a mezz'ora, ed era molto lunga. I test di strumentazione venivano eseguiti su ogni PR, i ritardi rallentavano molto lo sviluppo, e poi \u00e8 arrivata la fattura mensile con una cifra considerevole. In generale, si \u00e8 deciso di rinunciare a Firebase e sviluppare in-house, dato che il team era cresciuto a sufficienza.<\/p>\n<h3>Docker + Python + bash<\/h3>\n<p>\nAbbiamo preso Docker, ci abbiamo messo gli emulatori, e abbiamo scritto un semplice programma in Python che, al momento giusto, avvia il numero richiesto di emulatori nella versione desiderata e quando serve li ferma. E, ovviamente, qualche script bash \u2014 come potremmo farne a meno?<\/p>\n<p><strong><em>La creazione di un ambiente di test personalizzato ha richiesto cinque settimane.<\/em><\/strong><\/p>\n<p>Di conseguenza, per ogni pull request era presente un ampio elenco bloccante di controlli:<\/p>\n<ul>\n<li>Build APK;<\/li>\n<li>Test Junit;<\/li>\n<li>Lint;<\/li>\n<li>Controlli di Android Studio;<\/li>\n<li>Test di strumentazione;<\/li>\n<li>Test di screenshot.<\/li>\n<\/ul>\n<p>\nQuesto preveniva molti possibili guasti. Tecnicamente tutto funzionava, ma gli sviluppatori si lamentavano che il tempo di attesa per i risultati era troppo lungo.<\/p>\n<p>Troppo lungo \u2014 quanto? Abbiamo estratto i dati da Bitbucket e TeamCity nel sistema di analisi e abbiamo capito che <strong>il tempo medio di attesa \u00e8 di 45 minuti<\/strong>. Cio\u00e8, uno sviluppatore, aprendo una pull request, in media aspetta i risultati delle build per 45 minuti. A mio avviso, \u00e8 davvero troppo e non possiamo continuare cos\u00ec.<\/p>\n<p>Certo, abbiamo deciso di accelerare tutti i nostri build.<\/p>\n<h2>Acceleriamo<\/h2>\n<p>\nVedendo che spesso i build erano in coda, abbiamo prima di tutto <strong>acquistato pi\u00f9 hardware<\/strong> \u2014 lo sviluppo estensivo \u00e8 la cosa pi\u00f9 semplice. I build hanno smesso di stare in coda, ma il tempo di attesa \u00e8 diminuito solo leggermente, perch\u00e9 alcune verifiche si sono rivelate comunque molto lunghe.<\/p>\n<h3>Rimuoviamo le verifiche troppo lunghe<\/h3>\n<p>\nIl nostro Continuous Integration potrebbe rilevare tali tipi di errori e problemi.<\/p>\n<ul>\n<li><strong>Non si compila<\/strong>. CI pu\u00f2 catturare un errore di compilazione quando, a causa di modifiche in conflitto, qualcosa non si compila. Come ho gi\u00e0 detto, allora nessuno pu\u00f2 compilare nulla, lo sviluppo si ferma e tutti diventano nervosi.<\/li>\n<li><strong>Bug nel comportamento<\/strong>. Ad esempio, quando l'app si compila, ma si chiude premendo un pulsante, o il pulsante non funziona affatto. Questo \u00e8 problematico, perch\u00e9 un bug del genere pu\u00f2 arrivare all'utente.<\/li>\n<li><strong>Bug nel layout<\/strong>. Ad esempio, il pulsante funziona, ma si \u00e8 spostato di 10 pixel a sinistra.<\/li>\n<li><strong>Incremento del debito tecnico<\/strong>.<\/li>\n<\/ul>\n<p>\nGuardando a questa lista, abbiamo capito che solo i primi due punti erano critici. Vogliamo affrontare prima questi problemi. I bug nel layout vengono identificati nella fase di revisione del design e possono essere facilmente corretti in quel momento. Lavorare sul debito tecnico richiede un processo e una pianificazione separati, quindi abbiamo deciso di non controllarlo nella pull request.<\/p>\n<p>Basandoci su questa classificazione, abbiamo rivisto l'intera lista delle verifiche. <strong>Abbiamo escluso Lint<\/strong> e abbiamo spostato la sua esecuzione a notte: giusto per avere un report su quanti problemi ci sono nel progetto. Ci siamo accordati per lavorare separatamente sul debito tecnico, mentre <strong>abbiamo completamente rinunciato ai controlli di Android Studio<\/strong>. Android Studio in Docker per l'esecuzione delle ispezioni sembra interessante, ma porta molti problemi di supporto. Qualsiasi aggiornamento delle versioni di Android Studio \u00e8 una lotta con bug sconosciuti. \u00c8 stato altrettanto difficile mantenere i test di screenshot, perch\u00e9 la libreria non funzionava molto stabilmente, ci sono stati falsi allarmi. <strong>Abbiamo rimosso i test di screenshot dalla lista delle verifiche<\/strong>.<\/p>\n<p>Infine, ci sono rimasti:<\/p>\n<ul>\n<li>Build APK;<\/li>\n<li>Test Junit;<\/li>\n<li>Test di strumentazione.<\/li>\n<\/ul>\n<h3>Cache remota di Gradle<\/h3>\n<p>\nSenza verifiche pesanti, tutto \u00e8 migliorato. Ma non c'\u00e8 limite al perfezionamento!<\/p>\n<p>La nostra applicazione era gi\u00e0 divisa in circa 150 moduli Gradle. Di solito, in questo caso, la cache remota di Gradle funziona bene, e abbiamo deciso di provarla.<\/p>\n<p>Il cache remoto di Gradle \u00e8 un servizio che pu\u00f2 memorizzare nella cache gli artefatti di build per singole attivit\u00e0 in moduli separati. Gradle, invece di compilare realmente il codice, interroga il cache remoto via HTTP e chiede se qualcuno ha gi\u00e0 eseguito questo compito. Se s\u00ec, scarica semplicemente il risultato.<\/p>\n<p><strong><em>Avviare il cache remoto di Gradle \u00e8 facile, poich\u00e9 Gradle fornisce un'immagine Docker. Siamo riusciti a farlo in tre ore.<\/em><\/strong><\/p>\n<p>Bastava avviare Docker e scrivere una riga nel progetto. Ma anche se \u00e8 possibile avviarlo rapidamente, affinch\u00e9 tutto funzioni bene, ci vorr\u00e0 abbastanza tempo.<\/p>\n<p>Di seguito \u00e8 riportato il grafico dei miss cache.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/458ef23b5506b04f3bd385be0c18607a.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nAll'inizio, la percentuale di miss del cache era di circa il 65%. Dopo tre settimane, siamo riusciti a ridurre questo valore al 20%. \u00c8 emerso che i compiti che l'applicazione Android compila hanno strane dipendenze transitive, per cui Gradle mancava il cache.<\/p>\n<p>Collegando il cache, abbiamo accelerato notevolmente la build. Ma oltre alla build, vengono eseguiti anche test di strumentazione, e questi richiedono molto tempo. Forse non \u00e8 necessario eseguire tutti i test per ogni pull request. Per scoprirlo, utilizziamo l'analisi d'impatto.<\/p>\n<h3>Analisi d'impatto<\/h3>\n<p>\nPer la pull request, raccogliamo il diff di git e troviamo i moduli Gradle modificati.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/471ca37206a0da4d5741c8ee1dbb7f03.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nHa senso eseguire solo i test di strumentazione che verificano i moduli modificati e tutti i moduli che dipendono da essi. Non ha senso eseguire test per i moduli vicini: il codice non \u00e8 cambiato e non pu\u00f2 rompersi nulla.<\/p>\n<p>Non \u00e8 tutto cos\u00ec semplice con i test di strumentazione, poich\u00e9 devono trovarsi nel modulo dell'applicazione di livello pi\u00f9 alto. Abbiamo applicato un'euristica con l'analisi del bytecode per capire a quale modulo appartiene ogni test.<\/p>\n<p><strong><em>La modernizzazione del lavoro dei test di strumentazione, affinch\u00e9 verificassero solo i moduli coinvolti, ha richiesto circa otto settimane.<\/em><\/strong><\/p>\n<p>Le misure per accelerare i controlli hanno avuto successo. Da 45 minuti siamo scesi a circa 15. Aspettare un quarto d'ora per la build \u00e8 gi\u00e0 normale.<\/p>\n<p>Ma ora gli sviluppatori hanno iniziato a lamentarsi che non capiscono quali build vengano eseguite, dove vedere il log, perch\u00e9 la build \u00e8 rossa, quale test \u00e8 fallito, e cos\u00ec via.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/aa76151174cd4e5a45ff281818e312f5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nI problemi con il feedback ritardano lo sviluppo, quindi ci siamo sforzati di fornire informazioni il pi\u00f9 chiare e dettagliate possibile su ogni PR e build. Abbiamo iniziato con i commenti su Bitbucket ai PR, specificando quale build \u00e8 fallita e perch\u00e9, scrivendo messaggi mirati su Slack. Alla fine abbiamo creato una dashboard per la pagina PR con un elenco di tutte le build attualmente in esecuzione e il loro stato: in coda, in fase di esecuzione, fallita o completata. \u00c8 possibile cliccare sulla build e accedere al suo log.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/2d16b7a6b6d23e23903d291ef1526999.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n<strong><em>Ci sono volute sei settimane per un feedback dettagliato.<\/em><\/strong><\/p>\n<h2>Piani<\/h2>\n<p>\nPassiamo alla storia pi\u00f9 recente. Risolvendo le questioni relative al feedback, siamo passati a un nuovo livello \u2014 abbiamo deciso di costruire la nostra fattoria di emulatori. Quando ci sono molti test e emulatori, gestirli diventa complicato. Alla fine, tutti i nostri emulatori sono stati trasferiti nel cluster k8s con gestione delle risorse flessibile.<\/p>\n<p>Inoltre, ci sono altri piani.<\/p>\n<ul>\n<li><strong>Ripristinare Lint<\/strong> (e altro analisi statica). Stiamo gi\u00e0 lavorando in questa direzione.<\/li>\n<li>Eseguire sui PR come blocker tutti <strong>i test end-to-end<\/strong> su tutte le versioni del SDK.<\/li>\n<\/ul>\n<p>\nQuindi, abbiamo tracciato la storia dello sviluppo della Continuous Integration in Avito. Ora voglio dare alcuni consigli da esperto.<\/p>\n<h1>Consigli<\/h1>\n<p>\nSe potessi dare un solo consiglio, sarebbe questo:<\/p>\n<blockquote><p>Per favore, fate attenzione agli script shell!<\/p><\/blockquote>\n<p>\nBash \u00e8 uno strumento molto flessibile e potente, ed \u00e8 molto comodo e veloce scrivere script con esso. Ma ci si pu\u00f2 facilmente intrappolare, e purtroppo, ci siamo cascati.<\/p>\n<p>Tutto \u00e8 iniziato con script semplici, che venivano eseguiti sulle nostre macchine di build:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n.\/gradlew assembleDebug<\/code><\/pre>\n<p>\nMa, come si sa, tutto si sviluppa e si complica nel tempo \u2014 iniziamo a eseguire uno script da un altro, iniziamo a passare vari parametri \u2014 alla fine, abbiamo dovuto scrivere una funzione per determinare a quale livello di annidamento bash ci troviamo attualmente, per inserire le virgolette appropriate affinch\u00e9 tutto funzioni.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/244a574f7b5d3b4fc77cfc4ccda2db69.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPotete immaginare lo sforzo necessario per sviluppare tali script. Consiglio di non cadere in questa trappola.<\/p>\n<p>Cosa si pu\u00f2 utilizzare alternativamente?<\/p>\n<ul>\n<li>Qualsiasi linguaggio di scripting. Scrivere in <strong>Python o Kotlin Script<\/strong> \u00e8 pi\u00f9 comodo, perch\u00e9 si tratta di programmazione, non di script.<\/li>\n<li>Oppure descrivere tutta la logica delle build sotto forma di <strong>Tasks gradle personalizzati<\/strong> per il vostro progetto.<\/li>\n<\/ul>\n<p>\nAbbiamo deciso di optare per la seconda opzione e ora stiamo sistematicamente rimuovendo tutti gli script bash e scrivendo molte task gradle personalizzate.<\/p>\n<p><strong>Consiglio n. 2: conservare l'infrastruttura nel codice.<\/strong><\/p>\n<p>\u00c8 comodo quando la configurazione della Continuous Integration non \u00e8 memorizzata nell'interfaccia utente di Jenkins o TeamCity, ecc., ma \u00e8 in forma di file di testo direttamente nel repository del progetto. Questo permette di avere versioni. Non sar\u00e0 difficile tornare indietro o raccogliere il codice su un altro ramo.<\/p>\n<p>Gli script possono essere memorizzati nel progetto. E per quanto riguarda l'ambiente?<\/p>\n<p><strong>Consiglio n. 3: Docker pu\u00f2 aiutare con l'ambiente.<\/strong><\/p>\n<p>Sicuramente aiuter\u00e0 gli sviluppatori Android, mentre per iOS purtroppo no.<\/p>\n<p>Questo \u00e8 un esempio di un semplice Dockerfile che contiene jdk e android-sdk:<\/p>\n<pre><code class=\"plaintext\">FROM openjdk:8\n\nENV SDK_URL=\"https:\/\/dl.google.com\/android\/repository\/sdk-tools-linux-3859397.zip\" \n    ANDROID_HOME=\"\/usr\/local\/android-sdk\" \n    ANDROID_VERSION=26 \n    ANDROID_BUILD_TOOLS_VERSION=26.0.2\n\n# Scarica l'Android SDK\nRUN mkdir \"$ANDROID_HOME\" .android \n    &amp;&amp; cd \"$ANDROID_HOME\" \n    &amp;&amp; curl -o sdk.zip $SDK_URL \n    &amp;&amp; unzip sdk.zip \n    &amp;&amp; rm sdk.zip \n    &amp;&amp; yes | $ANDROID_HOME\/tools\/bin\/sdkmanager --licenses\n\n# Installa l'Android Build Tool e le librerie\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager --update\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager \"build-tools;${ANDROID_BUILD_TOOLS_VERSION}\" \n    \"platforms;android-${ANDROID_VERSION}\" \n    \"platform-tools\"\n\nRUN mkdir \/application\nWORKDIR \/application\n<\/code><\/pre>\n<p>\nHo scritto questo Dockerfile (ti dico in segreto che puoi anche non scriverlo, ma scaricarne uno gi\u00e0 pronto da GitHub) e, una volta creato l'immagine, ottieni una macchina virtuale su cui puoi compilare l'applicazione e lanciare test Junit.<\/p>\n<p>Due argomenti principali sul perch\u00e9 ci\u00f2 abbia senso: scalabilit\u00e0 e ripetibilit\u00e0. Utilizzando Docker, puoi rapidamente avviare una dozzina di agenti di build, che avranno esattamente lo stesso ambiente di prima. Questo semplifica notevolmente la vita agli ingegneri CI. Inserire l'android-sdk in Docker \u00e8 piuttosto semplice, con gli emulatori \u00e8 un po' pi\u00f9 complicato: dovrai fare un po' di sforzo (o scaricare di nuovo uno gi\u00e0 pronto da GitHub).<\/p>\n<p><strong>Consiglio n. 4: non dimenticare che i test vengono effettuati non per il gusto di farli, ma per le persone.<\/strong><\/p>\n<p>Per gli sviluppatori \u00e8 molto importante avere un feedback rapido e, soprattutto, chiaro: cosa si \u00e8 rotto, quale test \u00e8 fallito, dove pu\u00f2 essere visualizzato il log di build.<\/p>\n<p><strong>Consiglio n. 5: sii pragmatico nello sviluppo della Continuous Integration.<\/strong><\/p>\n<p>Comprendi chiaramente quali tipi di errori vuoi prevenire, quanto sei disposto a spendere in risorse, tempo e tempo macchina. Controlli troppo lunghi possono essere, ad esempio, spostati di notte. E rispetto a quelli che individuano errori di poca importanza, puoi anche rinunciare del tutto.<\/p>\n<p><strong>Consiglio n. 6: utilizza strumenti gi\u00e0 pronti.<\/strong><\/p>\n<p>Attualmente ci sono molte aziende che offrono CI in cloud.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/e57aabd49ec01d14e83b64331fd84ec5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nPer piccoli team \u00e8 una buona soluzione. Non \u00e8 necessario mantenere nulla, basta spendere qualche soldo, assemblare la propria applicazione e anche eseguire test di strumentazione.<\/p>\n<p><strong>Consiglio n. 7: in un grande team le soluzioni interne sono pi\u00f9 vantaggiose.<\/strong><\/p>\n<p>Ma prima o poi, con la crescita del team, le soluzioni interne diventeranno pi\u00f9 convenienti. C'\u00e8 un aspetto con queste soluzioni. In economia c'\u00e8 la legge dei rendimenti decrescenti: in qualsiasi progetto ogni miglioramento successivo diventa sempre pi\u00f9 difficile e richiede sempre pi\u00f9 investimenti.<\/p>\n<p>L'economia descrive tutta la nostra vita, compresa l'Integrazione Continua. Ho costruito un grafico dei costi di lavoro per ogni fase dello sviluppo della nostra Integrazione Continua.<\/p>\n<p><img decoding=\"async\" alt=\"L&#039;evoluzione del CI nel team di sviluppo mobile\" src=\"\/wp-content\/uploads\/2019\/04\/97bf8bff64f3ac9ae587fae195251da2.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n\u00c8 evidente che qualsiasi miglioramento diventa sempre pi\u00f9 difficile. Guardando questo grafico, si pu\u00f2 capire che \u00e8 necessario sviluppare l'Integrazione Continua in modo coerente con l'aumento delle dimensioni del team. Per un team di due persone spendere 50 giorni per sviluppare una fattoria interna di emulatori \u00e8 un'idea non proprio brillante. Ma per un grande team non occuparsi affatto dell'Integrazione Continua \u00e8 una cattiva idea, perch\u00e9 ci vorr\u00e0 ancora pi\u00f9 tempo per risolvere i problemi di integrazione, migliorare la comunicazione, ecc.<\/p>\n<p>All'inizio abbiamo detto che l'automazione \u00e8 necessaria, perch\u00e9 le persone sono costose, commettono errori e sono pigre. Ma a automatizzare ci sono sempre persone. Pertanto, questi stessi problemi si applicano anche all'automazione.<\/p>\n<ul>\n<li>Automatizzare \u00e8 costoso. Ricordate il grafico dei costi di lavoro.<\/li>\n<li>Nell'automazione le persone commettono errori.<\/li>\n<li>A volte \u00e8 molto difficile automatizzare, perch\u00e9 tanto gi\u00e0 funziona. Perch\u00e9 migliorare ulteriormente, perch\u00e9 tutta questa Integrazione Continua?<\/li>\n<\/ul>\n<p>\nMa ho delle statistiche: nel 20% delle compilazioni si trovano errori. E questo non accade perch\u00e9 i nostri sviluppatori scrivono codice male. Succede perch\u00e9 gli sviluppatori sono convinti che, se commettono qualche errore, non entrer\u00e0 in develop, verr\u00e0 catturato dai controlli automatizzati. Di conseguenza, gli sviluppatori possono spendere pi\u00f9 tempo a scrivere codice e cose interessanti, piuttosto che eseguire e verificare localmente.<\/p>\n<p><strong>Occupatevi dell'Integrazione Continua. Ma con misura.<\/strong><\/p>\n<blockquote><p>A proposito, Nikolai Nesterov non solo fa delle ottime presentazioni, ma fa anche parte del comitato programmatico <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\">AppsConf<\/a><\/noindex> e aiuta gli altri a preparare per voi interventi significativi. La completezza e l'utilit\u00e0 del programma della prossima conferenza possono essere valutate dai temi nel <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\/schedule\">programma<\/a><\/noindex>. E per maggiori dettagli venite il 22-23 aprile nello Spazio Informativo.<\/p><\/blockquote>\n<p>Fonte: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/oleg-bunin\/blog\/447608\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445. \u0423\u0441\u043b\u043e\u0432\u0438\u044f \u0443\u0441\u043f\u0435\u0445\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0441\u0445\u0435\u043c\u044b. \u041d\u0430\u043f\u0438\u0441\u0430\u0432 \u043a\u043e\u0434, \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043e\u043d: \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043b\u043e\u043c\u0430\u0435\u0442, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0432\u0430\u0448\u0438 \u043a\u043e\u043b\u043b\u0435\u0433\u0438. \u0415\u0441\u043b\u0438 \u043e\u0431\u0430 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f, \u0442\u043e \u0432\u044b \u043d\u0430 \u043f\u0443\u0442\u0438 \u043a \u0443\u0441\u043f\u0435\u0445\u0443. \u0427\u0442\u043e\u0431\u044b \u043b\u0435\u0433\u043a\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u044d\u0442\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0438 \u043d\u0435 \u0441\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0441 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":23270,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-31304","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"it_IT\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Evoluzione del CI nel team di sviluppo mobile | ProHoster","description":"Oggi la maggior parte dei prodotti software viene sviluppata in team.","canonical_url":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"it_IT","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster","og:description":"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.","og:url":"https:\/\/prohoster.info\/it\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T18:40:34+00:00","article:modified_time":"2019-10-31T18:40:34+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"31304","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-21 05:30:21","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 03:19:49","updated":"2026-01-21 05:30:21","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts\/31304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/comments?post=31304"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/posts\/31304\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/media\/23270"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/media?parent=31304"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/categories?post=31304"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/it\/wp-json\/wp\/v2\/tags?post=31304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}