Skip to main content

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

LaneCommandDescription
devicefastlane ios deviceBuild debug IPA and install on connected iPhone via xcrun devicectl
betafastlane ios betaBuild release IPA, increment build number, and upload to TestFlight
releasefastlane ios releaseBuild release IPA and submit to App Store Connect (manual review trigger)
provisionfastlane ios provisionRegister App IDs (app.lucidpal, app.lucidpal.widget) and App Group on Apple Developer Portal
generatefastlane ios generateRegenerate .xcodeproj from project.yml via XcodeGen
preparefastlane ios prepareCheck device connectivity via xcrun xctrace (USB or Wi-Fi, retries up to 10×)
certsfastlane ios certsSync App Store certificates and profiles via match
enable_groupsfastlane ios enable_groupsEnable App Groups capability on both App IDs

Lane Details

device

  1. Runs prepare → verifies device is reachable
  2. Runs generate → regenerates .xcodeproj
  3. Builds a Debug IPA with development export method
  4. On CI: writes the ASC API key .p8 to /tmp/AuthKey_<id>.p8 for provisioning, then deletes it
  5. Installs via xcrun devicectl device install app --device <DEVICE_CORE_ID>

beta

  1. Conditionally runs setup_ci (keychain init) — skipped on M3 Max self-hosted runner
  2. Runs generate
  3. Loads ASC API key from env vars
  4. Syncs appstore profiles via match (readonly)
  5. Increments build number to latest_testflight_build_number + 1
  6. Updates manual code signing settings for LucidPal and LucidPalWidget targets
  7. Builds a Release IPA with app-store export method
  8. Uploads to TestFlight (skip_waiting_for_build_processing: true)

release

  1. Runs generate
  2. Increments build number
  3. Builds a Release IPA with automatic provisioning
  4. 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)

PropertyValue
TriggerPush to main touching apps/lucidpal-ios/** (excludes README.md), or manual workflow_dispatch
Runnerself-hosted, macos, ios, arm64 (M3 Max)
Timeout60 minutes
Concurrencytestflight group — cancels in-progress runs

Steps:

  1. Checkout repo
  2. bundle install (Ruby gems)
  3. bundle exec fastlane ios beta with secrets injected as env vars

Deploy Docs (docs.yml)

PropertyValue
TriggerPush to main touching docs/**, or manual workflow_dispatch
Runnerubuntu-latest
Concurrencypages group — does not cancel in-progress

Steps:

  1. Install Node 22, npm install in docs/
  2. npm run build
  3. Upload build artifact → deploy to GitHub Pages

Required Secrets

Configure these in GitHub → Settings → Secrets → Actions:

SecretUsed ByDescription
APP_STORE_CONNECT_API_KEY_IDbeta, device (CI)ASC API Key ID (e.g. 9W74KCHBGG)
APP_STORE_CONNECT_API_ISSUER_IDbeta, device (CI)ASC API Issuer ID (UUID)
APP_STORE_CONNECT_API_KEY_CONTENTbeta, device (CI)Base64-encoded .p8 private key content
MATCH_PASSWORDbetaEncryption password for match cert repo
MATCH_GIT_BASIC_AUTHORIZATIONbetaBase64 user:token for accessing the match git repo

Local Env Vars (optional overrides)

VariableDefaultDescription
DEVICE_UDID00008150-000C08842604401CDevice UDID (xcrun xctrace list devices)
DEVICE_CORE_IDF8C4D569-E846-5445-B4EC-8B4B48714D01CoreDevice 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:

AspectLocalCI
KeychainLogin keychain (existing)Temporary CI keychain via setup_ci
ASC API keyOptional (Xcode session)Required via secrets
setup_ciSkipped (RUNNER_NAME starts with cicd-m3max)Runs on headless runners
Device installxcrun devicectl to physical deviceNot 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_ci is skipped
  • Supports native Xcode 26 builds (arm64)
  • Is identified in testflight.yml via runs-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