Dziś większość produktów programistycznych jest tworzonych w zespołach. Warunki sukcesu w pracy zespołowej można przedstawić w postaci prostej schemy.

Pisząc kod, musisz upewnić się, że on:
- Działa.
- Nie psuje niczego, w tym kodu napisanego przez twoich kolegów.
Jeśli oba warunki są spełnione, jesteś na drodze do sukcesu. Aby łatwo sprawdzać te warunki i nie zbaczać z korzystnej ścieżki, wymyślono Continuous Integration.
CI to proces roboczy, w którym możliwie jak najczęściej integrujesz swój kod z ogólnym kodem produktu. I nie tylko integrujesz, ale także nieustannie sprawdzasz, czy wszystko działa. Ponieważ trzeba często i dokładnie sprawdzać, warto pomyśleć o automatyzacji. Można wszystko sprawdzać ręcznie, ale nie warto, oto dlaczego.
- Ludzie są kosztowni. Godzina pracy dowolnego programisty jest droższa niż godzina pracy dowolnego serwera.
- Ludzie się mylą. Dlatego mogą wystąpić sytuacje, w których uruchamiasz testy na niewłaściwej gałęzi lub kompilujesz nie ten commit do testerów.
- Ludzie są leniwi. Czasami, gdy kończę jakiekolwiek zadanie, pojawia się myśl: "Co tu sprawdzać? Napisałem dwie linijki — na pewno wszystko działa!" Sądzę, że niektórzy z was też czasami mają takie myśli. Ale sprawdzać trzeba zawsze.
Jak wdrożono i rozwijano Continuous Integration w zespole mobilnym Avito, jak przeszli od 0 do 450 kompilacji dziennie i co maszyny budujące zbierają 200 godzin dziennie, opowiada Nikolaj Nesterow () — uczestnik wszystkich ewolucyjnych zmian CI/CD aplikacji na Androida.
Opowieść zbudowana jest na przykładzie zespołu Android, ale większość podejść jest również zastosowalna w iOS.

Dawno temu w zespole Android Avito pracowała tylko jedna osoba. Z definicji nie potrzebował on niczego z Continuous Integration: nie miał z kim się integrować.
Jednak aplikacja rosła, pojawiało się coraz więcej zadań, a w związku z tym powiększał się zespół. W pewnym momencie nastał czas, aby bardziej formalnie uregulować proces integracji kodu. Zdecydowano się na wykorzystanie Git flow.

