Entwickler nutzen Mac und iPhone fuer App Store-Einreichung und TestFlight-Release

In vielen DACH-Teams sieht App Store-Einreichung noch so aus: Product Management drängt, der iOS-Verantwortliche archiviert manuell in Xcode, TestFlight geht nachts raus, am naechsten Tag werden Metadaten erneut eingetragen. Der Code ist laengst gemerged — der Engpass ist die fehlende Engineering-Kette fuer Releases. Dieser Artikel zerlegt den Weg von Xcode bis zur Freigabe aus CI/CD-Sicht in sechs beobachtbare Phasen, definiert Grenzen fuer Signierung, Build, Upload, Einreichung und Release und liefert ein direkt umsetzbares Runbook. Wer noch die Build-Knoten-Frage klaeren will, findet in iOS-Pipeline-Strategien 2026 das Schichtenmodell fuer Mac-Build-Inseln.

Nach dem Lesen haben Sie: eine Sechs-Phasen-Einreichungskarte, eine Manuell-vs.-Automatisierung-Uebersicht, ein minimales fastlane- plus GitHub-Actions-YAML und eine Checkliste, um nach einer Ablehnung in 30 Minuten einen neuen TestFlight-Build zu liefern.

Kurz gesagt: Einreichung ist nicht einmal Product → Archive in Xcode, sondern Release-Engineering unter vier Bedingungen: vertrauenswuerdige Signierung + rueckverfolgbare Artefakte + konsistente Connect-Metadaten + pollbarer Review-Status.

Warum «Einreichung» eine Engineering-Kette ist — kein Checkliste-Dokument

Einsteiger verstehen Einreichung oft als Checkliste: Screenshots, Datenschutzerklaerung, auf «Senden» klicken. Das Problem: eine Checkliste speichert keinen Zustand und garantiert nicht, dass Release N dieselben Voraussetzungen hat wie Release 1. Aus Engineering-Sicht ist Einreichung eine gerichtete Pipeline, in der jede Phase klare Artefakte liefert und die naechste Phase nur deren Output konsumiert:

  • Reproduzierbar: Derselbe Git-Tag soll auf der Build-Insel — bei gleicher Xcode-Version und Dependency-Lock — dieselbe IPA erzeugen wie lokal
  • Beobachtbar: Jede Phase hat Logs, Laufzeit und Fehlercode; in Slack soll «Upload erfolgreich / in Verarbeitung / einreichbar» stehen, nicht «glaube, ist durch»
  • Rollback-faehig: Im Store bleibt der vorherige Build erhalten; in Git bleiben Tag und dSYM — Crash-Symbolisierung bricht nicht ab
  • Rollengetrennt: Engineering verwaltet Binary und Versionsnummer; Produkt/Marketing Store-Text und Screenshots — in CI durch separate Jobs oder Approval-Gates getrennt

Cross-Platform-Teams ohne Mac nutzen dieselbe Kette: Entwicklung auf Windows oder Linux, Release-Phasen auf einem macOS-Build-Knoten. Das Rollenmodell steht in Windows/Linux-Haupt-PC + Remote-Mac-Build-Insel.

Sechs Phasen: vom Xcode-Scheme bis zur App im Store

Die folgende Tabelle ist das Rueckgrat des Artikels. Das Runbook ordnet Jobs in dieser Reihenfolge — so vermeiden Sie «Binary hochgeladen, Build-Nummer nicht erhoeht».

PhaseHauptaktionTypische DauerAutomatisierungsgrad
① PreflightVersionsnummer, Dependency-Lock, Unit-Tests, Privacy-Manifest-Entwurf5–15 minhoch (Linux/Mac)
② Signierung vorbereitenZertifikat, Provisioning Profile, Keychain-Unlock, ASC API Keyeinmalig + Rotationmittel (Secret-Injection)
③ Archive & Exportxcodebuild archive, IPA exportieren10–40 minhoch (dedizierter Mac)
④ TestFlightUpload, Verarbeitung, interne Testgruppen15–60 minhoch
⑤ EinreichungMetadaten, Screenshots, Export Compliance, submit for review30 min manuell + Wartezeitmittel (Approval-Gate)
⑥ ReleaseReview bestanden, phased release / Vollausrollung24–48 h Reviewmittel (Status-Polling)

