CI/CD Pipeline
LucidPal uses Fastlane for build automation and GitHub Actions for continuous delivery.
Fastlane Lanes
All lanes run from apps/lucidpal-ios/ via bundle exec fastlane ios <lane>.
| Lane | Command | Description |
|---|---|---|
device | fastlane ios device | Build debug IPA and install on connected iPhone via xcrun devicectl |
beta | fastlane ios beta | Build release IPA, increment build number, and upload to TestFlight |
release | fastlane ios release | Build release IPA and submit to App Store Connect (manual review trigger) |
provision | fastlane ios provision | Register App IDs (app.lucidpal, app.lucidpal.widget) and App Group on Apple Developer Portal |
generate | fastlane ios generate | Regenerate .xcodeproj from project.yml via XcodeGen |
prepare | fastlane ios prepare | Check device connectivity via xcrun xctrace (USB or Wi-Fi, retries up to 10×) |
certs | fastlane ios certs | Sync App Store certificates and profiles via match |
enable_groups | fastlane ios enable_groups | Enable App Groups capability on both App IDs |
Lane Details
device
- Runs
prepare→ verifies device is reachable - Runs
generate→ regenerates.xcodeproj - Builds a
DebugIPA withdevelopmentexport method - On CI: writes the ASC API key
.p8to/tmp/AuthKey_<id>.p8for provisioning, then deletes it - Installs via
xcrun devicectl device install app --device <DEVICE_CORE_ID>
beta
- Conditionally runs
setup_ci(keychain init) — skipped on M3 Max self-hosted runner - Runs
generate - Loads ASC API key from env vars
- Syncs
appstoreprofiles viamatch(readonly) - Increments build number to
latest_testflight_build_number + 1 - Updates manual code signing settings for
LucidPalandLucidPalWidgettargets - Builds a
ReleaseIPA withapp-storeexport method - Uploads to TestFlight (
skip_waiting_for_build_processing: true)
release
- Runs
generate - Increments build number
- Builds a
ReleaseIPA with automatic provisioning - Submits via
deliver(does not auto-submit for review — manual trigger in App Store Connect)
provision
Run once after a bundle ID rename or new capability. Requires FASTLANE_USER and FASTLANE_PASSWORD (Apple ID credentials).
GitHub Actions Workflows
TestFlight (testflight.yml)
| Property | Value |
|---|---|
| Trigger | Push to main touching apps/lucidpal-ios/** (excludes README.md), or manual workflow_dispatch |
| Runner | self-hosted, macos, ios, arm64 (M3 Max) |
| Timeout | 60 minutes |
| Concurrency | testflight group — cancels in-progress runs |
Steps:
- Checkout repo
bundle install(Ruby gems)bundle exec fastlane ios betawith secrets injected as env vars
Deploy Docs (docs.yml)
| Property | Value |
|---|---|
| Trigger | Push to main touching docs/**, or manual workflow_dispatch |
| Runner | ubuntu-latest |
| Concurrency | pages group — does not cancel in-progress |
Steps:
- Install Node 22,
npm installindocs/ npm run build- Upload build artifact → deploy to GitHub Pages
Required Secrets
Configure these in GitHub → Settings → Secrets → Actions:
| Secret | Used By | Description |
|---|---|---|
APP_STORE_CONNECT_API_KEY_ID | beta, device (CI) | ASC API Key ID (e.g. 9W74KCHBGG) |
APP_STORE_CONNECT_API_ISSUER_ID | beta, device (CI) | ASC API Issuer ID (UUID) |
APP_STORE_CONNECT_API_KEY_CONTENT | beta, device (CI) | Base64-encoded .p8 private key content |
MATCH_PASSWORD | beta | Encryption password for match cert repo |
MATCH_GIT_BASIC_AUTHORIZATION | beta | Base64 user:token for accessing the match git repo |
Local Env Vars (optional overrides)
| Variable | Default | Description |
|---|---|---|
DEVICE_UDID | 00008150-000C08842604401C | Device UDID (xcrun xctrace list devices) |
DEVICE_CORE_ID | F8C4D569-E846-5445-B4EC-8B4B48714D01 | CoreDevice ID (xcrun devicectl list devices) |
Local Developer Workflow
# One-time setup: register App IDs and App Group
fastlane ios provision
# Regenerate Xcode project after project.yml changes
fastlane ios generate
# Deploy to your iPhone (USB or Wi-Fi)
fastlane ios device
For device installation, the ASC API key env vars are optional locally — Xcode's existing session handles provisioning automatically.
CI Workflow (GitHub Actions)
Push to main (apps/lucidpal-ios/**) →
self-hosted M3 Max runner →
bundle install →
fastlane ios beta →
generate → match certs → build Release → upload TestFlight
Key differences from local:
| Aspect | Local | CI |
|---|---|---|
| Keychain | Login keychain (existing) | Temporary CI keychain via setup_ci |
| ASC API key | Optional (Xcode session) | Required via secrets |
setup_ci | Skipped (RUNNER_NAME starts with cicd-m3max) | Runs on headless runners |
| Device install | xcrun devicectl to physical device | Not run (beta lane only) |
Self-Hosted Runner
The TestFlight workflow runs on a self-hosted M3 Max arm64 macOS runner (RUNNER_NAME prefix: cicd-m3max). This runner:
- Has a persistent login keychain —
setup_ciis skipped - Supports native Xcode 26 builds (arm64)
- Is identified in
testflight.ymlviaruns-on: [self-hosted, macos, ios, arm64]
The beta lane detects whether it's running on this runner via:
is_headless = ENV["CI"] && !ENV["RUNNER_NAME"]&.start_with?("cicd-m3max")
setup_ci if is_headless