Zum Hauptinhalt springen

iOS Apps mit Gitea Actions builden und deployen

iOS-Apps mit Gitea Actions bauen, signieren und zu TestFlight hochladen: via self-hosted Mac runner mit act_runner oder Cloud-Builds über einen Linux-Runner.

Gitea Actions Workflow, das eine signierte iOS-App für TestFlight baut

Gitea Actions kann iOS-Apps bauen, aber nur auf einem selbst registrierten Mac, da Gitea keine gehosteten Runner anbietet und Xcode nur auf macOS läuft. Die zwei gängigen Setups sind ein Mac, der act_runner im Host-Modus läuft, oder ein normaler Linux-Runner, der den iOS-Build an einen Cloud-Build-Service sendet. Dieser Guide zeigt beide Wege für eine Capacitor App, inklusive Workflow-Files für .gitea/workflows/.

Teams wählen Gitea, weil sie ihre code auf Infrastruktur betreiben wollen, die sie selbst kontrollieren. Nur weil die App Xcode benötigt, sollte sich daran nichts ändern. Beide unten gezeigten Setups halten den Quellcode auf deinem Server.

So führt Gitea Actions Jobs aus

Gitea Actions verwendet eine mit GitHub Actions kompatible Syntax, und Jobs laufen auf act_runner Instanzen, die du bei deinem Gitea-Server registrierst. Jeder Runner meldet Labels wie ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest. Der Teil nach dem Doppelpunkt bestimmt, wie Jobs ausgeführt werden:

  • docker://image führt den Job in einem Container aus. Das ist der Standard für Linux-Runner.
  • host führt den Job direkt auf der Maschine aus, mit allem, was dort installiert ist.

macOS kann nicht in einem Docker-Container laufen, daher muss ein Mac-Runner ein host Label verwenden. Das hat Konsequenzen, auf die wir noch kommen.

Zwei Kompatibilitäts-Hinweise, bevor du ein Workflow aus GitHub kopierst:

  • Gitea liest .gitea/workflows/ und fällt zurück auf .github/workflows/ wenn der erste Ordner nicht existiert.
  • uses: actions/checkout@v4 funktioniert, weil Gitea Actions standardmäßig aus GitHub auflöst (die DEFAULT_ACTIONS_URL Einstellung). Air-gapped Instanzen müssen die verwendeten Actions spiegeln und sie per vollständiger URL referenzieren.

Option 1: Ein Mac-Runner mit act_runner

Registriere den Mac

Laden Sie act_runner Binary für macOS (arm64 für Apple Silicon) von der Gitea-Releases-Seite herunter und hole dir einen Registrierungstoken von Site-Verwaltung > Actions > Runners (instanzweit) oder aus den Organisations- bzw. Repository-Einstellungen.

./act_runner register --no-interactive \
  --instance https://gitea.example.com \
  --token <registration_token> \
  --name mac-mini-01 \
  --labels macos-arm64:host

./act_runner daemon

Führe den Daemon als Launch-Agent unter einem dedizierten Benutzer aus, damit er nach Neustarts automatisch neu gestartet wird und über einen eigenen Login-Keychain verfügt.

Toolchain auf dem Host installieren

Host-Modus bedeutet, dass der Job die eigenen Tools des Macs verwendet. Installiere:

  • Xcode 26 (seit dem 28. April 2026 für App Store Connect Uploads erforderlich) und ausführen sudo xcodebuild -license accept
  • Node.js 22 oder neuer und Bun (Capacitor 8 benötigt Node 22)
  • Ruby und fastlane, oder verwende einen Gemfile mit bundle exec
  • CocoaPods, nur wenn dein iOS-Projekt es noch verwendet. Neue Capacitor 8 Projekte nutzen Swift Package Manager.

Der Workflow

# .gitea/workflows/ios-mac.yaml
name: iOS release (Mac runner)

on:
  push:
    tags:
      - 'v*'

