iOS Build with Xcode, xcodebuild and Swift Package Manager
How Swift and iOS apps are built, signed and archived with xcodebuild and Swift Package Manager, which files are involved, how to lint, scan and test, and a GitHub Actions pipeline.
Posted
DevOps pipeline loop motif with the title iOS Build with Xcode and SwiftPM
iOS Build with Xcode, xcodebuild and Swift Package Manager
Swift projects are built either with Swift Package Manager (packages, server-side Swift, libraries) or with Xcode / xcodebuild (iOS, macOS, watchOS and visionOS apps). App builds add steps other languages do not have: code signing with certificates and provisioning profiles, archiving, and exporting an .ipa for TestFlight or the App Store. fastlane automates most of it.
XCTest, Swift Testing, XCUITest; Xcode code coverage (xccov), Slather
Build output
.app, .xcarchive, .ipa; .xcframework for libraries
CI cache
SwiftPM .build/ and SourcePackages, DerivedData, Pods/
How the build works
In CI, each stage of the generic build pipeline maps to a Xcode command, plugin or scanner. Press play, or step through it:
Source + Package.swift: A commit triggers CI. Package.swift and Package.resolved pin the Swift packages, and the Xcode project or workspace defines the app.
Resolve packages: xcodebuild -resolvePackageDependencies fetches the exact versions in Package.resolved; CI caches SourcePackages and DerivedData.
Lint and format: swiftlint --strict fails on any lint warning; SwiftFormat checks formatting.
Compile: xcodebuild builds the app for the scheme; swift build -c release builds plain Swift packages.
Test and coverage: xcodebuild test runs XCTest and Swift Testing on an iOS Simulator with code coverage on; xcrun xccov reports it.
Security scans in parallel: mobsfscan applies MobSF static rules to our code, OSV-Scanner checks Package.resolved, and Gitleaks scans for secrets.
Quality gate: Tests, coverage and every scanner are required checks, so a single failure stops the pipeline here.
Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
Archive (pass): xcodebuild archive builds a Release .xcarchive.
Sign and export (pass): Signing needs a distribution certificate and provisioning profile (managed with fastlane match); -exportArchive then writes the .ipa.
Upload to TestFlight (pass): fastlane beta builds and uploads the .ipa to TestFlight through App Store Connect.
Schemes choose which targets to build, test and archive; mark them Shared so they are committed and visible to xcodebuild in CI.
Build settings come from the project file and .xcconfig files; keep environment-specific values (bundle ID, API URLs) in .xcconfig rather than editing the project in CI.
Signing needs a distribution certificate and a provisioning profile on the build machine. fastlane match stores them encrypted in a git repo or cloud bucket and installs them in CI.
macOS runners are required for iOS builds; they are slower and more expensive than Linux, so cache aggressively and run lint/SAST on Linux where possible.
Build files
File
Purpose
Commit?
Package.swift
SwiftPM manifest: products, targets, dependencies
Yes
Package.resolved
Locked package versions
Yes
*.xcodeproj, *.xcworkspace
Xcode project / workspace (or generated by Tuist/XcodeGen)
Yes (or the generator spec)
xcshareddata/xcschemes/*.xcscheme
Shared schemes used by CI
Yes
*.xcconfig
Build settings per configuration
Yes, without secrets
Info.plist, *.entitlements
App metadata and capabilities
Yes
ExportOptions.plist
Export method (app-store, ad-hoc), team ID, signing
Yes
Podfile, Podfile.lock
CocoaPods dependencies (legacy projects)
Yes
fastlane/Fastfile, Appfile, Matchfile
Automation lanes and signing config
Yes
*.p12, *.mobileprovision, App Store Connect API keys
Simulator names change with each Xcode release on hosted runners. Check the runner image’s installed simulators (xcrun simctl list devices) or select the Xcode version explicitly.
Best practices
Prefer Swift Package Manager for new dependencies; CocoaPods is no longer actively developed.
Commit Package.resolved and shared schemes so CI resolves and builds the same thing as Xcode.
Manage signing with fastlane match (or Xcode Cloud) and an App Store Connect API key; never commit .p12 files or passwords.
Generate the project with Tuist or XcodeGen on large teams to avoid .pbxproj merge conflicts.
Split jobs: lint and SAST on cheap Linux runners, build/test/archive on macOS.
Increment build numbers in CI (agvtool or fastlane increment_build_number) so every upload is unique.