{"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":"Die Evolution von CI im Mobile-Entwicklungsteam","gt_translate_keys":[{"key":"rendered","format":"text"}]},"content":{"rendered":"<p>Heutzutage werden die meisten Softwareprodukte in Teams entwickelt. Die Erfolgskriterien f\u00fcr die Teamentwicklung lassen sich in einem einfachen Schema darstellen.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/b283cfd7772d8f479be0754ebbe51b19.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nNachdem Sie den Code geschrieben haben, sollten 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 einfach zu \u00fcberpr\u00fcfen und nicht vom gewinnbringenden Kurs abzubiegen, wurde Continuous Integration entwickelt.<\/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 h\u00e4ufig viele \u00dcberpr\u00fcfungen erforderlich sind, sollten Sie \u00fcber Automatisierung nachdenken. Man kann alles manuell \u00fcberpr\u00fcfen, aber das sollte man nicht, und hier ist der Grund.<br \/>\n<noindex><a rel=\"nofollow\" name=\"habracut\"><\/a><\/noindex><\/p>\n<ul>\n<li><strong>Menschen sind teuer<\/strong>. Die Arbeitsstunde eines Programmierers kostet mehr als die eines Servers.<\/li>\n<li><strong>Menschen machen Fehler<\/strong>. Daher k\u00f6nnen Situationen entstehen, in denen Tests nicht im richtigen Branch durchgef\u00fchrt oder der falsche Commit f\u00fcr die Tester zusammengestellt wurde.<\/li>\n<li><strong>Menschen sind faul<\/strong>. Gelegentlich, wenn ich eine Aufgabe abgeschlossen habe, kommt mir der Gedanke: \u201eWarum soll ich das \u00fcberpr\u00fcfen? Ich habe zwei Zeilen geschrieben \u2013 das funktioniert bestimmt!\u201c Ich denke, einige von Ihnen haben auch manchmal solche Gedanken. Aber man sollte immer \u00fcberpr\u00fcfen.<\/li>\n<\/ul>\n<p>\nWie 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\u00e4hlt Nikolai Nesterov (<noindex><a rel=\"nofollow\" href=\"https:\/\/habr.com\/ru\/users\/nnesterov\/\" class=\"user_link\">nnesterov<\/a><\/noindex>) \u2013 ein Teil aller evolution\u00e4ren Ver\u00e4nderungen der CI\/CD-Android-Anwendung.<\/p>\n<p>Die Erz\u00e4hlung basiert auf dem Beispiel des Android-Teams, aber die meisten Ans\u00e4tze sind auch f\u00fcr 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 \/>\nFr\u00fcher arbeitete in der Android-Abteilung von Avito nur eine Person. Er ben\u00f6tigte per Definition nichts von Continuous Integration: Mit niemandem musste er sich integrieren.<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/f433effc5ab5a59de3d6bd20a7a87057.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDas Git-Flow-Konzept ist bekannt: In einem Projekt gibt es einen gemeinsamen Branch namens develop, und f\u00fcr jede neue Funktion erstellen die Entwickler einen separaten Branch, commiten ihren Code, pushen ihn und er\u00f6ffnen, wenn sie ihren Code in den Branch develop integrieren m\u00f6chten, einen Pull Request. Um Wissen auszutauschen und Ans\u00e4tze zu diskutieren, haben wir Code Reviews eingef\u00fchrt, bei denen Kollegen den Code des anderen \u00fcberpr\u00fcfen und best\u00e4tigen m\u00fcssen.<\/p>\n<h2>\u00dcberpr\u00fcfungen<\/h2>\n<p>\nCode durch das Prisma der eigenen Augen zu betrachten, ist gro\u00dfartig, reicht aber nicht aus. Deshalb werden automatisierte Pr\u00fcfungen eingef\u00fchrt.<\/p>\n<ul>\n<li>Zuerst \u00fcberpr\u00fcfen wir <strong>den ARC-Bau<\/strong>.<\/li>\n<li>Viele <strong>JUnit-Tests<\/strong>.<\/li>\n<li><strong>Wir berechnen die Codeabdeckung<\/strong>, da wir die Tests ausf\u00fchren.<\/li>\n<\/ul>\n<p>\nUm zu verstehen, wie diese Pr\u00fcfungen ausgef\u00fchrt werden sollten, schauen wir uns den Entwicklungsprozess bei Avito an.<\/p>\n<p>Schematisch kann man es so darstellen:<\/p>\n<ul>\n<li>Der Entwickler schreibt Code auf seinem Laptop. Man kann die Integrationspr\u00fcfungen direkt hier durchf\u00fchren - entweder mit einem Commit-Hook oder indem man die Pr\u00fcfungen im Hintergrund ausf\u00fchrt.<\/li>\n<li>Nachdem der Entwickler den Code gepusht hat, er\u00f6ffnet er einen Pull Request. Damit sein Code in den Branch develop gelangt, muss er das Code Review bestehen 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 zusammengef\u00fchrt werden.<\/li>\n<li>Nachdem der Pull-Request gemerged wurde und der Code in develop ist, kann man sich einen geeigneten Zeitpunkt ausw\u00e4hlen: zum Beispiel nachts, wenn die Server frei sind, und so viele Tests durchzuf\u00fchren, wie man m\u00f6chte.<\/li>\n<\/ul>\n<p>\nTests auf seinem Laptop auszuf\u00fchren, gefiel niemandem. Wenn der Entwickler die Funktion abgeschlossen hat, m\u00f6chte er sie schnell pushen und den Pull-Request er\u00f6ffnen. Wenn in diesem Moment lange Tests ausgef\u00fchrt werden, ist das nicht nur unpraktisch, sondern verlangsamt auch die Entwicklung: w\u00e4hrend der Laptop etwas testet, kann man darauf nicht gut arbeiten.<\/p>\n<p>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\u00e4ter beheben werde, weil jetzt in Jira eine tolle neue Aufgabe wartet, die ich unbedingt anpacken m\u00f6chte.<\/p>\n<p>Wenn Tests den Pull-Request blockieren, gibt es genug Motivation, denn solange die Builds nicht gr\u00fcn sind, kommt der Code nicht in develop, und das bedeutet, die Aufgabe wird nicht abgeschlossen.<\/p>\n<p>Wir haben letztendlich folgende Strategie gew\u00e4hlt: Nachts f\u00fchren wir die maximal m\u00f6glichen Tests durch, und die kritischsten, vor allem die schnellen, starten wir mit dem Pull Request. Aber dabei bleibt es nicht \u2013 parallel optimieren wir die Geschwindigkeit der Tests, um sie von der Nachtverarbeitung auf Pull Request-Tests umzustellen.<\/p>\n<p>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\u00fcr den Pull Request aktiviert. Wir haben das aktiviert, \u00fcberlegt \u2013 und uns dann von der Code Coverage verabschiedet, weil wir dachten, dass wir sie nicht ben\u00f6tigen.<\/p>\n<p><strong><em>F\u00fcr die gesamte Einrichtung der grundlegenden CI ben\u00f6tigten wir zwei Tage (die zeitliche Sch\u00e4tzung ist hier approximativ und dient zur Groborientierung). <\/em><\/strong><\/p>\n<p>Danach begannen wir weiter zu \u00fcberlegen \u2013 \u00fcberpr\u00fcfen wir \u00fcberhaupt richtig? Starten wir die Builds richtig f\u00fcr den Pull Request?<\/p>\n<p>Wir haben den Build beim letzten Commit des Branches gestartet, von dem der Pull Request ge\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 sollte der Zustand des Branches develop \u00fcberpr\u00fcft werden, nachdem die Funktion integriert wurde.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/c1a179b68b02c04031177f9bf51a81ed.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDaf\u00fcr haben wir ein einfaches Bash-Skript geschrieben. <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 abgerufen und in den aktuellen Branch integriert. Wir haben das Skript premerge.sh als ersten Schritt aller Builds hinzugef\u00fcgt und beginnen, genau das zu \u00fcberpr\u00fcfen, was wir wollen, n\u00e4mlich <strong>Integration.<\/strong>.<\/p>\n<p><strong><em>Die Lokalisierung der Probleme, die Suche nach einer L\u00f6sung und das Schreiben dieses Skripts haben drei Tage in Anspruch genommen.<\/em><\/strong><\/p>\n<p>Die Anwendung entwickelte sich weiter, es kamen immer mehr Aufgaben hinzu, das Team wuchs, und manchmal lie\u00df uns premerge.sh im Stich. In develop drangen konfliktbehaftete \u00c4nderungen ein, die den Build zerst\u00f6rten.<\/p>\n<p>Ein Beispiel daf\u00fcr, wie das passiert:<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/6762d9ce4e431549455c49f2317a8f20.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZwei Entwickler beginnen gleichzeitig, an den Funktionen A und B zu arbeiten. Der Entwickler der Funktion A entdeckt eine ungenutzte Funktion im Projekt, <code>answer()<\/code> und entfernt sie, wie ein guter Pfadfinder. W\u00e4hrenddessen f\u00fcgt der Entwickler der Funktion B in seinem Branch einen neuen Aufruf dieser Funktion hinzu.<\/p>\n<p>Die Entwickler beenden ihre Arbeit und er\u00f6ffnen gleichzeitig einen Pull-Request. Die Builds werden gestartet, premerge.sh \u00fcberpr\u00fcft beide Pull-Requests im Hinblick auf den aktuellen Zustand von develop \u2013 alle \u00dcberpr\u00fcfungen sind gr\u00fcn. Danach wird der Pull-Request der Funktion A gemerged, der Pull-Request der Funktion 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=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/0261dcc9f7f2e5e018ab136081b4679b.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nWenn sich das Develop nicht zusammenf\u00fcgt, ist das <strong>eine lokale Katastrophe<\/strong>. Das gesamte Team kann nichts zusammenstellen und zur Pr\u00fcfung \u00fcbergeben.<\/p>\n<p>Es hat sich ergeben, dass ich mich am h\u00e4ufigsten mit Infrastrukturaufgaben besch\u00e4ftigt 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.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/0eb1aadd05b33ce995027ab52d5c1e53.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDa uns das nicht gefiel, haben wir daran gearbeitet, wie wir das verhindern k\u00f6nnen.<\/p>\n<h2>Wie man das Develop nicht bricht<\/h2>\n<p>\nErste M\u00f6glichkeit: <strong>alle Pull Requests bei der Aktualisierung von Develop neu zu kompilieren. <\/strong>Wenn in unserem Beispiel der Pull Request mit Feature A zuerst in Develop gelangt, wird der Pull Request f\u00fcr Feature B neu kompiliert und die \u00dcberpr\u00fcfungen schlagen aufgrund eines Kompilierungsfehlers fehl.<\/p>\n<p>Um zu verstehen, wie viel Zeit dies in Anspruch nehmen wird, betrachten wir ein Beispiel mit zwei PRs. Wir \u00f6ffnen zwei PRs: zwei Builds, zwei \u00dcberpr\u00fcfungsdurchl\u00e4ufe. Nachdem der erste PR in Develop eingef\u00fcgt wurde, muss der zweite neu kompiliert werden. Insgesamt ben\u00f6tigt man f\u00fcr zwei PRs drei \u00dcberpr\u00fcfungsdurchl\u00e4ufe: 2 + 1 = 3.<\/p>\n<p>Im Grunde ist es in Ordnung. Wir haben jedoch die Statistiken \u00fcberpr\u00fcft, und in unserem Team war es \u00fcblich, 10 offene PRs zu haben. Die Anzahl der \u00dcberpr\u00fcfungen ist dann die Summe der Reihe: 10 + 9 + ... + 1 = 55. Das bedeutet, um 10 PRs anzunehmen, m\u00fcssen wir 55 Mal neu kompilieren. Und das in einer idealen Situation, in der alle \u00dcberpr\u00fcfungen beim ersten Mal bestehen und niemand einen zus\u00e4tzlichen Pull Request \u00f6ffnet, w\u00e4hrend die erste Gruppe bearbeitet wird.<\/p>\n<p>Stellen Sie sich vor, Sie sind ein Entwickler, der es eilig hat, als Erster auf die Schaltfl\u00e4che \u201emerge\u201c zu klicken, denn wenn das der Nachbar macht, m\u00fcssen Sie warten, bis alle Builds erneut durchlaufen werden... Nein, das kann nicht so weitergehen; das w\u00fcrde die Entwicklung ernsthaft verlangsamen.<\/p>\n<p>Eine zweite m\u00f6gliche Methode: <strong>Pull Requests nach der Code-\u00dcberpr\u00fcfung zusammenzustellen. <\/strong>Das bedeutet, Sie \u00f6ffnen 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\u00fchrt. In diesem Fall sind keine zus\u00e4tzlichen Neustarts erforderlich, aber das Feedback wird erheblich langsamer. Als Entwickler m\u00f6chte ich beim \u00d6ffnen eines Pull Requests sofort sehen, ob er kompiliert. Wenn ein Test fehlschl\u00e4gt, muss er schnell behoben werden. Bei verz\u00f6gerten Builds wird das Feedback langsamer, was die gesamte Entwicklung beeintr\u00e4chtigt. Das war auch f\u00fcr uns nicht akzeptabel.<\/p>\n<p>Am Ende blieb nur die dritte Option \u2014 <strong>die Sache selbst in die Hand zu nehmen<\/strong>. Unser ganzer Code, all unsere Quellcodes werden im Repository auf dem Bitbucket-Server gespeichert. Daher mussten wir ein Plugin f\u00fcr Bitbucket entwickeln.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/5e4b1f770ea1a3449b616c3101640294.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nDieses Plugin \u00fcberschreibt den Mechanismus zum Zusammenf\u00fchren von Pull Requests. Der Ablauf ist wie gewohnt: Ein PR wird ge\u00f6ffnet, alle Builds werden gestartet, und die Code-\u00dcberpr\u00fcfung wird durchgef\u00fchrt. Doch nachdem die Code-\u00dcberpr\u00fcfung abgeschlossen ist und der Entwickler beschlie\u00dft, auf \u201emerge\u201c zu klicken, \u00fcberpr\u00fcft das Plugin, auf welchem Stand von develop die Tests durchgef\u00fchrt wurden. Sollte develop nach den Builds aktualisiert worden sein, l\u00e4sst das Plugin einen solchen Pull Request nicht in den Hauptzweig einflie\u00dfen. Es wird die Builds einfach erneut auf dem aktuellen Stand von develop starten.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/0edee880e1f2d286a0c64033103ac136.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nIn unserem Beispiel mit den konfliktreichen \u00c4nderungen werden solche Builds aufgrund eines Kompilierungsfehlers fehlschlagen. Daher muss der Entwickler von Feature B den Code anpassen, die \u00dcberpr\u00fcfungen erneut starten, woraufhin das Plugin den Pull Request automatisch anwenden wird.<\/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 bemerkenswert, dass dieses Plugin einen Nachteil hat: Es startet den Build nur einmal neu. Das bedeutet, dass ein kleines Zeitfenster bleibt, in dem konfliktierende \u00c4nderungen in die Entwicklung 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 eingegangen. In zwei Jahren ist das nur einmal passiert, also war es wohl nicht umsonst.<\/p>\n<p><strong><em>F\u00fcr die Erstellung der ersten Version des Plugins f\u00fcr Bitbucket haben wir zwei Wochen gebraucht. <\/em><\/strong><\/p>\n<h3>Neue \u00dcberpr\u00fcfungen<\/h3>\n<p>\nIn der Zwischenzeit wuchs unser Team weiter. Es wurden neue \u00dcberpr\u00fcfungen hinzugef\u00fcgt.<\/p>\n<p>Wir dachten: Warum Fehler beheben, wenn wir sie verhindern k\u00f6nnen? Deshalb haben wir <strong>statische Code-Analyse<\/strong>implementiert. Wir begannen mit lint, das zum Android SDK geh\u00f6rt. Aber zu dieser Zeit konnte es \u00fcberhaupt nicht mit Kotlin-Code umgehen, und bereits 75 % unserer Anwendung waren in Kotlin geschrieben. Daher wurden zu lint die integrierten <strong>\u00dcberpr\u00fcfungen von Android Studio hinzugef\u00fcgt.<\/strong><\/p>\n<p>Daf\u00fcr 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\u00e4uft. Aber es hat funktioniert.<\/p>\n<p>Gleichzeitig begannen wir, viele <strong>Instrumentationstests<\/strong> zu schreiben und haben implementiert. <strong>Screenshot-Tests<\/strong>. Dabei wird ein Referenz-Screenshot f\u00fcr 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.<\/p>\n<p>Instrumentation-Tests und Screenshot-Tests m\u00fcssen jedoch auf Ger\u00e4ten durchgef\u00fchrt werden: auf Emulatoren oder echten Ger\u00e4ten. Angesichts der Vielzahl an Tests, die h\u00e4ufig durchgef\u00fchrt werden, ist eine gesamte Farm erforderlich. Eine eigene Farm aufzubauen ist jedoch sehr aufw\u00e4ndig, weshalb wir eine fertige L\u00f6sung gefunden haben \u2013 Firebase Test Lab.<\/p>\n<h3>Firebase Test Lab<\/h3>\n<p>\nwurde ausgew\u00e4hlt, weil Firebase ein Produkt von Google ist und daher zuverl\u00e4ssig sein sollte und nicht so schnell aussterben wird. Die Preise sind angemessen: 5 $ pro Stunde f\u00fcr die Nutzung eines physischen Ger\u00e4ts, 1 $ pro Stunde f\u00fcr einen Emulator.<\/p>\n<p><strong><em>Die Implementierung von Firebase Test Lab in unseren CI dauerte etwa drei Wochen.<\/em><\/strong><\/p>\n<p>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\u00fcgend Ger\u00e4te f\u00fcr die Tests verf\u00fcgbar 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\u00fchrt, und die Verz\u00f6gerungen verlangsamten die Entwicklung erheblich. Au\u00dferdem kam dann die Monatsrechnung mit einem hohen Betrag. Insgesamt wurde beschlossen, auf Firebase zu verzichten und eine eigene L\u00f6sung zu entwickeln, da das Team genug gewachsen war.<\/p>\n<h3>Docker + Python + bash<\/h3>\n<p>\nWir haben Docker verwendet, Emulatoren hineinverpackt und ein einfaches Programm in Python geschrieben, das zur richtigen Zeit die ben\u00f6tigte Anzahl an Emulatoren in der ben\u00f6tigten Version startet und sie bei Bedarf stoppt. Und nat\u00fcrlich ein paar Bash-Skripte - wo w\u00e4ren wir ohne sie?<\/p>\n<p><strong><em>Die Erstellung einer eigenen Testumgebung dauerte f\u00fcnf Wochen.<\/em><\/strong><\/p>\n<p>Infolgedessen gab es f\u00fcr jeden Pull Request eine umfangreiche, merge-blockierende Liste von Pr\u00fcfungen:<\/p>\n<ul>\n<li>ARK-Bau;<\/li>\n<li>Junit-Tests;<\/li>\n<li>Lint;<\/li>\n<li>Android Studio-Pr\u00fcfungen;<\/li>\n<li>Instrumentierungstests;<\/li>\n<li>Screenshot-Tests.<\/li>\n<\/ul>\n<p>\nDas hat viele m\u00f6gliche Ausf\u00e4lle verhindert. Technisch gesehen funktionierte alles, aber die Entwickler beklagten sich, dass sie zu lange auf die Ergebnisse warten mussten.<\/p>\n<p>Zu lange \u2013 wie lange ist das? Wir haben die Daten aus Bitbucket und TeamCity in das Analysetool exportiert und festgestellt, dass <strong>die durchschnittliche Wartezeit 45 Minuten betr\u00e4gt<\/strong>. Das bedeutet, dass ein Entwickler im Durchschnitt 45 Minuten auf die Ergebnisse der Builds wartet, wenn er einen Pull-Request \u00f6ffnet. Meiner Meinung nach ist das viel zu lang, so kann man nicht arbeiten.<\/p>\n<p>Nat\u00fcrlich haben wir beschlossen, unsere Builds zu beschleunigen.<\/p>\n<h2>Wir werden schneller<\/h2>\n<p>\nAls wir sahen, dass die Builds h\u00e4ufig in der Warteschlange standen, haben wir als Erstes <strong>zus\u00e4tzlich Hardware gekauft<\/strong> \u2013 eine extensive Entwicklung ist das Einfachste. Die Builds standen nicht mehr in der Warteschlange, aber die Wartezeit wurde nur geringf\u00fcgig verk\u00fcrzt, weil einige Tests selbst sehr lange dauerten.<\/p>\n<h3>Wir entfernen die zu langen Pr\u00fcfungen<\/h3>\n<p>\nUnsere Continuous Integration konnte solche Arten von Fehlern und Problemen erkennen.<\/p>\n<ul>\n<li><strong>Konnte nicht gebaut werden<\/strong>. CI kann einen Kompilierungsfehler erfassen, wenn aufgrund von konfliktreichen \u00c4nderungen etwas nicht gebaut werden kann. Wie bereits erw\u00e4hnt, kann dann niemand etwas zusammenbauen, die Entwicklung steht still, und alle sind nerv\u00f6s.<\/li>\n<li><strong>Fehler im Verhalten<\/strong>. Zum Beispiel, wenn die Anwendung erstellt wird, aber beim Dr\u00fccken des Knopfes abst\u00fcrzt, oder der Knopf \u00fcberhaupt nicht gedr\u00fcckt werden kann. Das ist schlecht, da ein solcher Fehler bis zum Benutzer gelangen kann.<\/li>\n<li><strong>Fehler im Layout<\/strong>. Zum Beispiel wird der Knopf gedr\u00fcckt, ist aber um 10 Pixel nach links verrutscht.<\/li>\n<li><strong>Zunahme der technischen Schulden<\/strong>.<\/li>\n<\/ul>\n<p>\nNach Durchsicht dieser Liste haben wir erkannt, dass nur die ersten beiden Punkte kritisch sind. Solche Probleme m\u00f6chten wir zuerst identifizieren. Layoutfehler 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, weshalb wir beschlossen haben, dies nicht im Pull Request zu \u00fcberpr\u00fcfen.<\/p>\n<p>Basierend auf dieser Klassifizierung haben wir die gesamte Liste der \u00dcberpr\u00fcfungen durchgesehen. <strong>Wir haben Lint gestrichen<\/strong> und dessen Ausf\u00fchrung 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 <strong>haben ganz auf die \u00dcberpr\u00fcfungen von Android Studio verzichtet.<\/strong>. Android Studio in Docker f\u00fcr Inspektionen klingt interessant, bringt jedoch viele Herausforderungen bei der Wartung mit sich. Jedes Update der Android Studio-Versionen ist ein Kampf gegen unerkl\u00e4rliche Bugs. Auch die Unterst\u00fctzung von Screenshots-Tests war kompliziert, da die Bibliothek nicht sehr stabil arbeitete und es zu Fehlalarmen kam. <strong>Screenshots-Tests wurden aus der Pr\u00fcfliste entfernt.<\/strong>.<\/p>\n<p>Am Ende haben wir folgende Tests \u00fcbrig:<\/p>\n<ul>\n<li>ARK-Bau;<\/li>\n<li>Junit-Tests;<\/li>\n<li>Instrumentationstests.<\/li>\n<\/ul>\n<h3>Gradle Remote-Cache<\/h3>\n<p>\nOhne umfangreiche Pr\u00fcfungen lief alles besser. Aber es gibt immer Raum f\u00fcr Verbesserungen!<\/p>\n<p>Unsere Anwendung war bereits in etwa 150 Gradle-Module unterteilt. In solchen F\u00e4llen funktioniert der Gradle Remote-Cache normalerweise gut, also haben wir beschlossen, ihn auszuprobieren.<\/p>\n<p>Der Gradle Remote-Cache ist ein Service, der Build-Artefakte f\u00fcr einzelne Aufgaben in separaten Modulen cachen kann. Gradle fragt, anstatt den Code tats\u00e4chlich zu kompilieren, \u00fcber HTTP beim Remote-Cache an, ob jemand diese Aufgabe bereits ausgef\u00fchrt hat. Falls ja, wird einfach das Ergebnis heruntergeladen.<\/p>\n<p><strong><em>Den Gradle Remote-Cache zu starten, ist einfach, da Gradle ein Docker-Image bereitstellt. Wir konnten dies in drei Stunden umsetzen.<\/em><\/strong><\/p>\n<p>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.<\/p>\n<p>Unten ist das Diagramm der Cache-Misses.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/458ef23b5506b04f3bd385be0c18607a.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nZu 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\u00e4ngigkeiten haben, wodurch Gradle am Cache vorbeikam.<\/p>\n<p>Durch die Aktivierung des Caches haben wir den Build-Prozess erheblich beschleunigt. Aber neben dem Build werden auch Instrumentation-Tests durchgef\u00fchrt, und diese dauern lange. M\u00f6glicherweise m\u00fcssen nicht alle Tests bei jedem Pull-Request ausgef\u00fchrt werden. Um das herauszufinden, verwenden wir eine Impact-Analyse.<\/p>\n<h3>Impact-Analyse<\/h3>\n<p>\nBei jedem Pull-Request erstellen wir einen git diff und ermitteln die ge\u00e4nderten Gradle-Module.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/471ca37206a0da4d5741c8ee1dbb7f03.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEs macht Sinn, nur die Instrumentation-Tests auszuf\u00fchren, die die ge\u00e4nderten Module und alle Module, die von ihnen abh\u00e4ngen, \u00fcberpr\u00fcfen. Es hat keinen Sinn, Tests f\u00fcr angrenzende Module durchzuf\u00fchren: der Code hat sich dort nicht ge\u00e4ndert, und es kann nichts kaputtgehen.<\/p>\n<p>Mit Instrumentierungstests ist es nicht ganz einfach, da sie sich im obersten Modul der Anwendung befinden m\u00fcssen. Wir haben eine Heuristik zur Bytecode-Analyse angewendet, um zu verstehen, zu welchem Modul jeder Test geh\u00f6rt.<\/p>\n<p><strong><em>Die Modernisierung der Instrumentierungstests, sodass sie nur die aktiven Module \u00fcberpr\u00fcfen, hat etwa acht Wochen gedauert.<\/em><\/strong><\/p>\n<p>Die Ma\u00dfnahmen zur Beschleunigung der Pr\u00fcfungen haben erfolgreich funktioniert. Statt 45 Minuten ben\u00f6tigen wir jetzt etwa 15. Es ist bereits normal, ein Viertelstunde auf einen Build zu warten.<\/p>\n<p>Nun beschweren sich die Entwickler dar\u00fcber, dass sie nicht verstehen, welche Builds gestartet werden, wo sie die Logs ansehen k\u00f6nnen, warum ein Build rot ist, welcher Test fehlgeschlagen ist usw.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/aa76151174cd4e5a45ff281818e312f5.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nProbleme mit dem Feedback verlangsamen die Entwicklung, daher haben wir versucht, so viel klare und detaillierte Informationen wie m\u00f6glich \u00fcber 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\u00fcr 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.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/2d16b7a6b6d23e23903d291ef1526999.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\n<strong><em>Es hat sechs Wochen f\u00fcr detailliertes Feedback gedauert.<\/em><\/strong><\/p>\n<h2>Pl\u00e4ne<\/h2>\n<p>\nKommen wir zur neuesten Entwicklung. Nachdem wir die Feedback-Frage gekl\u00e4rt hatten, haben wir beschlossen, unser eigenes Emulatoren-Farm aufzubauen. Wenn es viele Tests und Emulatoren gibt, ist deren Verwaltung schwierig. Schlie\u00dflich sind alle unsere Emulatoren in ein k8s-Cluster mit flexibler Ressourcenzuweisung umgezogen.<\/p>\n<p>Dar\u00fcber hinaus gibt es auch andere Pl\u00e4ne.<\/p>\n<ul>\n<li><strong>Lint zur\u00fcckbringen<\/strong> (und andere statische Analysen). Wir arbeiten bereits in diese Richtung.<\/li>\n<li>F\u00fchren Sie alle <strong>End-to-End-Tests<\/strong> auf allen SDK-Versionen aus.<\/li>\n<\/ul>\n<p>\nSo haben wir die Entwicklung der Continuous Integration bei Avito verfolgt. Jetzt m\u00f6chte ich einige Ratschl\u00e4ge aus der Perspektive eines Erfahreneren geben.<\/p>\n<h1>Tipps<\/h1>\n<p>\nWenn ich nur einen einzigen Rat geben k\u00f6nnte, dann w\u00e4re das dieser:<\/p>\n<blockquote><p>Bitte seien Sie vorsichtig mit Shell-Skripten!<\/p><\/blockquote>\n<p>\nBash 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.<\/p>\n<p>Es begann alles 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>\nDoch wie bekannt ist, entwickelt sich alles mit der Zeit und wird komplexer \u2013 lassen Sie uns ein Skript aus einem anderen starten und dabei einige Parameter \u00fcbergeben \u2013 letztendlich mussten wir eine Funktion schreiben, die bestimmt, auf welcher Tiefe wir uns gerade im Bash befinden, um die ben\u00f6tigten Anf\u00fchrungszeichen zu setzen, damit alles funktioniert.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/244a574f7b5d3b4fc77cfc4ccda2db69.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nK\u00f6nnten Sie sich den Aufwand f\u00fcr die Entwicklung solcher Skripte vorstellen? Ich empfehle, nicht in diese Falle zu tappen.<\/p>\n<p>Was kann man stattdessen verwenden?<\/p>\n<ul>\n<li>Jede Skriptsprache. Es ist einfacher, in <strong>Python oder Kotlin Script<\/strong> zu programmieren, da es sich um Programmierung und nicht um Skripte handelt.<\/li>\n<li>Oder die gesamte Logik der Builds in Form von <strong>Custom Gradle Tasks<\/strong> f\u00fcr Ihr Projekt zu beschreiben.<\/li>\n<\/ul>\n<p>\nWir haben uns entschieden, die zweite Variante zu w\u00e4hlen, und entfernen jetzt schrittweise alle Bash-Skripte und schreiben viele benutzerdefinierte Gradle-Tasks.<\/p>\n<p><strong>Tipp \u21162: Halten Sie die Infrastruktur in Code.<\/strong><\/p>\n<p>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\u00f6glicht Versionskontrolle. Es wird nicht schwer sein, zur\u00fcckzurollen oder Code auf einem anderen Branch zu erstellen.<\/p>\n<p>Skripte k\u00f6nnen im Projekt gespeichert werden. Aber was ist mit der Umgebung?<\/p>\n<p><strong>Tipp \u21163: Docker kann bei der Umgebung helfen.<\/strong><\/p>\n<p>F\u00fcr Android-Entwickler ist er auf jeden Fall hilfreich, f\u00fcr iOS leider noch nicht.<\/p>\n<p>Dies 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>\nWenn Sie diese Docker-Datei geschrieben haben (ein Geheimtipp, Sie k\u00f6nnten 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\u00fchren k\u00f6nnen.<\/p>\n<p>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\u00f6sung herunterladen).<\/p>\n<p><strong>Tipp Nr. 4: Vergessen Sie nicht, dass \u00dcberpr\u00fcfungen nicht nur um ihrer selbst willen durchgef\u00fchrt werden, sondern f\u00fcr die Menschen.<\/strong><\/p>\n<p>F\u00fcr Entwickler ist schnelles und vor allem verst\u00e4ndliches Feedback von gro\u00dfer Bedeutung: Was ist kaputtgegangen, welcher Test ist fehlgeschlagen, wo kann ich das Build-Log einsehen.<\/p>\n<p><strong>Tipp Nr. 5: Seien Sie pragmatisch bei der Weiterentwicklung der Continuous Integration.<\/strong><\/p>\n<p>Haben Sie ein klares Verst\u00e4ndnis davon, welche Fehlertypen Sie vermeiden m\u00f6chten, wie viele Ressourcen und Zeit Sie bereit sind zu investieren, einschlie\u00dflich der Maschinenlaufzeit. Zu lange \u00dcberpr\u00fcfungen k\u00f6nnen beispielsweise auf die Nacht verschoben werden. Und von jenen, die nicht ganz so wichtige Fehler erkennen, k\u00f6nnen Sie sogar ganz Abstand nehmen.<\/p>\n<p><strong>Tipp Nr. 6: Nutzen Sie fertige Tools.<\/strong><\/p>\n<p>Derzeit gibt es viele Unternehmen, die Cloud-CI anbieten.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" 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 Option. Es ist keine Wartung erforderlich, man bezahlt einfach ein wenig Geld, stellt seine Anwendung zusammen und f\u00fchrt sogar Instrumentierungstests durch.<\/p>\n<p><strong>Tipp Nr. 7: In gro\u00dfen Teams sind interne L\u00f6sungen vorteilhafter.<\/strong><\/p>\n<p>Aber irgendwann, mit dem Wachstum des Teams, werden interne L\u00f6sungen kosteng\u00fcnstiger. 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.<\/p>\n<p>Die Wirtschaft beschreibt unser ganzes Leben, einschlie\u00dflich der kontinuierlichen Integration. Ich habe ein Diagramm der Arbeitsaufw\u00e4nde f\u00fcr jede Phase der Entwicklung unserer kontinuierlichen Integration erstellt.<\/p>\n<p><img decoding=\"async\" alt=\"Die Evolution von CI im Mobile-Entwicklungsteam\" src=\"\/wp-content\/uploads\/2019\/04\/97bf8bff64f3ac9ae587fae195251da2.jpg\" style=\"display:block;margin: 0 auto;\" \/><br \/>\n<br \/>\nEs 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\u00f6\u00dfe erfolgen muss. F\u00fcr ein Team von zwei Personen ist es keine gute Idee, 50 Tage f\u00fcr die Entwicklung eines internen Emulator-Pools aufzuwenden. Aber f\u00fcr ein gro\u00dfes 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.<\/p>\n<p>Wir haben erkannt, dass Automatisierung notwendig ist, weil Menschen teuer sind, Fehler machen und faul sein k\u00f6nnen. Aber auch die Automatisierung wird von Menschen durchgef\u00fchrt. Daher gelten die gleichen Probleme auch f\u00fcr die Automatisierung.<\/p>\n<ul>\n<li>Automatisierung ist teuer. Denken Sie an den Aufwand.<\/li>\n<li>Bei der Automatisierung machen Menschen Fehler.<\/li>\n<li>Manchmal ist es sehr m\u00fchsam, zu automatisieren, da alles auch so funktioniert. Warum also etwas verbessern, warum die ganze Continuous Integration?<\/li>\n<\/ul>\n<p>\nIch 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 \u2013 er wird von automatisierten Tests erkannt. Folglich k\u00f6nnen die Entwickler mehr Zeit mit dem Schreiben von Code und interessanten Dingen verbringen, anstatt lokal etwas zu testen.<\/p>\n<p><strong>Betreiben Sie Continuous Integration. Aber mit Ma\u00df.<\/strong><\/p>\n<blockquote><p>\u00dcbrigens macht Nikolai Nesterov nicht nur selbst gro\u00dfartige Pr\u00e4sentationen, sondern ist auch im Programmkomitee. <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\">AppsConf<\/a><\/noindex> und hilft anderen, wertvolle Beitr\u00e4ge f\u00fcr Sie vorzubereiten. Die Vollst\u00e4ndigkeit und N\u00fctzlichkeit des Programms der n\u00e4chsten Konferenz kann anhand der Themen im <noindex><a rel=\"nofollow\" href=\"https:\/\/appsconf.ru\/moscow\/2019\/schedule\">Zeitplan<\/a><\/noindex>. Kommen Sie am 22. und 23. April ins Infopreneurium f\u00fcr weitere Details.<\/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 4.9.10 - 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. \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\" \/>\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) 4.9.10\" \/>\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. \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\" \/>\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. Die Erfolgsfaktoren in der Teamarbeit lassen sich in einem einfachen Schema darstellen. Nachdem Sie Code geschrieben haben, sollten Sie sicherstellen, dass er: Funktioniert. Nichts kaputt macht, einschlie\u00dflich des Codes, den Ihre Kollegen geschrieben haben. Wenn beide Voraussetzungen erf\u00fcllt sind, sind Sie auf dem richtigen Weg zum Erfolg. \u00dcberpr\u00fcfen Sie diese Kriterien einfach und leisten Sie keine Abstriche.","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. \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","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"},"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}]}}