jobs:
  ios:
    runs-on: macos-arm64
    env:
      APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
      APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
      APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
      P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
      APP_STORE_CONNECT_TEAM_ID: ${{ vars.APP_STORE_CONNECT_TEAM_ID }}
      PROFILE_NAME: ${{ vars.IOS_PROFILE_NAME }}
    steps:
      - uses: actions/checkout@v4
      - run: bun install --frozen-lockfile
      - run: bun run build
      - run: bunx cap sync ios
      - name: Decode signing files
        run: |
          echo "${{ secrets.IOS_CERTIFICATE_BASE64 }}" | base64 --decode > dist.p12
          echo "${{ secrets.IOS_PROFILE_BASE64 }}" | base64 --decode > profile.mobileprovision
      - run: bundle exec fastlane ios beta
      - name: Clean up
        if: always()
        run: rm -f dist.p12 profile.mobileprovision

Der beta lane importiert das Zertifikat in einen temporären Keychain, stellt das Projekt auf manuelles Signing um, erhöht die Build-Nummer aus TestFlight, archiviert und lädt hoch. Ein vollständiges Fastfile für ein Capacitor Projekt findest du in iOS-Apps mit Bitbucket Pipelines builden und deployen; auf Gitea funktioniert es unverändert. Für die fastlane-Hintergründe siehe den GitHub Actions iOS Guide.

Die Kosten für den Host-Modus

Der act_runner Die Dokumentation ist da eindeutig: Host-Jobs sind nicht isoliert. In der Praxis heißt das:

  • Alle Jobs teilen sich denselben User, dieselbe Disk und denselben Keychain. Ein Job, der mittendrin abbricht, kann einen temporären Keychain oder ein veraltetes Provisioning Profile hinterlassen, das den nächsten Job blockiert.
  • Jeder Workflow, der auf dieses Label abzielt, führt code auf diesem Mac mit den Rechten des Users aus. Registriere Host-Runner nur für Repositories, denen du vertraust, und setze sie nicht bei Pull Requests aus Forks ein.
  • DerivedData, Archives und Simulator-Runtimes wachsen, bis die Disk voll ist.
  • Xcode-Upgrades liegen bei dir, und Apple hebt jeden Frühling die Mindestanforderung für SDK an.
  • Ein Mac baut nur einen Job auf einmal. Am Release-Tag gibt es Queues.

Wenn dein Team bereits Macs betreibt und jemand sich darum kümmert, funktioniert das. Wenn nicht, nimmt die nächste Option den Mac aus deiner Infrastruktur.

Option 2: iOS mit Capgo Build von einem Linux-Runner bauen

Capgo Build kompiliert und signiert Capacitor Apps auf Capgo-verwalteten Macs. Dein Gitea-Job bleibt auf dem Linux-Runner, den du schon hast. Er checkt code aus, baut den Web-Layer und führt cap sync iosaus und ruft build request. Der Capgo CLI lädt das vorbereitete Native-Projekt hoch, streamt die Xcode-Logs zurück in das Gitea-Job-Log und beendet sich bei einem Fehler mit einem nicht-null-Exit-Code.

Dieses Modell passt gut zu selbst gehostetem Gitea:

  • Kein eingehender Zugriff. Capgo kloniert dein Repository nicht. Dein Runner pusht das vorbereitete Projekt per HTTPS nach außen, sodass ein Gitea-Server hinter einer Firewall oder VPN ohne Änderungen funktioniert.
  • Kein Repository-Token. Auf der Capgo-Seite muss keine Git-Verbindung konfiguriert werden.
  • Deine Web-Quellen bleiben zu Hause. Nur der native ios/ Ordner, die Capacitor-Konfiguration und die nativen Teile der Plugins werden hochgeladen. src/, .git/, und .env Dateien werden nicht hochgeladen.

Einmalige Einrichtung

Auf der Entwicklermaschine:

bunx @capgo/cli@latest login
bunx @capgo/cli@latest app add
bunx @capgo/cli@latest build init --platform ios

build init erstellt oder wiederverwendet dein Distribution-Zertifikat und das App Store Provisioning-Profil und speichert den App Store Connect API-Key. Führe lokal einen Build aus, um zu bestätigen, dass alles signiert wird:

bun run build && bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release

Exportiere anschließend die Credentials:

bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# select "Export to .env"

Secrets in Gitea hinzufügen

Öffne Repository-Einstellungen &gt; Actions &gt; Secrets und füge jeden Key aus der exportierten Datei hinzu, sowie deinen Capgo API-Key:

Secret Zweck
CAPGO_TOKEN Capgo API Key mit Upload-Berechtigung
BUILD_CERTIFICATE_BASE64 Distribution-Zertifikat (.p12)
P12_PASSWORD Zertifikatpasswort
CAPGO_IOS_PROVISIONING_MAP_BASE64 Provisioning-Profile-Mapping
APPLE_KEY_ID App Store Connect API Key-ID
APPLE_ISSUER_ID App Store Connect Issuer-ID
APPLE_KEY_CONTENT Base64 .p8 Key
APP_STORE_CONNECT_TEAM_ID Apple Team-ID

Gitea lehnt Secret- und Variablennamen ab, die beginnen mit GITEA_ oder GITHUB_, behalte diese Namen also einfach so bei. Gib nicht-geheime Werte wie die App-ID unter Variablen und lies sie mit ${{ vars.NAME }}. Secrets auf Organisationsebene werden von jedem Repository darin geteilt, was praktisch ist, wenn du mehrere Apps mit einem Apple-Team auslieferst. Lösche die exportierte Datei, wenn du fertig bist.

Release bei Tag-Push

# .gitea/workflows/ios-release.yaml
name: iOS release

on:
  push:
    tags:
      - 'v*'

jobs:
  release-ios:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: bun run build
      - run: bunx cap sync ios
      - name: Build, sign, and upload to TestFlight
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          CAPGO_IOS_PROVISIONING_MAP_BASE64: ${{ secrets.CAPGO_IOS_PROVISIONING_MAP_BASE64 }}
          APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
          APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
          APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
          APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
        run: |
          bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
            --platform ios \
            --build-mode release \
            --ios-distribution app_store \
            --store-release-name "${{ gitea.ref_name }}"

Hinweise:

  • ${{ gitea.ref_name }} ist der Tag-Name. Der github.* Kontext dient auch als Alias, was beim Portieren von Workflows hilft.
  • Gitea setzt CI=true in jedem Job, sodass Capgo CLI nie auf interaktive Eingaben wartet.
  • Capgo erhöht die Build-Nummer ausgehend vom letzten TestFlight-Build, damit zwei aufeinanderfolgende Tags nie kollidieren.
  • Ohne --submit-to-store-reviewlandet der Build in TestFlight und stoppt. Setze das Flag, wenn die CI auch für App Review einreichen soll.

Releasing ist jetzt git tag v2.3.0 && git push origin v2.3.0. Jeder, der einen v* tag kann shippen, also füge eine protected tag rule für v* unter Einstellungen &gt; Tags und beschränke sie auf Release-Manager.

Ad-hoc-Builds für Tester

Für QA-Builds, die direkt auf registrierten Geräten installiert werden, baue im Ad-hoc-Modus und erhalte einen Download-Link:

# .gitea/workflows/ios-adhoc.yaml
name: iOS ad hoc build

on:
  workflow_dispatch:

jobs:
  adhoc:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile && bun run build
      - run: bunx cap sync ios
      - name: Ad hoc build
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
          APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
          APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
          APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
        run: |
          echo "${{ secrets.ADHOC_PROFILE_BASE64 }}" | base64 --decode > adhoc.mobileprovision
          bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
            --platform ios \
            --ios-distribution ad_hoc \
            --ios-provisioning-profile ./adhoc.mobileprovision \
            --output-upload --output-retention 3d \
            --output-record /tmp/build.json
          bunx @capgo/cli@latest build last-output --path /tmp/build.json --field outputUrl

Ad-hoc-Builds brauchen ein Ad-hoc-Provisioning-Profil, das die Testgeräte auflistet. Speichere dieses Profil base64-kodiert als Secret (ADHOC_PROFILE_BASE64 oben), dekodiere es im Job und übergib es mit --ios-provisioning-profile. CLI Flags haben Vorrang vor Umgebungsvariablen, daher überschreibt dies das App-Store-Profil nur für diesen Build. Sammle UDIDs mit dem iOS UDID FinderFür die meisten Tester ist das interne TestFlight-Testing einfacher, da keine UDIDs benötigt werden.

Manuelle Release-Freigabe

GitHub ermöglicht es dir, Reviewer für eine Umgebung vorzuschreiben. Gitea Actions implementiert keine Environment-Protection-Rules, deshalb pausiert ein Job, der eine Umgebung angibt, nicht für eine Freigabe. Stattdessen funktionieren zwei Patterns:

  1. Geschützte Tags. Nur Release-Manager können v* Tags pushen, und der Tag-Workflow liefert aus.
  2. Build, dann promoten. Build bei jedem Push zu main mit --ios-distribution ad_hoc oder lade nur für internes TestFlight-Testing hoch, dann nutze einen workflow_dispatch Workflow (in aktuellen Gitea-Releases verfügbar), um den freigegebenen Commit damit neu zu builden. --submit-to-store-review.

