Claude Code for Mobile CI/CD: App Store Deployment with Fastlane and GitHub Actions — Claude Skills 360 Blog
Blog / DevOps / Claude Code for Mobile CI/CD: App Store Deployment with Fastlane and GitHub Actions
DevOps

Claude Code for Mobile CI/CD: App Store Deployment with Fastlane and GitHub Actions

Published: June 27, 2026
Read time: 9 min read
By: Claude Skills 360

Mobile CI/CD is harder than web CI/CD: code signing certificates, provisioning profiles, and app store submission requirements create friction that slows down releases. Fastlane automates the mechanical work — code signing, building, uploading to TestFlight — while GitHub Actions provides the trigger and environment. Claude Code generates Fastlane lanes and GitHub Actions workflows that handle both iOS and Android.

This guide covers mobile CI/CD with Claude Code: Fastlane setup, GitHub Actions workflows, code signing, and beta/production release automation.

Fastlane Setup

CLAUDE.md for Mobile Projects

## React Native / Mobile CI/CD

- Platform: iOS 17+ / Android API 33+
- Framework: React Native 0.73+
- CI: GitHub Actions (macOS-14 for iOS, ubuntu-latest for Android)
- Distribution: Fastlane for both platforms
- Beta: TestFlight (iOS), Firebase App Distribution (Android)
- Production: App Store Connect (iOS), Google Play (Android)

## Fastlane conventions
- Appfile: contains app identifiers and team IDs
- Fastfile: lanes organized by platform + shared lanes
- Environment: secrets in GitHub Actions secrets, loaded via dotenv in local dev
- Match: handles iOS code signing (preferred over manual cert management)
- Versioning: bump version and build number in lane (not manual)

## Release process
- Feature branch → PR → auto-deploy to beta on merge to main
- Production release: tag v* → automatic App Store/Play Store submission
# fastlane/Appfile
app_identifier(ENV["APP_IDENTIFIER"] || "com.mycompany.myapp")
apple_id(ENV["APPLE_ID"])
team_id(ENV["TEAM_ID"])
itc_team_id(ENV["ITC_TEAM_ID"])

iOS Fastfile

# fastlane/Fastfile
default_platform(:ios)

platform :ios do
  before_all do
    setup_ci if ENV['CI']  # Configure keychain for CI environment
  end
  
  desc "Sync code signing certificates"
  lane :certificates do
    match(
      type: "appstore",
      readonly: ENV['CI'],  # CI reads existing certs; local can create
      git_url: ENV['MATCH_GIT_URL'],
      git_branch: "main",
      app_identifier: ENV['APP_IDENTIFIER'],
    )
  end
  
  desc "Run tests"
  lane :test do
    run_tests(
      scheme: "MyApp",
      devices: ["iPhone 15 Pro"],
      reset_simulator: true,
    )
  end
  
  desc "Build and submit to TestFlight"
  lane :beta do
    certificates
    
    # Increment build number based on TestFlight's latest
    build_number = latest_testflight_build_number + 1
    increment_build_number(
      xcodeproj: "ios/MyApp.xcodeproj",
      build_number: build_number,
    )
    
    gym(
      scheme: "MyApp",
      workspace: "ios/MyApp.xcworkspace",
      configuration: "Release",
      export_method: "app-store",
      output_directory: "ios/build",
      output_name: "MyApp.ipa",
      clean: true,
    )
    
    upload_to_testflight(
      ipa: "ios/build/MyApp.ipa",
      skip_waiting_for_build_processing: true,  # Don't wait — proceed in CI
      changelog: ENV['CHANGELOG'] || changelog_from_git_commits,
    )
    
    # Notify Slack/Discord on success
    if ENV['SLACK_WEBHOOK_URL']
      slack(
        message: "✅ iOS beta build #{build_number} submitted to TestFlight",
        webhook_url: ENV['SLACK_WEBHOOK_URL'],
      )
    end
  end
  
  desc "Submit to App Store"
  lane :release do
    certificates
    
    # Get version from tag (e.g., "v2.3.1" → "2.3.1")
    version = ENV['RELEASE_VERSION']&.delete_prefix('v')
    
    increment_version_number(
      xcodeproj: "ios/MyApp.xcodeproj",
      version_number: version,
    )
    
    increment_build_number(
      xcodeproj: "ios/MyApp.xcodeproj",
      build_number: ENV['BUILD_NUMBER'] || Time.now.strftime('%Y%m%d%H%M'),
    )
    
    gym(
      scheme: "MyApp",
      workspace: "ios/MyApp.xcworkspace",
      configuration: "Release",
      export_method: "app-store",
    )
    
    upload_to_app_store(
      submit_for_review: true,
      automatic_release: false,  # Wait for manual App Store review approval
      force: true,  # Skip interactive confirmation
      submission_information: {
        add_id_info_uses_idfa: false,
        export_compliance_uses_encryption: false,
      },
    )
  end
