Heute werden die meisten Softwareprodukte in Teams entwickelt. Die Bedingungen für den Erfolg der Teamarbeit können in einem einfachen Schema dargestellt werden.

Nachdem Sie den Code geschrieben haben, müssen Sie sicherstellen, dass er:
- Funktioniert.
- Nichts kaputt macht, einschließlich des Codes, den Ihre Kollegen geschrieben haben.
Wenn beide Bedingungen erfüllt sind, sind Sie auf dem Weg zum Erfolg. Um diese Bedingungen leicht zu überprüfen und nicht vom profitablen Weg abzuweichen, wurde Continuous Integration (CI) erfunden.
CI ist ein Arbeitsprozess, bei dem Sie Ihren Code so oft wie möglich in den gemeinsamen Code des Produkts integrieren. Und nicht nur integrieren, sondern auch ständig überprüfen, ob alles funktioniert. Da es viel und häufig zu überprüfen gibt, sollte man über Automatisierung nachdenken. Man kann alles manuell überprüfen, aber das sollte man nicht, und hier ist warum.
- Menschen sind kostbar. Die Arbeitszeit eines Programmierers ist teurer als die eines Servers.
- Menschen machen Fehler. Daher können Situationen entstehen, in denen Tests nicht in dem richtigen Branch gestartet oder die falschen Commits für die Tester eingesammelt wurden.
- Menschen sind faul. Gelegentlich, wenn ich eine Aufgabe abgeschlossen habe, denke ich mir: "Was gibt es hier zu überprüfen? Ich habe zwei Zeilen geschrieben – das funktioniert doch sicher!" Ich denke, einigen von Ihnen kommen solche Gedanken auch manchmal in den Sinn. Aber man muss immer überprüfen.
Wie Continuous Integration im mobilen Entwicklungsteam von Avito eingeführt und weiterentwickelt wurde, wie man von 0 auf 450 Builds pro Tag kam und was die Build-Maschinen 200 Stunden am Tag sammeln, erzählt Nikolai Nesterov () – Teilnehmer aller evolutionären Änderungen am CI/CD-Android-Anwendungsprozess.
Die Erzählung basiert auf dem Beispiel des Android-Teams, aber die meisten Ansätze sind auch auf iOS anwendbar.

Vor langer Zeit arbeitete in dem Android-Team von Avito nur eine Person. Ihm war per Definition nichts von Continuous Integration notwendig: Es gab niemanden, mit dem er sich integrieren konnte.
Aber die Anwendung wuchs, es kamen immer mehr neue Aufgaben hinzu, und entsprechend wuchs das Team. Irgendwann war es an der Zeit, den Integrationsprozess formeller zu gestalten. Es wurde beschlossen, Git Flow zu verwenden.

