Die Evolution von CI im Mobile-Entwicklungsteam

Heutzutage werden die meisten Softwareprodukte in Teams entwickelt. Die Erfolgskriterien für die Teamentwicklung lassen sich in einem einfachen Schema darstellen.

Die Evolution von CI im Mobile-Entwicklungsteam

Nachdem Sie den Code geschrieben haben, sollten Sie sicherstellen, dass er:

  1. Funktioniert.
  2. 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 einfach zu überprüfen und nicht vom gewinnbringenden Kurs abzubiegen, wurde Continuous Integration entwickelt.

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 häufig viele Überprüfungen erforderlich sind, sollten Sie über Automatisierung nachdenken. Man kann alles manuell überprüfen, aber das sollte man nicht, und hier ist der Grund.

  • Menschen sind teuer. Die Arbeitsstunde eines Programmierers kostet mehr als die eines Servers.
  • Menschen machen Fehler. Daher können Situationen entstehen, in denen Tests nicht im richtigen Branch durchgeführt oder der falsche Commit für die Tester zusammengestellt wurde.
  • Menschen sind faul. Gelegentlich, wenn ich eine Aufgabe abgeschlossen habe, kommt mir der Gedanke: „Warum soll ich das überprüfen? Ich habe zwei Zeilen geschrieben – das funktioniert bestimmt!“ Ich denke, einige von Ihnen haben auch manchmal solche Gedanken. Aber man sollte immer überprüfen.

Wie Continuous Integration im Entwicklungsteam von Avito implementiert und weiterentwickelt wurde, wie man von 0 auf 450 Builds pro Tag kam und dass die Build-Maschinen 200 Stunden pro Tag arbeiten, erzählt Nikolai Nesterov (nnesterov) – ein Teil aller evolutionären Veränderungen der CI/CD-Android-Anwendung.

Die Erzählung basiert auf dem Beispiel des Android-Teams, aber die meisten Ansätze sind auch für iOS anwendbar.

Video abspielen

Früher arbeitete in der Android-Abteilung von Avito nur eine Person. Er benötigte per Definition nichts von Continuous Integration: Mit niemandem musste er sich integrieren.

Aber die App 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.

Die Evolution von CI im Mobile-Entwicklungsteam

Das Git-Flow-Konzept ist bekannt: In einem Projekt gibt es einen gemeinsamen Branch namens develop, und für jede neue Funktion erstellen die Entwickler einen separaten Branch, commiten ihren Code, pushen ihn und eröffnen, wenn sie ihren Code in den Branch develop integrieren möchten, einen Pull Request. Um Wissen auszutauschen und Ansätze zu diskutieren, haben wir Code Reviews eingeführt, bei denen Kollegen den Code des anderen überprüfen und bestätigen müssen.

Überprüfungen

Code durch das Prisma der eigenen Augen zu betrachten, ist großartig, reicht aber nicht aus. Deshalb werden automatisierte Prüfungen eingeführt.

  • Zuerst überprüfen wir den ARC-Bau.
  • Viele JUnit-Tests.
  • Wir berechnen die Codeabdeckung, da wir die Tests ausführen.

Um zu verstehen, wie diese Prüfungen ausgeführt werden sollten, schauen wir uns den Entwicklungsprozess bei Avito an.

Schematisch kann man es so darstellen:

  • Der Entwickler schreibt Code auf seinem Laptop. Man kann die Integrationsprüfungen direkt hier durchführen - entweder mit einem Commit-Hook oder indem man die Prüfungen im Hintergrund ausführt.
  • Nachdem der Entwickler den Code gepusht hat, eröffnet er einen Pull Request. Damit sein Code in den Branch develop gelangt, muss er das Code Review bestehen 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 zusammengeführt werden.
  • Nachdem der Pull-Request gemerged wurde und der Code in develop ist, kann man sich einen geeigneten Zeitpunkt auswählen: zum Beispiel nachts, wenn die Server frei sind, und so viele Tests durchzuführen, wie man möchte.

