iOS CI/CD Guide
This document explains how iOS builds work for Eli Health.
Overview
iOS builds are automated using Fastlane and GitHub Actions. When code is pushed to a branch, a build is automatically triggered, uploaded to TestFlight, and made available for testing.
Environments
| Branch | App Name | Bundle ID | Build Config | Backend |
|---|---|---|---|---|
development2 | Eli Dev 2 | health.eli.Eli.dev2 | Dev2.Release | dev.eli-app.com |
development | Eli Dev | health.eli.Eli.dev | Dev.Release | dev.eli-app.com |
staging | Eli Stage | health.eli.Eli.staging | Staging.Release | staging.eli-app.com |
main | Eli | health.eli.Eli | Prod.Release | app.eli-app.com |
Each environment has its own:
- Bundle identifier (appears in App Store Connect as separate apps)
- Display name (shown on device home screen)
- Associated domains (for deep linking)
- Firebase/Google configuration
How Builds Are Triggered
Builds trigger automatically on push:
| Workflow | Trigger | Fastlane Lane |
|---|---|---|
ios-dev2-build.yml | Push to development2 | dev2 |
ios-dev-build.yml | Push to development | dev |
ios-staging-build.yml | Push to staging | staging |
ios-prod-build.yml | Push to main | prod |
You can also manually trigger builds from GitHub Actions → Select workflow → "Run workflow".
Development2 Branch
The development2 branch is a secondary development environment for testing large features before they're merged to development.
When to Use development2
Use development2 for:
- Large refactors that need extended testing
- Features that may take several iterations to stabilize
- Changes that could disrupt regular development flow
How It Works
- Same backend as development - Both
developmentanddevelopment2point todev.eli-app.com - Separate TestFlight app - "Eli Dev 2" appears as a separate app on TestFlight
- Isolated testing - QA can test new features without affecting the main dev build
- Merge when ready - Once stable, merge
development2→development
Workflow Example
# Start a large feature
git checkout development2
git checkout -b EXM-123-big-feature
# Work on feature, create PR targeting development2
# After PR is merged, development2 builds "Eli Dev 2"
# QA tests on Eli Dev 2
# When feature is stable, merge development2 into development
git checkout development
git merge development2
git push origin development
Build Process
Key Steps
-
Configure Environment - Runs
npm run configure:[dev|stg|prod]to set bundle ID, display name, and associated domains -
Fetch Certificates - Uses Match to download signing certificates from
eli-health/eli-fastlane-matchrepository -
Build - Compiles the app with the correct configuration using Xcode 16
-
Upload - Sends the IPA to TestFlight using App Store Connect API
Certificates & Signing
Match
Certificates and provisioning profiles are stored encrypted in a separate Git repository:
- Repository:
git@github.com:eli-health/eli-fastlane-match.git - Encryption: Files are encrypted with
MATCH_PASSWORD
Certificate Types
| Type | Used For | Environments |
|---|---|---|
appstore | App Store & TestFlight distribution | All |
development | Local development | All |
Creating New Certificates
Run locally (not in CI):
cd eli-app/ios
MATCH_PASSWORD='xxx' \
APP_STORE_CONNECT_API_KEY_ID='xxx' \
APP_STORE_CONNECT_ISSUER_ID='xxx' \
bundle exec fastlane ios create_certs
GitHub Secrets
These secrets are configured in eli-health/eli-app repository settings:
| Secret | Description |
|---|---|
MATCH_PASSWORD | Decrypts certificates in match repository |
MATCH_GIT_PRIVATE_KEY | SSH key to access match repository |
APP_STORE_CONNECT_API_KEY | Contents of AuthKey.p8 file |
APP_STORE_CONNECT_API_KEY_ID | API Key ID from App Store Connect |
APP_STORE_CONNECT_ISSUER_ID | Issuer ID from App Store Connect |
App Store Connect API Key
The API key allows automated authentication without 2FA:
- Go to App Store Connect → Users and Access → Integrations
- Generate API Key with "App Manager" role
- Download the
.p8file (one-time download) - Note the Key ID and Issuer ID
Build Numbers
Build numbers auto-increment based on the highest of:
- Latest build on TestFlight for that app
- Current build number in Xcode project
This ensures unique build numbers even when multiple builds run concurrently.
File Structure
eli-app/
├── ios/
│ ├── fastlane/
│ │ ├── Fastfile # Build lanes
│ │ ├── Matchfile # Certificate config
│ │ └── Appfile # App metadata
│ ├── Gemfile # Ruby dependencies
│ └── Podfile # CocoaPods dependencies
├── .github/workflows/
│ ├── ios-build.yml # Reusable workflow
│ ├── ios-dev2-build.yml # Dev2 trigger (development2 branch)
│ ├── ios-dev-build.yml # Dev trigger
│ ├── ios-staging-build.yml # Staging trigger
│ └── ios-prod-build.yml # Production trigger
└── scripts/
└── configure-environment.js # Sets bundle ID, display name, etc.
Common Commands
Check Build Status
# List recent builds
gh run list --repo eli-health/eli-app --workflow "iOS Dev Build"
gh run list --repo eli-health/eli-app --workflow "iOS Staging Build"
gh run list --repo eli-health/eli-app --workflow "iOS Production Build"
# View specific run
gh run view <RUN_ID> --repo eli-health/eli-app
# View failed logs
gh run view <RUN_ID> --repo eli-health/eli-app --log-failed
Sync Certificates Locally
cd eli-app/ios
MATCH_PASSWORD='xxx' bundle exec fastlane match appstore --readonly
Build Locally (No Upload)
cd eli-app/ios
bundle exec fastlane ios build_dev
bundle exec fastlane ios build_staging
bundle exec fastlane ios build_prod
Troubleshooting
"No matching provisioning profiles found"
Run create_certs locally to create the missing profile:
MATCH_PASSWORD='xxx' bundle exec fastlane ios create_certs
"Maximum number of certificates generated"
Apple limits distribution certificates to 3 per account. Revoke unused ones at Apple Developer Portal.
Build takes too long
Typical build times:
- With warm cache: ~20-25 minutes
- Cold cache: ~25-30 minutes
The CocoaPods cache significantly reduces build time.
"must be built with iOS 18 SDK"
The workflow automatically selects Xcode 16. If this error appears, check that the Xcode selection step is present in the workflow.
Quick Reference
| Action | Command |
|---|---|
| Trigger dev2 build | Push to development2 branch |
| Trigger dev build | Push to development branch |
| Trigger staging build | Push to staging branch |
| Trigger production build | Push to main branch |
| Create certificates | fastlane ios create_certs (local only) |
| Sync certificates | fastlane match appstore --readonly |
| Build locally | fastlane ios build_[dev2|dev|staging|prod] |