Web-Änderungen ohne neue Binary deployen

Die meisten Commits zu einer Capacitor App ändern nur JavaScript, CSS und HTML. Ein iOS-Rebuild dafür kostet Minuten und App-Review-Zeit. Mit Capgo Live Updates, kann ein einziger Job den richtigen Pfad wählen:

      - name: Live update or native build
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          # plus the iOS signing secrets from above
        run: |
          if bunx @capgo/cli@latest build needed ${{ vars.APP_ID }} --channel production; then
            bunx @capgo/cli@latest bundle upload ${{ vars.APP_ID }} --channel production
          else
            bunx cap sync ios
            bunx @capgo/cli@latest build request ${{ vars.APP_ID }} --platform ios --build-mode release
          fi

build needed gibt 0 zurück, wenn die nativen Abhängigkeiten mit dem übereinstimmen, was im Channel live ist, und 1, wenn ein neues Binary erforderlich ist. Erzwingen Sie außerdem den nativen Pfad, wenn Dateien unter ios/ oder capacitor.config.* sich ändern, da sie nicht Teil des Abhängigkeitsvergleichs sind. Siehe Automatisch live update oder Native Build auswählen.

Mac-Runner vs. Cloud-Build

act_runner auf Ihrem Mac Capgo Build von Linux aus
Hardware Sie kaufen und hosten sie Keine
Xcode-Updates Manuell, pro Maschine Wird von Capgo verwaltet
Isolation zwischen Jobs Keine im Host-Modus Frische Build-Umgebung pro Job
Parallele Builds Einer pro Mac Mehrere, von jedem Runner
Signing-Dateien In Gitea Secrets, dekodiert auf dem Mac In Gitea-Secrets, pro Build gesendet
Funktioniert hinter einer Firewall Ja Ja, nur ausgehender HTTPS-Traffic
Native Swift Apps (ohne Capacitor) Ja Nein, nur Capacitor Apps
Build-Zeitlimit Frei wählbar 10 Minuten pro Build

Fehlerbehebung

Symptom Lösung
Job bleibt ewig in der Warteschlange Kein Online-Runner hat das Label runs-on. Überprüfe die Runner-Liste und die Schreibweise der Labels.
uses: actions/checkout@v4 Download fehlgeschlagen Der Runner kann GitHub nicht erreichen. Bilde die Action in deinem Gitea nach und verweise mit der vollständigen URL darauf.
Secret-Wert im Job ist leer Der Name beginnt mit GITEA_/GITHUB_, oder das Secret ist in einem anderen Repository definiert.
security: SecKeychainItemImport: MAC verification failed Falsch .p12 Passwort, oder ein .p12 erstellt mit OpenSSL 3 ohne -legacy. Exportiere es erneut.
No profiles for 'com.example.app' were found Das Profil in den Secrets stimmt nicht mit der Bundle ID oder dem Zertifikat überein. Erneut ausführen build init und exportiere es neu.
Upload für Version SDK abgelehnt Der Mac-Runner hat immer noch ein Xcode älter als 26.
cap sync ios warnt vor CocoaPods auf Linux Erwartet. Die Capgo-Build-Maschine installiert die Pods.

Weitere Fehlermodi findest du unter CI/CD für Capacitor: häufige Fallstricke und Capgo Build-Troubleshooting-Leitfaden.

Zusammenfassung

Gitea Actions verarbeitet iOS problemlos, sobald feststeht, wo Xcode läuft. Ein Mac-Runner im Host-Modus bietet volle Kontrolle und volle Verantwortung. Ein Linux-Job, der Capgo Build aufruft, lässt deine Gitea-Setup unverändert, benötigt keinen eingehenden Zugriff und macht aus einem TestFlight-Release einen Tag-Push. Egal, was du wählst: Halte Signaturmaterial in Gitea Secrets, schütze deine Release-Tags und versende nur Web-Änderungen als Live Updates, damit die native Pipeline nur dann läuft, wenn es sein muss.

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.