Skip to main content

Mobile CI/CD Skill

Mobile CI/CD Skill

When to Use This Skill

Use this skill when implementing mobile cicd patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Comprehensive mobile CI/CD automation for iOS and Android deployment pipelines.

Fastlane

iOS Setup

# Install Fastlane
gem install fastlane

# Initialize in project
cd ios && fastlane init

Fastfile (iOS)

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
setup_ci if ENV['CI']

# Match for code signing
match(
type: "appstore",
readonly: true,
git_url: ENV['MATCH_GIT_URL'],
app_identifier: "com.company.app"
)

# Increment build number
increment_build_number(
build_number: ENV['BUILD_NUMBER'] || latest_testflight_build_number + 1
)

# Build app
build_app(
workspace: "App.xcworkspace",
scheme: "App",
export_method: "app-store",
output_directory: "./build",
output_name: "App.ipa"
)

# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: ENV['CHANGELOG'] || "Bug fixes and improvements"
)

# Notify Slack
slack(
message: "iOS beta #{get_version_number} (#{get_build_number}) uploaded to TestFlight",
success: true
)
end

desc "Deploy to App Store"
lane :release do
setup_ci if ENV['CI']

match(type: "appstore", readonly: true)

build_app(
workspace: "App.xcworkspace",
scheme: "App",
export_method: "app-store"
)

upload_to_app_store(
submit_for_review: true,
automatic_release: false,
force: true,
precheck_include_in_app_purchases: false,
submission_information: {
add_id_info_serves_ads: false,
add_id_info_tracks_action: false,
add_id_info_tracks_install: false,
add_id_info_uses_idfa: false
}
)
end

desc "Build for testing"
lane :test do
run_tests(
workspace: "App.xcworkspace",
scheme: "App",
devices: ["iPhone 15"],
code_coverage: true
)
end
end

Fastfile (Android)

# android/fastlane/Fastfile
default_platform(:android)

platform :android do
desc "Build and upload to Play Store internal track"
lane :beta do
# Increment version code
increment_version_code(
gradle_file_path: "app/build.gradle",
version_code: ENV['BUILD_NUMBER'] || (google_play_track_version_codes(track: 'internal').max + 1)
)

# Build release APK/AAB
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV['KEYSTORE_PATH'],
"android.injected.signing.store.password" => ENV['KEYSTORE_PASSWORD'],
"android.injected.signing.key.alias" => ENV['KEY_ALIAS'],
"android.injected.signing.key.password" => ENV['KEY_PASSWORD']
}
)

# Upload to Play Store
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)

slack(
message: "Android beta uploaded to Play Store internal track",
success: true
)
end

desc "Promote internal to production"
lane :release do
upload_to_play_store(
track: "internal",
track_promote_to: "production",
skip_upload_aab: true,
skip_upload_metadata: false
)
end

desc "Run tests"
lane :test do
gradle(task: "test")
end
end

Match (iOS Code Signing)

# ios/fastlane/Matchfile
git_url(ENV["MATCH_GIT_URL"])
storage_mode("git")
type("appstore")
app_identifier(["com.company.app", "com.company.app.notification-service"])
username(ENV["APPLE_ID"])
team_id(ENV["TEAM_ID"])

EAS Build (Expo)

Configuration

// eas.json
{
"cli": {
"version": ">= 5.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": {
"simulator": true
}
},
"preview": {
"distribution": "internal",
"ios": {
"resourceClass": "m-medium"
},
"android": {
"buildType": "apk"
}
},
"production": {
"distribution": "store",
"ios": {
"resourceClass": "m-medium"
},
"android": {
"buildType": "app-bundle"
}
}
},
"submit": {
"production": {
"ios": {
"appleId": "developer@company.com",
"ascAppId": "1234567890",
"appleTeamId": "ABCDEF1234"
},
"android": {
"serviceAccountKeyPath": "./google-play-key.json",
"track": "internal"
}
}
}
}

EAS Commands

# Build for preview
eas build --platform all --profile preview

# Build for production
eas build --platform all --profile production

