Guides11 min read

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 archive

From 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.

Related docs