Koncepcja Git flow jest znana: w projekcie istnieje jedna wspólna gałąź develop, a dla każdej nowej funkcji programiści tworzą osobną gałąź, commitują do niej, puszczają, a gdy chcą włączyć swój kod do gałęzi develop, otwierają pull request. W celu wymiany wiedzy i omówienia podejść wprowadziliśmy code review, co oznacza, że koledzy muszą sprawdzić i zatwierdzić kod nawzajem.
Sprawdzenia
Patrzenie na kod oczami — to świetna sprawa, ale niewystarczająca. Dlatego wprowadzone są automatyczne kontrole.
- Na początek sprawdzamy kompilację ARC.
- Wiele testów Junit.
- Obliczamy zasięg kodu, skoro uruchamiamy testy.
Aby zrozumieć, jak należy uruchamiać te kontrole, przyjrzymy się procesowi rozwoju w Avito.
Schematycznie można go przedstawić w ten sposób:
- Programista pisze kod na swoim laptopie. Można uruchomić kontrole integracji tutaj — albo za pomocą hooka commit, albo po prostu uruchamiając kontrole w tle.
- Po tym, jak programista wypchnie kod, otwiera pull request. Aby jego kod trafił do gałęzi develop, musi przejść code review i uzyskać wymaganą liczbę zatwierdzeń. Można włączyć kontrole i kompilacje tutaj: dopóki nie wszystkie kompilacje są udane, pull request nie może zostać scalony.
- Po tym, jak pull request zostanie scalony i kod trafi do develop, można wybrać odpowiedni czas: na przykład w nocy, kiedy wszystkie serwery są wolne, i uruchomić kontrole na maxa.
Uruchamianie kontroli na swoim laptopie nikomu się nie podobało. Kiedy programista skończył funkcję, chciał jak najszybciej ją wypchnąć i otworzyć pull request. Jeśli w tym momencie uruchamiane są długie kontrole, to nie tylko jest to mało przyjemne, ale także spowalnia rozwój: dopóki laptop coś sprawdza, nie można na nim normalnie pracować.
Bardzo spodobało nam się uruchamianie kontroli w nocy, ponieważ jest dużo czasu i serwerów, można sobie poszaleć. Niestety, gdy kod funkcji trafił do develop, programista miał znacznie mniej motywacji, aby naprawić błędy, które znalazło CI. Czasami łapałem się na tym, że patrząc na poranny raport o wszystkich znalezionych błędach myślałem, że naprawię je kiedyś później, ponieważ teraz w Jira czeka niesamowite nowe zadanie, które chciałbym jak najszybciej zacząć realizować.
Jeśli kontrole blokują pull request, to motywacja jest wystarczająca, bo dopóki kompilacje nie będą zielone, kod nie trafi do develop, a to oznacza, że zadanie nie będzie zakończone.
Ostatecznie wybraliśmy taką strategię: w nocy uruchamiamy możliwie największy zestaw testów, a najważniejsze i przede wszystkim najszybsze uruchamiamy przy żądaniu pull request. Ale na tym nie przestajemy — równolegle optymalizujemy czas przechodzenia testów, aby przenieść je z trybu nocnego do testów przy żądaniu pull request.
W tamtym czasie wszystkie nasze kompilacje odbywały się wystarczająco szybko, dlatego po prostu włączyliśmy blokadę dla żądania pull request dla kompilacji ARC, testów Junit i obliczeń pokrycia kodu. Włączyliśmy, pomyśleliśmy — i zrezygnowaliśmy z pokrycia kodu, ponieważ uznaliśmy, że jest nam niepotrzebne.
Cała konfiguracja podstawowego CI zajęła nam dwa dni (tutaj i dalej oszacowanie czasowe jest przybliżone, potrzebne dla kontekstu).
Po tym zaczęliśmy się zastanawiać — czy właściwie w ogóle testujemy? Czy właściwie uruchamiamy budowy przy żądaniu pull request?
Uruchamialiśmy kompilację na ostatnim commicie gałęzi, na której otwarto pull request. Ale testy tego commita mogą pokazać tylko, że kod napisany przez programistę działa. Nie dowodzą one, że nic nie zepsuł. W rzeczywistości trzeba sprawdzić stan gałęzi develop po tym, jak funkcjonalność zostanie w nią włączona.

W tym celu napisaliśmy prosty skrypt bash premerge.sh:
#!/usr/bin/env bash
set -e
git fetch origin develop
git merge origin/developTutaj pobierane są wszystkie najnowsze zmiany z develop i włączane do bieżącej gałęzi. Dodaliśmy skrypt premerge.sh jako pierwszy krok wszystkich kompilacji i zaczęliśmy sprawdzać dokładnie to, co chcemy, czyli integrację.
Na lokalizację problemu, znalezienie rozwiązania i napisanie tego skryptu zajęło nam trzy dni.
Aplikacja rozwijała się, pojawiało się coraz więcej zadań, zespół się powiększał, a premerge.sh czasami zaczynał nas zawodzić. W develop przenikały konflikty zmian, które łamały kompilację.
Przykład tego, jak to się dzieje:

Dwaj programiści jednocześnie zaczynają pracować nad funkcjami A i B. Programista funkcji A odkrywa w projekcie nieużywaną funkcję answer() i, jak dobry skaut, ją usuwa. W tym samym czasie programista funkcji B w swojej gałęzi dodaje nowe wywołanie tej funkcji.
Programiści kończą pracę i w tym samym czasie otwierają pull request. Uruchamiane są kompilacje, premerge.sh sprawdza oba żądania pull request w odniesieniu do świeżego stanu develop — wszystkie testy są zielone. Po tym łączony jest pull request funkcji A, a następnie pull request funkcji B… Bum! Develop się łamie, ponieważ w kodzie develop jest wywołanie nieistniejącej funkcji.

Kiedy develop się nie kompiluje, to lokalna katastrofa. Cały zespół nie może nic zebrać i oddać do testów.
Zdarzyło się, że zajmowałem się głównie zadaniami infrastrukturalnymi: analityka, sieć, bazy danych. To znaczy, że to ja pisałem funkcje i klasy, które wykorzystują inni programiści. Z tego powodu często stawałem w podobnych sytuacjach. Miałem nawet jakiś czas taką grafikę.

Ponieważ to nam nie odpowiadało, zaczęliśmy opracowywać opcje, jak to zapobiec.
Jak nie łamać develop
Pierwsza opcja: przebudować wszystkie pull requesty przy aktualizacji develop. Jeśli w naszym przykładzie pull request z funkcjonalnością A jako pierwszy trafi do develop, pull request funkcjonalności B zostanie przebudowany i odpowiednio, kontrole nie przejdą z powodu błędu kompilacji.
Aby zrozumieć, ile czasu to zajmie, rozważmy przykład z dwoma PR. Otwieramy dwa PR: dwa buildy, dwa uruchomienia kontrolek. Po tym, jak pierwszy PR zostanie scalony z develop, drugi musi zostać przebudowany. W sumie, na dwa PR potrzeba trzech uruchomień kontrolek: 2 + 1 = 3.
W zasadzie to w porządku. Ale przyjrzeliśmy się statystykom, a typową sytuacją w naszym zespole było 10 otwartych PR, a wtedy liczba kontrolek to suma postępującej: 10 + 9 +… + 1 = 55. To znaczy, żeby zaakceptować 10 PR, trzeba przebudować 55 razy. I to w idealnej sytuacji, gdy wszystkie kontrole przechodzą za pierwszym razem, kiedy nikt nie otwiera dodatkowego pull requestu, podczas gdy ten dziesiątek jest przetwarzany.
Pomyśl sobie, że jesteś programistą, który musi zdążyć nacisnąć przycisk „merge” jako pierwszy, ponieważ jeśli to zrobi sąsiad, to trzeba będzie czekać, aż wszystkie buildy przejdą od nowa... Nie, tak nie może być, to poważnie spowolni rozwój.
Drugi możliwy sposób: zbierać pull request po code review. To znaczy, otwierasz pull request, zbierasz potrzebną liczbę akceptacji od kolegów, poprawiasz to, co trzeba, a następnie uruchamiasz buildy. Jeśli są udane, pull request scalany jest z develop. W tym przypadku nie ma dodatkowych ponownych uruchomień, ale znacznie zwalnia to informację zwrotną. Jako programista, otwierając pull request, od razu chcę widzieć, czy on się skompiluje. Na przykład, jeśli jakiś test nie przeszedł, muszę go szybko naprawić. W przypadku opóźnionego builda zwalnia to informację zwrotną, a tym samym całościowy rozwój. To także nam nie odpowiadało.
W rezultacie została tylko trzecia opcja — wymysłać. Wszystkie nasze kody, wszystkie nasze źródła są przechowywane w repozytorium na serwerze Bitbucket. W związku z tym musieliśmy opracować wtyczkę do Bitbucket.