# Submit to stores
eas submit --platform ios --profile production
eas submit --platform android --profile production

# OTA update
eas update --branch production --message "Bug fixes"

GitHub Actions

iOS Workflow

# .github/workflows/ios.yml
name: iOS Build

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: macos-14
steps:
- uses: actions/checkout@v4

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ios

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install CocoaPods
run: cd ios && pod install --repo-update

- name: Run tests
run: cd ios && bundle exec fastlane test

- name: Build and upload to TestFlight
if: github.ref == 'refs/heads/main'
run: cd ios && bundle exec fastlane beta
env:
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.ASC_API_KEY }}
BUILD_NUMBER: ${{ github.run_number }}

Android Workflow

# .github/workflows/android.yml
name: Android Build

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup JDK
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'gradle'

- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Decode keystore
if: github.ref == 'refs/heads/main'
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore

- name: Run tests
run: cd android && ./gradlew test

- name: Build and upload to Play Store
if: github.ref == 'refs/heads/main'
run: cd android && bundle exec fastlane beta
env:
KEYSTORE_PATH: ${{ github.workspace }}/android/app/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
BUILD_NUMBER: ${{ github.run_number }}

Expo/EAS Workflow

# .github/workflows/expo.yml
name: Expo Build

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Setup Expo
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}

- run: npm ci

- name: Build iOS
run: eas build --platform ios --profile production --non-interactive

- name: Build Android
run: eas build --platform android --profile production --non-interactive

- name: Submit to stores
if: github.ref == 'refs/heads/main'
run: |
eas submit --platform ios --profile production --non-interactive
eas submit --platform android --profile production --non-interactive

App Center

Configuration

# appcenter-pre-build.sh
#!/usr/bin/env bash
set -ex

# Install dependencies
npm ci

# Set environment variables
echo "REACT_APP_API_URL=$API_URL" >> .env
echo "REACT_APP_ENV=production" >> .env

App Center CLI

# Install CLI
npm install -g appcenter-cli

# Login
appcenter login

# Create app
appcenter apps create -d "My App" -o iOS -p React-Native

# Distribute to testers
appcenter distribute release \
--app "Company/App-iOS" \
--file ./build/App.ipa \
--group "Beta Testers" \
--release-notes "Bug fixes"

# Create distribution group
appcenter distribute groups create \
--app "Company/App-iOS" \
--name "Beta Testers"

Code Signing

iOS Certificates

# Create certificates and provisioning profiles
lane :setup_signing do
# Create App Store certificate
match(
type: "appstore",
readonly: false,
force_for_new_devices: true
)

# Create Development certificate
match(
type: "development",
readonly: false,
force_for_new_devices: true
)

# Create Ad Hoc for testing
match(
type: "adhoc",
readonly: false,
force_for_new_devices: true
)
end

# Register new device
lane :register_device do |options|
register_devices(
devices: {
options[:name] => options[:udid]
}
)
match(type: "development", force_for_new_devices: true)
match(type: "adhoc", force_for_new_devices: true)
end

Android Keystore

# Generate release keystore
keytool -genkeypair \
-v \
-storetype PKCS12 \
-keystore release.keystore \
-alias my-key-alias \
-keyalg RSA \
-keysize 2048 \
-validity 10000

# List keystore contents
keytool -list -v -keystore release.keystore

Version Management

# Fastfile version lanes
lane :bump_version do |options|
type = options[:type] || "patch"

case type
when "major"
increment_version_number(bump_type: "major")
when "minor"
increment_version_number(bump_type: "minor")
when "patch"
increment_version_number(bump_type: "patch")
end

# Also update Android
android_set_version_name(
version_name: get_version_number,
gradle_file: "../android/app/build.gradle"
)

commit_version_bump(
message: "Bump version to #{get_version_number}",
xcodeproj: "App.xcodeproj"
)

push_to_git_remote
end

Usage Examples

Setup Fastlane Pipeline

Apply mobile-cicd skill to configure Fastlane for iOS TestFlight and Android Play Store deployment

Implement Code Signing

Apply mobile-cicd skill to setup Match for iOS code signing with CI/CD integration