Die wichtigste Trennlinie liegt zwischen Phase ④ und ⑤: TestFlight sollte moeglichst automatisiert laufen (jeder main-Merge oder Nightly kann ein Binary liefern); die Einreichung braucht ein manuelles oder PM-Genehmigungs-Gate, damit unvollstaendige Store-Texte nicht in die Review-Queue gelangen. Phase ③ muss auf einem version-gepinnten Mac laufen — genau dafuer eignet sich Cloud Mac als iOS-CI-Knoten, nicht fuer taegliches Swift-Schreiben.

Signierung und Zertifikate: die teuerste Nicht-Deterministik in CI

Lokal archiviert Xcode problemlos — in CI kommen errSecInternalComponent oder No profiles for .... Fast immer liegt es an nicht fixierter Identitaet und Umgebung, nicht am Projekt selbst.

Drei Credential-Typen — nicht in einem Secret mischen

CredentialZweckCI-Speicherempfehlung
Distribution-Zertifikat (p12)codesign fuer installierbare Buildsverschluesseltes Secret; Build-Insel-Keychain oder temporaerer Keychain
Provisioning ProfileBundle ID, Capabilities, Geraete bindenper UUID in Job-Privatverzeichnis; kein globales Ueberschreiben
ASC API Key (.p8)Upload, Build-Status, deliver-EinreichungGitHub Encrypted Secret; kein Keychain noetig

Bei paralleler Signierung in mehreren Jobs sind Keychain-Konflikte und Profile-Race-Conditions haeufige Langzeitfehler. Runner-eigener User plus Job-temporaerer Keychain ist die gaengige Loesung — Details in Codesign-Parallelitaet und Keychain-Isolation FAQ.

Minimal-Skript fuer CI-Keychain (Xcode 16 / macOS 14 angenommen)

# 在 self-hosted Mac runner 上,每个 release job 开头
KEYCHAIN=$RUNNER_TEMP/build.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
Hinweis: Beim ersten Zertifikatsimport auf einem Remote-Mac ist oft eine VNC-Grafiksitzung noetig fuer Keychain-Vertrauen und TCC-Freigabe; danach laufen Jobs headless per SSH. SSH- vs. VNC-Aufteilung in Enterprise Remote CI: SSH vs. VNC.

Build-Artefakte: Archive → Export → TestFlight-Upload

Der Unterschied zwischen Release- und Debug-Build liegt nicht nur in einem Optimierungsschalter, sondern darin, dass Signierungskonfiguration, Export-Optionen und Versions-Metadaten mit dem App-Eintrag in App Store Connect uebereinstimmen muessen.

xcodebuild Archive (Kommandozeilen-Aequivalent zu Xcode Product → Archive)

xcodebuild -scheme MyApp -configuration Release \
  -destination 'generic/platform=iOS' \
  -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
  archive \
  DEVELOPMENT_TEAM=XXXXXXXXXX \
  -allowProvisioningUpdates

IPA exportieren und hochladen

xcodebuild -exportArchive \
  -archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
  -exportPath "$RUNNER_TEMP/export" \
  -exportOptionsPlist ExportOptions.plist

# ExportOptions.plist 中 method 应为 app-store
# 上传可用 xcrun altool(旧)或 fastlane pilot / deliver

Engineering-Punkte:

  • CFBundleVersion (Build-Nummer) muss bei jedem Upload steigen — an Git-Tag oder CI-Run-Nummer binden und im Runbook festhalten
  • .xcarchive und dSYM aufbewahren — in GitHub Artifacts oder S3; Ablehnungen und Produktions-Crashes brauchen Symboltabellen
  • DerivedData-Cache-Key an Xcode-Hauptversion koppeln — verhindert «kompiliert mit schmutzigem Cache, Signierung schlaegt fehl» nach Xcode-Upgrade

Disk- und Cache-Strategie auf der Build-Insel beeinflusst die Laufzeitstabilitaet von Phase ③ direkt — siehe Self-hosted Runner Cache und Disk FAQ.

Einreichung und Review: was sich neben dem Menschen automatisieren laesst

Apple-Review bleibt ueberwiegend manuell — aber viel Routine vor und nach der Pruefung gehoert in die Pipeline:

Gut automatisierbar

  • Build-Verarbeitungsstatus pollen (processingvalid)
  • Pruefen, ob gespeicherte Antworten zu Export Compliance, Urheberrecht und Werbe-ID noch passen
  • Metadaten und Screenshots per fastlane deliver oder App Store Connect API einreichen (wenn Screenshot-Quellen in Git liegen)
  • Nach Freigabe automatisch Slack-Nachricht oder phased-release-Prozentsatz ausloesen

