Bitrise CI für iOS optimal nutzen - automatisiert und schnell

30. August 2019

Nachdem wir das Basis-Setup erfolgreich abgeschlossen haben, können wir uns nun daran machen, wiederkehrende Aufgaben zu automatisieren und schneller zu machen. Insbesondere soll um diese Punkte gehen:

  • SwiftLint konfigurieren, um nach Fehlern im Codestil zu suchen
  • Builds auf PRs und Code-Pushes automatisch ausführen lassen
  • Build-Ergebnisse an GitHub übermitteln, damit wir sie dort sehen können
  • Abhängigkeiten im Cache hinterlegen, um nachfolgende Builds zu beschleunigen
  • Einen archivierten Build erstellen und bei Tag-Pushes in App Store Connect hochladen

Wie ihr eure CI-Konfigurationen projektübergreifend wiederverwenden könnt, erfahrt ihr hier bald in einem weiteren Artikel.

Builds auf PRs und Code Pushes ausführen

Um zu konfigurieren, wann ein Build automatisch auf Bitrise getriggert werden soll, müssen wir zuerst die Trigger in unserem Workflow festlegen. Öffnet dazu den Workflow-Editor und navigiert zur Registerkarte Triggers. Dort werdet ihr sehen, dass es bereits einen Trigger für Pushes auf * - also für Pushes auf einen beliebigen Branch - unter Verwendung des primary Workflows enthält. Das wollen wir aber nicht, wir wollen nur Builds für Pushes auf den stable Branch triggern. Also lasst uns das korrigieren, indem wir auf den Abschnitt Push branch klicken und es zu unserem Development Branch namens stable ändern.

Alt text

Wechseln wir nun zur Registerkarte PULL REQUEST. Hier können wir sogar angeben, von welchem zu welchem Zweig die PRs Builds veranlassen sollen. Standardmäßig löst es wiederum Builds für alle PRs aus. Das funktioniert gut für unsere Anforderungen, also behalten wir es bei. Wir speichern (wegen der vorherigen Änderung) und machen weiter. Jetzt, wo die Trigger gesetzt sind, könnten wir denken, wir wären fertig. Aber das stimmt nicht ganz. Bitrise muss noch irgendwie informiert werden, dass wir neue Code-Pushes gemacht oder PRs auf GitHub erstellt/aktualisiert haben. Dazu fügen wir Webhooks auf GitHub hinzu. Um den Webhook-Link in GitHub zu konfigurieren, schließen wir den Workflow-Editor, klicken auf unseren Projektnamen im oberen Pfad, um zur Projektübersicht zu gelangen, wählen die Registerkarte Code und klicken auf SETUP MANUALLY.

Alt text

Dort haben wir die Webhook-URL, die auf GitHub (oder einem von euch gewählten Dienst) zu konfigurieren ist. Beachtet, dass Bitrise eine Schritt-für-Schritt-Anleitung für verschiedene Dienste anbietet, die direkt unter der Webhook-URL bequem verlinkt sind. Sobald unser Webhook gesetzt ist, sollten die Trigger wie erwartet funktionieren.

Übermitteln der Ergebnisse an GitHub

Wenn wir schon dabei sind, können wir auch gleich noch den umgekehrten Informationsfluss einrichten. Das bedeutet, dass GitHub Informationen über Build-Ergebnisse auf Bitrise erhält. Mit dieser Konfiguration können wir direkt in der GitHub-Historie sehen, ob ein bestimmter Build auf dem CI erfolgreich war oder nicht und erfahren auch, ob Build-Fehler bei Pull-Requests auftreten. Beachtet, dass Projekte auf GitHub so konfiguriert werden können, dass PRs nur dann gemergt werden können, wenn das CI durchgeht. Diese Einstellung ist besonders bei Open-Source-Projekten zu empfehlen.

Dafür müssen wir den so genannten "Service Credential User" unseres Projekts mit GitHub verbinden. Um diesen User zu überprüfen oder zu konfigurieren, müssen wir unsere Projekteinstellungen öffnen und die Registerkarte "Team" aufrufen.

Alt text

In unserem Fall wird der " Service Credential User " für den Eigentümer, d.h. unseren Account, konfiguriert. Wenn wir auf den Button "Test the git connection ...." klicken, erhalten wir den folgenden Fehler:

Error: We found an issue with the connected service user's Github account connection. Issue: no GitHub token found for user. For more information check out this guide.`

Wenn wir den Link zu dem im Fehler angegebenen Leitfaden anklicken, finden wir einige Hinweise zur Fehlerbehebung. Aber da wir unser Konto noch nicht mit einem GitHub-Konto verknüpft haben, wird das natürlich nicht funktionieren. Um unser Konto zu verlinken, müssen wir zu unseren "Kontoeinstellungen" gehen, indem wir diese in der Dropdown-Liste oben rechts auf dem Bildschirm auswählen. Dadurch werden die Einstellungen geöffnet, bei denen wir auf der linken Seite die GitHub-Verbindung aktivieren müssen.

