Build and sign a Capacitor iOS app in GitHub Actions
A complete GitHub Actions workflow to build, sign, and upload a Capacitor iOS app — certificates, provisioning profiles, and the secrets setup — plus shipping the web layer with OtaKit.
Building a Capacitor iOS app in CI is mostly straightforward — until code signing. The certificates and provisioning profiles that make Apple happy are exactly the part that's fiddly to automate. This guide sets up a GitHub Actions workflow that builds, signs, and can upload a Capacitor iOS app, and shows where OTA fits so your web-layer changes don't need this pipeline at all.
Mental model: use this native pipeline for store builds (native changes). Use the OTA pipeline for everything in the web layer — which is most of your releases.
What you need in secrets
Store these as encrypted repository secrets, never in the workflow file:
- Distribution certificate (a base64-encoded
.p12) and its password. - Provisioning profile (base64-encoded
.mobileprovision). - An App Store Connect API key if you'll upload from CI.
The workflow
iOS builds require a macOS runner. This installs the signing assets into a temporary keychain, syncs Capacitor, and archives the app:
# .github/workflows/ios.yml
name: iOS build
on:
push:
tags: ["v*"] # native builds on version tags
jobs:
build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run build # your web build
- run: npx cap sync ios
- name: Import signing certificate
env:
CERT_P12: ${{ secrets.IOS_CERT_P12 }}
CERT_PASSWORD: ${{ secrets.IOS_CERT_PASSWORD }}
PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
run: |
echo "$CERT_P12" | base64 --decode > cert.p12
security create-keychain -p "" build.keychain
security import cert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security list-keychains -s build.keychain
security unlock-keychain -p "" build.keychain
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
echo "$PROFILE" | base64 --decode > ~/Library/MobileDevice/Provisioning\ Profiles/app.mobileprovision
- name: Archive
run: |
xcodebuild -workspace ios/App/App.xcworkspace \
-scheme App -configuration Release \
-archivePath build/App.xcarchive archiveFrom the archive you can export an IPA and upload with xcrun altool / the App Store Connect API. If this feels like a lot, it is — which is the point of the next section.
Consider fastlane match
Managing certificates by hand is error-prone. fastlane match stores signing assets in a private repo and installs them in CI with one command, which removes most of the keychain wrangling above. See Fastlane + OtaKit for combining it with OTA releases.
Where OTA changes the picture
This native pipeline should run rarely — only when native code changes. Everything in your web layer (UI, logic, fixes) ships over the air with a far simpler workflow and no signing at all:
npm run build otakit upload --release
So the healthy pattern is: native iOS builds on version tags (this workflow), OTA releases on every merge (the OTA workflow).
Remember to bump runtimeVersion whenever you cut a native build, so OTA bundles built afterward only reach the new shell. See versioning.
Where to go next
See the CI automation docs, and the Android build guide for the other platform.