Skip to content

Swift / SwiftUI — Distribution

OWNER: martijn, valentin ALSO_USED_BY: alexander (design integration) LAST_VERIFIED: 2026-03-26 GE_STACK_VERSION: Xcode 26 / Fastlane / GitHub Actions


Overview

GE uses Fastlane with GitHub Actions for automated iOS builds, signing, and distribution. This page covers code signing, TestFlight, App Store submission, and CI/CD pipeline setup. Every GE iOS project follows this pipeline — no manual Xcode uploads.

Code Signing

Overview

Apple requires all apps to be cryptographically signed before they can run on devices. Two components: signing certificate (identity) and provisioning profile (entitlements + devices).

Fastlane Match (GE Standard)

GE uses fastlane match to share signing identities across the team. Certificates and profiles are stored in a private Git repository.

# Matchfile
git_url("git@github.com:growing-europe/ios-certificates.git")
storage_mode("git")
type("appstore")
app_identifier(["com.growingeurope.clientapp"])
username("ios@growingeurope.com")

CHECK: Agent is setting up code signing for a new project. IF: Agent generates certificates manually in Xcode or Apple Developer Portal. THEN: Stop. Use fastlane match to generate and store certificates in the shared repo.

Match Commands

# Generate development certificates + profiles
fastlane match development

# Generate App Store distribution certificates + profiles
fastlane match appstore

# Generate Ad Hoc profiles (for TestFlight alternatives)
fastlane match adhoc

# Nuclear option — revoke all and regenerate
# WARNING: disables all existing Ad Hoc/Enterprise builds
fastlane match nuke distribution
fastlane match appstore

ANTI_PATTERN: Running match nuke without team notification. FIX: match nuke revokes ALL certificates. Notify the team first — existing Ad Hoc builds stop working.

Automatic vs Manual Signing

GE convention:

  • Debug/Development — Automatic signing (Xcode managed).
  • Release/Distribution — Manual signing via Fastlane Match profiles.
# Fastfile — set signing for release builds
lane :release do
    sync_code_signing(type: "appstore", readonly: true)
    build_app(
        scheme: "ClientApp",
        export_method: "app-store",
        export_options: {
            signingStyle: "manual",
            provisioningProfiles: {
                "com.growingeurope.clientapp" => "match AppStore com.growingeurope.clientapp"
            }
        }
    )
end

Provisioning Profiles

Types

Profile Use Case Devices
Development Debug builds on registered devices Up to 100
Ad Hoc Testing builds distributed outside TestFlight Up to 100
App Store Production distribution All (via App Store)
Enterprise In-house distribution (requires Enterprise account) Unlimited

CHECK: Agent is configuring a provisioning profile. IF: Project uses wildcard bundle ID (com.growingeurope.*). THEN: Use explicit bundle ID (com.growingeurope.clientapp). Wildcards cannot use push notifications, App Groups, or Sign in with Apple.

TestFlight

Automated Upload

lane :beta do
    sync_code_signing(type: "appstore", readonly: true)
    increment_build_number(
        build_number: ENV["GITHUB_RUN_NUMBER"]
    )
    build_app(scheme: "ClientApp", export_method: "app-store")
    upload_to_testflight(
        skip_waiting_for_build_processing: true,
        distribute_external: false
    )
end

TestFlight Groups

  • Internal — GE team, auto-distributed on upload, no review required.
  • External (Client) — Requires Beta App Review on first build, then auto-distributed.

CHECK: Agent is distributing a TestFlight build. IF: Build goes to external testers for the first time. THEN: Expect 24-48h delay for Beta App Review. Include beta description and contact info.

App Store Submission

Metadata Management

lane :release do
    sync_code_signing(type: "appstore", readonly: true)
    build_app(scheme: "ClientApp", export_method: "app-store")
    upload_to_app_store(
        submit_for_review: true,
        automatic_release: false,  # Client approves release timing
        force: true,               # Skip HTML report
        precheck_include_in_app_purchases: false
    )