Alt text

Jetzt sollten die Ergebnisse gesendet werden können. Wenn nicht, lest die obige Anleitung erneut durch, um weitere Tipps zur Fehlerbehandlung zu erhalten. Einmal konfiguriert, seht ihr etwas in der Art in euren Pull-Requests, einschließlich eines direkten Links zum fehlerhaften Build:

Alt text

Sobald die Branch Protection Rules so konfiguriert sind, dass das CI erfolgreich verlaufen muss, wird die "Merge Pull Request" deaktiviert oder rot markiert (für Administratoren). Weitere Informationen zu solchen Regeln finden sich in der Dokumentation eures Git-Anbieters, z.B. auf dieser Seite für GitHub.

Abhängigkeiten im Cache für schnellere nachfolgende Builds speichern

Standardmäßig baut das CI unser gesamtes Projekt immer von Grund auf neu und führt dementsprechend alle Tests in einem Clean State, ohne zwischengespeicherte Artefakte, durch. Dies ist eigentlich auch das gewünschte Verhalten, um genau zu testen, ob unser Projekt noch funktioniert - selbst wenn es von jemandem, der zu unserem Team hinzugekommen ist, neu ausgecheckt wurde. Aber ein Punkt, an dem dieses Verhalten sehr lästig werden kann, ohne einen Mehrwert zu bieten, sind die Abhängigkeiten: Auch diese werden standardmäßig jedes Mal neu erstellt, wenn das CI einen Build ausführt - selbst wenn sie unverändert sind. Daraus ergeben sich potenziell sehr hohe Buildzeiten. Und da Framework-Autoren ihre Projekte (hoffentlich) separat testen, um sicherzustellen, dass alles weiterhin korrekt gebaut wird, sind Buildzeiten für Abhängigkeiten wirklich nur verschwendete Zeit (und Geld). Entsprechend wäre es gut, sie so weit wie möglich zu minimieren - was bei Bitrise eine einfache Aufgabe ist: Da sich die Umgebung eines Builds standardmäßig immer in einem Clean State befindet, müssen wir sicherstellen, dass die Build-Produkte unserer Abhängigkeiten bei einem nachfolgenden CI-Build intakt bleiben. Um dies zu erreichen, verfügt Bitrise über zwei Steps, die gemeinsam verwendet werden müssen:

  1. Cache:Push - Dieser Befehl lädt den Inhalt der angegebenen Verzeichnisse an einen gemeinsamen Speicherort (Amazon S3) hoch, der über mehrere CI-Builds hinweg verfügbar ist.
  2. Cache:Pull - Dieser Befehl lädt den Inhalt des zwischengespeicherten Verzeichnisses vom gemeinsamen Speicherort (Amazon S3) herunter.

Damit dies funktioniert, müssen wir die Build-Verzeichnisordner unseres bevorzugten Dependency-Managers ermitteln und über Cache:Push hochladen, nachdem der Step des Dependency-Managers abgeschlossen ist. Zusätzlich müssen wir sie über Cache:Pull herunterladen, bevor der Step des Dependency-Managers beginnt. Praktischerweise beinhalten die von Bitrise bereitgestellten Standard-Steps bereits diese Steps und sind auch schon in der richtigen Reihenfolge angeordnet:

Alt text

Der einfachste Weg, das gewünschte Verzeichnis zum Push-Step hinzuzufügen, ist, eine neue Zeile zum Textfeld der Cache paths mit Accios Build Product Output Directory hinzuzufügen, das ~/Library/Caches/Accio/Cache ist. Um zusätzlich zu verhindern, dass die angegebenen Verzeichnisse erneut hochgeladen werden, obwohl keine Änderungen an den Abhängigkeiten vorgenommen wurden, können wir eine Datei angeben, die die aufgelösten Versionen unserer Abhängigkeiten enthält. Diese Datei ist ein guter Indikator für Änderungen an Abhängigkeiten. Solange sich die Datei nicht ändert, muss der Cache nicht erneut hochgeladen werden. Wir können dies bei Bitrise festlegen, indem wir mit einem -> das angegebene Verzeichnis auf die aufgelöste Versionsdatei verweisen, wie zum Beispiel hier: ~/Library/Caches/Accio/Cache -> Package.resolved Fügen wir also diese Zeile hinzu; anschließend speichern und bauen wir. Bei diesem ersten nächsten Build sehen wir noch keine Verbesserungen der Buildzeit. Aber wir können sehen, dass das angegebene Verzeichnis gecacht und erfolgreich hochgeladen wurde.

Alt text

