Bitrise CI für iOS optimal nutzen - projektübergreifende Konfigurationen

6. September 2019

Mit der Zeit stecken wir viel Arbeit in die CI-Setups unserer Projekte. Die bisherigen Punkte aus Teil 1 und Teil 2 dieser Artikelserie decken nur die Grundlagen ab, um den Einstieg zu erleichtern. Tatsächlich gibt es eine Vielzahl weiterer Funktionen und Möglichkeiten, die man mit einem CI machen kann.

Um sich ein Bild von den Möglichkeiten zu machen, öffnet einfach die Step-Bibliothek, indem ihr auf einen der "+"-Buttons zwischen euren bestehenden Steps klickt und entdeckt, was dort möglich ist. Es gibt auch ein offizielles Community-Forum von Bitrise, in dem ihr viele How-Tos für verschiedene Aufgaben finden, Fragen stellen und neue Funktionen anfordern könnt.

So wie wir es mit unserem Code tun, wäre es toll, unsere Konfigurationen über mehrere Projekte hinweg wiederzuverwenden. Bei der Konfiguration des "deploy"-Workflows ist euch vielleicht schon aufgefallen, dass wir einige Step-Details kopieren und einfügen mussten. Aber keine Sorge, Bitrise hat eine Lösung für beide Probleme:

  1. Alle Konfigurationsdetails werden in einer YAML-Konfigurationsdatei gespeichert.
  2. Utility-Workflows können innerhalb von Workflows definiert und zusammengestellt werden.
  3. Darüber hinaus können Environment-Variablen wie Secret Vars definiert werden.

Um das alles besser zu verstehen, werfen wir einen Blick auf unser aktuelles Setup. Dazu öffnen wir die Konfigurationsdatei, indem wir auf den Tab .bitrise.yml klicken:

Alt text

Beachtet, dass wir in der YAML-Datei die gesamte Konfiguration der trigger_map sowie eine Liste der workflows sehen können, die mit deploy beginnen, und alle auszuführenden Steps enthält. Außerdem ist neben jedem Step eine Version angegeben (z.B. @4.0.3) und einige haben auch zusätzliche Optionen konfiguriert. Da wir alle Elemente dieser YAML-Datei mit dem Workflow-Editor bearbeiten können, müsst ihr nicht alle Details der Konfigurationsdatei genau verstehen oder die Datei manuell bearbeiten. Tatsächlich könntet ihr den gesamten Inhalt der YAML-Datei aus diesem Projekt kopieren, in ein anderes Projekt einfügen und dann speichern. Das würde ohne Weiteres funktionieren.

Allerdings hat das andere Projekt natürlich einen anderen Namen und kann sich auch in anderer Weise unterscheiden, wie z.B. im Pfad zur Datei Info.plist. Aber genau wie beim Programmieren können wir diese Unterschiede in (Umgebungs-)Variablen extrahieren und für jedes Projekt unterschiedliche Werte festlegen, während die Konfiguration gleich bleibt. In der Tat erstellt Bitrise standardmäßig ein paar Umgebungsvariablen für uns, die wir sehen können, wenn wir in der YAML-Datei ganz nach unten scrollen:

Alt text

Dort sind bereits drei Variablen definiert, nämlich BITRISE_PROJECT_PATH, BITRISE_SCHEME und BITRISE_EXPORT_METHOD. Wenn ihr euch an den Anfang dieses Artikels erinnert, sind das genau die drei Dinge, die Bitrise uns gefragt hat, als wir unser Projekt zum ersten Mal erstellt haben. Hier wurden sie also festgelegt. Eine Übersicht über alle Umgebungsvariablen erhalten wir auch, wenn wir im Workflow-Editor zur Registerkarte Env Vars wechseln:

Alt text

Wie ihr seht, sind sie alle gleich. Und wenn wir uns die beiden folgenden Abschnitte ansehen, sehen wir auch, dass es sogar möglich ist, einige Variablen in bestimmten Workflows bei Bedarf zu überschreiben. Seid euch jedoch bewusst, dass Umgebungsvariablen ein von den geheimen Variablen unabhängiges Konzept sind. Obwohl sie sehr ähnlich verwendet werden (einfach durch Voranstellen von $), werden geheime Variablen nirgendwo angezeigt, nicht einmal in der YAML-Konfigurationsdatei. Umgebungsvariablen hingegen sollen in Build-Logs und in der YAML-Datei angezeigt werden. Deshalb listet der Workflow-Editor die geheimen Variablen im Tab "Secrets" auch separat auf:

Alt text

