Was zuerst wichtig ist
- Archive ≠ Build: Archive läuft immer mit Release + Any iOS Device und erzeugt ein
.xcarchive. Ein lokaler Debug-Build, der durchläuft, garantiert kein erfolgreiches Archive. - CI-Hänger clustern in vier Segmenten: Scheme/Konfiguration, Compile-Cache, Signing & Schlüsselbund und exportArchive — erst das Segment in den Logs finden, dann fixen.
xcodebuild archiveentspricht dem Menüpunkt, aber die CI muss explizit-scheme,-configuration Releaseund-destination 'generic/platform=iOS'übergeben.- „Hängt ohne Output“ bedeutet meist Warten auf Schlüsselbund-Freigabe, Provisioning-Profile-Download oder Swap unter Speicherdruck — kein toter xcodebuild-Prozess.
- Self-hosted Cloud-Mac-Runner mit persistentem DerivedData und dediziertem Signing-Schlüsselbund sind oft eine Größenordnung stabiler als kaltes Archive auf ephemeren Hosted-Runnern.
1. Archive vs. Build: was sich wirklich ändert
Viele Teams fassen CI-Fehler als „xcodebuild kaputt“ zusammen. Tatsächlich sind Cmd+B Build und Product → Archive zwei verschiedene Pfade:
| Dimension | Build (⌘B) | Archive |
|---|---|---|
| Typische Configuration | Debug (lokale Entwicklung) | Release (App Store / TestFlight) |
| Destination | Simulator oder angeschlossenes Gerät | Any iOS Device (arm64) |
| Output | .app in DerivedData |
.xcarchive + exportierbare IPA |
| Signing | Development-Zertifikat reicht für manche Targets | Distribution-Zertifikat + Provisioning Profile erforderlich |
| Optimierung | Niedrig — schnelle Kompilierung | Volle Optimierung — deutlich längere Compile-Zeit |
„3 Minuten lokal“ und „20 Minuten in der CI, dann Fehler“ widersprechen sich also nicht: lokal läuft oft Debug + Simulator, in der CI Release + Gerätearchitektur + vollständige Signing-Kette. Vergleichbare Bedingungen herstellen, bevor ihr optimiert. Speziell zu Compile-Zeit-Unterschieden: warum xcodebuild in der CI 2–3× langsamer ist als lokal.
2. Vom Menü zur CLI: Product → Archive in der Shell
Ein Klick auf Product → Archive in der Xcode-GUI entspricht in etwa:
- Ein Shared Scheme mit Release-Konfiguration auswählen;
- Destination auf Any iOS Device setzen (nicht Simulator);
xcodebuild archiveausführen und Output nachARCHIVE_PATHschreiben;- (Optional) Distribute App im Organizer → entspricht
xcodebuild -exportArchive.
Minimaler Archive-Befehl für die CI (Pfade an euer Projekt anpassen):
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=XXXXXXXXXX \
| tee archive.log
Drei Parameter, die Teams oft vergessen:
-destination 'generic/platform=iOS': ohne diesen Parameter fällt xcodebuild oft auf einen Simulator zurück — Archive schlägt fehl oder verhält sich unvorhersehbar.-archivePath: muss beschreibbar sein; Berechtigungsprobleme in Hosted-Runner-Temp-Verzeichnissen führen zu „kompiliert, aber Archive konnte nicht geschrieben werden“.- Scheme muss Shared sein und in Git liegen: ein nicht geteiltes lokales Scheme existiert nach CI-Checkout nicht →
scheme not found.
Offizielle Referenzen: Apple — Building your app, Distributing your app.
3. Vier-Phasen-Pipeline im Überblick
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 1. Resolve │ → │ 2. Archive │ → │ 3. Sign │ → │ 4. Export │
│ SPM/Pods │ │ Release voll │ │ Certs/Profile│ │ IPA / Upload │
│ DerivedData │ │ → .xcarchive │ │ Schlüsselbund│ │ TestFlight │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
pod install xcodebuild codesign exportArchive
oft 5–15 Min. archive 8–25 Min. stille Hängerzone plist-Fehler häufig
Wenn die CI „hier hängenbleibt“, fragt zuerst: zu welcher Phase gehört die letzte Log-Zeile? Stoppt es bei CompileSwift, ist es Compile; bei CodeSign Signing; bei exportArchive Export-Konfiguration. Phasen zu vermischen führt zu endlosem Cache-Löschen.
4. Phase 1: Resolve & Compile (warum CI langsamer ist)
Dependency-Resolution und Kompilierung vor dem Archive sind lokal meist „warm“, in der CI „kalt“:
- CocoaPods / SPM: die CI führt
pod installpro Job von Grund auf aus — Logs vollerInstalling …und 10 Minuten weg, bevor Archive startet. Siehe Flutter-iOS-CI: Cache-Propagation für Pods. - Kaltes DerivedData: voller Release-Build ohne Cache bedeutet, dass jedes native Pod-Modul neu kompiliert wird.
DERIVED_DATA_PATHauf einem SSD-gestützten Self-hosted-Runner zu pinnen halbiert oft das zweite Archive. - Speicherdruck: Archive hat höhere Spitzen als Debug; eine 16-GB-Maschine mit parallelen Jobs swapt — CPU bleibt niedrig, aber nichts bewegt sich. Siehe Runner-Speicher und Swap-Steuerung.
Schnellcheck: zeigen die Logs viel CompileC / SwiftCompile für Drittanbieter-Pods, die ihr nicht angefasst habt, zuerst Cache fixen — nicht Signing.
5. Phase 2: Archive (.xcarchive erzeugen)
Nach erfolgreichem xcodebuild archive sollte unter -archivePath diese Struktur stehen:
MyApp.xcarchive/
Info.plist
Products/Applications/MyApp.app
dSYMs/...
Typische Fehlermuster:
- Scheme-Archive-Action ohne angehaktes Target: lokal baut ein anderes Scheme, in der CI archiviert das CI-Scheme nichts.
- Multi-Target / Extension falsch konfiguriert: Haupt-App läuft durch, Notification Service Extension scheitert am Signing — ganzes Archive fällt durch.
- Build-Nummer / Versionskonflikt: die CI erhöht
CFBundleVersionnicht; Upload scheitert später, wird aber „langsamem Archive“ angelastet.
Sofort validieren: ls -la "$ARCHIVE_PATH" und plutil -p "$ARCHIVE_PATH/Info.plist" ausführen und prüfen, ob ApplicationProperties existiert — nicht erst beim Export-Fehler feststellen, dass das Archive unvollständig ist.
6. Phase 3: Code Sign (die häufigste CI-Falle)
Lokal fragt Archive vielleicht nach Schlüsselbund-Zugriff; ihr klickt „Immer erlauben“ und die Pipeline läuft weiter. Unbeaufsichtigte CI hat diesen Schritt nicht, deshalb:
errSecInternalComponent,User interaction is not allowed: privater Schlüssel liegt im Login-Schlüsselbund; CI-Benutzer hat keine GUI-Freigabe.- Abgelaufenes Profil / Bundle-ID-Mismatch: Log stoppt bei
CodeSignmitProvisioning profile … doesn't match. -allowProvisioningUpdatesversehentlich aktiviert: erfordert interaktiven Apple-ID-Login; Headless-Umgebungen warten ewig.- Schlüsselbund nicht entsperrt: Job fehlt
security unlock-keychain/set-key-partition-listam Anfang.
Empfohlen auf Cloud-Mac / Self-hosted-Runnern: eine dedizierte Schlüsselbund-Datei (nicht der Login-Schlüsselbund), Distribution-Zertifikat + privaten Schlüssel importieren, am Job-Start entsperren und als Default setzen. Vollständige Kette: Cloud-Mac iOS-CI: codesign und Schlüsselbund-Grenzen.
# Job-Anfang (Passwort über CI-Secret)
KEYCHAIN=$RUNNER_TEMP/ci-signing.keychain-db
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import dist.p12 -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -s "$KEYCHAIN" login.keychain-db
Bei fastlane match oder Xcode Cloud für Credentials bleibt das Prinzip gleich: der CI-Prozess muss ohne Interaktion auf den privaten Schlüssel zugreifen können — nicht nur „das Zertifikat liegt irgendwo auf der Maschine“. Siehe fastlane match.
7. Phase 4: IPA exportieren (exportArchive)
Archive erzeugt nur .xcarchive; für TestFlight-Upload braucht es noch den Export. Distribute App im Menü entspricht:
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ExportOptions.plist
Der method-Schlüssel in ExportOptions.plist legt den Export-Typ fest (app-store, ad-hoc, development usw.). Typische CI-Probleme:
methodpasst nicht zum Profil-Typ: Development-Profil mitapp-store-Methode.- Fehlende
teamID/signingCertificate: Manual-Signing-Fehler tauchen beim Export auf, werden aber „langsamem Archive“ zugeschrieben. - Upload mit Export gebündelt:
altool/ Transporter-Netzwerklatenz wird als „Archive-Timeout“ gezählt.
archive, export und upload in drei getimte Schritte aufteilen. Upload über xcrun altool --upload-app oder xcrun notarytool (macOS-Distribution) — vom Archive entkoppelt.
8. Sieben Ursachen: Symptom / Phase / Maßnahme
| Symptom / Log-Schlüsselwort | Phase | Erste Maßnahme |
|---|---|---|
| Lange Stille, niedrige CPU | Sign | Schlüsselbund-Unlock prüfen; GUI-abhängige Provisioning-Auto-Updates deaktivieren |
scheme 'Foo' not found |
Pre-flight | Scheme als Shared markieren und committen; in CI mit -list verifizieren |
No profiles for … were found |
Sign | Bundle ID prüfen; Profile im Repo oder match-Fetch bestätigen |
Viel CompileC bei unberührten Pods |
Compile | DerivedData persistieren; kein clean innerhalb des Jobs |
pod install > 8 Minuten |
Resolve | ios/Pods cachen; pod install --deployment nutzen |
| Job von Plattform gekillt (kein Stacktrace) | Global | Timeout erhöhen; Swap / volle Platte prüfen (Disk-Governance) |
exportArchive failed + plist-Fehler |
Export | Export isoliert ausführen; ExportOptions.plist-Methode verifizieren |
Im Einsatz: Logs von unten nach dem ersten error: scannen und in der Tabelle zuordnen — schneller als pauschales flutter clean.
9. Troubleshooting-Runbook: Segment zuerst, Fix danach
- Release-Archive lokal reproduzieren: Product → Scheme → Edit Scheme → Archive nutzt Release; Destination ist Any iOS Device. Scheitert es lokal, keine CI-Minuten verbrennen.
- Vier-Segment-Timing in der CI:
pod install→archive→exportArchive→upload; jede Dauer ins Step-Summary schreiben. - Vollständige Logs behalten: zu aggressive Log-Prettifier vermeiden; Signing-Fehler brauchen rohe
CodeSign-Zeilen. - Xcode-Version pinnen: Pfad mit
xcode-selectodersudo xcode-select -sfixieren; mit.xcode-versionoder Workflow-macos-15abgleichen. - 48-Stunden-Validierung: denselben Commit zweimal archivieren; der zweite Lauf sollte spürbar schneller sein (Cache-Hit). Wenn nicht: Speicher und Disk prüfen.
Für Release-Sprints oder temporäre Build-Kapazität: kurzfristiger Remote-Mac für Release-Sprints statt Archive-Timeouts auf ephemeren Hosted-Runnern zu bekämpfen.
10. GitHub-Actions-Vorlage
jobs:
archive-ios:
runs-on: [self-hosted, macOS, ios] # oder macos-14 Hosted-Runner
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Archive
run: |
set -o pipefail
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
| tee archive.log
env:
DEVELOPER_DIR: /Applications/Xcode_16.4.app/Contents/Developer
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/MyApp.xcarchive" \
-exportPath "$RUNNER_TEMP/export" \
-exportOptionsPlist ios/ExportOptions.plist
Für Self-hosted-Runner-Label-Routing, launchd-Keepalive und CI-Benutzer-Isolation: Mac-mini-Self-hosted-Runner-Setup und Flutter + GitHub Actions Architektur-Überblick.
11. Fazit
Product → Archive ist nicht „etwas länger bauen“ — es ist die vollständige Kette aus Release-Kompilierung + Archive + Distribution-Signing + optionalem Export. Wenn die CI hier hängenbleibt, ist Xcode selten kaputt. Häufiger wenden Teams Debug-Gewohnheiten auf Release an, behandeln Signing als reines GUI-Thema oder verschmelzen Export und Archive zu einem undurchsichtigen Schritt.
Konkrete nächste Schritte: lokales Archive zuerst durchbekommen → Vier-Segment-CI-Timing einbauen → dedizierter Schlüsselbund + persistenter Cache → dann Maschinen ergänzen oder Specs hochziehen. Für Apple-Plattform-Teams spart ein Self-hosted Cloud-Mac-Runner oft mehr Gesamtzeit als endloses Hosted-Runner-Cache-Tuning — weil Archive einen stabilen Warm Path braucht, keinen Einmal-Chip.
Die komplette Archive-Pipeline auf Cloud Mac
Dediziertes M4-Bare-Metal für xcodebuild archive, dedizierter Signing-Schlüsselbund und persistentes DerivedData. Tagesmiete, um Archive-Zeit und Stabilität zu benchmarken; Monats-Tarif, wenn ihr einen permanenten Runner wollt.