ADR Reference: ADR-MOB-003
Mobile CI/CD presents unique engineering challenges absent from backend pipelines: iOS builds require macOS infrastructure, code signing involves certificates and provisioning profiles that expire and must be shared across a team, both app stores have multi-day review processes, and staged rollouts require coordination between the deployment pipeline and store dashboards. This ADR eliminates the class of failures that arise from ad-hoc, undocumented mobile release processes.

ADR Metadata

Field Value
ADR Reference ADR-MOB-003
Version 1.0
Date Raised May 2025
Review Date November 2025
Author Solutions Architecture Practice — Ascendion
Status ACCEPTED
Domain Mobile DevOps / Platform Engineering
ARB Approval Required
Stakeholders Mobile DevOps Engineers · Release Managers · Platform Engineers · Security Architects · QA Lead

Executive Summary

Decision: Fastlane combined with GitHub Actions is adopted as the standard mobile CI/CD pipeline for all Ascendion mobile projects. Fastlane manages code signing via fastlane Match, build automation, test execution, and store submission. GitHub Actions orchestrates the pipeline using matrix strategy to run Android and iOS builds in parallel. Xcode Cloud is adopted as a supplementary option for iOS-only projects. The pipeline enforces test coverage gates from ADR-MOB-001: Use Cases above 90%, ViewModels above 80%. All production releases require QA sign-off as a mandatory pipeline gate.

Decision Drivers

Priority Quality Attribute Weight Rationale
1 Code Signing Reliability 25% Signing failures are the most common iOS release delay cause
2 Quality Gate Enforcement 22% Test coverage and snapshot gates must be automated
3 Pipeline Speed 18% Target under 30 minutes total. Slow pipelines are bypassed
4 Automation Coverage 15% Build, test, sign, distribute, submit — zero manual steps
5 Cost 12% macOS runner cost is 10× Linux. Minimise macOS minutes
6 Maintainability 8% Pipeline as code, versioned in Git, operable by any engineer

Considered Options

Option A — Fastlane + GitHub Actions (ADOPTED)

Weighted score: 4.81 / 5.0. fastlane Match solves code signing at the root — all certificates stored encrypted in a dedicated Git repo, every CI machine and developer clones fresh. GitHub Actions matrix runs Android (Linux) and iOS (macOS) in parallel, halving total pipeline time.

Option B — Bitrise

Weighted score: 3.46 / 5.0 — DISMISSED. Cost at scale ($450–900/month) exceeds GitHub Actions with self-hosted macOS runner. Vendor lock-in on pipeline configuration prevents migration.

Option C — Xcode Cloud (iOS SUPPLEMENT)

Apple-native CI. 25 free compute hours/month. Direct TestFlight integration. iOS/macOS only. Adopted as supplementary for iOS-only projects — cannot replace Fastlane for Android.

Option D — Codemagic

Weighted score: 3.72 / 5.0 — DISMISSED. Valid for Flutter projects but adds separate vendor dependency. GitHub Actions + Fastlane provides equivalent capability at lower per-project cost at Ascendion engagement volume.

Decision

Fastlane + GitHub Actions is mandatory for all Ascendion mobile projects. The pipeline runs the six stages below, with the quality gates enforced as blocking conditions before merge and before production deploy.

Pipeline Architecture

Stage Trigger Android iOS Time
Validate Every PR Detekt lint, unit tests (90% gate), snapshot diff, security scan SwiftLint, unit tests (90% gate), snapshot diff ~8 min
Build Merge to main Gradle assembleRelease, R8 full mode Xcode Archive, Release config ~15 min
Sign Post-build Keystore decode from base64 secret fastlane match, appstore profile ~3 min
Distribute Post-sign Firebase App Distribution, Play internal track TestFlight internal ~5 min
QA Gate Manual GitHub Environment protection rule GitHub Environment protection rule 24-48hr
Release Post-QA Play staged rollout 1%→5%→20%→100% App Store review submission ~10 min

Mandatory Quality Gates