Das Konzept von Git Flow ist bekannt: Im Projekt gibt es einen gemeinsamen Branch namens develop, und für jedes neue Feature erstellen die Entwickler einen separaten Branch, committen in diesen, pushen und öffnen, wenn sie ihren Code in den develop-Branch einfügen möchten, einen Pull Request. Um den Wissensaustausch und die Diskussion von Ansätzen zu fördern, haben wir Code-Reviews eingeführt, das heißt, die Kollegen müssen den Code des anderen überprüfen und bestätigen.
Überprüfungen
Den Code mit eigenen Augen zu betrachten, ist großartig, aber nicht ausreichend. Daher werden automatische Prüfungen eingeführt.
- Zuerst überprüfen wir den ARC-Build.
- Viele JUnit-Tests.
- Wir betrachten die Codeabdeckung, schließlich führen wir die Tests aus.
Um zu verstehen, wie diese Überprüfungen durchgeführt werden sollten, schauen wir uns den Entwicklungsprozess bei Avito an.
Schematically lässt sich das so darstellen:
- Der Entwickler schreibt Code auf seinem Laptop. Hier können Integrationstests direkt ausgeführt werden – entweder per Commit-Hook oder einfach im Hintergrund.
- Nachdem der Entwickler den Code gepusht hat, öffnet er einen Pull Request. Damit sein Code in den develop-Branch gelangt, muss er das Code-Review durchlaufen und die erforderliche Anzahl an Bestätigungen sammeln. Hier können Prüfungen und Builds aktiviert werden: solange nicht alle Builds erfolgreich sind, kann der Pull Request nicht gemerged werden.
- Nachdem der Pull Request gemerged wurde und der Code in develop ist, kann ein passender Zeitpunkt gewählt werden: zum Beispiel nachts, wenn alle Server frei sind, und so viele Prüfungen wie möglich durchgeführt werden.
Es gefiel uns nicht, Prüfungen auf dem eigenen Laptop durchzuführen. Wenn ein Entwickler ein Feature abgeschlossen hat, möchte er es so schnell wie möglich pushen und einen Pull Request öffnen. Wenn in diesem Moment einige langwierige Überprüfungen gestartet werden, ist das nicht nur unangenehm, sondern verlangsamt auch die Entwicklung: Während der Laptop etwas überprüft, kann man nicht normal arbeiten.
Es gefiel uns sehr, die Prüfungen nachts auszuführen, da viel Zeit und Serverkapazität zur Verfügung stehen. Leider hat der Entwickler, wenn der Feature-Code in develop ist, viel weniger Motivation, die Fehler zu beheben, die CI gefunden hat. Ich ertappte mich manchmal dabei, als ich im morgendlichen Bericht auf alle festgestellten Fehler schaute, dass ich sie irgendwann später beheben würde, weil jetzt in Jira eine coole neue Aufgabe liegt, die ich unbedingt anfangen möchte.
Wenn Prüfungen den Pull Request blockieren, ist die Motivation ausreichend, denn solange die Builds nicht erfolgreich sind, gelangt der Code nicht in den develop-Branch, und somit wird die Aufgabe nicht abgeschlossen.
Letztendlich haben wir eine Strategie gewählt: Nachts führen wir so viele Prüfungen wie möglich durch, und die kritischsten und am wichtigsten schnellen Prüfungen starten wir bei Pull-Requests. Aber dabei bleibt es nicht – parallel optimieren wir die Geschwindigkeit der Prüfungen, um sie vom Nachtmodus in Prüfungen bei Pull-Requests zu überführen.
Zu diesem Zeitpunkt liefen alle unsere Builds schnell genug, daher haben wir einfach den ARK-Build, Junit-Tests und die Berechnung der Codeabdeckung als Blockierer für Pull-Requests aktiviert. Wir haben das aktiviert, darüber nachgedacht – und uns von der Codeabdeckung verabschiedet, weil wir dachten, dass wir sie nicht brauchen.
Für die gesamte Einrichtung der Basis-CI benötigten wir zwei Tage (hier und im Folgenden ist die Zeitabschätzung ungefähren, dient zur Maßstabserhöhung).
Nachdem wir dies abgeschlossen hatten, begannen wir weiter zu denken – überprüfen wir überhaupt richtig? Starten wir die Builds bei Pull-Requests korrekt?
Wir führten die Build-Tests mit dem letzten Commit des Branches durch, von dem der Pull-Request eröffnet wurde. Aber die Prüfungen dieses Commits können nur zeigen, dass der Code, den der Entwickler geschrieben hat, funktioniert. Sie beweisen jedoch nicht, dass er nichts kaputt gemacht hat. Tatsächlich sollten wir den Zustand des develop-Branches prüfen, nachdem das Feature integriert wurde.

Dafür schrieben wir ein einfaches Bash-Skript premerge.sh:
#!/usr/bin/env bash
set -e
git fetch origin develop
git merge origin/developHier werden einfach alle neuesten Änderungen aus develop abgeholt und in den aktuellen Branch gemerged. Wir haben das Skript premerge.sh als ersten Schritt aller Builds hinzugefügt und begannen genau das zu überprüfen, was wir möchten, also Integration.
Für die Lokalisierung der Probleme, die Suche nach Lösungen und das Schreiben dieses Skripts benötigten wir drei Tage.
Die Anwendung entwickelte sich weiter, die Aufgaben wurden zahlreicher, das Team wuchs, und manchmal begann premerge.sh uns im Stich zu lassen. In develop drangen konfliktbehaftete Änderungen ein, die den Build brachen.
Ein Beispiel dafür, wie das passiert:

Zwei Entwickler beginnen gleichzeitig, an den Features A und B zu arbeiten. Der Entwickler des Features A entdeckt im Projekt eine nicht verwendete Funktion answer() und, wie ein guter Pfadfinder, löscht er sie. Gleichzeitig fügt der Entwickler des Features B in seinem Branch einen neuen Aufruf dieser Funktion hinzu.
Die Entwickler beenden ihre Arbeit und öffnen gleichzeitig einen Pull-Request. Die Builds werden gestartet, premerge.sh überprüft beide Pull-Requests bezüglich des aktuellen Zustands von develop – alle Prüfungen sind grün. Danach wird der Pull-Request des Features A gemerged, der Pull-Request des Features B wird gemerged… Boom! Develop bricht zusammen, weil im Code von develop ein Aufruf einer nicht existierenden Funktion vorhanden ist.

Wenn develop nicht kompilierbar ist, ist das lokale Katastrophe. Das gesamte Team kann nichts sammeln und zur Prüfung übergeben.
Es hat sich so ergeben, dass ich mich am häufigsten mit infrastrukturellen Aufgaben beschäftigt habe: Analytik, Netzwerk, Datenbanken. Das heißt, ich habe genau die Funktionen und Klassen geschrieben, die von anderen Entwicklern verwendet werden. Daher bin ich sehr oft in ähnliche Situationen geraten. Eine Zeit lang hatte ich sogar ein solches Bild.

Da uns das nicht zufriedenstellte, begannen wir, Optionen zu erarbeiten, wie wir das verhindern können.
Wie man develop nicht kaputt macht
Die erste Option: alle Pull Requests bei der Aktualisierung von develop neu zu erstellen. Wenn in unserem Beispiel der Pull Request mit Feature A zuerst in develop kommt, wird der Pull Request von Feature B neu erstellt, und entsprechend werden die Prüfungen aufgrund eines Kompilierungsfehlers nicht bestehen.
Um zu verstehen, wie viel Zeit das in Anspruch nehmen wird, betrachten wir ein Beispiel mit zwei PRs. Wir öffnen zwei PRs: zwei Builds, zwei Prüfungen. Nachdem der erste PR in develop eingefügt wurde, muss der zweite neu erstellt werden. Insgesamt benötigt man für zwei PRs drei Prüfungen: 2 + 1 = 3.
Im Prinzip ist das in Ordnung. Aber wir haben die Statistiken angeschaut, und eine typische Situation in unserem Team war, dass 10 PRs offen waren, und dann ergibt sich die Anzahl der Prüfungen als Summe der Progression: 10 + 9 +… + 1 = 55. Das bedeutet, um 10 PRs anzunehmen, muss man 55 Mal neu erstellen. Und das in einer idealen Situation, in der alle Prüfungen beim ersten Mal bestehen, und in der niemand einen zusätzlichen Pull Request öffnet, während wir diese zehn bearbeiten.
Stellen Sie sich einen Entwickler vor, der es eilig hat, als erster auf die Schaltfläche „Merge“ zu drücken, denn wenn das der Nachbar macht, muss man warten, bis alle Builds von neuem durchlaufen werden… Nein, das geht nicht, das würde die Entwicklung ernsthaft verlangsamen.
Die zweite mögliche Möglichkeit: Pull Requests nach dem Code-Review zu erstellen. Das heißt, Sie öffnen einen Pull Request, sammeln die erforderliche Anzahl von Genehmigungen von Kollegen, beheben, was nötig ist, und starten dann die Builds. Wenn sie erfolgreich sind, wird der Pull Request mit develop zusammengeführt. In diesem Fall gibt es keine zusätzlichen Neustarts, aber das Feedback verlangsamt sich erheblich. Ich möchte als Entwickler, der einen Pull Request eröffnet, sofort sehen, ob er erfolgreich kompiliert. Wenn ein Test fehlschlägt, muss er schnell behoben werden. Bei einem verzögerten Build verlangsamt sich das Feedback, was auch die gesamte Entwicklung verlangsamt. Das war uns auch nicht recht.
Am Ende blieb nur die dritte Option — alles selbst zu machen. Unser gesamter Code, alle unsere Quellcodes werden in einem Repository auf dem Bitbucket-Server gespeichert. Daher mussten wir ein Plugin für Bitbucket entwickeln.