Dort sehen wir die beiden Variablen, die wir bei der Konfiguration des Deployments hinzugefügt haben. Um unsere YAML-Konfiguration so wiederverwendbar wie möglich zu machen, sollten wir alle Optionen, die sich von Projekt zu Projekt ändern können, in Umgebungsvariablen extrahieren. Ein Beispiel ist der Pfad zur Datei Info.plist, den wir im Step "Set Xcode Project Build Number" konfiguriert haben. Der einfachste Weg, sie in eine Umgebungsvariable zu extrahieren, besteht darin, die Registerkarte "Env Vars" zu öffnen, im oberen Bereich auf die Schaltfläche "Add new" zu klicken, einen Namen zu vergeben (z.B. INFO_PLIST_PATH) und den Wert festzulegen ($BITRISE_SOURCE_DIR/App/SupportingFiles/Info.plist):

Alt text

Als nächstes öffnen wir die Steps, in denen wir diesen Wert bereits verwendet haben, löschen ihn dort und klicken auf die Schaltfläche "Insert Variable" (die nur erscheint, wenn das Textfeld ausgewählt ist). Dann wählen wir dort unsere neue Variable INFO_PLIST_PATH aus.

Alt text

Großartig. Eine weitere Möglichkeit, unsere YAML-Datei leichter wiederverwendbar zu machen, besteht darin, unsere Steps zukunftssicher zu gestalten. Da Steps bei Bitrise Fehler aufweisen oder veraltet sein können, werden sie von Zeit zu Zeit versioniert und aktualisiert. (Eigentlich wird diese Arbeit sogar öffentlich auf GitHub erledigt.) Um immer die neueste Version eines Steps zu nutzen, müssen wir unsere Version auf "always latest" setzen. Dies muss für jeden Step einzeln durchgeführt werden, aber es ist eine schnelle Angelegenheit, die sich lohnt. Also, lasst uns das machen und speichern.

Alt text

Zuletzt möchten wir noch die Redundanz innerhalb unserer Konfigurationsdatei reduzieren, indem wir wiederverwendbare Teile in ihre eigenen Workflows extrahieren. Es wäre aber nicht sinnvoll, diese Teile nur für sich allein als Workflows auszuführen. Zudem muss dem Workflow-Editor angezeigt werden, dass sie hier sind, um innerhalb von Workflows erstellt zu werden. Deshalb gibt Bitrise uns die Möglichkeit, so genannte "Utility-Workflows" zu erstellen, indem wir ihren Namen einfach mit einem Unterstrich (_) beginnen. Beginnen wir mit dem Extraktionsprozess mit den drei Aufgaben, die wir normalerweise am Anfang immer benötigen, indem wir einen Utility-Workflow namens "_begin" erstellen, der auf dem bestehenden Workflow "deploy" basiert:

Alt text

Dies führt zu einem neuen Workflow mit den gleichen Steps wie beim Workflow "deploy". Wir wollen aber nur die ersten drei Steps behalten, also klicken wir die anderen Steps nacheinander an und löschen sie mit dem Button oben rechts oder dem riesigen Button ganz unten (beide machen das Gleiche):

Alt text

Die drei Steps unseres neuen Utility-Workflows sind jedoch immer noch Teil der "primären" und "deployten" Workflows. Also öffnen wir diese beiden Workflows nacheinander und löschen die drei Steps dort. Nun klicken wir innerhalb von "primary" oben auf den Button "Add Workflow before" und wählen "_begin".

Alt text

Man beachte, dass der "_begin"-Workflow nun direkt im "primären" Workflow angezeigt und ausgegraut wird, um anzuzeigen, dass er nicht direkt Teil des aktuellen Workflows ist. Wiederholen wir die gleichen Schritte für den Workflow "deploy" und speichern ihn. Auf diese Weise können wir noch weitere Steps in kombinierbare Utility-Workflows extrahieren. Berücksichtigt dabei, dass Utility-Workflows entweder vor oder nach den "normalen" Workflow-Steps platziert werden können. Oben gibt es auch einen "Neu anordnen"-Button, mit dem ihr umsortieren könnt. Ein gutes Set von Utility-Workflows zum Einstieg ist das folgende:

  1. _begin: Übernimmt alle Vorarbeiten, die in jedem Workflow erforderlich sind.
  2. _run-linters: Lässt alle Linter laufen, um den Codestil durchzusetzen und Konflikte zu vermeiden.
  3. _prepare-build: Installiert alle Abhängigkeiten wie Tools & Frameworks.
  4. _run-tests: Führt Unit- und UI-Tests durch und schlägt fehl, wenn einer von ihnen nicht erfolgreich ist.
  5. _upload-to-connect: Erstellt ein Archiv und lädt es in App Store Connect hoch.
  6. _end: Erledigt alle Aufräumarbeiten, die in jedem Workflow erforderlich sind.

