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
- Review the patterns and examples below
- Apply the relevant patterns to your implementation
- 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-Pattern | Problem | Solution |
|---|---|---|
| Hardcoded credentials in Fastfile | Security vulnerability | Use environment variables and secrets management |
| Committing keystore to repository | Exposes signing keys | Store in secrets, base64-encode for CI |
| No version management automation | Manual errors, duplicates | Automate build number increment, version bumping |
| Running builds sequentially | Slow pipeline (iOS then Android) | Run iOS and Android builds in parallel |
| Skipping tests before upload | Broken builds in stores | Always run tests in beta lane |
| No rollback strategy | Bad builds stuck in production | Keep previous version artifacts, use staged rollout |
| Ignoring build warnings | Technical debt accumulation | Fail 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