React Native Build Pipeline - mit GitHub-Actions und EAS
Eine How-to Anweisung für eine automatisierte Build-Pipeline mit Codebeispielen, Tipps und Semantic Versioning.

Ausgangssituation
In meinem aktuellen Projekt muss ich eine native iOS-App entwickeln. Ich habe mich für React Native mit Expo als Tech-Stack entschieden. Dank meiner Erfahrung mit TypeScript, React und Redux Stores kann ich sofort loslegen. Außerdem kann ich bei Bedarf problemlos Android-Apps erstellen.
Der Schwerpunkt dieses Artikels liegt auf der Automatisierung der Build-Pipeline. Als Code-Repository verwende ich GitHub und damit GitHub-Actions für die Build-Pipeline. Ich muss die Anwendung über das MDM-System (Mobile Device Management) meines Kunden verteilen. Daher benötige ich ein versioniertes Build-Artefakt, das ich in das MDM-System hochladen kann.
Voraussetzung ist die Einrichtung des iOS-Zertifikats und die Verwendung der Apple-Dev-Tools. Auch dies kann automatisiert werden, ich möchte mich jedoch auf den Build selbst konzentrieren.
Zielsetzung
Vor der Automatisierung muss ich meine Ziele definieren. Los geht's:
- Für jeden Push im Main-Branch eine neue Version erstellen.
- Semantische Versionierung zur Bestimmung der neuen App-Version verwenden.
- Eine versionierte IPA-Datei als Build-Artefakt erzeugen.
Lösung
Schritt 1: Continuous Integration
Ich habe in meinem Repository eine ci.yml
, die auf allen Branches ausgeführt wird, sodass die Qualitätsprüfungen auch die Feature-Zweige abdecken.
name: CI
on:
push:
branches:
- "**"
Qualitätsprüfungen
Vor der App-Entwicklung habe ich einige Schritte zur Qualitätssicherung durchgeführt:
- Auditierung der NPM-Pakete mit
better-npm-audit
. Dieser Wrapper um das standardmäßigenpm audit
ermöglicht es mir bei Bedarf, Schwachstellen zu ignorieren, indem ich dies in der Datei.nsprc
konfiguriere. - Rechtschreibprüfung mit
cspell
. Tippfehler, z. B. in Variablennamen, führen oft zu schwerwiegenden Fehlern. Ich kann neben Englisch mehrere Sprachen konfigurieren, falls mein Code unter anderem deutsche Textpassagen enthält. Zusätzlich kann ich ein Wörterbuch mit bekannten Wörtern alscspell-words.txt
hinzufügen. - Lint-Prüfung des Codes mithilfe meiner Lint-Regeln, einschließlich
prettier
, die für jedes gute Projekt obligatorisch sind. Dies vermeidet Diskussionen über die Code-Formatierung. Der Standard wird einfach angewendet. - TypeScript-Prüfung. Ich verwende TypeScript für Autovervollständigung, einfacheres Refactoring und als ein Sicherheitsnetz zur Compile-Time.
- (Unit-)Tests stellen sicher, dass alles wie erwartet funktioniert. Dies ist Teil meiner testgetriebenen Philosophie und hilft beim Refactoring, Nebeneffekte zu erkennen.
jobs:
initial_checks:
name: Verify build and prepare cache
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Read .nvmrc
run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT
id: nvm
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
- name: Cache dependencies
id: cache
uses: actions/cache@v4
with:
path: |
./node_modules
!node_modules/.cache
# The hashFiles function generates a unique key for the cache based on the contents of the package-lock.json file.
# This ensures that the cache is only invalidated when the dependencies of the project change.
key: ${{ runner.os }}-node-modules-${{ hashFiles('package-lock.json') }}
# 'restore-keys' are fallback keys used if a cache miss occurs on the 'key' field. They provide a way to restore
# a cache from a previous state that doesn't match the 'key' exactly. They are checked sequentially until a cache hit is found.
# A list of restore keys is useful when restoring a cache from another branch because restore keys can partially match cache keys.
restore-keys: |
${{ runner.os }}-node-
${{ runner.os }}-
- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci --ignore-scripts
- name: NPM Audit
run: npm run audit
- name: Run spell check
run: npm run cspell:lint
- name: Run ESLint
run: npm run lint
- name: Run Typescript checks
run: npm run ts
- name: Run Tests
env:
NODE_OPTIONS: "--max_old_space_size=8192"
run: npm run test -- --ci --maxWorkers=2
Tagging
In meine Main-Branch tagge ich meinen Code gemäß den Zielen mit einem semantischen Release-Ansatz. Für eine React Native + Expo-Anwendung muss ich sicherstellen, dass alle relevanten Dateien aktualisiert werden. Dazu gehören:
package.json
und die zugehörige Sperrdatei mit der neuesten Version.app.json
aus der Expo → Die korrekte Versionsnummer im EAS-Build ist unerlässlich!
Dazu muss ich diese Dateien in git commit
und git push
meiner semantischen Versionskonfiguration auflisten. Daher muss .releaserc
wie folgt konfiguriert werden:
{
"tagFormat": "${version}",
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github",
[
"@semantic-release/npm",
{ "npmPublish": false }
],
[
"semantic-release-expo",
{
"manifests": ["app.json"]
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"package-lock.json",
"app.json"
],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
]
]
}
Hinweis: Um sicherzustellen, dass die semantische Freigabe funktioniert, stellst du sicher, dass die Commit-Nachrichten die Syntaxregeln einhalten. Dies kann durch Git-Hooks und den entsprechenden NPM-Paket @semantic-release/commit-analyzer
erreicht werden.
Schritt 2: Release
In meinem Repository habe ich eine release.yml
, die bei allen neu erstellten Tags ausgeführt wird, sodass mein Artefakt erstellt wird.
name: Release
on:
push:
tags:
- "**"
release:
types:
- created
Ich triggere diesen Flow für jeden von ci.yml
geschriebenen Tag. Im Rahmen dieses Flows triggere ich den Build auf EAS. EAS Build ist ein gehosteter Dienst zum Erstellen von App-Binärdateien für Expo- und React-Native-Projekte.
Die GitHub-Action ist abgeschlossen, sobald der Auftrag in die EAS-Build-Warteschlange gestellt wurde. Die Erstellung der Anwendung durch EAS selbst kann mehrere Minuten bis zu einer Stunde dauern – diese Zeit wird nicht zu den Build-Minuten meiner GitHub-Action hinzugerechnet! So spare ich durch Automatisierung viel Zeit, ohne die Kosten zu erhöhen.
jobs:
build:
name: Install and build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Read .nvmrc
run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_OUTPUT
id: nvm
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: ${{ steps.nvm.outputs.NODE_VERSION }}
cache: npm
- name: Setup Expo and EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: npm ci
- name: Build on EAS
run: eas build --platform ios --profile production --non-interactive --no-wait
Fazit
Mithilfe von GitHub-Actions und dem Expo-Tool EAS wird eine zuverlässige und zeitsparende Automatisierung eingerichtet, die die Ziele erfüllt, für jeden Push auf den Main-Branch eine neue semantische Version mit einem IPA-Artefakt als Ausgabe zu erstellen.