end

Required Metadata

Field Notes
App name Max 30 characters
Subtitle Max 30 characters
Description Up to 4000 characters
Keywords Max 100 characters total, comma-separated
Screenshots Min 1 per required device size (6.5" or 6.9" iPhone, 13" iPad)
Privacy policy URL MANDATORY — must be publicly accessible
Support URL MANDATORY
Category Primary + optional secondary
Age rating Questionnaire in App Store Connect
App Review notes Login credentials for review team if auth is required

App Store Connect API Key

GE uses API keys (not Apple ID + password) for automated uploads.

# Appfile
app_identifier("com.growingeurope.clientapp")
itc_team_id("YOUR_TEAM_ID")

# API key stored in CI secrets
lane :release do
    api_key = app_store_connect_api_key(
        key_id: ENV["ASC_KEY_ID"],
        issuer_id: ENV["ASC_ISSUER_ID"],
        key_filepath: ENV["ASC_KEY_PATH"]
    )
    upload_to_app_store(api_key: api_key)
end

CI/CD with GitHub Actions

Workflow Structure

name: iOS Build & Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: sudo xcode-select -s /Applications/Xcode_26.app

      - name: Install dependencies
        run: |
          brew install fastlane
          swift package resolve

      - name: Run tests
        run: fastlane test

  deploy_testflight:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: macos-15
    steps:
      - uses: actions/checkout@v4

      - name: Select Xcode
        run: sudo xcode-select -s /Applications/Xcode_26.app

      - name: Install Fastlane
        run: brew install fastlane

      - name: Deploy to TestFlight
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_AUTH }}
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY_PATH: ${{ secrets.ASC_KEY_PATH }}
          GITHUB_RUN_NUMBER: ${{ github.run_number }}
        run: |
          fastlane match appstore --readonly
          fastlane beta

CI-Specific Fastlane Setup

# Fastfile
before_all do
    if is_ci
        setup_ci  # Creates temporary keychain for CI
    end
end

lane :test do
    run_tests(
        scheme: "ClientApp",
        devices: ["iPhone 16"],
        clean: true
    )
end

CHECK: Agent is configuring CI. IF: CI uses match without readonly: true. THEN: Add readonly: true (or is_ci guard). CI should NEVER generate new certificates — only use existing ones.

Secrets Management

Secret Purpose
MATCH_PASSWORD Decrypts certificates from Git repo
MATCH_GIT_BASIC_AUTHORIZATION Base64-encoded user:token for HTTPS Git clone
ASC_KEY_ID App Store Connect API key ID
ASC_ISSUER_ID App Store Connect issuer ID
ASC_KEY_PATH Path to .p8 key file (stored as secret file)

ANTI_PATTERN: Committing .p8 key files, certificates, or passwords to the app repository. FIX: Store in GitHub Actions secrets. The .p8 file is written to a temp path during CI only.

Version Numbering

GE convention:

  • Version (CFBundleShortVersionString): Semantic versioning — 1.2.3.
  • Build (CFBundleVersion): GitHub Actions run number — auto-incremented.
increment_build_number(build_number: ENV["GITHUB_RUN_NUMBER"])

GE-Specific Conventions

  • Fastlane Match for all signing — No manual certificate management.
  • GitHub Actions for CI/CD — No Jenkins, no CircleCI, no Bitrise.
  • TestFlight for all beta distribution — No Ad Hoc unless client-required.
  • API keys over Apple ID auth — More secure, no 2FA prompts in CI.
  • Client approves release timingautomatic_release: false by default.

Cross-References

READ_ALSO: wiki/docs/stack/swift-swiftui/index.md READ_ALSO: wiki/docs/stack/swift-swiftui/checklist.md READ_ALSO: wiki/docs/stack/swift-swiftui/pitfalls.md