Hier ist die Gesamtübersicht des Builds. Achtet darauf, wie lange die Installation der Abhängigkeiten braucht:

Alt text

Nun builden wir erneut, nachdem unser Cache erfolgreich hochgeladen wurde:

Alt text

Wir können sehen, dass im Step Cache:Pull nun tatsächlich etwas heruntergeladen wurde (was 6 Sekunden brauchte). Desweiteren ist unser Step zur Installation von Abhängigkeiten viel schneller. Er reduziert sich von 12 Minuten auf nur noch etwa 3 Minuten (was vor allem auf die Installation des Dependency Management Tools selbst zurückzuführen ist). Außerdem lädt der Step Cache:Push den Cache nicht erneut hoch, da sich der Inhalt unserer Package.resolved-Datei nicht geändert hat, was 2 zusätzliche Sekunden pro Build spart. Das mag nicht beeindruckend klingen, aber der Unterschied wird mit der Anzahl unserer Abhängigkeiten bedeutender werden, also ist es gut, das eingerichtet zu haben. Insgesamt konnten wir dank Caching unsere Buildzeit von ~15 Minuten auf nur ~6 Minuten reduzieren. Eine großartige Gesamtverbesserung von ~60 Prozent bei nur einer einzigen Zeilenerweiterung der Konfiguration! 🎉

Hochladen von markierten Commits in App Store Connect

Um unsere App automatisch in App Store Connect zu deployen, können wir einen der vielen integrierten Steps verwenden, die dies für uns tun - sei es für Beta-Testzwecke per Testflight oder für aktuelle Neuerscheinungen im App Store. In unserem Fall verwenden wir den Step "Deploy to iTunes Connect - Application Loader", der auf dem Application Loader basiert. (Es gibt auch einen ohne den Zusatz "Application Loader", der auf Fastlane basiert).

Aber wo sollen wir diesen Step einfügen? Es gibt noch keinen archivierten Build, der von einem der vorhandenen Steps in unserem Workflow ausgeführt wurde. Und selbst wenn wir einen hätten, würde unser aktueller Workflow auch dann ausgeführt, wenn Code-Pushes oder PRs nach unseren Triggerregeln durchgeführt würden. Aber wir wollen unsere App nur bei getaggten Commit-Pushes deployen. Und zu guter Letzt muss auch die Code Signatur im CI vorhanden sein, damit der Upload funktioniert. Deshalb müssen wir drei Dinge tun, um das zum Laufen zu bringen:

  • Konfigurieren eines neuen Workflows ausschließlich für markierte Commits
  • Konfigurieren eines neuen tagbasierten Triggers, um unseren neuen Workflow zu starten.
  • Hochladen der korrekten Zertifikate und Provisionierungsprofile

Praktischerweise hat Bitrise bereits standardmäßig einen neuen Workflow namens "deploy" für uns erstellt. Wir finden ihn, indem wir oben links auf die Dropdown-Liste "Workflow" klicken, in der derzeit der "primäre" Workflow ausgewählt ist:

Alt text

Beachtet, dass Bitrise in diesem Workflow bereits einen Step für einen archivierten Build für uns gesetzt hat. Aber seid euch bewusst, dass sowohl für "Do anything with Script step" als auch für "Cache:Push" immer noch die Standardeinstellungen gelten, da sie unabhängige Instanzen der Steps sind, die wir zuvor im "primary" Workflow konfiguriert haben. Daher müssen wir sie wie zuvor neu konfigurieren, um die gleiche Funktionalität (Dependency Manager & Caching) in unserem Workflow "deploy" zu haben. Tun wir das, indem wir die Konfiguration aus dem "primary" Workflow kopieren und einfügen. Zusätzlich fügen wir auch den neuen Step "Deploy to iTunes Connect - Application Loader" hinzu, um die archivierten Build-Produkte auf App Store Connect hochzuladen. Denkt daran, den Scriptstep umzubenennen und wenn ihr schon dabei seid, korrigiert auch gleich noch den Namen "iTunes Connect" zu "App Store Connect":

Alt text

Damit das Deployment funktioniert, müssen wir einige Anmeldeinformationen für den Step angeben. Wie ihr sehen könnt, ist das Feld "Apple ID" als Pflichtfeld markiert und es gibt zwei verschiedene Passwortfelder. Wir müssen zumindest eine der beiden Angaben machen: Unter "Passwort" könnten wir einfach unser Apple ID-Passwort eingeben und Bitrise diese sensiblen Daten anvertrauen. Da wir die 2-Faktor-Authentifizierung auf unserer Apple ID aktiviert haben, könnten wir das durchaus tun. Aber es gibt noch eine sicherere Variante: Unter "Application Specific Password" können wir ein Passwort zur Verfügung stellen, das allein für diesen Service gilt und separat von unserem eigentlichen Passwort verwaltet werden kann. Eine Schritt-für-Schritt-Anleitung wie man eins erstellt, findet sich hier.

Bitte berücksichtigt, dass es eine gute Idee ist, zusätzlich einen *separaten Account** in eurem Team auf App Store Connect mit der Rolle "App Manager" für automatisierte Build-Uploads zu erstellen. Die Vergabe von Administrator-Zugriffsrechten für Drittanbieter-Dienste sollte nach Möglichkeit verhindert werden, selbst wenn Sicherheitsmaßnahmen wie 2FA aktiviert sind.*

Bevor ihr eure Zugangsdaten für den Step direkt eingebt: Achtet bitte darauf, dass sich unter dem Textfeld "Apple ID" eine riesige lilafarbene Schaltfläche " Select secret variable " befindet. Dies ist ein Sicherheitsmerkmal von Bitrise, das sicherstellt, dass sensible Daten in den Steps nicht an den falschen Stellen gespeichert und preisgegeben werden. Stattdessen wird empfohlen, in den Steps "secrect variables" zu verwenden und ihre Werte an anderer Stelle zu definieren. So wird sichergestellt, dass Sicherheitsmaßnahmen getroffen und die Werte nicht an Stellen wie z.B. dem Build Log angezeigt werden. Wenn wir auf den Button klicken, erhalten wir ein Formular, in dem wir direkt eine neue geheime Variable erstellen können, nennen wir sie APPLE_ID:

Alt text

Mit einem Klick auf "Add new" erstellt Bitrise sowohl die geheime Variable für uns als auch den Variablenbezug $APPLE_ID in das Apple ID Textfeld. Ebenso konfigurieren wir auch das "Application Specific Password" (die Schaltfläche erscheint, sobald wir den Cursor in das Textfeld setzen), wir nennen es APPLE_ID_PASSWORD und klicken auf "Add new". Das war's für diesen Step. Ein zweiter Step, den wir unserem Workflow hinzufügen sollten, heißt "Set Xcode Project Build Number". Er sollte direkt über "Xcode Archive" platziert werden und stellt sicher, dass wir nie die gleiche Buildnummer zweimal hochladen. Wenn wir das täten, würden wir während des Upload-Prozesses einen Fehler erhalten, der einen doppelten Upload-Fehler angibt. Das Hinzufügen dieses Steps verhindert dies, indem die Build-Nummer auf die Bitrise-Build-Nummer gesetzt wird, die kontinuierlich ab 1 nach oben gezählt wird. Damit es funktioniert, müssen wir den Pfad zu unserer Info.plist-Datei angeben, die in unserem Beispiel $BITRISE_SOURCE_DIR/App/SupportingFiles/Info.plist ist:

Alt text

Nun speichern wir unsere Änderungen und sind mit der Konfiguration des "deploy"-Workflows fertig. Als nächstes konfigurieren wir den Build-Trigger für alle markierten Commits. Dazu öffnen wir im Workflow-Editor die Registerkarte "Triggers" und wechseln in den TAG-Bereich:

Alt text

Wichtig: Es gibt bereits einen von Bitrise vorkonfigurierten Trigger für das Tag * (d.h. alle Tags), der den "deploy"-Workflow automatisch startet. Das ist genau das, was wir brauchen! Lasst uns mit der Code-Signatur fortfahren. Weil es sehr aufwendig sein kann, die richtigen Zertifikate und Provisioning-Profile für ein bestimmtes Target in einem iOS-Projekt zu finden, stellt Bitrise ein Kommandozeilen-Skript zur Verfügung, das uns viel Zeit spart. Der einfachste Weg das Skript zu finden, ist, die Stelle zu öffnen, an der wir am Ende sowieso unsere Codesignatur konfigurieren müssen. Also wählen wir im Workflow-Editor die Registerkarte "Code Signing". Dort sehen wir oben das Skript, das wir ausführen müssen.

Alt text

Um es zu verwenden, öffnen wir unsere Befehlszeile auf unserem Mac-Entwicklungsgerät, ändern das Arbeitsverzeichnis in das Root-Verzeichnis unseres Projekts (mit der .xcodeproj-Datei), kopieren und fügen die obige Zeile ein und drücken Enter. Das Skript lädt automatisch das codesigntool von Bitrise herunter und leitet uns schrittweise an, die richtigen Code-Signaturdateien für den Upload zu erkennen. Wir müssen nur den Anweisungen folgen. Als Ergebnis erhalten wir eine Reihe von Dateien, die wir einfach per Drag & Drop in die entsprechenden Abschnitte auf die bereits geöffnete Registerkarte in Bitrise ziehen können. Nachdem auch das eingerichtet ist, sind wir fertig. Neue Tag-Pushes sorgen nun automatisch dafür, dass ein neuer Build erfolgreich in App Store Connect bereitgestellt wird. 🎉