Besser mit manuellem Gate

  • Ersteinreichung in neuer Region, sensible Kategorien (Zahlung, Gesundheit, Kinder)
  • Marketing-Text, Preview-Video und finale Store-Screenshots
  • Der «Zur Pruefung senden»-Schritt selbst — per GitHub Environment required_reviewers oder manuellem workflow_dispatch

Bei Rejection oder Metadata Rejected sollte das Runbook vorschreiben: zuerst Git fixen, dann Build-Nummer erhoehen, dann ③→④ — nicht nur Connect im Browser aendern ohne neues Binary (ausser bei reinen Metadaten-Problemen). Haeufige Gruende — fehlendes Privacy Manifest, keine Demo-Accounts bei Login, unvollstaendige Drittanbieter-SDK-Deklaration — gehoeren in den Preflight-Job mit Skript-Scan von PrivacyInfo.xcprivacy und relevanten Info.plist-Schluesseln.

CI/CD-Runbook: GitHub Actions + fastlane Minimal-Loop

Unten der kleinste Workflow von «main gemerged» bis «TestFlight sichtbar». Der Einreichungs-Job ist standardmaessig aus — er startet per release/*-Tag oder manuell.

Fastfile-Ausschnitt (TestFlight-Upload)

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  lane :beta do
    setup_ci if ENV['CI']
    match(type: "appstore", readonly: true)
    build_app(
      scheme: "MyApp",
      export_method: "app-store",
      output_directory: "./build"
    )
    upload_to_testflight(
      skip_waiting_for_build_processing: false,
      api_key_path: "asc_api_key.json"
    )
  end

  lane :release do
    upload_to_app_store(
      submit_for_review: true,
      automatic_release: false,
      precheck_include_in_app_purchases: false
    )
  end
end

GitHub Actions: Preflight + Release (Self-hosted Mac)

name: App Store Release
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      submit_review:
        description: 'Submit to App Review'
        type: boolean
        default: false

jobs:
  preflight:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: swiftlint --strict
      - run: swift test --parallel
      - name: Check Privacy Manifest exists
        run: test -f MyApp/PrivacyInfo.xcprivacy

  beta:
    needs: preflight
    runs-on: [self-hosted, macos, ios-release]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: ~/ci/DerivedData
          key: dd-xcode16-${{ hashFiles('**/Package.resolved') }}
      - name: TestFlight upload
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
        run: bundle exec fastlane beta

  submit-review:
    if: github.event.inputs.submit_review == true
    needs: beta
    runs-on: ubuntu-latest
    environment: app-store-review
    steps:
      - uses: actions/checkout@v4
      - run: bundle exec fastlane release

Orchestrierung bleibt in GitHub Actions, Ausfuehrung auf der Self-hosted Mac-Build-Insel — dieselbe Philosophie wie in der Hybrid-Pipeline: Linux fuer schnelle Checks, Mac nur fuer Archive und Upload. Temporaere Runner in Release-Wochen: Abschnitt «Release Burst» in jenem Artikel.

Anti-Patterns: wann vollautomatisches submit_for_review scheitert

Engineering heisst nicht «jeden Knopf dem Bot uebergeben». In diesen Faellen lohnt sich kein blindes submit_for_review:

  • Store-Texte leben noch in Google Docs — Abweichung zwischen Connect und Dokument fuehrt zu Metadata Rejected
  • Backend-API und App-Version sind stark gekoppelt — Server-Graurollout muss vor Review stehen
  • Erstmalige ATT-, Payment- oder HealthKit-Integration — Review-Antworten und Demo-Videos brauchen oft Produkt vor Ort
  • Signierung auf geteilten Hosted-macOS-Runnern — Umgebungsdrift ist schwerer zu debuggen als Queue-Zeit; Release-Kette gehoert auf dedizierten Mac

Die robuste Default-Strategie: vollautomatisch bis TestFlight, halbautomatisch bis App Review. Interne Gruppe verifiziert, PM genehmigt im Environment, dann laeuft der submit-review-Job.

FAQ

Braucht man fuer die App Store-Einreichung zwingend einen lokalen Mac?