Ta wtyczka nadpisuje mechanizm scalania pull requestów. Rozpoczynamy standardowo: otwiera się PR, uruchamiają się wszystkie kompilacje, przechodzi przegląd kodu. Ale po przejściu przeglądu kodu, gdy deweloper decyduje się nacisnąć „merge”, wtyczka sprawdza, w odniesieniu do jakiego stanu develop uruchamiane były testy. Jeśli po kompilacji develop zdążył się zaktualizować, wtyczka nie pozwala wlać takiego pull requesta do głównej gałęzi. Po prostu uruchomi ponownie kompilacje w odniesieniu do świeżego develop.

W naszym przykładzie z konfliktującymi zmianami takie kompilacje nie przejdą z powodu błędu kompilacji. W związku z tym deweloper funkcji B będzie musiał poprawić kod, uruchomić testy, a wtedy wtyczka automatycznie zastosuje pull request.
Przed wdrożeniem tej wtyczki mieliśmy średnio 2,7 uruchomienia testu na jeden pull request. Z wtyczką jest ich 3,6. To było dla nas satysfakcjonujące.
Warto zauważyć, że ta wtyczka ma wadę: uruchamia kompilację tylko jeden raz. Oznacza to, że wciąż pozostaje małe okno, przez które do develop mogą trafić konfliktujące zmiany. Ale prawdopodobieństwo tego jest niskie, i zdecydowaliśmy się na ten kompromis między liczbą uruchomień a prawdopodobieństwem awarii. Przez dwa lata zdarzyło się to tylko raz, więc pewnie nie bez powodu.
Na napisanie pierwszej wersji wtyczki do Bitbucket zajęło nam dwa tygodnie.
Nowe testy
W międzyczasie nasz zespół nadal rastał. Pojawiały się nowe testy.
Pomyśleliśmy: po co naprawiać błędy, skoro można je zapobiegać? Dlatego wdrożyliśmy statyczną analizę kodu. Zaczęliśmy od linta, który wchodzi w skład Android SDK. Ale w tym czasie w ogóle nie potrafił pracować z kodem Kotlin, a już 75% aplikacji było napisane w Kotlin. Dlatego do linta dodano wbudowane kontrole Android Studio.
Do tego musieliśmy się nieco wysilić: wziąć Android Studio, zapakować ją w Dockerze i uruchomić w CI z wirtualnym monitorem, aby myślała, że jest uruchomiona na prawdziwym laptopie. Ale to działało.
Również w tym czasie zaczęliśmy pisać wiele testów instrumentalnych i wdrożyliśmy testowanie zrzutów ekranuTo jest sytuacja, w której generowany jest wzorcowy zrzut ekranu dla określonego małego widoku, a test polega na robieniu zrzutu ekranu z tego widoku i bezpośrednim porównywaniu go z wzorcem pixel po pixelu. Jeśli występuje różnica, oznacza to, że gdzieś nastąpił błąd w układzie lub coś jest nie tak w stylach.
Jednak testy instrumentacyjne i testy zrzutów ekranu muszą być uruchamiane na urządzeniach: na emulatorach lub na rzeczywistych urządzeniach. Biorąc pod uwagę, że testów jest dużo i są często wykonywane, potrzebna jest cała farmę. Ustawianie własnej farmy jest zbyt pracochłonne, dlatego znaleźliśmy gotowe rozwiązanie — Firebase Test Lab.
Firebase Test Lab
Został wybrany, ponieważ Firebase to produkt Google, co znaczy, że powinien być niezawodny i mało prawdopodobne, że kiedykolwiek zniknie. Ceny są przystępne: 5 USD za godzinę pracy rzeczywistego urządzenia, 1 USD za godzinę pracy emulatora.
Wdrożenie Firebase Test Lab w naszym CI zajęło nam około trzech tygodni.
Jednak zespół nadal rósł, a Firebase, niestety, zaczynał nas zawodzić. W tym czasie nie miał żadnego SLA. Czasami Firebase zmuszał do czekania, aż uwolni się potrzebna ilość urządzeń do testów, a nie zaczynał ich wykonywać od razu, jak tego chcieliśmy. Czekanie w kolejce zajmowało do pół godziny, co jest bardzo długo. Testy instrumentacyjne były uruchamiane przy każdym PR, a opóźnienia znacznie spowalniały rozwój, a potem przychodził jeszcze rachunek za miesiąc z okrągłą sumą. Ostatecznie postanowiliśmy zrezygnować z Firebase i stworzyć własne rozwiązanie, skoro zespół wystarczająco urósł.
Docker + Python + bash
Wzięliśmy dockera, umieściliśmy w nim emulatory, napisaliśmy prosty program w Pythonie, który w odpowiednim momencie uruchamia potrzebną ilość emulatorów w odpowiedniej wersji, a gdy trzeba, je zatrzymuje. I oczywiście kilka skryptów bash — gdzieżby bez nich?
Na stworzenie własnego środowiska testowego potrzebowaliśmy pięciu tygodni.
W rezultacie na każdy pull request przypadał obszerny, blokujący połączenie, lista kontroli:
- Budowanie APK;
- Testy Junit;
- Lint;
- Kontrole Android Studio;
- Testy instrumentacyjne;
- Testy zrzutów ekranu.
To zapobiegało wielu możliwym awariom. Technicznie wszystko działało, ale programiści narzekali, że czekanie na wyniki trwa zbyt długo.
Zbyt długo — to ile? Wyeksportowaliśmy dane z Bitbucket i TeamCity do systemu analizy i odkryliśmy, że średni czas oczekiwania wynosi 45 minut. To znaczy, że programista, otwierając pull request, średnio czeka na wyniki budowy 45 minut. Moim zdaniem to bardzo dużo i tak pracować nie można.
Oczywiście, postanowiliśmy przyspieszyć wszystkie nasze budowy.
Przyspieszamy
Zauważając, że często budowy czekają w kolejce, najpierw dokupiliśmy sprzętu — intensywne rozwijanie to najprostsza opcja. Budowy przestały czekać w kolejce, ale czas oczekiwania zmniejszył się tylko nieznacznie, ponieważ niektóre kontrole same w sobie trwają bardzo długo.
Usuwamy zbyt długie sprawdzenia
Nasz Continuous Integration mógł wychwycić takie typy błędów i problemów.
- Nie kompiluje się. CI może wychwycić błąd kompilacji, gdy z powodu konfliktujących zmian coś się nie kompiluje. Jak już wspomniałem, wtedy nikt nie może nic skompilować, rozwój staje w miejscu, a wszyscy się denerwują.
- Błąd w zachowaniu. Na przykład, gdy aplikacja się kompiluje, ale podczas kliknięcia przycisku się zawiesza, lub przycisk w ogóle nie reaguje. To źle, ponieważ taki błąd może dotrzeć do użytkownika.
- Błąd w układzie. Na przykład, przycisk działa, ale przesunął się o 10 pikseli w lewo.
- Wzrost długu technicznego.
Zerkając na tę listę, zrozumieliśmy, że krytyczne są tylko dwa pierwsze punkty. Takie problemy chcemy wychwytywać w pierwszej kolejności. Błędy w układzie są wykrywane na etapie przeglądu projektów i wówczas łatwo je naprawić. Praca z długiem technicznym wymaga osobnego procesu i planowania, dlatego postanowiliśmy nie sprawdzać go w pull requestach.
Na podstawie tej klasyfikacji przetrząsnęliśmy całą listę sprawdzeń. Skreśliliśmy Lint i przenieśliśmy jego uruchamianie na noc: po prostu, aby dostarczał raport o liczbie problemów w projekcie. Z długiem technicznym postanowiliśmy pracować osobno, a zrezygnowaliśmy całkowicie z kontroli Android Studio. Android Studio w Dockerze do uruchamiania inspekcji brzmi interesująco, ale sprawia wiele problemów w utrzymaniu. Każda aktualizacja wersji Android Studio to walka z niejasnymi błędami. Również trudno było utrzymać testy zrzutów ekranu, ponieważ biblioteka nie działała zbyt stabilnie i zdarzały się fałszywe alarmy. Testy zrzutów ekranu usunięto z listy sprawdzeń.
W rezultacie zostały nam:
- Budowanie APK;
- Testy Junit;
- Testy Instrumentacyjne.
Zdalna pamięć podręczna Gradle
Bez ciężkich kontroli wszystko stało się lepsze. Ale nie ma granic doskonałości!
Nasza aplikacja była już podzielona na około 150 modułów gradle. Zwykle w takim przypadku dobrze działa zdalna pamięć podręczna Gradle i postanowiliśmy spróbować.
Gradle remote cache to usługa, która może przechowywać artefakty budowy dla poszczególnych zadań w indywidualnych modułach. Zamiast rzeczywiście kompilować kod, Gradle łączy się z remote cache przez HTTP i pyta, czy ktoś już wykonał to zadanie. Jeśli tak, po prostu pobiera wynik.
Uruchomienie Gradle remote cache jest łatwe, ponieważ Gradle dostarcza obraz Docker. Udało nam się to zrobić w trzy godziny.
Wystarczyło uruchomić Docker i dodać jedną linię w projekcie. Choć można go szybko uruchomić, aby wszystko działało dobrze, potrzeba na to znacznie więcej czasu.
Poniżej przedstawiony wykres cache misses.