Gate Tool Threshold Failure Action
Use Case Coverage JaCoCo / Xcode Coverage > 90% Block PR merge
ViewModel Coverage JaCoCo / Xcode Coverage > 80% Block PR merge
Snapshot Test Diff Paparazzi / iOSSnapshot Zero pixel regressions Block PR merge
Lint and Static Analysis Detekt / SwiftLint Zero error-level violations Block PR merge
Dependency CVE Scan OWASP Dependency Check No P1/P2 CVEs Block merge, alert SA
QA Sign-off GitHub Environment Protection QA Lead approval Block production deploy

Trade-off Analysis

Trade-off Accepted Consequence Mitigation
macOS runners cost ~10× Linux iOS build minutes dominate pipeline cost Matrix strategy runs Android on Linux; macOS minutes confined to the iOS build/sign jobs only
Mandatory QA gate adds 24–48hr latency Production release is not fully automated GitHub Environment Protection makes the gate explicit and auditable; human QA catches what automated tests miss
fastlane Match requires a shared encrypted certificate repo Coordination + MATCH_PASSWORD secret management overhead Certificates auto-renew 30 days before expiry; any CI machine or developer with the passphrase can sign

Implementation Guidance

iOS Code Signing — fastlane Match (Mandatory)

All certificates and provisioning profiles stored encrypted in a dedicated private Git repository. Every CI machine and developer clones fresh using MATCH_PASSWORD encrypted secret. Certificate rotation: Match regenerates automatically 30 days before expiry. Eliminates the certificate-locked-to-one-Mac failure mode permanently.

Android Code Signing — Keystore via GitHub Secrets (Mandatory)

Release Keystore encoded as base64 stored as KEYSTORE_B64 encrypted secret. CI decodes to temporary file, configures Gradle signingConfigs, deletes after signing. Enrolled in Google Play App Signing — Google manages distribution key.

Pipeline Setup Steps

  1. Configure GitHub Actions matrix to run Android (ubuntu-latest) and iOS (macos-latest) in parallel
  2. Initialise fastlane Match against a dedicated encrypted certificate repository; store MATCH_PASSWORD as an encrypted secret
  3. Encode the Android release keystore as base64 and store as KEYSTORE_B64 encrypted secret; enrol in Play App Signing
  4. Wire the six quality gates as blocking checks before merge
  5. Configure GitHub Environment Protection requiring QA Lead approval before the production environment
  6. Configure staged rollout via fastlane supply (Play) and fastlane deliver (App Store)

Compliance Checkpoints

Checkpoint Trigger Owner SLA
Pipeline Architecture Review New mobile project setup Platform Engineer Before first release
Code Signing Setup Review Project initialisation Mobile DevOps Lead Before first build
Dependency CVE Gate Every CI build — automated CI Pipeline Real-time, blocks on P1/P2
QA Sign-off Gate Every production release QA Lead Blocks production deploy
Coverage Gate Every PR — automated CI Pipeline Real-time
Reference Title Relationship
ADR-MOB-001 Mobile Architecture Pattern Test coverage gates reference standards from this ADR
ADR-MOB-002 Mobile Platform Selection Platform determines Android vs iOS pipeline configuration
ADR-SEC-011 Mobile Security Controls Dependency CVE scanning gate references security standards

Flowchart

