Oggi, la maggior parte dei prodotti software è sviluppata in team. Le condizioni per il successo dello sviluppo di team possono essere rappresentate in uno schema semplice.

Dopo aver scritto il codice, devi assicurarti che esso:
- Funziona.
- Non rompe nulla, compreso il codice che hanno scritto i tuoi colleghi.
Se entrambe le condizioni sono soddisfatte, sei sulla buona strada per il successo. Per controllare facilmente queste condizioni e non deviare da un percorso vantaggioso, è stato inventato il Continuous Integration.
Il CI è un processo di lavoro in cui integri il tuo codice nel codice comune del prodotto il più spesso possibile. E non solo integri, ma controlli anche costantemente che tutto funzioni. Poiché è necessario controllare molto e frequentemente, vale la pena considerare l'automazione. È possibile verificare tutto manualmente, ma non conviene, ecco perché.
- Le persone sono costose. Il costo di un’ora di lavoro di qualsiasi programmatore è maggiore del costo orario di qualsiasi server.
- Le persone sbagliano. Pertanto, possono verificarsi situazioni in cui esegui test su un ramo sbagliato o raccogli un commit errato per i tester.
- Le persone sono pigre. Periodicamente, quando finisco qualche compito, mi viene in mente: "Ma cosa devo controllare? Ho scritto due righe — certo funziona tutto!" Penso che anche a qualcuno di voi venga in mente qualche pensiero del genere. Ma è sempre necessario controllare.
Come è 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 () — partecipante a tutti i cambiamenti evolutivi del CI/CD dell'applicazione Android.
La storia è basata sull'esempio del team Android, ma la maggior parte degli approcci è applicabile anche su iOS.

Tanto 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.
Ma l'applicazione cresceva, c'erano sempre più nuovi compiti, di conseguenza, il team crescevano. A un certo punto, è arrivato il momento di organizzare in modo più formale il processo di integrazione del codice. È stato deciso di utilizzare il Git flow.

La concezione del Git flow è nota: nel progetto c'è un ramo comune chiamato develop, e per ogni nuova funzionalità 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.
Verifiche
Guardare il codice con gli occhi è fantastico, ma non basta. Per questo vengono introdotte verifiche automatiche.
- Prima di tutto controlliamo la build ARC.
- Molti test Junit.
- Calcoliamo la code coverage, visto che avviamo i test.
Per capire come avviare queste verifiche, guardiamo il processo di sviluppo in Avito.
Schematicamente si può rappresentare così:
- Lo sviluppatore scrive codice sul proprio laptop. Può avviare le verifiche di integrazione proprio qui — o tramite hooks di commit, oppure semplicemente eseguendo le verifiche in background.
- Dopo che lo sviluppatore ha spinto il codice, apre una pull request. Affinché il suo codice venga unito nel ramo develop, deve passare la code review e ottenere il numero necessario di approvazioni. È possibile attivare le verifiche e le build qui: fino a quando tutte le build non saranno riuscite, la pull request non può essere unita.
- Dopo che la pull request è stata unita e il codice è entrato in develop, si può scegliere un momento conveniente: ad esempio, di notte, quando tutti i server sono liberi, e far girare le verifiche il più possibile.
A nessuno è piaciuto avviare le verifiche sul proprio laptop. Quando lo sviluppatore ha terminato la funzionalità, vuole caricarla il prima possibile e aprire la pull request. Se in quel momento vengono avviate delle verifiche lunghe, non è solo poco piacevole, ma rallenta anche lo sviluppo: mentre il laptop fa delle verifiche, non è possibile lavorare normalmente.
Avviare le verifiche di notte ci è piaciuto molto, perché ci sono molti server e tempo, si può sbizzarrirsi. Ma, sfortunatamente, quando il codice della funzionalità è stato inserito in develop, lo sviluppatore ha già 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ù tardi, perché ora c'era un fantastico nuovo compito in Jira che volevo iniziare a fare.
Se le verifiche bloccano la pull request, allora c'è abbastanza motivazione, perché finché le build non diventano verdi, il codice non entrerà in develop, e quindi il compito non sarà completato.
Alla fine abbiamo scelto questa strategia: di notte eseguiamo il massimo numero possibile di controlli, mentre i più critici e, soprattutto, i più veloci, li lanciamo sulla pull request. Ma non ci fermiamo qui: parallelamente ottimizziamo la velocità di esecuzione dei controlli in modo da trasferirli dalla modalità notturna ai controlli sulla pull request.
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é ritenuto non necessaria.
Per configurare il CI di base ci sono voluti due giorni (qui e oltre, la stima temporale è indicativa, utile per il contesto).
Dopo ciò abbiamo iniziato a riflettere: stiamo controllando correttamente? Avviamo correttamente le build sulla pull request?
Eseguivamo la build sull'ultimo commit del ramo da cui è 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à, è necessario controllare lo stato del ramo develop dopo che la funzionalità è stata integrata.