Na początku procent nietrafień w cache wynosił około 65. Po trzech tygodniach udało się obniżyć tę wartość do 20%. Okazało się, że zadania, które zbiera aplikacja na Androida, mają dziwne zależności przejrzyste, przez co Gradle nie trafił do cache.
Włączając cache, znacznie przyspieszyliśmy budowę. Ale oprócz budowy, przeprowadzane są również testy instrumentalne, które trwają długo. Możliwe, że nie wszystkie testy trzeba uruchamiać dla każdego pull requesta. Aby to ustalić, stosujemy analizę wpływu.
Analiza wpływu
Na pull request gromadzimy git diff i znajdujemy zmienione moduły Gradle.

Ma sens uruchamianie tylko tych testów instrumentalnych, które sprawdzają zmienione moduły oraz wszystkie moduły, które od nich zależą. Nie ma sensu uruchamiać testów dla sąsiednich modułów: tam kod się nie zmienił i nic nie może się zepsuć.
Z testami instrumentalnymi nie jest tak prosto, ponieważ muszą znajdować się w najwyższym module Application. Zastosowaliśmy heurystykę z analizą kodu bajtowego, aby zrozumieć, do którego modułu odnosi się każdy test.
Modernizacja pracy testów instrumentalnych, aby sprawdzały tylko zaangażowane moduły, zajęła około ośmiu tygodni.
Działania mające na celu przyspieszenie weryfikacji zakończyły się sukcesem. Z 45 minut zeszliśmy do około 15. Kwadrans czekania na build już jest akceptowalny.
Ale teraz deweloperzy zaczęli narzekać, że nie wiedzą, jakie buildy są uruchamiane, gdzie mogą sprawdzić logi, dlaczego build jest czerwony, który test nie przeszedł itd.