Tests auf seinem Laptop auszuführen, gefiel niemandem. Wenn der Entwickler die Funktion abgeschlossen hat, möchte er sie schnell pushen und den Pull-Request eröffnen. Wenn in diesem Moment lange Tests ausgeführt werden, ist das nicht nur unpraktisch, sondern verlangsamt auch die Entwicklung: während der Laptop etwas testet, kann man darauf nicht gut arbeiten.

Das Testen in der Nacht gefiel uns sehr, denn es gibt viel Zeit und Server, man kann sich richtig austoben. Leider hat der Entwickler jedoch viel weniger Motivation, die Fehler zu beheben, die die CI gefunden hat, sobald der Code der Funktion in develop ist. Ich ertappe mich gelegentlich dabei, dass ich beim Blick auf den morgendlichen Bericht mit all den gefundenen Fehlern denke, dass ich sie irgendwann später beheben werde, weil jetzt in Jira eine tolle neue Aufgabe wartet, die ich unbedingt anpacken möchte.

Wenn Tests den Pull-Request blockieren, gibt es genug Motivation, denn solange die Builds nicht grün sind, kommt der Code nicht in develop, und das bedeutet, die Aufgabe wird nicht abgeschlossen.

Wir haben letztendlich folgende Strategie gewählt: Nachts führen wir die maximal möglichen Tests durch, und die kritischsten, vor allem die schnellen, starten wir mit dem Pull Request. Aber dabei bleibt es nicht – parallel optimieren wir die Geschwindigkeit der Tests, um sie von der Nachtverarbeitung auf Pull Request-Tests umzustellen.

Zu diesem Zeitpunkt liefen alle unsere Builds schnell genug, also haben wir einfach die ARC-Builds, Junit-Tests und die Berechnung der Code Coverage als Blocker für den Pull Request aktiviert. Wir haben das aktiviert, überlegt – und uns dann von der Code Coverage verabschiedet, weil wir dachten, dass wir sie nicht benötigen.

Für die gesamte Einrichtung der grundlegenden CI benötigten wir zwei Tage (die zeitliche Schätzung ist hier approximativ und dient zur Groborientierung).

Danach begannen wir weiter zu überlegen – überprüfen wir überhaupt richtig? Starten wir die Builds richtig für den Pull Request?

Wir haben den Build beim letzten Commit des Branches gestartet, von dem der Pull Request geö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 sollte der Zustand des Branches develop überprüft werden, nachdem die Funktion integriert wurde.

Die Evolution von CI im Mobile-Entwicklungsteam

Dafür haben wir ein einfaches Bash-Skript geschrieben. premerge.sh:

#!/usr/bin/env bash

set -e

git fetch origin develop

git merge origin/develop

Hier werden einfach alle neuesten Änderungen aus develop abgerufen und in den aktuellen Branch integriert. Wir haben das Skript premerge.sh als ersten Schritt aller Builds hinzugefügt und beginnen, genau das zu überprüfen, was wir wollen, nämlich Integration..

Die Lokalisierung der Probleme, die Suche nach einer Lösung und das Schreiben dieses Skripts haben drei Tage in Anspruch genommen.

Die Anwendung entwickelte sich weiter, es kamen immer mehr Aufgaben hinzu, das Team wuchs, und manchmal ließ uns premerge.sh im Stich. In develop drangen konfliktbehaftete Änderungen ein, die den Build zerstörten.

Ein Beispiel dafür, wie das passiert:

Die Evolution von CI im Mobile-Entwicklungsteam

Zwei Entwickler beginnen gleichzeitig, an den Funktionen A und B zu arbeiten. Der Entwickler der Funktion A entdeckt eine ungenutzte Funktion im Projekt, answer() und entfernt sie, wie ein guter Pfadfinder. Währenddessen fügt der Entwickler der Funktion B in seinem Branch einen neuen Aufruf dieser Funktion hinzu.

Die Entwickler beenden ihre Arbeit und eröffnen gleichzeitig einen Pull-Request. Die Builds werden gestartet, premerge.sh überprüft beide Pull-Requests im Hinblick auf den aktuellen Zustand von develop – alle Überprüfungen sind grün. Danach wird der Pull-Request der Funktion A gemerged, der Pull-Request der Funktion B wird gemerged… Boom! Develop bricht zusammen, weil im Code von develop ein Aufruf einer nicht existierenden Funktion vorhanden ist.