Configure EAS Build

Apply mobile-cicd skill to setup Expo EAS Build with GitHub Actions for automated releases

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: mobile-cicd

Completed:
- [x] Fastlane configured for {iOS|Android|both}
- [x] Code signing setup: {Match|Keystore}
- [x] CI/CD pipeline created: {GitHub Actions|GitLab CI|CircleCI}
- [x] Build lane tested: {beta|release}
- [x] App store upload successful: {TestFlight|Play Store}

Outputs:
- Platform: {iOS|Android|Both}
- Fastlane Fastfile: {path}
- CI Workflow: {path}
- Code Signing: {configured}
- Build Number: {build_number}
- Upload Status: {success|failed}
- App Store Link: {url}

Completion Checklist

Before marking this skill as complete, verify:

  • Fastlane installed and initialized (Gemfile, Fastfile created)
  • iOS code signing configured (Match setup with git_url, team_id, certificates synced)
  • Android keystore generated and secured (release.keystore, credentials in secrets)
  • Build lane implemented (increment version, build app, upload to store)
  • CI/CD workflow configured (GitHub Actions or equivalent)
  • Environment secrets set (MATCH_PASSWORD, KEYSTORE_PASSWORD, API keys)
  • Test build executed successfully (beta lane runs end-to-end)
  • App uploaded to store (TestFlight for iOS, internal track for Android)
  • Slack/notification integration working (build status notifications)
  • Version management automated (build number increment, version bumping)

Failure Indicators

This skill has FAILED if:

  • ❌ Fastlane installation fails or bundle install errors
  • ❌ iOS code signing fails (Match cannot access certificates repository)
  • ❌ Android keystore missing or password incorrect
  • ❌ Build fails (compilation errors, missing dependencies)
  • ❌ Upload to app store fails (authentication error, invalid provisioning)
  • ❌ CI workflow fails on push (environment variables not set)
  • ❌ Version increment logic broken (duplicate build numbers)
  • ❌ Notification integration not working (build completes but no alert)
  • ❌ EAS Build fails with authentication or configuration errors

When NOT to Use

Do NOT use this skill when:

  • Local development builds (use Xcode/Android Studio directly)
  • Non-mobile projects (web, backend, desktop apps)
  • Manual release process preferred (no CI/CD automation needed)
  • App store distribution not required (internal enterprise distribution only)
  • Single platform only and other platform never planned (use platform-specific tools)
  • Prototype/demo app not intended for distribution
  • Legacy build system migration in progress (coordinate carefully)

Use alternatives:

  • Local builds: Xcode/Android Studio for development
  • Manual uploads: App Store Connect/Play Console for one-off releases
  • Platform tools: Xcode Cloud (iOS), Firebase App Distribution (both)
  • Simple scripts: Bash scripts for basic automation without Fastlane

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Hardcoded credentials in FastfileSecurity vulnerabilityUse environment variables and secrets management
Committing keystore to repositoryExposes signing keysStore in secrets, base64-encode for CI
No version management automationManual errors, duplicatesAutomate build number increment, version bumping
Running builds sequentiallySlow pipeline (iOS then Android)Run iOS and Android builds in parallel
Skipping tests before uploadBroken builds in storesAlways run tests in beta lane
No rollback strategyBad builds stuck in productionKeep previous version artifacts, use staged rollout
Ignoring build warningsTechnical debt accumulationFail builds on warnings for critical checks

Principles

This skill embodies the following CODITECT principles:

  • #1 Automation First - Fully automated build, sign, upload pipeline
  • #5 Eliminate Ambiguity - Explicit environment variable requirements, clear error messages
  • #6 Clear, Understandable, Explainable - Fastlane lane names and steps self-documenting
  • #8 No Assumptions - Verify code signing, dependencies, secrets before build
  • Security by Default - Credentials in secrets, no hardcoding, Match for iOS certificates
  • Separation of Concerns - Separate lanes for test, beta, release; separate workflows per platform

Version: 1.1.0 | Created: 2025-12-22 | Updated: 2026-01-04 | Author: CODITECT Team