Die ersten fünf sollten eingerichtet werden, bevor der eigentliche Workflow läuft, der letzte sollte danach durchgeführt werden. Auf diese Weise beinhalten die beiden oben definierten Workflows im Grunde keine eigenen Steps mehr, sondern bestehen nur noch aus komponierten Utility-Workflows, was auch ihre YAML-Definition viel lesbarer macht:

    primary: 
        before_run: 
        - _begin 
        - _run-linters 
        - _prepare-build 
        - _run-tests 
        after_run: 
        - _end

    deploy: 
        before_run: 
        - _begin 
        - _prepare-build 
        - _upload-to-connect 
        after_run: 
        - _end

Wie wir feststellen, besteht der einzige Unterschied darin, dass der "primary" Workflow Linter und Tests ausführt, die wir für "deploy" beide überspringen. Stattdessen fügen wir dort den _upload-to-connect Utility-Workflow hinzu. Wenn wir uns jemals entscheiden sollten, dass wir unsere Tests auch im Workflow "deploy" ausführen müssen, könnten wir das mit einer einzigen Textzeile konfigurieren. Optisch sieht das Ergebnis so aus:

Alt text

Nachdem unsere YAML-Konfigurationsdatei nun wirklich wiederverwendbar ist, ist das Teilen umso wirkungsvoller geworden. Um dies zu beweisen, ist hier ein GitHub Gist mit der endgültigen Konfigurationsdatei unseres Beispielprojekts, das wir gemeinsam konfiguriert haben:

    format_version: '7'
    default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
    project_type: ios
    trigger_map:
    - push_branch: stable
      workflow: primary
    - pull_request_source_branch: "*"
      workflow: primary
    - tag: "*"
      workflow: deploy
    workflows:
      deploy:
        before_run:
        - _begin
        - _prepare-build
        - _run-tests
        - _upload-to-connect
        after_run:
        - _end
      primary:
        before_run:
        - _begin
        - _run-linters
        - _prepare-build
        - _run-tests
        after_run:
        - _end
      _begin:
        steps:
        - activate-ssh-key:
            run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}'
        - git-clone: {}
        - cache-pull: {}
      _run-linters:
        steps:
        - swiftlint:
            inputs:
            - strict: 'yes'
            - linting_path: "$BITRISE_SOURCE_DIR"
        before_run: []
      _prepare-build:
        steps:
        - script:
            title: Install Dependencies using Accio
            inputs:
            - content: |-
                #!/usr/bin/env bash

                # fail step if any command fails (-e) & debug log (-x)
                set -e -x

                # install Accio
                brew tap JamitLabs/Accio https://github.com/JamitLabs/Accio.git
                brew install accio

                # install dependencies using Accio
                accio install
        - certificate-and-profile-installer: {}
        before_run: []
      _run-tests:
        steps:
        - xcode-test:
            inputs:
            - project_path: "$BITRISE_PROJECT_PATH"
            - scheme: "$BITRISE_SCHEME"
        before_run: []
      _upload-to-connect:
        steps:
        - set-xcode-build-number:
            inputs:
            - plist_path: "$INFO_PLIST_PATH"
        - xcode-archive:
            inputs:
            - project_path: "$BITRISE_PROJECT_PATH"
            - scheme: "$BITRISE_SCHEME"
            - export_method: "$BITRISE_EXPORT_METHOD"
        - deploy-to-itunesconnect-application-loader:
            title: Deploy to App Store Connect - Application Loader
            inputs:
            - app_password: "$APPLE_ID_PASSWORD"
            - itunescon_user: "$APPLE_ID"
        before_run: []
      _end:
        steps:
        - deploy-to-bitrise-io: {}
        - cache-push:
            inputs:
            - cache_paths: |-
                $BITRISE_CACHE_DIR
                ~/Library/Caches/Accio/Cache -> Package.resolved
        before_run: []
    app:
      envs:
      - opts:
          is_expand: false
        BITRISE_PROJECT_PATH: NewProjectTemplate.xcodeproj
      - opts:
          is_expand: false
        BITRISE_SCHEME: App
      - opts:
          is_expand: false
        BITRISE_EXPORT_METHOD: app-store
      - INFO_PLIST_PATH: "$BITRISE_SOURCE_DIR/App/SupportingFiles/Info.plist"
        opts:
          is_expand: false

Einfach kopieren, in euer eigenes iOS-Projekt einfügen und die Umgebungs- und Geheimvariablen auf die richtigen Werte eurer App setzen. Wir bereiten sogar ähnliche YAML-Konfigurationsdateien für Swift Framework-Autoren und sogar für Android vor, die ihr mit eurem Team teilen könnt. Wir hoffen, dass uns dies in Zukunft viel Zeit sparen wird. Wir freuen uns über Feedback und Verbesserungsvorschläge:

JamitLabs/BitriseTemplates

Ich hoffe, dass diese Artikel-Serie euch hilft, mit CI in euren iOS-Projekten zu beginnen! 🤖 ✅