Problemy z informacją zwrotną spowalniają rozwój, dlatego staraliśmy się zapewnić jak najbardziej zrozumiałe i szczegółowe informacje o każdym PR i buildzie. Zaczęliśmy od komentarzy w Bitbucket do PR z informacją, który build się nie powiódł i dlaczego, pisaliśmy adresowe wiadomości w Slack. Ostatecznie zrobiliśmy stronę dashboard PR z listą wszystkich buildów, które są teraz uruchamiane i ich stanem: w kolejce, uruchamiany, nie powiódł się lub zakończony. Można kliknąć na build i przejść do jego logu.

Na szczegółową informację zwrotną poświęciliśmy sześć tygodni.
Plany
Przechodzimy do najnowszej historii. Rozwiązując problem informacyjny, weszliśmy na nowy poziom — postanowiliśmy zbudować własną farmę emulatorów. Kiedy jest wiele testów i emulatorów, trudno nimi zarządzać. W rezultacie wszystkie nasze emulatory przeniosły się do klastra k8s z elastycznym zarządzaniem zasobami.
Ponadto istnieją także inne plany.
- Przywrócić Lint (i inny statyczny analizator). Już pracujemy w tym kierunku.
- Uruchomić na PR blokującego wszystkie testy end-to-end na wszystkich wersjach SDK.
A więc, prześledziliśmy historię rozwoju Continuous Integration w Avito. Teraz chciałbym dać kilka rad z perspektywy doświadczonego.
Porady
Gdybym mógł dać tylko jedną radę, byłaby to ta:
Proszę, bądźcie ostrożni z skryptami shellowymi!
Bash to bardzo elastyczne i potężne narzędzie, na którym bardzo łatwo i szybko pisze się skrypty. Ale można w nim wpaść w pułapkę, i niestety my w nią wpadliśmy.
Zaczęło się od prostych skryptów, które były uruchamiane na naszych maszynach budowlanych:
#!/usr/bin/env bash
./gradlew assembleDebugAle jak wiadomo, wszystko z biegiem czasu się rozwija i komplikuje — uruchommy jeden skrypt z drugiego, przekazujmy mu jakieś parametry — w rezultacie trzeba było napisać funkcję, która określa, na jakim poziomie zagnieżdżenia bash obecnie się znajdujemy, aby wstawić odpowiednie cudzysłowy, aby to wszystko się uruchomiło.