Nein. Die taegliche Entwicklung kann auf Windows oder Linux laufen, aber Archive, Codesign und TestFlight-Upload muessen auf macOS ausgefuehrt werden. Teams ohne lokalen Mac koennen die gesamte Einreichungskette per SSH oder VNC auf einem Cloud Mac oder einer Self-hosted Build-Insel abwickeln.

Koennen TestFlight und App-Store-Pruefung in derselben CI-Pipeline laufen?

Ja — Build- und Signierungsphasen koennen geteilt werden, aber als zwei Jobs aufteilen: zuerst upload_to_testflight fuer internes Testing; der Einreichungs-Job braucht ein manuelles oder Approval-Gate und prueft Metadaten, Screenshots und Privacy Manifest separat.

fastlane oder natives xcodebuild — was waehlen?

xcodebuild uebernimmt Kompilierung und Archive; fastlane kapselt Signierung, Upload, Metadaten und deliver-Einreichung. Kleine Teams koennen fastlane den gesamten Pfad ueberlassen; grosse Repos rufen xcodebuild in CI explizit auf und halten fastlane nur auf der Release-Ebene.

ASC API Key oder p12-Zertifikat — was passt besser fuer CI?

Fuer Binary-Upload und Review-Status-Polling bevorzugen Sie einen ASC API Key (.p8) — kein Keychain-Import noetig. codesign braucht weiterhin ein Distribution-Zertifikat und Provisioning Profile. Gaengige Aufteilung: API Key fuer Connect, p12 fuer Signierung, getrennt als Secrets gespeichert.

Wie liefert man nach einer Ablehnung schnell einen neuen Build?

Git-Tag und dSYM des abgelehnten Builds aufbewahren; nach dem Fix Build-Nummer erhoehen und dieselbe Release-Pipeline upload_to_testflight laufen lassen, auf TestFlight verifizieren, dann erneut submit_for_review. Build-Nummern muessen monoton steigen und die Metadaten-Version muss konsistent bleiben.

Muessen Export Compliance und Privacy Manifest manuell ausgefuellt werden?

Beim ersten Mal in App Store Connect ausfuellen und als Vorlage speichern; CI kann freigegebene Antworten per fastlane deliver oder App Store Connect API schreiben. Nutzt die App nur HTTPS fuer Verschluesselung, waehlen die meisten Teams die Standardbefreiung und tragen sie in Info.plist ein.

Wie lange dauert es von Archive bis zur Freigabe?

Build: 10–40 Minuten je nach Projektgroesse; Upload und Verarbeitung: 15–60 Minuten; Review: 24–48 Stunden ueblich, an Feiertagen laenger. Das Engineering-Ziel ist, alles ausser dem Warten auf Apple zu komprimieren und planbar zu machen.

Eignet sich ein remote Cloud Mac als Einreichungs-Knoten?

Ja. Ein dedizierter physischer Mac mit fixierter Xcode-Version, persistentem Keychain und DerivedData ist fuer Signierungsstabilitaet und Release-Deadlines zuverlaessiger als geteilte Hosted Runner; fuer erstmalige TCC-Einrichtung VNC nutzen, danach headless per SSH.

Fazit

App Store-Einreichung ist aus CI/CD-Sicht eine sechsstufige Release-Kette — kein einmaliges Archive in Xcode. Wenn Sie Signierung, Build, TestFlight, Einreichung und Release getrennt betreiben, finden Sie Fehler in der richtigen Phase statt bei «Einreichung ist wieder gescheitert».

  • Preflight und TestFlight moeglichst vollautomatisch; Einreichung mit Approval-Gate
  • Signierungs-Credentials in drei Kategorien; bei parallelen Builds Keychain und Profile isolieren
  • Build-Nummer an Git-Tag binden; dSYM und Archive aufbewahren
  • Dedizierte Mac-Build-Insel fuer Phase ③④, Linux fuer Phase ①
  • Nach Ablehnung dieselbe Pipeline mit erhoehter Build-Nummer — nicht nur Connect im Browser aendern

Naechster Schritt: In Ihrem Repo einen Workflow nur fuer TestFlight-Upload einfuehren, durchlaufen lassen, dann den Einreichungs-Job mit environment: app-store-review ergaenzen. Als Build-Knoten reicht zunaechst ein tageweiser Cloud Mac fuer eine Woche — danach entscheiden, ob ein dauerhafter Mac mini sinnvoll ist.

Weiterlesen