Per questo abbiamo scritto un semplice script bash premerge.sh:
#!/usr/bin/env bash
set -e
git fetch origin develop
git merge origin/developQui vengono semplicemente recuperate tutte le modifiche più 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ò che vogliamo, cioè l'integrazione.
Per la localizzazione dei problemi, la ricerca di soluzioni e la scrittura di questo script ci sono voluti tre giorni.
L'applicazione si è evoluta, i compiti sono aumentati, il team è cresciuto e premerge.sh ha iniziato a darci problemi. Cambiamenti conflittuali entravano in develop, causando errori nelle build.
Ecco un esempio di come accade:

Due sviluppatori iniziano contemporaneamente a lavorare sulle funzionalità A e B. Lo sviluppatore della funzionalità A scopre una funzione non utilizzata nel progetto answer() e, da buon boy scout, la elimina. Nel frattempo, lo sviluppatore della funzionalità B nella sua branch aggiunge una nuova chiamata a quella funzione.
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ò viene unita la pull request della funzionalità A, viene unita la pull request della funzionalità B... Boom! Develop si rompe, perché nel codice di develop c'è una chiamata a una funzione inesistente.

Quando develop non si compila, questo disastro locale. Tutto il team non riesce a raccogliere nulla e a consegnarlo per i test.
È 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'è stato anche un periodo in cui avevo un'immagine del genere appesa.

Poiché non ci soddisfaceva, abbiamo iniziato a esplorare opzioni su come prevenire questo.
Come non rompere develop
Prima opzione: ricompilare tutte le pull request quando si aggiorna develop. Se nel nostro esempio la pull request con la funzionalità A entra per prima in develop, la pull request della funzionalità B verrà ricompilata e, quindi, i controlli non passeranno a causa di un errore di compilazione.
Per capire quanto tempo ci vorrà, consideriamo un esempio con due PR. Apriamo due PR: due build, due avvii di controlli. Dopo che il primo PR è stato fuso in develop, il secondo deve essere ricompilato. In totale, per due PR servono tre avvi di controlli: 2 + 1 = 3.
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 è la somma della progressione: 10 + 9 +… + 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.
Immaginate di essere uno sviluppatore che deve premere per primo il pulsante "merge", perché se lo fa un vicino, dovrà aspettare che tutte le build vengano eseguite di nuovo... No, così non va, questo rallenterà seriamente lo sviluppo.
Secondo possibile modo: compilare la pull request dopo la revisione del codice. Cioè, apri la pull request, ottieni il numero necessario di approvazioni dai colleghi, correggi ciò che serve, dopodiché 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ù 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.
Alla fine, è rimasta solo la terza opzione — fare da sé. 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.

Questo plugin ridefinisce il meccanismo di unione delle pull request. Inizialmente è standard: si apre una PR, si avviano tutte le build e si passa alla revisione del codice. Ma dopo che la revisione del codice è stata superata e lo sviluppatore decide di fare clic su "merge", il plugin controlla in base a quale stato di develop sono state eseguite le verifiche. Se dopo le build il develop è stato aggiornato, il plugin non permetterà di unire tale pull request nel branch principale. Riavvierà semplicemente le build in base al develop aggiornato.

