Sample iOS app (PipelineGeneric) with GitHub Actions CI/CD: reusable jobs, Fastlane gym with distribution certificate + App Store provisioning profile from GitHub Actions secrets (same pattern as base64 P12 + profile in YAML), optional TestFlight upload, SwiftLint-based code health, and CodeQL.
| Workflow | When | What |
|---|---|---|
| iOS CI/CD | push / pull_request to main or master; workflow_dispatch |
SwiftLint + SwiftFormat → security (Semgrep, TruffleHog, Snyk) → in parallel: Debug Simulator .app + unit tests (≥80% line coverage); both pick the newest iOS runtime’s first available iPhone Simulator via .github/scripts/ios_first_iphone_sim_udid.py (platform=…,name=…,OS=…, not a hardcoded UDID) → archive on main/master push or manual dispatch: install signing assets from secrets + gym IPA |
| iOS code health | Same branches as above | SwiftLint + tech-debt + heuristic security reports → artifact ios-code-health-reports |
| CodeQL Swift | main / master push & PR; weekly Monday 06:00 UTC |
Swift analysis → Security → Code scanning (enable Code scanning on the repo) |
| Dependabot | Weekly | Opens PRs to bump GitHub Actions pins |
These are invoked by .github/workflows/ios.yml. ios-release.yml can also be run standalone (Actions → iOS — archive (staging) → Run workflow) with the same inputs as the orchestrator’s archive job:
| File | Role |
|---|---|
ios-job-lint.yml |
SwiftLint + SwiftFormat (--lint, .swiftformat) on app / test targets |
ios-job-security.yml |
Semgrep, TruffleHog, Snyk |
ios-job-simulator-artifact.yml |
Unsigned Debug Simulator .app (default artifact name: PipelineGeneric-iphonesimulator-Debug) |
ios-job-test.yml |
xcodebuild test + xccov coverage gate |
ios-release.yml |
bundle exec fastlane staging_build + optional upload_testflight_ipa |
From Actions → iOS CI/CD → Run workflow:
- Runs the same DAG; archive (keychain +
gym) always runs on manual dispatch. - Upload IPA to TestFlight runs only when you enable that input (uses the App Store Connect API key secrets).
To run only the archive (skip lint, tests, and simulator jobs), use Actions → iOS — archive (staging) → Run workflow (same app_identifier and upload to TestFlight inputs).
| Secret | Used by |
|---|---|
SNYK_TOKEN |
ios-job-security.yml (Snyk only) |
Without SNYK_TOKEN, or without a root Package.swift / Podfile (or Podfile.lock), the Snyk step is skipped (notice in the job log) so snyk test is not run on unsupported Xcode-only layouts. Add SNYK_TOKEN under Settings → Secrets and variables → Actions to enable Snyk when you have a supported manifest.
| Secret | Used by |
|---|---|
DIST_CERTIFICATE_BASE64 |
Distribution .p12 (or .p12 exported from Keychain) encoded with base64 (no line breaks), same idea as IOS_DEV_CERTIFICATE_BASE64 in the Edward Jones POC workflow |
DIST_CERTIFICATE_PASSWORD |
Password for that .p12 |
DIST_PROFILE_BASE64 |
App Store provisioning profile (.mobileprovision) base64-encoded |
APP_STORE_CONNECT_KEY_ID |
TestFlight upload (upload_testflight_ipa); still declared required by the reusable workflow |
APP_STORE_CONNECT_ISSUER_ID |
TestFlight upload |
APP_STORE_CONNECT_API_KEY |
API key PEM contents (plain text, not base64) |
The archive job creates a temp keychain, imports the certificate, decodes the profile, installs it as {UUID}.mobileprovision, reads Name / TeamIdentifier from the profile for manual signing, then runs bundle exec fastlane staging_build. On GitHub Actions, staging_build bumps CFBundleVersion to latest TestFlight build + 1 (App Store Connect API) so TestFlight uploads do not fail with “bundle version must be higher than the previously uploaded version.”
| Secret | Used by |
|---|---|
IOS_CODESIGN_IDENTITY |
Full Code Signing Identity string (e.g. Apple Distribution: …) when multiple distribution identities are present in the imported .p12 |
- Code scanning enabled for the repository so CodeQL results appear under Security.
- Ruby / Fastlane: use Ruby 3.1.x for
bundle install(same as archive CI): fastlane → xcodeproj → CFPropertyList 3.x does not support Ruby 3.2+. Thenbundle exec fastlane staging_buildafter you match CI: import the same distribution cert into your login keychain, install the App Store profile under~/Library/MobileDevice/Provisioning Profiles/as{UUID}.mobileprovision, and exportAPPLE_TEAM_ID,IOS_PROVISIONING_PROFILE_NAME(profile Name from the portal / plist), andAPP_IDENTIFIER(bundle id). Or install fastlane via Homebrew. - SwiftLint: config is
.swiftlint.yml; CI runs it inios-job-lint.yml(main DAG) and iOS code health (JSON + reports artifact). - SwiftFormat: config is
.swiftformat; install withbrew install swiftformat, then e.g.swiftformat --lint PipelineGeneric/PipelineGeneric PipelineGeneric/PipelineGenericTests PipelineGeneric/PipelineGenericUITests(same asios-job-lint.yml).
- App: PipelineGeneric/PipelineGeneric.xcodeproj, Release bundle id
com.codeandtheory.edward-jones-poc.dist(Debug:com.codeandtheory.edward-jones-poc.dev). - Shared Xcode scheme (required for CI):
PipelineGeneric.xcodeproj/xcshareddata/xcschemes/PipelineGeneric.xcscheme— must stay committed (do not rely only onxcuserdataschemes). - Scripts:
.github/scripts/— simulator UDID helper, code-health reports. - Fastlane: fastlane/Fastfile — lanes
staging_build,upload_testflight_ipa. - Cursor / agent rules:
.cursor/rules/(bootstrap, CI reference, simulator artifact runbook).
# List schemes (should show PipelineGeneric)
xcodebuild -list -project PipelineGeneric/PipelineGeneric.xcodeproj
# Tests + coverage (same destination picker as CI: name + OS, not a machine-specific UDID)
DEST="$(python3 .github/scripts/ios_first_iphone_sim_udid.py)"
xcodebuild test \
-project PipelineGeneric/PipelineGeneric.xcodeproj \
-scheme PipelineGeneric \
-destination "${DEST}" \
-enableCodeCoverage YES \
-derivedDataPath /tmp/PGDerived \
-resultBundlePath /tmp/PGTestResults.xcresult
# Code health generators (writes under Reports/, gitignored)
python3 .github/scripts/tech_debt_scan.py
python3 .github/scripts/security_patterns_scan.py
python3 .github/scripts/codebase_health_report.pyDo not commit a nested ios-devops-cursor-kit/ folder at repo root — keep a single .github/workflows/ tree here; kit content should be merged once at the root.