Dieses Plugin überschreibt den Merge-Mechanismus von Pull-Requests. Der Beginn ist standardmäßig: Ein PR wird geöffnet, alle Builds werden gestartet und eine Code-Überprüfung erfolgt. Aber nachdem die Code-Überprüfung bestanden ist und der Entwickler beschließt, auf „merge“ zu klicken, überprüft das Plugin, auf welchem Stand develop die Tests durchgeführt wurden. Wenn sich develop nach den Builds aktualisiert hat, erlaubt das Plugin nicht, dass dieser Pull-Request in den Hauptzweig eingefügt wird. Es wird einfach die Builds entsprechend dem aktuellen develop neu starten.

In unserem Beispiel mit konfliktierten Änderungen werden solche Builds aufgrund eines Kompilierungsfehlers nicht bestehen. Somit muss der Entwickler der Funktion B den Code anpassen, die Überprüfungen erneut starten, dann wird das Plugin den Pull-Request automatisch anwenden.
Vor der Einführung dieses Plugins hatten wir im Durchschnitt 2,7 Prüfungen pro Pull-Request. Mit dem Plugin sind es nun 3,6 Prüfungen. Damit sind wir zufrieden.
Es ist anzumerken, dass dieses Plugin einen Nachteil hat: Es startet den Build nur einmal neu. Das bedeutet, dass immer noch ein kleines Zeitfenster bleibt, in dem konflikttierte Änderungen in develop gelangen können. Die Wahrscheinlichkeit dafür ist jedoch gering, und wir haben diesen Kompromiss zwischen der Anzahl der Starts und der Wahrscheinlichkeit eines Fehlers in Kauf genommen. In zwei Jahren ist es nur einmal aufgetreten, also war es wahrscheinlich nicht umsonst.
Für die Erstellung der ersten Version des Plugins für Bitbucket haben wir zwei Wochen benötigt.
Neue Überprüfungen
Inzwischen wuchs unser Team weiter. Es wurden neue Überprüfungen hinzugefügt.
Wir haben uns gefragt: Warum Fehler beheben, wenn man sie verhindern kann? Deshalb haben wir eingeführt statische Codeanalyse. Wir haben mit lint begonnen, der im Android SDK enthalten ist. Aber zu der Zeit konnte er überhaupt nicht mit Kotlin-Code arbeiten, während schon 75 % der Anwendung in Kotlin geschrieben waren. Daher wurden dem lint die eingebauten Android Studio-Überprüfungen hinzugefügt.
Dafür mussten wir ziemlich kreativ sein: Wir haben Android Studio in Docker verpackt und sie auf CI mit einem virtuellen Monitor gestartet, damit sie denkt, dass sie auf einem echten Laptop läuft. Aber es hat funktioniert.
Auch in dieser Zeit begannen wir, viele Instrumentierungstests zu schreiben und führten ein Screenshot-Testing ein.. Es handelt sich um die Generierung eines Referenz-Screenshot für eine separate kleine Ansicht, und der Test besteht darin, dass ein Screenshot von dieser Ansicht aufgenommen und pixelgenau mit dem Referenzbild verglichen wird. Wenn es Abweichungen gibt, deutet das darauf hin, dass das Layout irgendwo nicht stimmt oder es Probleme mit den Styles gibt.
Aber Instrumentierungstests und Screenshot-Tests müssen auf Geräten durchgeführt werden: auf Emulatoren oder echten Geräten. Da es viele Tests gibt und diese häufig ausgeführt werden, ist eine komplette Farm erforderlich. Eine eigene Farm einzurichten, ist jedoch sehr aufwendig, daher haben wir eine fertige Lösung gefunden – Firebase Test Lab.
Firebase Test Lab
Es wurde ausgewählt, weil Firebase ein Produkt von Google ist, was bedeutet, dass es zuverlässig sein sollte und kaum jemals eingestellt wird. Die Preise sind vernünftig: 5 $ pro Stunde für die Arbeit mit einem echten Gerät, 1 $ pro Stunde für die Arbeit mit einem Emulator.
Die Implementierung von Firebase Test Lab in unser CI hat etwa drei Wochen gedauert.
Aber das Team wuchs weiter, und Firebase hat uns leider angefangen im Stich zu lassen. Zu diesem Zeitpunkt gab es keinen SLA. Manchmal ließ Firebase uns warten, bis genügend Geräte für die Tests verfügbar waren, anstatt diese sofort auszuführen, wie wir es gewünscht hätten. Das Warten in der Warteschlange dauerte bis zu einer halben Stunde, was sehr lange ist. Instrumentierungstests wurden bei jedem PR ausgeführt, die Verzögerungen haben die Entwicklung stark verlangsamt, und dann kam auch noch die Rechnung für den Monat mit einem großen Betrag. Insgesamt wurde entschieden, auf Firebase zu verzichten und eine interne Lösung zu entwickeln, da das Team ausreichend gewachsen war.
Docker + Python + bash
Wir haben Docker genommen, die Emulatoren hinein gepackt und ein einfaches Programm in Python geschrieben, das zum richtigen Zeitpunkt die benötigte Anzahl an Emulatoren in der richtigen Version hochfährt und sie bei Bedarf anhält. Und natürlich ein paar bash-Skripte – wo kämen wir ohne die hin?
Für den Aufbau einer eigenen Testumgebung benötigten wir fünf Wochen.
In der Folge gab es für jeden Pull Request eine umfangreiche, die Zusammenführung blockierende Liste von Prüfungen:
- Build-ARK;
- Junit-Tests;
- Lint;
- Überprüfungen in Android Studio;
- Instrumentierungstests;
- Screenshot-Tests.
Dies verhinderte viele mögliche Fehler. Technisch hat alles funktioniert, aber die Entwickler klagten darüber, dass das Warten auf die Ergebnisse zu lange dauert.
Wie lange ist "zu lange"? Wir haben die Daten aus Bitbucket und TeamCity in ein Analysesystem exportiert und festgestellt, dass die durchschnittliche Wartezeit 45 Minuten beträgt. Das heißt, ein Entwickler wartet im Durchschnitt 45 Minuten auf die Ergebnisse der Builds, wenn er einen Pull Request öffnet. Meiner Meinung nach ist das sehr viel, und so kann man nicht arbeiten.
Natürlich haben wir beschlossen, all unsere Builds zu beschleunigen.
Wir beschleunigen uns
Als wir sahen, dass oft Builds in der Warteschlange standen, haben wir zuerst zusätzliches Material gekauft — eine extensive Entwicklung ist am einfachsten. Die Builds stehen nicht mehr in der Warteschlange, aber die Wartezeit hat sich nur minimal verringert, da einige Prüfungen von sich aus sehr lange dauerten.
Wir entfernen zu lange Prüfungen
Unsere Continuous Integration konnte solche Arten von Fehlern und Problemen erfassen.
- Wird nicht gebaut. CI kann einen Kompilierungsfehler erfassen, wenn aufgrund von Konflikten etwas nicht gebaut werden kann. Wie ich bereits sagte, kann dann niemand etwas bauen, die Entwicklung kommt zum Stillstand und alle sind nervös.
- Fehler im Verhalten. Zum Beispiel, wenn die Anwendung gebaut wird, aber beim Klicken auf die Schaltfläche abstürzt, oder die Schaltfläche überhaupt nicht reagiert. Das ist schlecht, denn ein solcher Fehler kann den Benutzer erreichen.
- Fehler im Layout. Zum Beispiel, wenn die Schaltfläche gedrückt wird, aber um 10 Pixel nach links verschoben ist.
- Zunahme der technischen Schulden.
Nachdem wir uns diese Liste angesehen hatten, stellte sich heraus, dass nur die ersten beiden Punkte kritisch sind. Solche Probleme wollen wir zuerst erfassen. Layout-Fehler werden in der Design-Überprüfung entdeckt und können dort leicht behoben werden. Die Arbeit mit technischen Schulden erfordert einen eigenen Prozess und Planung, deshalb haben wir beschlossen, sie nicht im Pull Request zu überprüfen.
Basierend auf dieser Klassifizierung haben wir die gesamte Liste der Prüfungen durchgesehen. Wir haben Lint gestrichen und seine Ausführung auf die Nacht verschoben: einfach um einen Bericht darüber zu erstellen, wie viele Probleme im Projekt bestehen. Mit den technischen Schulden haben wir beschlossen, getrennt zu arbeiten, und auf die Android Studio Checks ganz verzichtet. Android Studio in Docker für die Ausführung von Inspektionen klingt interessant, bringt jedoch viele Probleme in der Wartung mit sich. Jedes Update der Android Studio-Versionen stellt einen Kampf gegen unverständliche Bugs dar. Es war auch schwierig, Screenshot-Tests aufrechtzuerhalten, weil die Bibliothek nicht sehr stabil arbeitete und es manchmal zu Fehlalarmen kam. Screenshot-Tests haben wir aus der Liste der Prüfungen entfernt.
Am Ende hatten wir:
- Build-ARK;
- Junit-Tests;
- Instrumentierungstests.
Gradle Remote Cache
Ohne schwere Prüfungen ist alles besser geworden. Aber es gibt kein Limit für die Perfektion!
Unsere Anwendung war bereits in etwa 150 Gradle-Module aufgeteilt. Normalerweise funktioniert in einem solchen Fall der Gradle Remote Cache gut, und wir haben beschlossen, ihn auszuprobieren.
Der Gradle-Remote-Cache ist ein Dienst, der Build-Artefakte für einzelne Aufgaben in verschiedenen Modulen cachen kann. Gradle fragt den Remote-Cache über HTTP an und fragt, ob jemand diese Aufgabe bereits ausgeführt hat, anstatt den Code tatsächlich zu kompilieren. Wenn ja, wird das Ergebnis einfach heruntergeladen.
Es ist einfach, den Gradle-Remote-Cache zu starten, da Gradle ein Docker-Image bereitstellt. Wir haben das in drei Stunden geschafft.
Man musste lediglich Docker starten und eine Zeile im Projekt hinzufügen. Obwohl man ihn schnell starten kann, benötigt es doch eine beträchtliche Zeit, damit alles gut läuft.
Nachfolgend die Grafik der Cache-Fehlversuche.