Nel nostro esempio di modifiche in conflitto, tali build non passeranno a causa di un errore di compilazione. Di conseguenza, lo sviluppatore della funzionalità B dovrà correggere il codice e riavviare le verifiche, quindi il plugin applicherà automaticamente la pull request.
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.
È 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à di ciò è bassa e abbiamo accettato questo compromesso tra numero di esecuzioni e probabilità di malfunzionamenti. In due anni è successo solo una volta, quindi probabilmente ne è valsa la pena.
Ci sono volute due settimane per scrivere la prima versione del plugin per Bitbucket.
Nuove verifiche
Nel frattempo, il nostro team continuava a crescere. Venivano aggiunte nuove verifiche.
Abbiamo pensato: perché correggere gli errori se possiamo prevenirli? E quindi abbiamo implementato statica del codice. 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à il 75% dell'app scritto in Kotlin. Quindi a lint si sono aggiunti i controlli di Android Studio.
Per fare ciò, 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.
In questo periodo abbiamo anche iniziato a scrivere molti test di strumentazione e abbiamo implementato test di screenshot. Questo è 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'è un problema nel layout o qualcosa non va negli stili.
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 è troppo dispendioso, quindi abbiamo trovato una soluzione pronta: Firebase Test Lab.
Firebase Test Lab
È stato scelto perché Firebase è un prodotto Google, quindi dovrebbe essere affidabile e difficilmente morirà mai. I prezzi sono accessibili: 5$ all'ora per l'uso di un dispositivo reale, 1$ all'ora per l'emulatore.
Per implementare Firebase Test Lab nel nostro CI ci sono volute circa tre settimane.
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é 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 è arrivata la fattura mensile con una cifra considerevole. In generale, si è deciso di rinunciare a Firebase e sviluppare in-house, dato che il team era cresciuto a sufficienza.
Docker + Python + bash
Abbiamo 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 — come potremmo farne a meno?
La creazione di un ambiente di test personalizzato ha richiesto cinque settimane.
Di conseguenza, per ogni pull request era presente un ampio elenco bloccante di controlli:
- Build APK;
- Test Junit;
- Lint;
- Controlli di Android Studio;
- Test di strumentazione;
- Test di screenshot.
Questo preveniva molti possibili guasti. Tecnicamente tutto funzionava, ma gli sviluppatori si lamentavano che il tempo di attesa per i risultati era troppo lungo.
Troppo lungo — quanto? Abbiamo estratto i dati da Bitbucket e TeamCity nel sistema di analisi e abbiamo capito che il tempo medio di attesa è di 45 minuti. Cioè, uno sviluppatore, aprendo una pull request, in media aspetta i risultati delle build per 45 minuti. A mio avviso, è davvero troppo e non possiamo continuare così.
Certo, abbiamo deciso di accelerare tutti i nostri build.
Acceleriamo
Vedendo che spesso i build erano in coda, abbiamo prima di tutto acquistato più hardware — lo sviluppo estensivo è la cosa più semplice. I build hanno smesso di stare in coda, ma il tempo di attesa è diminuito solo leggermente, perché alcune verifiche si sono rivelate comunque molto lunghe.
Rimuoviamo le verifiche troppo lunghe
Il nostro Continuous Integration potrebbe rilevare tali tipi di errori e problemi.
- Non si compila. CI può catturare un errore di compilazione quando, a causa di modifiche in conflitto, qualcosa non si compila. Come ho già detto, allora nessuno può compilare nulla, lo sviluppo si ferma e tutti diventano nervosi.
- Bug nel comportamento. Ad esempio, quando l'app si compila, ma si chiude premendo un pulsante, o il pulsante non funziona affatto. Questo è problematico, perché un bug del genere può arrivare all'utente.
- Bug nel layout. Ad esempio, il pulsante funziona, ma si è spostato di 10 pixel a sinistra.
- Incremento del debito tecnico.
Guardando 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.
Basandoci su questa classificazione, abbiamo rivisto l'intera lista delle verifiche. Abbiamo escluso Lint 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 abbiamo completamente rinunciato ai controlli di Android Studio. Android Studio in Docker per l'esecuzione delle ispezioni sembra interessante, ma porta molti problemi di supporto. Qualsiasi aggiornamento delle versioni di Android Studio è una lotta con bug sconosciuti. È stato altrettanto difficile mantenere i test di screenshot, perché la libreria non funzionava molto stabilmente, ci sono stati falsi allarmi. Abbiamo rimosso i test di screenshot dalla lista delle verifiche.
Infine, ci sono rimasti:
- Build APK;
- Test Junit;
- Test di strumentazione.
Cache remota di Gradle
Senza verifiche pesanti, tutto è migliorato. Ma non c'è limite al perfezionamento!
La nostra applicazione era già divisa in circa 150 moduli Gradle. Di solito, in questo caso, la cache remota di Gradle funziona bene, e abbiamo deciso di provarla.
Il cache remoto di Gradle è un servizio che può memorizzare nella cache gli artefatti di build per singole attività in moduli separati. Gradle, invece di compilare realmente il codice, interroga il cache remoto via HTTP e chiede se qualcuno ha già eseguito questo compito. Se sì, scarica semplicemente il risultato.
Avviare il cache remoto di Gradle è facile, poiché Gradle fornisce un'immagine Docker. Siamo riusciti a farlo in tre ore.
Bastava avviare Docker e scrivere una riga nel progetto. Ma anche se è possibile avviarlo rapidamente, affinché tutto funzioni bene, ci vorrà abbastanza tempo.
Di seguito è riportato il grafico dei miss cache.