Die Evolution von CI im Mobile-Entwicklungsteam

Wenn sich das Develop nicht zusammenfügt, ist das eine lokale Katastrophe. Das gesamte Team kann nichts zusammenstellen und zur Prüfung übergeben.

Es hat sich ergeben, dass ich mich am häufigsten mit Infrastrukturaufgaben beschäftigt habe: Analytik, Netzwerk, Datenbanken. Das bedeutet, dass ich die Funktionen und Klassen geschrieben habe, die andere Entwickler nutzen. Aufgrund dessen bin ich sehr oft in solche Situationen geraten. Sogar eine Zeit lang hatte ich ein solches Bild.

Die Evolution von CI im Mobile-Entwicklungsteam

Da uns das nicht gefiel, haben wir daran gearbeitet, wie wir das verhindern können.

Wie man das Develop nicht bricht

Erste Möglichkeit: alle Pull Requests bei der Aktualisierung von Develop neu zu kompilieren. Wenn in unserem Beispiel der Pull Request mit Feature A zuerst in Develop gelangt, wird der Pull Request für Feature B neu kompiliert und die Überprüfungen schlagen aufgrund eines Kompilierungsfehlers fehl.

Um zu verstehen, wie viel Zeit dies in Anspruch nehmen wird, betrachten wir ein Beispiel mit zwei PRs. Wir öffnen zwei PRs: zwei Builds, zwei Überprüfungsdurchläufe. Nachdem der erste PR in Develop eingefügt wurde, muss der zweite neu kompiliert werden. Insgesamt benötigt man für zwei PRs drei Überprüfungsdurchläufe: 2 + 1 = 3.

Im Grunde ist es in Ordnung. Wir haben jedoch die Statistiken überprüft, und in unserem Team war es üblich, 10 offene PRs zu haben. Die Anzahl der Überprüfungen ist dann die Summe der Reihe: 10 + 9 + ... + 1 = 55. Das bedeutet, um 10 PRs anzunehmen, müssen wir 55 Mal neu kompilieren. Und das in einer idealen Situation, in der alle Überprüfungen beim ersten Mal bestehen und niemand einen zusätzlichen Pull Request öffnet, während die erste Gruppe bearbeitet wird.

Stellen Sie sich vor, Sie sind ein Entwickler, der es eilig hat, als Erster auf die Schaltfläche „merge“ zu klicken, denn wenn das der Nachbar macht, müssen Sie warten, bis alle Builds erneut durchlaufen werden... Nein, das kann nicht so weitergehen; das würde die Entwicklung ernsthaft verlangsamen.

Eine zweite mögliche Methode: Pull Requests nach der Code-Überprüfung zusammenzustellen. Das bedeutet, Sie öffnen einen Pull Request, sammeln die erforderliche Anzahl an Genehmigungen von Kollegen, beheben die notwendigen Probleme und starten dann die Builds. Wenn diese erfolgreich sind, wird der Pull Request mit dem develop-Zweig zusammengeführt. In diesem Fall sind keine zusätzlichen Neustarts erforderlich, aber das Feedback wird erheblich langsamer. Als Entwickler möchte ich beim Öffnen eines Pull Requests sofort sehen, ob er kompiliert. Wenn ein Test fehlschlägt, muss er schnell behoben werden. Bei verzögerten Builds wird das Feedback langsamer, was die gesamte Entwicklung beeinträchtigt. Das war auch für uns nicht akzeptabel.

Am Ende blieb nur die dritte Option — die Sache selbst in die Hand zu nehmen. Unser ganzer Code, all unsere Quellcodes werden im Repository auf dem Bitbucket-Server gespeichert. Daher mussten wir ein Plugin für Bitbucket entwickeln.

Die Evolution von CI im Mobile-Entwicklungsteam