Możecie sobie wyobrazić koszty pracy związane z rozwojem takich skryptów. Radzę nie wpadać w tę pułapkę.
Czym można to zastąpić?
- Każdym językiem skryptowym. Pisanie w Pythonie lub Kotlin Script jest wygodniejsze, ponieważ to programowanie, a nie skrypty.
- Lub opisz całą logikę buildów w postaci Custom gradle tasks dla twojego projektu.
Zdecydowaliśmy się na drugą opcję i teraz systematycznie usuwamy wszystkie skrypty bash i piszemy wiele niestandardowych zadań gradle.
Rada nr 2: przechowuj infrastrukturę w kodzie.
To wygodne, gdy konfiguracja Continuous Integration nie jest przechowywana w interfejsie UI Jenkins czy TeamCity itp., a w formie plików tekstowych bezpośrednio w repozytorium projektu. To zapewnia wersjonowalność. Nie będzie trudno cofnąć się lub zbudować kod na innej gałęzi.
Skrypty można przechowywać w projekcie. A co z otoczeniem?
Wskazówka nr 3: pomoc w otoczeniu może zaoferować Docker.
Z pewnością pomoże on programistom Android, niestety na razie nie dla iOS.
To przykład prostego pliku docker, który zawiera jdk i 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
# Pobierz 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
# Zainstaluj Android Build Tool i biblioteki
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
Napisałem ten plik docker (powiem w tajemnicy, że można go nie pisać, a po prostu pobrać gotowy z GitHub) i po zbudowaniu obrazu otrzymujesz maszynę wirtualną, na której możesz tworzyć aplikację i uruchamiać testy Junit.
Dwa główne argumenty, dlaczego to ma sens: skalowalność i powtarzalność. Używając Dockera, można szybko uruchomić dziesiątki agentów budowlanych, które będą miały dokładnie takie samo otoczenie jak poprzednie. To bardzo ułatwia życie inżynierom CI. Wpakowanie android-sdk do dockera jest całkiem proste, z emulatorami jest trochę trudniej: trzeba się trochę postarać (no, albo znów pobrać gotowe z GitHub).
Wskazówka nr 4: pamiętaj, że kontrole są przeprowadzane nie dla samych kontroli, ale dla ludzi.
Dla programistów bardzo ważna jest szybka i przede wszystkim zrozumiała informacja zwrotna: co się zepsuło, jaki test nie przeszedł, gdzie można zobaczyć logi budowy.
Wskazówka nr 5: bądź pragmatyczny w rozwoju Continuous Integration.
Dokładnie rozumiej, jakie typy błędów chcesz zapobiegać, ile jesteś gotów poświęcić zasobów, czasu, czasu maszynowego. Zbyt długie kontrole można na przykład przenieść na noc. A z tych, które wyłapują niezbyt ważne błędy, można całkowicie zrezygnować.
Wskazówka nr 6: korzystaj z gotowych narzędzi.
Obecnie istnieje wiele firm, które oferują chmurowe CI.

Dla małych zespołów to dobre rozwiązanie. Nie musisz niczego utrzymywać, wystarczy, że zapłacisz trochę pieniędzy, zbierzesz swoją aplikację i nawet uruchomisz testy instrumentalne.
Rada nr 7: w dużym zespole bardziej opłacalne są rozwiązania in-house.
Ale prędzej czy później, wraz ze wzrostem zespołu, rozwiązania in-house staną się bardziej opłacalne. Jest jeden aspekt tych rozwiązań. W ekonomii obowiązuje zasada malejących przychodów: w każdym projekcie każde kolejne usprawnienie pojawia się coraz trudniej, wymaga coraz większych inwestycji.
Ekonomia opisuje całe nasze życie, w tym Continuous Integration. Stworzyłem wykres nakładów pracy na każdym etapie rozwoju naszego Continuous Integration.

Widać, że każde usprawnienie staje się coraz trudniejsze i trudniejsze. Patrząc na ten wykres, można zrozumieć, że rozwijanie Continuous Integration powinno być zgodne z rozwojem wielkości zespołu. Dla zespołu liczącego dwa osoby wydawanie 50 dni na rozwój wewnętrznej farmy emulatorów to niezbyt dobry pomysł. Ale z kolei, dla dużego zespołu w ogóle nie zajmowanie się Continuous Integration to też zła idea, ponieważ na problemy z integracją, naprawę komunikacji itp. będzie trzeba poświęcić jeszcze więcej czasu.
Zaczęliśmy od tego, że automatyzacja jest potrzebna, ponieważ ludzie są drodzy, popełniają błędy i są leniwi. Jednak automatyzują też ludzie. Dlatego te same problemy odnoszą się również do automatyzacji.
- Automatyzacja jest kosztowna. Pamiętaj o wykresie nakładów pracy.
- Podczas automatyzacji ludzie popełniają błędy.
- Czasami trudno jest się zmobilizować do automatyzacji, ponieważ wszystko działa jak należy. Po co jeszcze coś usprawniać, po co cały ten Continuous Integration?
Ale mam statystyki: w 20% kompilacji wykrywane są błędy. I to nie dlatego, że nasi programiści źle piszą kod. To dlatego, że programiści są pewni, że jeśli popełnią jakikolwiek błąd, nie trafi on do develop, bo wykryją go automatyczne kontrole. W związku z tym programiści mogą poświęcać więcej czasu na pisanie kodu i ciekawe rzeczy, a nie na lokalne uruchamianie i sprawdzanie.
Zajmijcie się Continuous Integration. Ale z umiarem.
Przy okazji, Nikołaj Nesterow nie tylko sam robi świetne prezentacje, ale także jest członkiem komitetu programowego i pomaga innym przygotowywać dla was merytoryczne wystąpienia. Pełnię i użyteczność programu nadchodzącej konferencji można ocenić po tematach w . A po szczegóły przyjdźcie 22-23 kwietnia do Infoprzestrzeni.
Źródło: habr.com