Zu Beginn lag der Prozentsatz der Cache-Fehlversuche bei etwa 65. Nach drei Wochen gelang es uns, diesen Wert auf 20 % zu senken. Es stellte sich heraus, dass die Aufgaben, die die Android-Anwendung zusammenstellt, seltsame transitive Abhängigkeiten haben, durch die Gradle am Cache vorbeigeschossen ist.
Mit der Aktivierung des Caches haben wir den Build-Prozess erheblich beschleunigt. Aber zusätzlich zum Build müssen auch die Instrumentierungstests durchgeführt werden, die lange dauern. Es ist möglich, dass nicht alle Tests bei jedem Pull Request ausgeführt werden müssen. Um das herauszufinden, verwenden wir die Impact-Analyse.
Impact-Analyse
Für den Pull Request erstellen wir den Git-Diff und finden die geänderten Gradle-Module.

Es macht Sinn, nur die Instrumentierungstests auszuführen, die die geänderten Module und alle Module, die von ihnen abhängen, überprüfen. Tests für benachbarte Module auszuführen, macht keinen Sinn, da sich der Code dort nicht geändert hat und nichts kaputtgehen kann.
Bei den Instrumentierungstests ist es nicht so einfach, da sie sich im obersten Anwendungsmodul befinden müssen. Wir haben eine Heuristik zur Bytecode-Analyse angewendet, um herauszufinden, zu welchem Modul jeder Test gehört.
Die Modernisierung der Instrumentierungstests, um nur die betroffenen Module zu überprüfen, dauerte etwa acht Wochen.
Die Maßnahmen zur Beschleunigung der Überprüfungen haben erfolgreich funktioniert. Von 45 Minuten sind wir auf etwa 15 Minuten gekommen. Eine viertel Stunde auf den Build zu warten, ist schon in Ordnung.
Aber jetzt haben die Entwickler begonnen, sich zu beschweren, dass sie nicht nachvollziehen können, welche Builds gestartet werden, wo sie die Logs ansehen können, warum der Build rot ist, welcher Test fehlgeschlagen ist usw.