Dieses Plugin überschreibt den Mechanismus zum Mergen von Pull Requests. Der Ablauf bleibt standardmäßig: Ein PR wird geöffnet, alle Builds werden angestoßen und das Code-Review erfolgt. Aber nachdem das Code-Review bestanden ist und der Entwickler auf „Merge“ klicken möchte, überprüft das Plugin, in welchem Zustand sich der Develop-Branch beim Start der Überprüfungen befand. Sollte der Develop-Branch sich nach den Builds aktualisiert haben, erlaubt das Plugin nicht, dass dieser Pull Request in den Hauptbranch gemergt wird. Es startet stattdessen die Builds mit dem aktuellen Zustand des Develop-Branches neu.

Die Evolution von CI im Mobile-Entwicklungsteam

In unserem Beispiel mit den konfliktreichen Änderungen werden solche Builds aufgrund eines Kompilierungsfehlers fehlschlagen. Daher muss der Entwickler von Feature B den Code anpassen, die Überprüfungen erneut starten, woraufhin das Plugin den Pull Request automatisch anwenden wird.

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 bemerkenswert, dass dieses Plugin einen Nachteil hat: Es startet den Build nur einmal neu. Das bedeutet, dass ein kleines Zeitfenster bleibt, in dem konfliktierende Änderungen in die Entwicklung 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 eingegangen. In zwei Jahren ist das nur einmal passiert, also war es wohl nicht umsonst.

Für die Erstellung der ersten Version des Plugins für Bitbucket haben wir zwei Wochen gebraucht.

Neue Überprüfungen

In der Zwischenzeit wuchs unser Team weiter. Es wurden neue Überprüfungen hinzugefügt.

Wir dachten: Warum Fehler beheben, wenn wir sie verhindern können? Deshalb haben wir statische Code-Analyseimplementiert. Wir begannen mit lint, das zum Android SDK gehört. Aber zu dieser Zeit konnte es überhaupt nicht mit Kotlin-Code umgehen, und bereits 75 % unserer Anwendung waren in Kotlin geschrieben. Daher wurden zu lint die integrierten Überprüfungen von Android Studio hinzugefügt.

Dafür mussten wir uns stark anstrengen: wir haben Android Studio in Docker verpackt und auf CI mit einem virtuellen Monitor gestartet, damit es denkt, dass es auf einem echten Laptop läuft. Aber es hat funktioniert.

Gleichzeitig begannen wir, viele Instrumentationstests zu schreiben und haben implementiert. Screenshot-Tests. Dabei wird ein Referenz-Screenshot für eine bestimmte kleine Ansicht generiert, und der Test besteht darin, einen Screenshot von der Ansicht aufzunehmen und pixelgenau mit dem Referenzbild zu vergleichen. Wenn es Unterschiede gibt, stimmt das Layout oder die Styles irgendwo nicht.

Instrumentation-Tests und Screenshot-Tests müssen jedoch auf Geräten durchgeführt werden: auf Emulatoren oder echten Geräten. Angesichts der Vielzahl an Tests, die häufig durchgeführt werden, ist eine gesamte Farm erforderlich. Eine eigene Farm aufzubauen ist jedoch sehr aufwändig, weshalb wir eine fertige Lösung gefunden haben – Firebase Test Lab.

Firebase Test Lab

wurde ausgewählt, weil Firebase ein Produkt von Google ist und daher zuverlässig sein sollte und nicht so schnell aussterben wird. Die Preise sind angemessen: 5 $ pro Stunde für die Nutzung eines physischen Geräts, 1 $ pro Stunde für einen Emulator.

Die Implementierung von Firebase Test Lab in unseren CI dauerte etwa drei Wochen.

Das Team wuchs weiter, und leider begann Firebase, uns im Stich zu lassen. Zu diesem Zeitpunkt gab es keinen SLA. Manchmal mussten wir warten, bis genügend Geräte für die Tests verfügbar waren, anstatt dass sie sofort gestartet wurden, wie wir es wollten. Die Wartezeit in der Schlange betrug bis zu einer halben Stunde, was sehr lang ist. Die Instrumentierungstests wurden bei jedem PR durchgeführt, und die Verzögerungen verlangsamten die Entwicklung erheblich. Außerdem kam dann die Monatsrechnung mit einem hohen Betrag. Insgesamt wurde beschlossen, auf Firebase zu verzichten und eine eigene Lösung zu entwickeln, da das Team genug gewachsen war.

Docker + Python + bash