All'inizio, la percentuale di miss del cache era di circa il 65%. Dopo tre settimane, siamo riusciti a ridurre questo valore al 20%. È emerso che i compiti che l'applicazione Android compila hanno strane dipendenze transitive, per cui Gradle mancava il cache.
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 è necessario eseguire tutti i test per ogni pull request. Per scoprirlo, utilizziamo l'analisi d'impatto.
Analisi d'impatto
Per la pull request, raccogliamo il diff di git e troviamo i moduli Gradle modificati.

Ha 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 è cambiato e non può rompersi nulla.
Non è tutto così semplice con i test di strumentazione, poiché devono trovarsi nel modulo dell'applicazione di livello più alto. Abbiamo applicato un'euristica con l'analisi del bytecode per capire a quale modulo appartiene ogni test.
La modernizzazione del lavoro dei test di strumentazione, affinché verificassero solo i moduli coinvolti, ha richiesto circa otto settimane.
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 è già normale.
Ma ora gli sviluppatori hanno iniziato a lamentarsi che non capiscono quali build vengano eseguite, dove vedere il log, perché la build è rossa, quale test è fallito, e così via.

I problemi con il feedback ritardano lo sviluppo, quindi ci siamo sforzati di fornire informazioni il più chiare e dettagliate possibile su ogni PR e build. Abbiamo iniziato con i commenti su Bitbucket ai PR, specificando quale build è fallita e perché, 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. È possibile cliccare sulla build e accedere al suo log.

Ci sono volute sei settimane per un feedback dettagliato.
Piani
Passiamo alla storia più recente. Risolvendo le questioni relative al feedback, siamo passati a un nuovo livello — 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.
Inoltre, ci sono altri piani.
- Ripristinare Lint (e altro analisi statica). Stiamo già lavorando in questa direzione.
- Eseguire sui PR come blocker tutti i test end-to-end su tutte le versioni del SDK.
Quindi, abbiamo tracciato la storia dello sviluppo della Continuous Integration in Avito. Ora voglio dare alcuni consigli da esperto.
Consigli
Se potessi dare un solo consiglio, sarebbe questo:
Per favore, fate attenzione agli script shell!
Bash è uno strumento molto flessibile e potente, ed è molto comodo e veloce scrivere script con esso. Ma ci si può facilmente intrappolare, e purtroppo, ci siamo cascati.
Tutto è iniziato con script semplici, che venivano eseguiti sulle nostre macchine di build:
#!/usr/bin/env bash
./gradlew assembleDebugMa, come si sa, tutto si sviluppa e si complica nel tempo — iniziamo a eseguire uno script da un altro, iniziamo a passare vari parametri — alla fine, abbiamo dovuto scrivere una funzione per determinare a quale livello di annidamento bash ci troviamo attualmente, per inserire le virgolette appropriate affinché tutto funzioni.