end

Android Fastfile

platform :android do
  desc "Build and submit to Firebase App Distribution"
  lane :beta do
    # Increment version code
    version_code = google_play_track_version_codes(track: 'internal').first.to_i + 1
    
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/",
      properties: {
        "android.injected.signing.store.file" => ENV['KEYSTORE_FILE'],
        "android.injected.signing.store.password" => ENV['KEYSTORE_PASSWORD'],
        "android.injected.signing.key.alias" => ENV['KEY_ALIAS'],
        "android.injected.signing.key.password" => ENV['KEY_PASSWORD'],
        "versionCode" => version_code,
      },
    )
    
    firebase_app_distribution(
      app: ENV['FIREBASE_APP_ID_ANDROID'],
      groups: "internal-testers, qa-team",
      release_notes: ENV['CHANGELOG'] || changelog_from_git_commits,
      service_credentials_file: ENV['FIREBASE_SERVICE_ACCOUNT_FILE'],
    )
  end
  
  desc "Submit to Google Play"
  lane :release do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/",
      properties: {
        "android.injected.signing.store.file" => ENV['KEYSTORE_FILE'],
        "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(
      track: 'internal',  # internal → alpha → beta → production
      aab: "android/app/build/outputs/bundle/release/app-release.aab",
    )
  end
end

GitHub Actions Workflows

Run tests on every PR, deploy to beta on merge to main,
and release to production when a version tag is pushed.
# .github/workflows/ci.yml
name: CI

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

jobs:
  test-ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true
      
      - name: Install CocoaPods
        run: cd ios && pod install --repo-update
      
      - name: Run iOS tests
        run: bundle exec fastlane ios test

  test-android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - name: Run Android unit tests
        run: cd android && ./gradlew test
# .github/workflows/beta.yml
name: Beta Release

on:
  push:
    branches: [main]
    paths-ignore:
      - '**.md'
      - 'docs/**'

jobs:
  beta-ios:
    runs-on: macos-14
    environment: beta  # Requires approval in GitHub Environments
    
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # For changelog generation
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      
      - name: Install dependencies
        run: yarn install --frozen-lockfile
      
      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3'
          bundler-cache: true
      
      - name: Install CocoaPods
        run: cd ios && pod install
      
      - name: Deploy to TestFlight
        run: bundle exec fastlane ios beta
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          ITC_TEAM_ID: ${{ secrets.APPLE_ITC_TEAM_ID }}
          MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
          APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
          APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

  beta-android:
    runs-on: ubuntu-latest
    environment: beta
    
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'
      
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
      
      - name: Decode keystore
        run: |
          echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/keystore.jks
      
      - name: Deploy to Firebase App Distribution
        run: bundle exec fastlane android beta
        env:
          KEYSTORE_FILE: android/keystore.jks
          KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
          FIREBASE_APP_ID_ANDROID: ${{ secrets.FIREBASE_APP_ID_ANDROID }}
          FIREBASE_SERVICE_ACCOUNT_FILE: ${{ runner.temp }}/service-account.json
# .github/workflows/release.yml
name: Production Release

on:
  push:
    tags:
      - 'v*'  # e.g., v2.3.1

jobs:
  release-ios:
    runs-on: macos-14
    environment: production  # Requires manual approval
    
    steps:
      - uses: actions/checkout@v4
      
      # ... same setup as beta ...
      
      - name: Submit to App Store
        run: bundle exec fastlane ios release
        env:
          RELEASE_VERSION: ${{ github.ref_name }}
          # ... same secrets as beta ...

Certificate Management with Match

Our iOS cert management is chaotic — different developers
have different certificates installed. Set up match.
# Initialize match (run once, creates the certificate repo)
bundle exec fastlane match init
# Prompts for: Git URL for certificate repo, app identifier

# Generate and store certificates
bundle exec fastlane match appstore    # Production cert
bundle exec fastlane match development # Development cert

# Now all developers run:
bundle exec fastlane match development --readonly
# Downloads and installs certificates from the shared Git repo

Match encrypts certificates with a passphrase and stores them in a private Git repository. CI uses readonly: true (ENV[‘CI’]) — it only downloads existing certs, never creates new ones. This prevents CI from consuming certificate slots.

For the React Native code that runs in these mobile apps, see the React Native guide. For E2E testing of mobile apps using Detox before the CI/CD pipeline deploys them, see the mobile testing guide. The Claude Skills 360 bundle includes mobile CI/CD skill sets for Fastlane and App Store automation. Start with the free tier to generate Fastlane lanes for your mobile project.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free