Wir haben Docker verwendet, Emulatoren hineinverpackt und ein einfaches Programm in Python geschrieben, das zur richtigen Zeit die benötigte Anzahl an Emulatoren in der benötigten Version startet und sie bei Bedarf stoppt. Und natürlich ein paar Bash-Skripte - wo wären wir ohne sie?

Die Erstellung einer eigenen Testumgebung dauerte fünf Wochen.

Infolgedessen gab es für jeden Pull Request eine umfangreiche, merge-blockierende Liste von Prüfungen:

  • ARK-Bau;
  • Junit-Tests;
  • Lint;
  • Android Studio-Prüfungen;
  • Instrumentierungstests;
  • Screenshot-Tests.

Das hat viele mögliche Ausfälle verhindert. Technisch gesehen funktionierte alles, aber die Entwickler beklagten sich, dass sie zu lange auf die Ergebnisse warten mussten.

Zu lange – wie lange ist das? Wir haben die Daten aus Bitbucket und TeamCity in das Analysetool exportiert und festgestellt, dass die durchschnittliche Wartezeit 45 Minuten beträgt. Das bedeutet, dass ein Entwickler im Durchschnitt 45 Minuten auf die Ergebnisse der Builds wartet, wenn er einen Pull-Request öffnet. Meiner Meinung nach ist das viel zu lang, so kann man nicht arbeiten.

Natürlich haben wir beschlossen, unsere Builds zu beschleunigen.

Wir werden schneller

Als wir sahen, dass die Builds häufig in der Warteschlange standen, haben wir als Erstes zusätzlich Hardware gekauft – eine extensive Entwicklung ist das Einfachste. Die Builds standen nicht mehr in der Warteschlange, aber die Wartezeit wurde nur geringfügig verkürzt, weil einige Tests selbst sehr lange dauerten.

Wir entfernen die zu langen Prüfungen

Unsere Continuous Integration konnte solche Arten von Fehlern und Problemen erkennen.

  • Konnte nicht gebaut werden. CI kann einen Kompilierungsfehler erfassen, wenn aufgrund von konfliktreichen Änderungen etwas nicht gebaut werden kann. Wie bereits erwähnt, kann dann niemand etwas zusammenbauen, die Entwicklung steht still, und alle sind nervös.
  • Fehler im Verhalten. Zum Beispiel, wenn die Anwendung erstellt wird, aber beim Drücken des Knopfes abstürzt, oder der Knopf überhaupt nicht gedrückt werden kann. Das ist schlecht, da ein solcher Fehler bis zum Benutzer gelangen kann.
  • Fehler im Layout. Zum Beispiel wird der Knopf gedrückt, ist aber um 10 Pixel nach links verrutscht.
  • Zunahme der technischen Schulden.

Nach Durchsicht dieser Liste haben wir erkannt, dass nur die ersten beiden Punkte kritisch sind. Solche Probleme möchten wir zuerst identifizieren. Layoutfehler 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, weshalb wir beschlossen haben, dies nicht im Pull Request zu überprüfen.

Basierend auf dieser Klassifizierung haben wir die gesamte Liste der Überprüfungen durchgesehen. Wir haben Lint gestrichen und dessen Ausführung auf die Nacht verschoben: einfach um einen Bericht zu erstellen, wie viele Probleme im Projekt vorhanden sind. Mit den technischen Schulden haben wir uns darauf geeinigt, separat zu arbeiten, und haben ganz auf die Überprüfungen von Android Studio verzichtet.. Android Studio in Docker für Inspektionen klingt interessant, bringt jedoch viele Herausforderungen bei der Wartung mit sich. Jedes Update der Android Studio-Versionen ist ein Kampf gegen unerklärliche Bugs. Auch die Unterstützung von Screenshots-Tests war kompliziert, da die Bibliothek nicht sehr stabil arbeitete und es zu Fehlalarmen kam. Screenshots-Tests wurden aus der Prüfliste entfernt..

Am Ende haben wir folgende Tests übrig:

  • ARK-Bau;
  • Junit-Tests;
  • Instrumentationstests.

Gradle Remote-Cache

Ohne umfangreiche Prüfungen lief alles besser. Aber es gibt immer Raum für Verbesserungen!

