{"id":31304,"date":"2019-10-31T21:40:34","date_gmt":"2019-10-31T18:40:34","guid":{"rendered":"https:\/\/prohoster.info\/blog\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\/"},"modified":"2019-10-31T21:40:34","modified_gmt":"2019-10-31T18:40:34","slug":"evolyutsiya-ci-v-komande-mobilnoj-razrabotki","status":"publish","type":"post","link":"https:\/\/prohoster.info\/de\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","title":{"rendered":"Evolution von CI im Team der mobilen Entwicklung","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Heute werden die meisten Softwareprodukte in Teams entwickelt. Die Bedingungen f\u00fcr den Erfolg der Teamarbeit k\u00f6nnen in einem einfachen Schema dargestellt werden.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/b283cfd7772d8f479be0754ebbe51b19.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nNachdem Sie den Code geschrieben haben, m\u00fcssen Sie sicherstellen, dass er:<\/p>\n<ol>\n<li>Funktioniert.<\/li>\n<li>Nichts kaputt macht, einschlie\u00dflich des Codes, den Ihre Kollegen geschrieben haben.<\/li>\n<\/ol>\n<p>\nWenn beide Bedingungen erf\u00fcllt sind, sind Sie auf dem Weg zum Erfolg. Um diese Bedingungen leicht zu \u00fcberpr\u00fcfen und nicht vom profitablen Weg abzuweichen, wurde Continuous Integration (CI) erfunden.<\/p>\n<p>CI ist ein Arbeitsprozess, bei dem Sie Ihren Code so oft wie m\u00f6glich in den gemeinsamen Code des Produkts integrieren. Und nicht nur integrieren, sondern auch st\u00e4ndig \u00fcberpr\u00fcfen, ob alles funktioniert. Da es viel und h\u00e4ufig zu \u00fcberpr\u00fcfen gibt, sollte man \u00fcber Automatisierung nachdenken. Man kann alles manuell \u00fcberpr\u00fcfen, aber das sollte man nicht, und hier ist warum.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<ul>\n<li><strong>Menschen sind kostbar<\/strong>. Die Arbeitszeit eines Programmierers ist teurer als die eines Servers.<\/li>\n<li><strong>Menschen machen Fehler<\/strong>. Daher k\u00f6nnen Situationen entstehen, in denen Tests nicht in dem richtigen Branch gestartet oder die falschen Commits f\u00fcr die Tester eingesammelt wurden.<\/li>\n<li><strong>Menschen sind faul<\/strong>. Gelegentlich, wenn ich eine Aufgabe abgeschlossen habe, denke ich mir: \"Was gibt es hier zu \u00fcberpr\u00fcfen? Ich habe zwei Zeilen geschrieben \u2013 das funktioniert doch sicher!\" Ich denke, einigen von Ihnen kommen solche Gedanken auch manchmal in den Sinn. Aber man muss immer \u00fcberpr\u00fcfen.<\/li>\n<\/ul>\n<p>\nWie Continuous Integration im mobilen Entwicklungsteam von Avito eingef\u00fchrt und weiterentwickelt wurde, wie man von 0 auf 450 Builds pro Tag kam und was die Build-Maschinen 200 Stunden am Tag sammeln, erz\u00e4hlt Nikolai Nesterov (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/users\/nnesterov\/\" class=\"user_link\">nnesterov<\/a><\/noindex>) \u2013 Teilnehmer aller evolution\u00e4ren \u00c4nderungen am CI\/CD-Android-Anwendungsprozess.<\/p>\n<p>Die Erz\u00e4hlung basiert auf dem Beispiel des Android-Teams, aber die meisten Ans\u00e4tze sind auch auf iOS anwendbar.<\/p>\n<p><center><div class=\"youtube-placeholder\" data-id=\"lz8MNATTUCU\" onclick=\"loadVideo(this)\">\r\n        <img decoding=\"async\" src=\"https:\/\/img.youtube.com\/vi\/lz8MNATTUCU\/hqdefault.jpg\" alt=\"Video abspielen\" loading=\"lazy\" width=\"480\" height=\"360\" style=\"width:100%;height:auto;\">\r\n        <div class=\"play-button\"><\/div>\r\n    <\/div><\/center><br \/>\nVor 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.<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/f433effc5ab5a59de3d6bd20a7a87057.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDas Konzept von Git Flow ist bekannt: Im Projekt gibt es einen gemeinsamen Branch namens develop, und f\u00fcr jedes neue Feature erstellen die Entwickler einen separaten Branch, committen in diesen, pushen und \u00f6ffnen, wenn sie ihren Code in den develop-Branch einf\u00fcgen m\u00f6chten, einen Pull Request. Um den Wissensaustausch und die Diskussion von Ans\u00e4tzen zu f\u00f6rdern, haben wir Code-Reviews eingef\u00fchrt, das hei\u00dft, die Kollegen m\u00fcssen den Code des anderen \u00fcberpr\u00fcfen und best\u00e4tigen.<\/p>\n<h2>\u00dcberpr\u00fcfungen<\/h2>\n<p>\nDen Code mit eigenen Augen zu betrachten, ist gro\u00dfartig, aber nicht ausreichend. Daher werden automatische Pr\u00fcfungen eingef\u00fchrt.<\/p>\n<ul>\n<li>Zuerst \u00fcberpr\u00fcfen wir <strong>den ARC-Build<\/strong>.<\/li>\n<li>Viele <strong>JUnit-Tests<\/strong>.<\/li>\n<li><strong>Wir betrachten die Codeabdeckung<\/strong>, schlie\u00dflich f\u00fchren wir die Tests aus.<\/li>\n<\/ul>\n<p>\nUm zu verstehen, wie diese \u00dcberpr\u00fcfungen durchgef\u00fchrt werden sollten, schauen wir uns den Entwicklungsprozess bei Avito an.<\/p>\n<p>Schematically l\u00e4sst sich das so darstellen:<\/p>\n<ul>\n<li>Der Entwickler schreibt Code auf seinem Laptop. Hier k\u00f6nnen Integrationstests direkt ausgef\u00fchrt werden \u2013 entweder per Commit-Hook oder einfach im Hintergrund.<\/li>\n<li>Nachdem der Entwickler den Code gepusht hat, \u00f6ffnet er einen Pull Request. Damit sein Code in den develop-Branch gelangt, muss er das Code-Review durchlaufen und die erforderliche Anzahl an Best\u00e4tigungen sammeln. Hier k\u00f6nnen Pr\u00fcfungen und Builds aktiviert werden: solange nicht alle Builds erfolgreich sind, kann der Pull Request nicht gemerged werden.<\/li>\n<li>Nachdem der Pull Request gemerged wurde und der Code in develop ist, kann ein passender Zeitpunkt gew\u00e4hlt werden: zum Beispiel nachts, wenn alle Server frei sind, und so viele Pr\u00fcfungen wie m\u00f6glich durchgef\u00fchrt werden.<\/li>\n<\/ul>\n<p>\nEs gefiel uns nicht, Pr\u00fcfungen auf dem eigenen Laptop durchzuf\u00fchren. Wenn ein Entwickler ein Feature abgeschlossen hat, m\u00f6chte er es so schnell wie m\u00f6glich pushen und einen Pull Request \u00f6ffnen. Wenn in diesem Moment einige langwierige \u00dcberpr\u00fcfungen gestartet werden, ist das nicht nur unangenehm, sondern verlangsamt auch die Entwicklung: W\u00e4hrend der Laptop etwas \u00fcberpr\u00fcft, kann man nicht normal arbeiten.<\/p>\n<p>Es gefiel uns sehr, die Pr\u00fcfungen nachts auszuf\u00fchren, da viel Zeit und Serverkapazit\u00e4t zur Verf\u00fcgung 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\u00e4ter beheben w\u00fcrde, weil jetzt in Jira eine coole neue Aufgabe liegt, die ich unbedingt anfangen m\u00f6chte.<\/p>\n<p>Wenn Pr\u00fcfungen 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.<\/p>\n<p>Letztendlich haben wir eine Strategie gew\u00e4hlt: Nachts f\u00fchren wir so viele Pr\u00fcfungen wie m\u00f6glich durch, und die kritischsten und am wichtigsten schnellen Pr\u00fcfungen starten wir bei Pull-Requests. Aber dabei bleibt es nicht \u2013 parallel optimieren wir die Geschwindigkeit der Pr\u00fcfungen, um sie vom Nachtmodus in Pr\u00fcfungen bei Pull-Requests zu \u00fcberf\u00fchren.<\/p>\n<p>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\u00fcr Pull-Requests aktiviert. Wir haben das aktiviert, dar\u00fcber nachgedacht \u2013 und uns von der Codeabdeckung verabschiedet, weil wir dachten, dass wir sie nicht brauchen.<\/p>\n<p><strong><em>F\u00fcr die gesamte Einrichtung der Basis-CI ben\u00f6tigten wir zwei Tage (hier und im Folgenden ist die Zeitabsch\u00e4tzung ungef\u00e4hren, dient zur Ma\u00dfstabserh\u00f6hung). <\/em><\/strong><\/p>\n<p>Nachdem wir dies abgeschlossen hatten, begannen wir weiter zu denken \u2013 \u00fcberpr\u00fcfen wir \u00fcberhaupt richtig? Starten wir die Builds bei Pull-Requests korrekt?<\/p>\n<p>Wir f\u00fchrten die Build-Tests mit dem letzten Commit des Branches durch, von dem der Pull-Request er\u00f6ffnet wurde. Aber die Pr\u00fcfungen dieses Commits k\u00f6nnen nur zeigen, dass der Code, den der Entwickler geschrieben hat, funktioniert. Sie beweisen jedoch nicht, dass er nichts kaputt gemacht hat. Tats\u00e4chlich sollten wir den Zustand des develop-Branches pr\u00fcfen, nachdem das Feature integriert wurde.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/c1a179b68b02c04031177f9bf51a81ed.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDaf\u00fcr schrieben wir ein einfaches Bash-Skript <strong>premerge.sh:<\/strong><\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n\nset -e\n\ngit fetch origin develop\n\ngit merge origin\/develop<\/code><\/pre>\n<p>\nHier werden einfach alle neuesten \u00c4nderungen aus develop abgeholt und in den aktuellen Branch gemerged. Wir haben das Skript premerge.sh als ersten Schritt aller Builds hinzugef\u00fcgt und begannen genau das zu \u00fcberpr\u00fcfen, was wir m\u00f6chten, also <strong>Integration<\/strong>.<\/p>\n<p><strong><em>F\u00fcr die Lokalisierung der Probleme, die Suche nach L\u00f6sungen und das Schreiben dieses Skripts ben\u00f6tigten wir drei Tage.<\/em><\/strong><\/p>\n<p>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 \u00c4nderungen ein, die den Build brachen.<\/p>\n<p>Ein Beispiel daf\u00fcr, wie das passiert:<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/6762d9ce4e431549455c49f2317a8f20.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZwei Entwickler beginnen gleichzeitig, an den Features A und B zu arbeiten. Der Entwickler des Features A entdeckt im Projekt eine nicht verwendete Funktion <code>answer()<\/code> und, wie ein guter Pfadfinder, l\u00f6scht er sie. Gleichzeitig f\u00fcgt der Entwickler des Features B in seinem Branch einen neuen Aufruf dieser Funktion hinzu.<\/p>\n<p>Die Entwickler beenden ihre Arbeit und \u00f6ffnen gleichzeitig einen Pull-Request. Die Builds werden gestartet, premerge.sh \u00fcberpr\u00fcft beide Pull-Requests bez\u00fcglich des aktuellen Zustands von develop \u2013 alle Pr\u00fcfungen sind gr\u00fcn. Danach wird der Pull-Request des Features A gemerged, der Pull-Request des Features B wird gemerged\u2026 Boom! Develop bricht zusammen, weil im Code von develop ein Aufruf einer nicht existierenden Funktion vorhanden ist.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/0261dcc9f7f2e5e018ab136081b4679b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nWenn develop nicht kompilierbar ist, ist das <strong>lokale Katastrophe<\/strong>. Das gesamte Team kann nichts sammeln und zur Pr\u00fcfung \u00fcbergeben.<\/p>\n<p>Es hat sich so ergeben, dass ich mich am h\u00e4ufigsten mit infrastrukturellen Aufgaben besch\u00e4ftigt habe: Analytik, Netzwerk, Datenbanken. Das hei\u00dft, ich habe genau die Funktionen und Klassen geschrieben, die von anderen Entwicklern verwendet werden. Daher bin ich sehr oft in \u00e4hnliche Situationen geraten. Eine Zeit lang hatte ich sogar ein solches Bild.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/0eb1aadd05b33ce995027ab52d5c1e53.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDa uns das nicht zufriedenstellte, begannen wir, Optionen zu erarbeiten, wie wir das verhindern k\u00f6nnen.<\/p>\n<h2>Wie man develop nicht kaputt macht<\/h2>\n<p>\nDie erste Option: <strong>alle Pull Requests bei der Aktualisierung von develop neu zu erstellen. <\/strong>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\u00fcfungen aufgrund eines Kompilierungsfehlers nicht bestehen.<\/p>\n<p>Um zu verstehen, wie viel Zeit das in Anspruch nehmen wird, betrachten wir ein Beispiel mit zwei PRs. Wir \u00f6ffnen zwei PRs: zwei Builds, zwei Pr\u00fcfungen. Nachdem der erste PR in develop eingef\u00fcgt wurde, muss der zweite neu erstellt werden. Insgesamt ben\u00f6tigt man f\u00fcr zwei PRs drei Pr\u00fcfungen: 2 + 1 = 3.<\/p>\n<p>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\u00fcfungen als Summe der Progression: 10 + 9 +\u2026 + 1 = 55. Das bedeutet, um 10 PRs anzunehmen, muss man 55 Mal neu erstellen. Und das in einer idealen Situation, in der alle Pr\u00fcfungen beim ersten Mal bestehen, und in der niemand einen zus\u00e4tzlichen Pull Request \u00f6ffnet, w\u00e4hrend wir diese zehn bearbeiten.<\/p>\n<p>Stellen Sie sich einen Entwickler vor, der es eilig hat, als erster auf die Schaltfl\u00e4che \u201eMerge\u201c zu dr\u00fccken, denn wenn das der Nachbar macht, muss man warten, bis alle Builds von neuem durchlaufen werden\u2026 Nein, das geht nicht, das w\u00fcrde die Entwicklung ernsthaft verlangsamen.<\/p>\n<p>Die zweite m\u00f6gliche M\u00f6glichkeit: <strong>Pull Requests nach dem Code-Review zu erstellen. <\/strong>Das hei\u00dft, Sie \u00f6ffnen einen Pull Request, sammeln die erforderliche Anzahl von Genehmigungen von Kollegen, beheben, was n\u00f6tig ist, und starten dann die Builds. Wenn sie erfolgreich sind, wird der Pull Request mit develop zusammengef\u00fchrt. In diesem Fall gibt es keine zus\u00e4tzlichen Neustarts, aber das Feedback verlangsamt sich erheblich. Ich m\u00f6chte als Entwickler, der einen Pull Request er\u00f6ffnet, sofort sehen, ob er erfolgreich kompiliert. Wenn ein Test fehlschl\u00e4gt, muss er schnell behoben werden. Bei einem verz\u00f6gerten Build verlangsamt sich das Feedback, was auch die gesamte Entwicklung verlangsamt. Das war uns auch nicht recht.<\/p>\n<p>Am Ende blieb nur die dritte Option \u2014 <strong>alles selbst zu machen<\/strong>. Unser gesamter Code, alle unsere Quellcodes werden in einem Repository auf dem Bitbucket-Server gespeichert. Daher mussten wir ein Plugin f\u00fcr Bitbucket entwickeln.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/5e4b1f770ea1a3449b616c3101640294.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDieses Plugin \u00fcberschreibt den Mechanismus zum Mergen von Pull Requests. Der Standardablauf beginnt damit, dass ein PR ge\u00f6ffnet wird, alle Builds gestartet werden und eine Code-\u00dcberpr\u00fcfung stattfindet. Sobald die Code-\u00dcberpr\u00fcfung abgeschlossen ist und der Entwickler auf \u201emerge\u201c klickt, \u00fcberpr\u00fcft das Plugin, auf welchen Zustand der develop-Branch die Tests durchgef\u00fchrt wurden. Wenn der develop-Branch nach den Builds aktualisiert wurde, erlaubt das Plugin nicht, dass dieser Pull Request in den Hauptbranch gemergt wird. Es wird einfach die Builds basierend auf dem aktuellen develop-Branch neu gestartet.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/0edee880e1f2d286a0c64033103ac136.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nIn unserem Beispiel mit konfliktierten \u00c4nderungen werden solche Builds aufgrund eines Kompilierungsfehlers nicht bestehen. Somit muss der Entwickler der Funktion B den Code anpassen, die \u00dcberpr\u00fcfungen erneut starten, dann wird das Plugin den Pull-Request automatisch anwenden.<\/p>\n<p>Vor der Einf\u00fchrung dieses Plugins hatten wir im Durchschnitt 2,7 Pr\u00fcfungen pro Pull-Request. Mit dem Plugin sind es nun 3,6 Pr\u00fcfungen. Damit sind wir zufrieden.<\/p>\n<p>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 \u00c4nderungen in develop gelangen k\u00f6nnen. Die Wahrscheinlichkeit daf\u00fcr 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.<\/p>\n<p><strong><em>F\u00fcr die Erstellung der ersten Version des Plugins f\u00fcr Bitbucket haben wir zwei Wochen ben\u00f6tigt. <\/em><\/strong><\/p>\n<h3>Neue \u00dcberpr\u00fcfungen<\/h3>\n<p>\nInzwischen wuchs unser Team weiter. Es wurden neue \u00dcberpr\u00fcfungen hinzugef\u00fcgt.<\/p>\n<p>Wir haben uns gefragt: Warum Fehler beheben, wenn man sie verhindern kann? Deshalb haben wir eingef\u00fchrt <strong>statische Codeanalyse<\/strong>. Wir haben mit lint begonnen, der im Android SDK enthalten ist. Aber zu der Zeit konnte er \u00fcberhaupt nicht mit Kotlin-Code arbeiten, w\u00e4hrend schon 75 % der Anwendung in Kotlin geschrieben waren. Daher wurden dem lint die eingebauten <strong>Android Studio-\u00dcberpr\u00fcfungen hinzugef\u00fcgt.<\/strong><\/p>\n<p>Daf\u00fcr 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\u00e4uft. Aber es hat funktioniert.<\/p>\n<p>Auch in dieser Zeit begannen wir, viele <strong>Instrumentierungstests<\/strong> zu schreiben und f\u00fchrten ein <strong>Screenshot-Testing ein.<\/strong>. Es handelt sich um die Generierung eines Referenz-Screenshot f\u00fcr 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.<\/p>\n<p>Aber Instrumentierungstests und Screenshot-Tests m\u00fcssen auf Ger\u00e4ten durchgef\u00fchrt werden: auf Emulatoren oder echten Ger\u00e4ten. Da es viele Tests gibt und diese h\u00e4ufig ausgef\u00fchrt werden, ist eine komplette Farm erforderlich. Eine eigene Farm einzurichten, ist jedoch sehr aufwendig, daher haben wir eine fertige L\u00f6sung gefunden \u2013 Firebase Test Lab.<\/p>\n<h3>Firebase Test Lab<\/h3>\n<p>\nEs wurde ausgew\u00e4hlt, weil Firebase ein Produkt von Google ist, was bedeutet, dass es zuverl\u00e4ssig sein sollte und kaum jemals eingestellt wird. Die Preise sind vern\u00fcnftig: 5 $ pro Stunde f\u00fcr die Arbeit mit einem echten Ger\u00e4t, 1 $ pro Stunde f\u00fcr die Arbeit mit einem Emulator.<\/p>\n<p><strong><em>Die Implementierung von Firebase Test Lab in unser CI hat etwa drei Wochen gedauert.<\/em><\/strong><\/p>\n<p>Aber das Team wuchs weiter, und Firebase hat uns leider angefangen im Stich zu lassen. Zu diesem Zeitpunkt gab es keinen SLA. Manchmal lie\u00df Firebase uns warten, bis gen\u00fcgend Ger\u00e4te f\u00fcr die Tests verf\u00fcgbar waren, anstatt diese sofort auszuf\u00fchren, wie wir es gew\u00fcnscht h\u00e4tten. Das Warten in der Warteschlange dauerte bis zu einer halben Stunde, was sehr lange ist. Instrumentierungstests wurden bei jedem PR ausgef\u00fchrt, die Verz\u00f6gerungen haben die Entwicklung stark verlangsamt, und dann kam auch noch die Rechnung f\u00fcr den Monat mit einem gro\u00dfen Betrag. Insgesamt wurde entschieden, auf Firebase zu verzichten und eine interne L\u00f6sung zu entwickeln, da das Team ausreichend gewachsen war.<\/p>\n<h3>Docker + Python + bash<\/h3>\n<p>\nWir haben Docker genommen, die Emulatoren hinein gepackt und ein einfaches Programm in Python geschrieben, das zum richtigen Zeitpunkt die ben\u00f6tigte Anzahl an Emulatoren in der richtigen Version hochf\u00e4hrt und sie bei Bedarf anh\u00e4lt. Und nat\u00fcrlich ein paar bash-Skripte \u2013 wo k\u00e4men wir ohne die hin?<\/p>\n<p><strong><em>F\u00fcr den Aufbau einer eigenen Testumgebung ben\u00f6tigten wir f\u00fcnf Wochen.<\/em><\/strong><\/p>\n<p>In der Folge gab es f\u00fcr jeden Pull Request eine umfangreiche, die Zusammenf\u00fchrung blockierende Liste von Pr\u00fcfungen:<\/p>\n<ul>\n<li>Build-ARK;<\/li>\n<li>Junit-Tests;<\/li>\n<li>Lint;<\/li>\n<li>\u00dcberpr\u00fcfungen in Android Studio;<\/li>\n<li>Instrumentierungstests;<\/li>\n<li>Screenshot-Tests.<\/li>\n<\/ul>\n<p>\nDies verhinderte viele m\u00f6gliche Fehler. Technisch hat alles funktioniert, aber die Entwickler klagten dar\u00fcber, dass das Warten auf die Ergebnisse zu lange dauert.<\/p>\n<p>Wie lange ist \"zu lange\"? Wir haben die Daten aus Bitbucket und TeamCity in ein Analysesystem exportiert und festgestellt, dass <strong>die durchschnittliche Wartezeit 45 Minuten betr\u00e4gt<\/strong>. Das hei\u00dft, ein Entwickler wartet im Durchschnitt 45 Minuten auf die Ergebnisse der Builds, wenn er einen Pull Request \u00f6ffnet. Meiner Meinung nach ist das sehr viel, und so kann man nicht arbeiten.<\/p>\n<p>Nat\u00fcrlich haben wir beschlossen, all unsere Builds zu beschleunigen.<\/p>\n<h2>Wir beschleunigen uns<\/h2>\n<p>\nAls wir sahen, dass oft Builds in der Warteschlange standen, haben wir zuerst <strong>zus\u00e4tzliches Material gekauft<\/strong> \u2014 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\u00fcfungen von sich aus sehr lange dauerten.<\/p>\n<h3>Wir entfernen zu lange Pr\u00fcfungen<\/h3>\n<p>\nUnsere Continuous Integration konnte solche Arten von Fehlern und Problemen erfassen.<\/p>\n<ul>\n<li><strong>Wird nicht gebaut<\/strong>. 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\u00f6s.<\/li>\n<li><strong>Fehler im Verhalten<\/strong>. Zum Beispiel, wenn die Anwendung gebaut wird, aber beim Klicken auf die Schaltfl\u00e4che abst\u00fcrzt, oder die Schaltfl\u00e4che \u00fcberhaupt nicht reagiert. Das ist schlecht, denn ein solcher Fehler kann den Benutzer erreichen.<\/li>\n<li><strong>Fehler im Layout<\/strong>. Zum Beispiel, wenn die Schaltfl\u00e4che gedr\u00fcckt wird, aber um 10 Pixel nach links verschoben ist.<\/li>\n<li><strong>Zunahme der technischen Schulden<\/strong>.<\/li>\n<\/ul>\n<p>\nNachdem 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-\u00dcberpr\u00fcfung entdeckt und k\u00f6nnen 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 \u00fcberpr\u00fcfen.<\/p>\n<p>Basierend auf dieser Klassifizierung haben wir die gesamte Liste der Pr\u00fcfungen durchgesehen. <strong>Wir haben Lint gestrichen<\/strong> und seine Ausf\u00fchrung auf die Nacht verschoben: einfach um einen Bericht dar\u00fcber zu erstellen, wie viele Probleme im Projekt bestehen. Mit den technischen Schulden haben wir beschlossen, getrennt zu arbeiten, und <strong>auf die Android Studio Checks ganz verzichtet<\/strong>. Android Studio in Docker f\u00fcr die Ausf\u00fchrung von Inspektionen klingt interessant, bringt jedoch viele Probleme in der Wartung mit sich. Jedes Update der Android Studio-Versionen stellt einen Kampf gegen unverst\u00e4ndliche Bugs dar. Es war auch schwierig, Screenshot-Tests aufrechtzuerhalten, weil die Bibliothek nicht sehr stabil arbeitete und es manchmal zu Fehlalarmen kam. <strong>Screenshot-Tests haben wir aus der Liste der Pr\u00fcfungen entfernt<\/strong>.<\/p>\n<p>Am Ende hatten wir:<\/p>\n<ul>\n<li>Build-ARK;<\/li>\n<li>Junit-Tests;<\/li>\n<li>Instrumentierungstests.<\/li>\n<\/ul>\n<h3>Gradle Remote Cache<\/h3>\n<p>\nOhne schwere Pr\u00fcfungen ist alles besser geworden. Aber es gibt kein Limit f\u00fcr die Perfektion!<\/p>\n<p>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.<\/p>\n<p>Der Gradle-Remote-Cache ist ein Dienst, der Build-Artefakte f\u00fcr einzelne Aufgaben in verschiedenen Modulen cachen kann. Gradle fragt den Remote-Cache \u00fcber HTTP an und fragt, ob jemand diese Aufgabe bereits ausgef\u00fchrt hat, anstatt den Code tats\u00e4chlich zu kompilieren. Wenn ja, wird das Ergebnis einfach heruntergeladen.<\/p>\n<p><strong><em>Es ist einfach, den Gradle-Remote-Cache zu starten, da Gradle ein Docker-Image bereitstellt. Wir haben das in drei Stunden geschafft.<\/em><\/strong><\/p>\n<p>Man musste lediglich Docker starten und eine Zeile im Projekt hinzuf\u00fcgen. Obwohl man ihn schnell starten kann, ben\u00f6tigt es doch eine betr\u00e4chtliche Zeit, damit alles gut l\u00e4uft.<\/p>\n<p>Nachfolgend die Grafik der Cache-Fehlversuche.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/458ef23b5506b04f3bd385be0c18607a.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZu 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\u00e4ngigkeiten haben, durch die Gradle am Cache vorbeigeschossen ist.<\/p>\n<p>Mit der Aktivierung des Caches haben wir den Build-Prozess erheblich beschleunigt. Aber zus\u00e4tzlich zum Build m\u00fcssen auch die Instrumentierungstests durchgef\u00fchrt werden, die lange dauern. Es ist m\u00f6glich, dass nicht alle Tests bei jedem Pull Request ausgef\u00fchrt werden m\u00fcssen. Um das herauszufinden, verwenden wir die Impact-Analyse.<\/p>\n<h3>Impact-Analyse<\/h3>\n<p>\nF\u00fcr den Pull Request erstellen wir den Git-Diff und finden die ge\u00e4nderten Gradle-Module.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/471ca37206a0da4d5741c8ee1dbb7f03.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEs macht Sinn, nur die Instrumentierungstests auszuf\u00fchren, die die ge\u00e4nderten Module und alle Module, die von ihnen abh\u00e4ngen, \u00fcberpr\u00fcfen. Tests f\u00fcr benachbarte Module auszuf\u00fchren, macht keinen Sinn, da sich der Code dort nicht ge\u00e4ndert hat und nichts kaputtgehen kann.<\/p>\n<p>Bei den Instrumentierungstests ist es nicht so einfach, da sie sich im obersten Anwendungsmodul befinden m\u00fcssen. Wir haben eine Heuristik zur Bytecode-Analyse angewendet, um herauszufinden, zu welchem Modul jeder Test geh\u00f6rt.<\/p>\n<p><strong><em>Die Modernisierung der Instrumentierungstests, um nur die betroffenen Module zu \u00fcberpr\u00fcfen, dauerte etwa acht Wochen.<\/em><\/strong><\/p>\n<p>Die Ma\u00dfnahmen zur Beschleunigung der \u00dcberpr\u00fcfungen 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.<\/p>\n<p>Aber jetzt haben die Entwickler begonnen, sich zu beschweren, dass sie nicht nachvollziehen k\u00f6nnen, welche Builds gestartet werden, wo sie die Logs ansehen k\u00f6nnen, warum der Build rot ist, welcher Test fehlgeschlagen ist usw.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/aa76151174cd4e5a45ff281818e312f5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nR\u00fcckmeldungsprobleme verz\u00f6gern die Entwicklung, daher haben wir uns bem\u00fcht, so verst\u00e4ndlich und detailliert wie m\u00f6glich 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\u00dflich haben wir ein Dashboard f\u00fcr die PR-Seite erstellt, das eine Liste aller aktuell laufenden Builds und deren Status enth\u00e4lt: in der Warteschlange, wird gestartet, fehlgeschlagen oder abgeschlossen. Man kann auf den Build klicken und zu seinem Log gelangen.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/2d16b7a6b6d23e23903d291ef1526999.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n<strong><em>F\u00fcr detailliertes Feedback wurden sechs Wochen aufgewendet.<\/em><\/strong><\/p>\n<h2>Pl\u00e4ne<\/h2>\n<p>\nKommen wir zur neuesten Geschichte. Nachdem wir das Feedback-Problem gel\u00f6st hatten, haben wir ein neues Niveau erreicht \u2014 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.<\/p>\n<p>Dar\u00fcber hinaus gibt es weitere Pl\u00e4ne.<\/p>\n<ul>\n<li><strong>Lint zur\u00fcckbringen<\/strong> (und weitere statische Analysen). Wir arbeiten bereits in diese Richtung.<\/li>\n<li>Alle <strong>End-to-End-Tests<\/strong> auf allen SDK-Versionen im PR blockieren.<\/li>\n<\/ul>\n<p>\nSo haben wir die Entwicklung der Continuous Integration bei Avito nachverfolgt. Nun m\u00f6chte ich einige Ratschl\u00e4ge aus der Sicht eines Erfahrenen geben.<\/p>\n<h1>Tipps<\/h1>\n<p>\nWenn ich nur einen Rat geben k\u00f6nnte, w\u00e4re es dieser:<\/p>\n<blockquote><p>Bitte seid vorsichtig mit Shell-Skripten!<\/p><\/blockquote>\n<p>\nBash ist ein sehr flexibles und m\u00e4chtiges Werkzeug, und es ist sehr bequem und schnell, Skripte darauf zu schreiben. Aber man kann in eine Falle tappen, und wir sind leider hineingefallen.<\/p>\n<p>Alles begann mit einfachen Skripten, die auf unseren Build-Maschinen ausgef\u00fchrt wurden:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env bash\n.\/gradlew assembleDebug<\/code><\/pre>\n<p>\nAber wie bekannt ist, entwickelt sich alles mit der Zeit weiter und wird komplizierter \u2014 lass uns ein Skript aus einem anderen starten, lass uns einige Parameter \u00fcbergeben \u2014 letztendlich mussten wir eine Funktion schreiben, die bestimmt, auf welcher Verschachtelungsebene sich Bash gerade befindet, um die richtigen Anf\u00fchrungszeichen einzuf\u00fcgen, damit alles ausgef\u00fchrt werden kann.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/244a574f7b5d3b4fc77cfc4ccda2db69.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nK\u00f6nnt ihr euch den Aufwand f\u00fcr die Entwicklung solcher Skripte vorstellen? Ich empfehle, nicht in diese Falle zu tappen.<\/p>\n<p>Wodurch kann man das ersetzen?<\/p>\n<ul>\n<li>Durch jede Skriptsprache. Es ist bequemer, in <strong>Python oder Kotlin Script<\/strong> zu schreiben, weil es sich um Programmierung handelt, nicht um Skripte.<\/li>\n<li>Oder die gesamte Logik der Builds in Form von <strong>benutzerdefinierten Gradle-Tasks<\/strong> f\u00fcr euer Projekt zu beschreiben.<\/li>\n<\/ul>\n<p>\nWir haben uns entschieden, die zweite Option zu w\u00e4hlen, und entfernen derzeit systematisch alle Bash-Skripte und schreiben viele benutzerdefinierte Gradle-Tasks.<\/p>\n<p><strong>Rat Nr. 2: Die Infrastruktur im Code speichern.<\/strong><\/p>\n<p>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\u00f6glicht Versionskontrolle. Es wird nicht schwierig sein, zur\u00fcckzurollen oder den Code in einem anderen Branch zu erstellen.<\/p>\n<p>Skripte k\u00f6nnen im Projekt gespeichert werden. Was ist aber mit der Umgebung?<\/p>\n<p><strong>Tipp Nr. 3: Docker kann bei der Umgebung helfen.<\/strong><\/p>\n<p>Es wird den Android-Entwicklern sicherlich helfen, iOS leider noch nicht.<\/p>\n<p>Das ist ein Beispiel f\u00fcr eine einfache Docker-Datei, die jdk und android-sdk enth\u00e4lt:<\/p>\n<pre><code class=\"plaintext\">FROM openjdk:8\n\nENV SDK_URL=\"https:\/\/dl.google.com\/android\/repository\/sdk-tools-linux-3859397.zip\" \n    ANDROID_HOME=\"\/usr\/local\/android-sdk\" \n    ANDROID_VERSION=26 \n    ANDROID_BUILD_TOOLS_VERSION=26.0.2\n\n# Android SDK herunterladen\nRUN mkdir \"$ANDROID_HOME\" .android \n    &amp;&amp; cd \"$ANDROID_HOME\" \n    &amp;&amp; curl -o sdk.zip $SDK_URL \n    &amp;&amp; unzip sdk.zip \n    &amp;&amp; rm sdk.zip \n    &amp;&amp; yes | $ANDROID_HOME\/tools\/bin\/sdkmanager --licenses\n\n# Android Build Tool und Bibliotheken installieren\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager --update\nRUN $ANDROID_HOME\/tools\/bin\/sdkmanager \"build-tools;${ANDROID_BUILD_TOOLS_VERSION}\" \n    \"platforms;android-${ANDROID_VERSION}\" \n    \"platform-tools\"\n\nRUN mkdir \/application\nWORKDIR \/application\n<\/code><\/pre>\n<p>\nIch 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\u00fchren k\u00f6nnen.<\/p>\n<p>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).<\/p>\n<p><strong>Tipp Nr. 4: Vergessen Sie nicht, dass Pr\u00fcfungen nicht der Pr\u00fcfungen wegen durchgef\u00fchrt werden, sondern f\u00fcr die Menschen.<\/strong><\/p>\n<p>Entwicklern ist schnelle und vor allem verst\u00e4ndliche R\u00fcckmeldung sehr wichtig: Was ist kaputt, welcher Test ist fehlgeschlagen, wo kann man das Build-Log einsehen?<\/p>\n<p><strong>Tipp Nr. 5: Seien Sie pragmatisch in der Weiterentwicklung der Continuous Integration.<\/strong><\/p>\n<p>Verstehen Sie klar, welche Arten von Fehlern Sie verhindern m\u00f6chten, wie viele Ressourcen, Zeit und Rechenzeit Sie bereit sind zu investieren. Zu lange Pr\u00fcfungen k\u00f6nnen beispielsweise auf die Nacht verschoben werden. Und die, die nicht so wichtige Fehler finden, k\u00f6nnen ganz weggelassen werden.<\/p>\n<p><strong>Tipp Nr. 6: Nutzen Sie fertige Werkzeuge.<\/strong><\/p>\n<p>Es gibt jetzt viele Unternehmen, die Cloud-CI anbieten.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/e57aabd49ec01d14e83b64331fd84ec5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nF\u00fcr kleine Teams ist das eine gute L\u00f6sung. Man muss nichts unterst\u00fctzen, einfach ein wenig Geld zahlen, die eigene Anwendung zusammenstellen und sogar Instrumentierungstests durchf\u00fchren.<\/p>\n<p><strong>Tipp Nr. 7: In gro\u00dfen Teams sind Inhouse-L\u00f6sungen wirtschaftlicher.<\/strong><\/p>\n<p>Aber fr\u00fcher oder sp\u00e4ter, mit dem Wachstum des Teams werden Inhouse-L\u00f6sungen wirtschaftlicher. Bei diesen L\u00f6sungen gibt es einen Punkt. In der Wirtschaft gilt das Gesetz des abnehmenden Ertrags: In jedem Projekt f\u00e4llt es immer schwerer, jede n\u00e4chste Verbesserung zu erzielen, was immer mehr Investitionen erfordert.<\/p>\n<p>Die Wirtschaft beschreibt unser ganzes Leben, einschlie\u00dflich der Continuous Integration. Ich habe ein Diagramm \u00fcber den Arbeitsaufwand f\u00fcr jede Phase der Entwicklung unserer Continuous Integration erstellt.<\/p>\n<p><img decoding=\"async\" alt=\"Evolution von CI im Team der mobilen Entwicklung\" src=\"\/wp-content\/uploads\/2019\/04\/97bf8bff64f3ac9ae587fae195251da2.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEs 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\u00f6\u00dfe abgestimmt werden muss. F\u00fcr ein Team von zwei Personen ist es eine schlechte Idee, 50 Tage mit der Entwicklung einer internen Emulatorfarm zu verbringen. Aber f\u00fcr ein gro\u00dfes Team ist es auch keine gute Idee, sich \u00fcberhaupt nicht mit Continuous Integration zu besch\u00e4ftigen, denn Probleme mit der Integration, die Behebung der Kommunikation usw. werden noch mehr Zeit in Anspruch nehmen.<\/p>\n<p>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.<\/p>\n<ul>\n<li>Automatisierung ist teuer. Denken Sie an das Diagramm \u00fcber den Arbeitsaufwand.<\/li>\n<li>Bei der Automatisierung machen die Menschen Fehler.<\/li>\n<li>Manchmal ist es sehr l\u00e4stig zu automatisieren, weil alles ohnehin schon funktioniert. Warum sollte man etwas verbessern, warum all diese Continuous Integration?<\/li>\n<\/ul>\n<p>\nAber 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 \u00fcberzeugt sind, dass, wenn sie einen Fehler machen, dieser nicht in den develop gelangt, da ihn die automatisierten Tests auffangen. Dementsprechend k\u00f6nnen die Entwickler mehr Zeit mit dem Schreiben von Code und interessanten Sachen verbringen, anstatt lokal etwas durchzuf\u00fchren und zu \u00fcberpr\u00fcfen.<\/p>\n<p><strong>K\u00fcmmern Sie sich um Continuous Integration. Aber in Ma\u00dfen.<\/strong><\/p>\n<blockquote><p>\u00dcbrigens handelt Nikolai Nesterov nicht nur selbst von gro\u00dfartigen Vortr\u00e4gen, sondern ist auch im Programmkomitee <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\">AppsConf<\/a><\/noindex> und hilft anderen, aufschlussreiche Beitr\u00e4ge f\u00fcr Sie vorzubereiten. Die Vollst\u00e4ndigkeit und N\u00fctzlichkeit des Programms der n\u00e4chsten Konferenz kann an den Themen im <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\/schedule\">Zeitplan<\/a><\/noindex>. F\u00fcr weitere Details kommen Sie am 22.-23. April in den Inforaum.<\/p><\/blockquote>\n<p>Quelle: <a content=\"nofollow\" rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/company\/oleg-bunin\/blog\/447608\/\">habr.com<\/a><\/p>","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"excerpt":{"rendered":"<p>\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445. \u0423\u0441\u043b\u043e\u0432\u0438\u044f \u0443\u0441\u043f\u0435\u0445\u0430 \u043a\u043e\u043c\u0430\u043d\u0434\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u0438\u0442\u044c \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u0441\u0445\u0435\u043c\u044b. \u041d\u0430\u043f\u0438\u0441\u0430\u0432 \u043a\u043e\u0434, \u0432\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0443\u0431\u0435\u0434\u0438\u0442\u044c\u0441\u044f, \u0447\u0442\u043e \u043e\u043d: \u0420\u0430\u0431\u043e\u0442\u0430\u0435\u0442. \u041d\u0438\u0447\u0435\u0433\u043e \u043d\u0435 \u043b\u043e\u043c\u0430\u0435\u0442, \u0432 \u0442\u043e\u043c \u0447\u0438\u0441\u043b\u0435 \u043a\u043e\u0434, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043d\u0430\u043f\u0438\u0441\u0430\u043b\u0438 \u0432\u0430\u0448\u0438 \u043a\u043e\u043b\u043b\u0435\u0433\u0438. \u0415\u0441\u043b\u0438 \u043e\u0431\u0430 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u044e\u0442\u0441\u044f, \u0442\u043e \u0432\u044b \u043d\u0430 \u043f\u0443\u0442\u0438 \u043a \u0443\u0441\u043f\u0435\u0445\u0443. \u0427\u0442\u043e\u0431\u044b \u043b\u0435\u0433\u043a\u043e \u043f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u044d\u0442\u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u044f \u0438 \u043d\u0435 \u0441\u0432\u043e\u0440\u0430\u0447\u0438\u0432\u0430\u0442\u044c \u0441 [&hellip;]<\/p>\n","protected":false,"gt_translate_keys":[{"key":"rendered","format":"html"}]},"author":1,"featured_media":23270,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[688],"tags":[],"class_list":["post-31304","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-administrirovanie"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Yuri Gagarin\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/prohoster.info\/de\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"de_DE\" \/>\n\t\t<meta property=\"og:site_name\" content=\"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster\" \/>\n\t\t<meta property=\"og:description\" content=\"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/prohoster.info\/de\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg\" \/>\n\t\t<meta property=\"og:image:width\" content=\"350\" \/>\n\t\t<meta property=\"og:image:height\" content=\"350\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2019-10-31T18:40:34+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<meta property=\"article:author\" content=\"https:\/\/www.facebook.com\/prohoster\" \/>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"\ud83e\udd47Evolution von CI im Team der mobilen Entwicklung | ProHoster","description":"Heute werden die meisten Softwareprodukte im Team entwickelt.","canonical_url":"https:\/\/prohoster.info\/de\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":null,"og:locale":"de_DE","og:site_name":"ProHoster | \u041a\u0443\u043f\u0438\u0442\u044c \u043d\u0430\u0434\u0435\u0436\u043d\u044b\u0439 \u0445\u043e\u0441\u0442\u0438\u043d\u0433 \u0434\u043b\u044f \u0441\u0430\u0439\u0442\u043e\u0432 \u0441 \u0437\u0430\u0449\u0438\u0442\u043e\u0439 \u043e\u0442 DDoS, VPS VDS \u0441\u0435\u0440\u0432\u0435\u0440\u044b","og:type":"article","og:title":"\ud83e\udd47\u042d\u0432\u043e\u043b\u044e\u0446\u0438\u044f CI \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0435 \u043c\u043e\u0431\u0438\u043b\u044c\u043d\u043e\u0439 \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0438 | ProHoster","og:description":"\u0421\u0435\u0433\u043e\u0434\u043d\u044f \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u043e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u044b\u0445 \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u043e\u0432 \u0440\u0430\u0437\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u0445.","og:url":"https:\/\/prohoster.info\/de\/blog\/administrirovanie\/evolyutsiya-ci-v-komande-mobilnoj-razrabotki","og:image":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:secure_url":"https:\/\/prohoster.info\/wp-content\/uploads\/2021\/11\/logo-350.jpg","og:image:width":350,"og:image:height":350,"article:published_time":"2019-10-31T18:40:34+00:00","article:modified_time":"2019-10-31T18:40:34+00:00","article:publisher":"https:\/\/www.facebook.com\/prohoster","article:author":"https:\/\/www.facebook.com\/prohoster"},"aioseo_meta_data":{"post_id":"31304","title":null,"description":null,"keywords":null,"keyphrases":null,"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":null,"og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"","isEnabled":true},"graphs":[]},"schema_type":null,"schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":null,"robots_max_videopreview":null,"robots_max_imagepreview":"large","priority":null,"frequency":null,"local_seo":null,"seo_analyzer_scan_date":"2026-01-21 05:30:21","breadcrumb_settings":null,"limit_modified_date":false,"reviewed_by":null,"ai":null,"created":"2021-03-01 03:19:49","updated":"2026-01-21 05:30:21","focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"gt_translate_keys":[{"key":"link","format":"url"}],"_links":{"self":[{"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/posts\/31304","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/comments?post=31304"}],"version-history":[{"count":0,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/posts\/31304\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/media\/23270"}],"wp:attachment":[{"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/media?parent=31304"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/categories?post=31304"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/prohoster.info\/de\/wp-json\/wp\/v2\/tags?post=31304"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}