Potete immaginare lo sforzo necessario per sviluppare tali script. Consiglio di non cadere in questa trappola.
Cosa si può utilizzare alternativamente?
- Qualsiasi linguaggio di scripting. Scrivere in Python o Kotlin Script è più comodo, perché si tratta di programmazione, non di script.
- Oppure descrivere tutta la logica delle build sotto forma di Tasks gradle personalizzati per il vostro progetto.
Abbiamo deciso di optare per la seconda opzione e ora stiamo sistematicamente rimuovendo tutti gli script bash e scrivendo molte task gradle personalizzate.
Consiglio n. 2: conservare l'infrastruttura nel codice.
È comodo quando la configurazione della Continuous Integration non è memorizzata nell'interfaccia utente di Jenkins o TeamCity, ecc., ma è in forma di file di testo direttamente nel repository del progetto. Questo permette di avere versioni. Non sarà difficile tornare indietro o raccogliere il codice su un altro ramo.
Gli script possono essere memorizzati nel progetto. E per quanto riguarda l'ambiente?
Consiglio n. 3: Docker può aiutare con l'ambiente.
Sicuramente aiuterà gli sviluppatori Android, mentre per iOS purtroppo no.
Questo è un esempio di un semplice Dockerfile che contiene jdk e android-sdk:
FROM openjdk:8
ENV SDK_URL="https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip"
ANDROID_HOME="/usr/local/android-sdk"
ANDROID_VERSION=26
ANDROID_BUILD_TOOLS_VERSION=26.0.2
# Scarica l'Android SDK
RUN mkdir "$ANDROID_HOME" .android
&& cd "$ANDROID_HOME"
&& curl -o sdk.zip $SDK_URL
&& unzip sdk.zip
&& rm sdk.zip
&& yes | $ANDROID_HOME/tools/bin/sdkmanager --licenses
# Installa l'Android Build Tool e le librerie
RUN $ANDROID_HOME/tools/bin/sdkmanager --update
RUN $ANDROID_HOME/tools/bin/sdkmanager "build-tools;${ANDROID_BUILD_TOOLS_VERSION}"
"platforms;android-${ANDROID_VERSION}"
"platform-tools"
RUN mkdir /application
WORKDIR /application
Ho scritto questo Dockerfile (ti dico in segreto che puoi anche non scriverlo, ma scaricarne uno già pronto da GitHub) e, una volta creato l'immagine, ottieni una macchina virtuale su cui puoi compilare l'applicazione e lanciare test Junit.
Due argomenti principali sul perché ciò abbia senso: scalabilità e ripetibilità. 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 è piuttosto semplice, con gli emulatori è un po' più complicato: dovrai fare un po' di sforzo (o scaricare di nuovo uno già pronto da GitHub).
Consiglio n. 4: non dimenticare che i test vengono effettuati non per il gusto di farli, ma per le persone.
Per gli sviluppatori è molto importante avere un feedback rapido e, soprattutto, chiaro: cosa si è rotto, quale test è fallito, dove può essere visualizzato il log di build.
Consiglio n. 5: sii pragmatico nello sviluppo della Continuous Integration.
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.
Consiglio n. 6: utilizza strumenti già pronti.
Attualmente ci sono molte aziende che offrono CI in cloud.

Per piccoli team è una buona soluzione. Non è necessario mantenere nulla, basta spendere qualche soldo, assemblare la propria applicazione e anche eseguire test di strumentazione.
Consiglio n. 7: in un grande team le soluzioni interne sono più vantaggiose.
Ma prima o poi, con la crescita del team, le soluzioni interne diventeranno più convenienti. C'è un aspetto con queste soluzioni. In economia c'è la legge dei rendimenti decrescenti: in qualsiasi progetto ogni miglioramento successivo diventa sempre più difficile e richiede sempre più investimenti.
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.

È evidente che qualsiasi miglioramento diventa sempre più difficile. Guardando questo grafico, si può capire che è 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 è un'idea non proprio brillante. Ma per un grande team non occuparsi affatto dell'Integrazione Continua è una cattiva idea, perché ci vorrà ancora più tempo per risolvere i problemi di integrazione, migliorare la comunicazione, ecc.
All'inizio abbiamo detto che l'automazione è necessaria, perché 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.
- Automatizzare è costoso. Ricordate il grafico dei costi di lavoro.
- Nell'automazione le persone commettono errori.
- A volte è molto difficile automatizzare, perché tanto già funziona. Perché migliorare ulteriormente, perché tutta questa Integrazione Continua?
Ma ho delle statistiche: nel 20% delle compilazioni si trovano errori. E questo non accade perché i nostri sviluppatori scrivono codice male. Succede perché gli sviluppatori sono convinti che, se commettono qualche errore, non entrerà in develop, verrà catturato dai controlli automatizzati. Di conseguenza, gli sviluppatori possono spendere più tempo a scrivere codice e cose interessanti, piuttosto che eseguire e verificare localmente.
Occupatevi dell'Integrazione Continua. Ma con misura.
A proposito, Nikolai Nesterov non solo fa delle ottime presentazioni, ma fa anche parte del comitato programmatico e aiuta gli altri a preparare per voi interventi significativi. La completezza e l'utilità del programma della prossima conferenza possono essere valutate dai temi nel . E per maggiori dettagli venite il 22-23 aprile nello Spazio Informativo.
Fonte: habr.com