Unsere Anwendung war bereits in etwa 150 Gradle-Module unterteilt. In solchen Fällen funktioniert der Gradle Remote-Cache normalerweise gut, also haben wir beschlossen, ihn auszuprobieren.

Der Gradle Remote-Cache ist ein Service, der Build-Artefakte für einzelne Aufgaben in separaten Modulen cachen kann. Gradle fragt, anstatt den Code tatsächlich zu kompilieren, über HTTP beim Remote-Cache an, ob jemand diese Aufgabe bereits ausgeführt hat. Falls ja, wird einfach das Ergebnis heruntergeladen.

Den Gradle Remote-Cache zu starten, ist einfach, da Gradle ein Docker-Image bereitstellt. Wir konnten dies in drei Stunden umsetzen.

Es war nur notwendig, Docker zu starten und eine Zeile im Projekt zu schreiben. Obwohl man es schnell starten kann, wird es eine Weile dauern, bis alles gut funktioniert.

Unten ist das Diagramm der Cache-Misses.

Die Evolution von CI im Mobile-Entwicklungsteam

Zu Beginn lag die Fehlerquote bei etwa 65 %. Nach drei Wochen konnten wir diesen Wert auf 20 % senken. Es stellte sich heraus, dass die Aufgaben, die die Android-Anwendung sammelt, seltsame transitive Abhängigkeiten haben, wodurch Gradle am Cache vorbeikam.

Durch die Aktivierung des Caches haben wir den Build-Prozess erheblich beschleunigt. Aber neben dem Build werden auch Instrumentation-Tests durchgeführt, und diese dauern lange. Möglicherweise müssen nicht alle Tests bei jedem Pull-Request ausgeführt werden. Um das herauszufinden, verwenden wir eine Impact-Analyse.

Impact-Analyse

Bei jedem Pull-Request erstellen wir einen git diff und ermitteln die geänderten Gradle-Module.

Die Evolution von CI im Mobile-Entwicklungsteam

Es macht Sinn, nur die Instrumentation-Tests auszuführen, die die geänderten Module und alle Module, die von ihnen abhängen, überprüfen. Es hat keinen Sinn, Tests für angrenzende Module durchzuführen: der Code hat sich dort nicht geändert, und es kann nichts kaputtgehen.

Mit Instrumentierungstests ist es nicht ganz einfach, da sie sich im obersten Modul der Anwendung befinden müssen. Wir haben eine Heuristik zur Bytecode-Analyse angewendet, um zu verstehen, zu welchem Modul jeder Test gehört.

Die Modernisierung der Instrumentierungstests, sodass sie nur die aktiven Module überprüfen, hat etwa acht Wochen gedauert.

Die Maßnahmen zur Beschleunigung der Prüfungen haben erfolgreich funktioniert. Statt 45 Minuten benötigen wir jetzt etwa 15. Es ist bereits normal, ein Viertelstunde auf einen Build zu warten.

Nun beschweren sich die Entwickler darüber, dass sie nicht verstehen, welche Builds gestartet werden, wo sie die Logs ansehen können, warum ein Build rot ist, welcher Test fehlgeschlagen ist usw.

Die Evolution von CI im Mobile-Entwicklungsteam

Probleme mit dem Feedback verlangsamen die Entwicklung, daher haben wir versucht, so viel klare und detaillierte Informationen wie möglich über jede PR und jeden Build bereitzustellen. Wir haben mit Kommentaren in Bitbucket zu PRs begonnen, in denen wir angegeben haben, welcher Build fehlgeschlagen ist und warum, und haben gezielte Nachrichten in Slack geschrieben. Letztendlich haben wir ein Dashboard für die PR-Seite erstellt, mit einer Liste aller Builds, die gerade laufen, und ihrem Status: in der Warteschlange, wird gestartet, fehlgeschlagen oder abgeschlossen. Man kann auf den Build klicken und zu seinen Logs gelangen.

Die Evolution von CI im Mobile-Entwicklungsteam

Es hat sechs Wochen für detailliertes Feedback gedauert.

Pläne