Rückmeldungsprobleme verzögern die Entwicklung, daher haben wir uns bemüht, so verständlich und detailliert wie möglich Informationen zu jedem PR und Build bereitzustellen. Wir begannen mit Kommentaren in Bitbucket zu PRs, die angaben, welcher Build fehlschlug und warum, und schrieben gezielte Nachrichten in Slack. Schließlich haben wir ein Dashboard für die PR-Seite erstellt, das eine Liste aller aktuell laufenden Builds und deren Status enthält: in der Warteschlange, wird gestartet, fehlgeschlagen oder abgeschlossen. Man kann auf den Build klicken und zu seinem Log gelangen.

Für detailliertes Feedback wurden sechs Wochen aufgewendet.
Pläne
Kommen wir zur neuesten Geschichte. Nachdem wir das Feedback-Problem gelöst hatten, haben wir ein neues Niveau erreicht — wir haben beschlossen, unsere eigene Emulatorfarm aufzubauen. Wenn es viele Tests und Emulatoren gibt, ist es schwierig, diese zu verwalten. Am Ende wurden alle unsere Emulatoren in einen k8s-Cluster mit flexibler Ressourcenverwaltung umgezogen.
Darüber hinaus gibt es weitere Pläne.
- Lint zurückbringen (und weitere statische Analysen). Wir arbeiten bereits in diese Richtung.
- Alle End-to-End-Tests auf allen SDK-Versionen im PR blockieren.
So haben wir die Entwicklung der Continuous Integration bei Avito nachverfolgt. Nun möchte ich einige Ratschläge aus der Sicht eines Erfahrenen geben.
Tipps
Wenn ich nur einen Rat geben könnte, wäre es dieser:
Bitte seid vorsichtig mit Shell-Skripten!
Bash ist ein sehr flexibles und mächtiges Werkzeug, und es ist sehr bequem und schnell, Skripte darauf zu schreiben. Aber man kann in eine Falle tappen, und wir sind leider hineingefallen.
Alles begann mit einfachen Skripten, die auf unseren Build-Maschinen ausgeführt wurden:
#!/usr/bin/env bash
./gradlew assembleDebugAber wie bekannt ist, entwickelt sich alles mit der Zeit weiter und wird komplizierter — lass uns ein Skript aus einem anderen starten, lass uns einige Parameter übergeben — letztendlich mussten wir eine Funktion schreiben, die bestimmt, auf welcher Verschachtelungsebene sich Bash gerade befindet, um die richtigen Anführungszeichen einzufügen, damit alles ausgeführt werden kann.

Könnt ihr euch den Aufwand für die Entwicklung solcher Skripte vorstellen? Ich empfehle, nicht in diese Falle zu tappen.
Wodurch kann man das ersetzen?
- Durch jede Skriptsprache. Es ist bequemer, in Python oder Kotlin Script zu schreiben, weil es sich um Programmierung handelt, nicht um Skripte.
- Oder die gesamte Logik der Builds in Form von benutzerdefinierten Gradle-Tasks für euer Projekt zu beschreiben.
Wir haben uns entschieden, die zweite Option zu wählen, und entfernen derzeit systematisch alle Bash-Skripte und schreiben viele benutzerdefinierte Gradle-Tasks.
Rat Nr. 2: Die Infrastruktur im Code speichern.
Es ist praktisch, wenn die Konfiguration der Continuous Integration nicht im UI-Interface von Jenkins oder TeamCity usw. gespeichert wird, sondern in Form von Textdateien direkt im Projekt-Repository. Das ermöglicht Versionskontrolle. Es wird nicht schwierig sein, zurückzurollen oder den Code in einem anderen Branch zu erstellen.
Skripte können im Projekt gespeichert werden. Was ist aber mit der Umgebung?
Tipp Nr. 3: Docker kann bei der Umgebung helfen.
Es wird den Android-Entwicklern sicherlich helfen, iOS leider noch nicht.
Das ist ein Beispiel für eine einfache Docker-Datei, die jdk und android-sdk enthält:
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
# Android SDK herunterladen
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
# Android Build Tool und Bibliotheken installieren
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
Ich habe diese Docker-Datei geschrieben (ich sage es im Vertrauen, man kann sie auch einfach von GitHub herunterladen) und nachdem Sie das Image erstellt haben, erhalten Sie eine virtuelle Maschine, auf der Sie die Anwendung kompilieren und Junit-Tests ausführen können.
Die zwei Hauptargumente, warum das sinnvoll ist: Skalierbarkeit und Wiederholbarkeit. Mit Docker kann man schnell ein Dutzend Build-Agenten hochziehen, die genau dieselbe Umgebung haben wie zuvor. Das erleichtert das Leben der CI-Ingenieure erheblich. Android-sdk in Docker zu packen, ist ganz einfach; mit Emulatoren ist es etwas komplizierter: Man muss sich ein wenig anstrengen (oder wieder von GitHub ein fertiges herunterladen).
Tipp Nr. 4: Vergessen Sie nicht, dass Prüfungen nicht der Prüfungen wegen durchgeführt werden, sondern für die Menschen.
Entwicklern ist schnelle und vor allem verständliche Rückmeldung sehr wichtig: Was ist kaputt, welcher Test ist fehlgeschlagen, wo kann man das Build-Log einsehen?
Tipp Nr. 5: Seien Sie pragmatisch in der Weiterentwicklung der Continuous Integration.
Verstehen Sie klar, welche Arten von Fehlern Sie verhindern möchten, wie viele Ressourcen, Zeit und Rechenzeit Sie bereit sind zu investieren. Zu lange Prüfungen können beispielsweise auf die Nacht verschoben werden. Und die, die nicht so wichtige Fehler finden, können ganz weggelassen werden.
Tipp Nr. 6: Nutzen Sie fertige Werkzeuge.
Es gibt jetzt viele Unternehmen, die Cloud-CI anbieten.

Für kleine Teams ist das eine gute Lösung. Man muss nichts unterstützen, einfach ein wenig Geld zahlen, die eigene Anwendung zusammenstellen und sogar Instrumentierungstests durchführen.
Tipp Nr. 7: In großen Teams sind Inhouse-Lösungen wirtschaftlicher.
Aber früher oder später, mit dem Wachstum des Teams werden Inhouse-Lösungen wirtschaftlicher. Bei diesen Lösungen gibt es einen Punkt. In der Wirtschaft gilt das Gesetz des abnehmenden Ertrags: In jedem Projekt fällt es immer schwerer, jede nächste Verbesserung zu erzielen, was immer mehr Investitionen erfordert.
Die Wirtschaft beschreibt unser ganzes Leben, einschließlich der Continuous Integration. Ich habe ein Diagramm über den Arbeitsaufwand für jede Phase der Entwicklung unserer Continuous Integration erstellt.

Es ist klar, dass jede Verbesserung immer schwieriger zu erreichen ist. Wenn man sich dieses Diagramm ansieht, wird deutlich, dass die Entwicklung der Continuous Integration mit dem Wachstum der Teamgröße abgestimmt werden muss. Für ein Team von zwei Personen ist es eine schlechte Idee, 50 Tage mit der Entwicklung einer internen Emulatorfarm zu verbringen. Aber für ein großes Team ist es auch keine gute Idee, sich überhaupt nicht mit Continuous Integration zu beschäftigen, denn Probleme mit der Integration, die Behebung der Kommunikation usw. werden noch mehr Zeit in Anspruch nehmen.
Wir haben damit begonnen, dass Automatisierung notwendig ist, weil Menschen teuer sind, Fehler machen und faul sind. Aber auch Menschen automatisieren. Deshalb betreffen all diese Probleme auch die Automatisierung.
- Automatisierung ist teuer. Denken Sie an das Diagramm über den Arbeitsaufwand.
- Bei der Automatisierung machen die Menschen Fehler.
- Manchmal ist es sehr lästig zu automatisieren, weil alles ohnehin schon funktioniert. Warum sollte man etwas verbessern, warum all diese Continuous Integration?
Aber ich habe Statistiken: Bei 20 % der Builds werden Fehler entdeckt. Und das geschieht nicht, weil unsere Entwickler schlechten Code schreiben. Es liegt daran, dass die Entwickler überzeugt sind, dass, wenn sie einen Fehler machen, dieser nicht in den develop gelangt, da ihn die automatisierten Tests auffangen. Dementsprechend können die Entwickler mehr Zeit mit dem Schreiben von Code und interessanten Sachen verbringen, anstatt lokal etwas durchzuführen und zu überprüfen.
Kümmern Sie sich um Continuous Integration. Aber in Maßen.
Übrigens handelt Nikolai Nesterov nicht nur selbst von großartigen Vorträgen, sondern ist auch im Programmkomitee und hilft anderen, aufschlussreiche Beiträge für Sie vorzubereiten. Die Vollständigkeit und Nützlichkeit des Programms der nächsten Konferenz kann an den Themen im . Für weitere Details kommen Sie am 22.-23. April in den Inforaum.
Quelle: habr.com
