Code Signing for iOS in GitHub Actions
Sign iOS builds in CI by loading a certificate and profile into a throwaway keychain.
A real iOS release has to be signed, which means CI needs your distribution certificate and provisioning profile available on the runner. The safe pattern is to base64-encode both, store them as secrets, and import them into a temporary keychain that exists only for the run. This recipe shows that setup.
What the pipeline does
- Decodes a base64 certificate and provisioning profile from secrets.
- Creates a temporary keychain and imports the signing identity.
- Installs the provisioning profile where Xcode expects it.
- Runs a signed xcodebuild archive.
The workflow
Store the .p12 certificate, its password, and the provisioning profile as base64 secrets, then materialize them at runtime.
name: iOS Signed Build
on:
push:
branches: [main]
jobs:
archive:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Import signing assets
env:
CERT_BASE64: \${{ secrets.IOS_CERT_BASE64 }}
CERT_PASSWORD: \${{ secrets.IOS_CERT_PASSWORD }}
PROFILE_BASE64: \${{ secrets.IOS_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: \${{ secrets.KEYCHAIN_PASSWORD }}
run: |
echo "$CERT_BASE64" | base64 --decode > cert.p12
echo "$PROFILE_BASE64" | base64 --decode > profile.mobileprovision
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import cert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security list-keychains -d user -s build.keychain
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp profile.mobileprovision ~/Library/MobileDevice/Provisioning\ Profiles/
- name: Archive
run: |
xcodebuild archive \
-scheme MyApp \
-archivePath build/MyApp.xcarchive \
-configuration ReleaseNotes for this platform
Signing requires a macos runner, so isolate this in a release-only job to limit costly macOS minutes. Set a keychain timeout (set-keychain-settings -lut) so the unlocked keychain self-locks. The set-key-partition-list step is the one people miss -- without it codesign prompts for access and the build hangs. fastlane match is a higher-level alternative that manages certs in a git repo. Managed runners auto-retry transient keychain and network failures so a single flaky import does not waste a whole macOS run.
Key takeaways
- Import the cert and profile into a temporary keychain that lives only for the run.
- Run set-key-partition-list or codesign will hang waiting for keychain access.
- Keep signing in a release-only macOS job to limit expensive minutes.