Kommen wir zur neuesten Entwicklung. Nachdem wir die Feedback-Frage geklärt hatten, haben wir beschlossen, unser eigenes Emulatoren-Farm aufzubauen. Wenn es viele Tests und Emulatoren gibt, ist deren Verwaltung schwierig. Schließlich sind alle unsere Emulatoren in ein k8s-Cluster mit flexibler Ressourcenzuweisung umgezogen.

Darüber hinaus gibt es auch andere Pläne.

  • Lint zurückbringen (und andere statische Analysen). Wir arbeiten bereits in diese Richtung.
  • Führen Sie alle End-to-End-Tests auf allen SDK-Versionen aus.

So haben wir die Entwicklung der Continuous Integration bei Avito verfolgt. Jetzt möchte ich einige Ratschläge aus der Perspektive eines Erfahreneren geben.

Tipps

Wenn ich nur einen einzigen Rat geben könnte, dann wäre das dieser:

Bitte seien Sie vorsichtig mit Shell-Skripten!

Bash ist ein sehr flexibles und leistungsstarkes Werkzeug, und es ist sehr einfach und schnell, Skripte darauf zu schreiben. Aber man kann in eine Falle geraten, und wir sind leider hineingetappt.

Es begann alles mit einfachen Skripten, die auf unseren Build-Maschinen ausgeführt wurden:

#!/usr/bin/env bash
./gradlew assembleDebug

Doch wie bekannt ist, entwickelt sich alles mit der Zeit und wird komplexer – lassen Sie uns ein Skript aus einem anderen starten und dabei einige Parameter übergeben – letztendlich mussten wir eine Funktion schreiben, die bestimmt, auf welcher Tiefe wir uns gerade im Bash befinden, um die benötigten Anführungszeichen zu setzen, damit alles funktioniert.

Die Evolution von CI im Mobile-Entwicklungsteam

Könnten Sie sich den Aufwand für die Entwicklung solcher Skripte vorstellen? Ich empfehle, nicht in diese Falle zu tappen.

Was kann man stattdessen verwenden?

  • Jede Skriptsprache. Es ist einfacher, in Python oder Kotlin Script zu programmieren, da es sich um Programmierung und nicht um Skripte handelt.
  • Oder die gesamte Logik der Builds in Form von Custom Gradle Tasks für Ihr Projekt zu beschreiben.

Wir haben uns entschieden, die zweite Variante zu wählen, und entfernen jetzt schrittweise alle Bash-Skripte und schreiben viele benutzerdefinierte Gradle-Tasks.

Tipp №2: Halten Sie die Infrastruktur in Code.

Es ist praktisch, wenn die Konfiguration der Continuous Integration nicht im UI-Interface von Jenkins oder TeamCity usw. gespeichert ist, sondern als Textdateien direkt im Repository des Projekts. Das ermöglicht Versionskontrolle. Es wird nicht schwer sein, zurückzurollen oder Code auf einem anderen Branch zu erstellen.

Skripte können im Projekt gespeichert werden. Aber was ist mit der Umgebung?

Tipp №3: Docker kann bei der Umgebung helfen.

Für Android-Entwickler ist er auf jeden Fall hilfreich, für iOS leider noch nicht.

Dies 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

Wenn Sie diese Docker-Datei geschrieben haben (ein Geheimtipp, Sie könnten auch eine fertige von GitHub herunterladen) und das Image gebaut haben, erhalten Sie eine virtuelle Maschine, auf der Sie die Anwendung erstellen und JUnit-Tests ausführen können.

Zwei Hauptargumente, warum das sinnvoll ist: Skalierbarkeit und Wiederholbarkeit. Mit Docker lassen sich schnell mehrere Build-Agenten einrichten, die genau dieselbe Umgebung wie zuvor besitzen. Das erleichtert den CI-Ingenieuren die Arbeit erheblich. Android-SDK in Docker zu integrieren, ist ganz einfach, jedoch wird es mit Emulatoren etwas komplizierter: Hier muss man sich etwas anstrengen (oder erneut von GitHub eine fertige Lösung herunterladen).

Tipp Nr. 4: Vergessen Sie nicht, dass Überprüfungen nicht nur um ihrer selbst willen durchgeführt werden, sondern für die Menschen.