%%{init:{'theme':'base','themeVariables':{'fontSize':'14px','fontFamily':'IBM Plex Sans, system-ui, sans-serif','primaryColor':'#DBEAFE','primaryTextColor':'#1e3a5f','primaryBorderColor':'#2563EB','lineColor':'#374151','clusterBkg':'#F9FAFB','clusterBorder':'#D1D5DB','edgeLabelBackground':'#FFFFFF'},'flowchart':{'curve':'orthogonal','padding':30,'nodeSpacing':65,'rankSpacing':75,'useMaxWidth':true}}}%% flowchart TD GIT["<b>git push to main</b><br/>Trunk-based · Feature branch max 2 days"] subgraph VALIDATE["Stage 1 — Validate · &lt; 8 min · Every PR"] direction LR LINT["<b>Lint · Static Analysis</b><br/>Detekt · SwiftLint<br/>Zero violations — blocks merge"] UNIT_CI["<b>Unit Tests</b><br/>Use Cases ≥ 90%<br/>ViewModels ≥ 80%"] SNAP_CI["<b>Snapshot Diff</b><br/>Zero pixel regressions<br/>All themes · Dynamic Type"] SEC_CI["<b>Security Scan</b><br/>OWASP Dependency Check<br/>GitLeaks · Blocks P1/P2 CVE"] end subgraph BUILD["Stage 2 — Parallel Build · Merge to Main"] direction LR AND_BUILD["<b>Android — ubuntu-latest</b><br/>Gradle assembleRelease<br/>R8 Full Mode · Baseline Profiles"] IOS_BUILD["<b>iOS — macos-latest</b><br/>Xcode Archive · Release<br/>dSYM for Crashlytics"] end subgraph SIGN["Stage 3 — Code Signing"] direction LR AND_SIGN["<b>Android Keystore</b><br/>Base64 GitHub Secret<br/>Play App Signing enrolled"] IOS_SIGN["<b>fastlane Match</b><br/>Encrypted Git repo<br/>Auto-renews 30d before expiry"] end subgraph DIST["Stage 4 — QA Distribution"] direction LR FIREBASE["<b>Firebase App Distribution</b><br/>Android internal builds"] TF["<b>TestFlight Internal</b><br/>iOS internal testing"] end QA_CI{"<b>Stage 5 — QA Gate</b><br/>GitHub Environment Protection<br/>QA Lead approval required"} subgraph RELEASE["Stage 6 — Production Release"] direction LR PLAY["<b>Google Play</b><br/>fastlane supply<br/>1%→5%→25%→50%→100%"] APPSTORE["<b>App Store</b><br/>fastlane deliver<br/>Expedited review available"] end GIT --> VALIDATE --> BUILD --> SIGN --> DIST --> QA_CI QA_CI -->|"Approved"| RELEASE QA_CI -->|"Rejected"| GIT style GIT fill:#1D4ED8,stroke:#1e3a5f,color:#ffffff style VALIDATE fill:#DCFCE7,stroke:#16A34A,stroke-width:2px style BUILD fill:#DBEAFE,stroke:#2563EB,stroke-width:2px style SIGN fill:#FEE2E2,stroke:#DC2626,stroke-width:2px style DIST fill:#FEF9C3,stroke:#CA8A04,stroke-width:2px style QA_CI fill:#FEF3C7,stroke:#D97706,stroke-width:2px style RELEASE fill:#F3E8FF,stroke:#7C3AED,stroke-width:2px style LINT fill:#BBF7D0,stroke:#16A34A,color:#14532D style UNIT_CI fill:#BBF7D0,stroke:#16A34A,color:#14532D style SNAP_CI fill:#BBF7D0,stroke:#16A34A,color:#14532D style SEC_CI fill:#BBF7D0,stroke:#16A34A,color:#14532D style AND_BUILD fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f style IOS_BUILD fill:#BFDBFE,stroke:#2563EB,color:#1e3a5f style AND_SIGN fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d style IOS_SIGN fill:#FCA5A5,stroke:#DC2626,color:#7f1d1d style FIREBASE fill:#FDE68A,stroke:#CA8A04,color:#713f12 style TF fill:#FDE68A,stroke:#CA8A04,color:#713f12 style PLAY fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95 style APPSTORE fill:#E9D5FF,stroke:#7C3AED,color:#4C1D95

References

  1. Fastlane Documentation. docs.fastlane.tools
  2. GitHub Actions — Environments. docs.github.com/en/actions/deployment
  3. Google — Play Console Staged Rollouts. support.google.com/googleplay/android-developer
  4. Forsgren et al. — Accelerate. IT Revolution, 2018.
  5. OWASP — Dependency Check. owasp.org/www-project-dependency-check

⚠ Manual Code Signing — Engineer manually downloading provisioning profiles, building locally, uploading to TestFlight. Undocumented, unreproducible, fails when that engineer is unavailable.
CORRECT: fastlane Match. All signing materials in encrypted repository. Any CI machine or developer with the passphrase can sign and release.

⚠ No QA Gate Before Production — Pipeline automatically promotes to production after passing automated tests. Automated tests catch regressions; human QA catches usability and visual issues automated tests miss.
CORRECT: GitHub Environment Protection Rule requiring QA Lead approval before any production release.