Für Entwickler ist schnelles und vor allem verständliches Feedback von großer Bedeutung: Was ist kaputtgegangen, welcher Test ist fehlgeschlagen, wo kann ich das Build-Log einsehen.

Tipp Nr. 5: Seien Sie pragmatisch bei der Weiterentwicklung der Continuous Integration.

Haben Sie ein klares Verständnis davon, welche Fehlertypen Sie vermeiden möchten, wie viele Ressourcen und Zeit Sie bereit sind zu investieren, einschließlich der Maschinenlaufzeit. Zu lange Überprüfungen können beispielsweise auf die Nacht verschoben werden. Und von jenen, die nicht ganz so wichtige Fehler erkennen, können Sie sogar ganz Abstand nehmen.

Tipp Nr. 6: Nutzen Sie fertige Tools.

Derzeit gibt es viele Unternehmen, die Cloud-CI anbieten.

Die Evolution von CI im Mobile-Entwicklungsteam

Für kleine Teams ist das eine gute Option. Es ist keine Wartung erforderlich, man bezahlt einfach ein wenig Geld, stellt seine Anwendung zusammen und führt sogar Instrumentierungstests durch.

Tipp Nr. 7: In großen Teams sind interne Lösungen vorteilhafter.

Aber irgendwann, mit dem Wachstum des Teams, werden interne Lösungen kostengünstiger. Es gibt dabei jedoch einen Punkt. In der Wirtschaft gibt es das Gesetz des abnehmenden Ertrags: In jedem Projekt wird jede weitere Verbesserung immer schwieriger und erfordert immer mehr Investitionen.

Die Wirtschaft beschreibt unser ganzes Leben, einschließlich der kontinuierlichen Integration. Ich habe ein Diagramm der Arbeitsaufwände für jede Phase der Entwicklung unserer kontinuierlichen Integration erstellt.

Die Evolution von CI im Mobile-Entwicklungsteam

Es ist deutlich zu sehen, dass jede Verbesserung zunehmend schwieriger wird. Anhand dieses Diagramms wird ersichtlich, dass die Entwicklung der kontinuierlichen Integration im Einklang mit dem Wachstum der Teamgröße erfolgen muss. Für ein Team von zwei Personen ist es keine gute Idee, 50 Tage für die Entwicklung eines internen Emulator-Pools aufzuwenden. Aber für ein großes Team nicht an der kontinuierlichen Integration zu arbeiten, ist ebenfalls keine gute Idee, denn auf Integrationsprobleme, die Behebung von Kommunikationsproblemen usw. wird noch mehr Zeit verwendet werden.

Wir haben erkannt, dass Automatisierung notwendig ist, weil Menschen teuer sind, Fehler machen und faul sein können. Aber auch die Automatisierung wird von Menschen durchgeführt. Daher gelten die gleichen Probleme auch für die Automatisierung.

  • Automatisierung ist teuer. Denken Sie an den Aufwand.
  • Bei der Automatisierung machen Menschen Fehler.
  • Manchmal ist es sehr mühsam, zu automatisieren, da alles auch so funktioniert. Warum also etwas verbessern, warum die ganze Continuous Integration?

Ich habe jedoch Statistiken: In 20 % der Builds werden Fehler entdeckt. Das passiert nicht, weil unsere Entwickler schlechten Code schreiben. Es geschieht, weil die Entwickler glauben, dass, wenn sie einen Fehler machen, dieser nicht in die Entwicklung gelangen wird – er wird von automatisierten Tests erkannt. Folglich können die Entwickler mehr Zeit mit dem Schreiben von Code und interessanten Dingen verbringen, anstatt lokal etwas zu testen.

Betreiben Sie Continuous Integration. Aber mit Maß.

Übrigens macht Nikolai Nesterov nicht nur selbst großartige Präsentationen, sondern ist auch im Programmkomitee. AppsConf und hilft anderen, wertvolle Beiträge für Sie vorzubereiten. Die Vollständigkeit und Nützlichkeit des Programms der nächsten Konferenz kann anhand der Themen im Zeitplan. Kommen Sie am 22. und 23. April ins Infopreneurium für weitere Details.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster