Agent skill
fastlane
iOS and Android app deployment automation with Fastlane. Use when building, signing, and distributing apps to TestFlight, App Store, or Google Play. Covers Match code signing, CI/CD keychain setup, and Tauri integration. Triggers on Fastfile, Appfile, Matchfile, fastlane commands.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/fastlane
SKILL.md
Fastlane Deployment Skill
Automate iOS and Android app building, code signing, and distribution.
When to Apply
Reference this skill when:
- Setting up Fastlane for a new project
- Configuring code signing with Match
- Building lanes for TestFlight or App Store distribution
- Setting up Android Play Store deployment
- Debugging code signing or build failures
- Configuring CI/CD pipelines for mobile apps
- Integrating Fastlane with Tauri v2 projects
Quick Start
Minimal Fastfile Structure
ruby
default_platform(:ios)
platform :ios do
desc "Build and upload to TestFlight"
lane :beta do
match(type: "appstore", readonly: true)
build_app(scheme: "MyApp")
upload_to_testflight
end
end
platform :android do
desc "Build and upload to Play Store beta"
lane :beta do
gradle(task: "bundle", build_type: "Release")
upload_to_play_store(track: "beta")
end
end
Tool Aliases
Fastlane provides shorter aliases for common actions:
| Alias | Action | Purpose |
|---|---|---|
gym |
build_app |
Build and sign iOS/macOS apps |
pilot |
upload_to_testflight |
Upload to TestFlight |
deliver |
upload_to_app_store |
Submit to App Store |
supply |
upload_to_play_store |
Upload to Google Play |
match |
sync_code_signing |
Sync certificates and profiles |
cert |
get_certificates |
Download signing certificates |
sigh |
get_provisioning_profile |
Download provisioning profiles |
scan |
run_tests |
Run unit and UI tests |
snapshot |
capture_screenshots |
Automated App Store screenshots |
frameit |
frame_screenshots |
Add device frames to screenshots |
produce |
create_app_online |
Create app in App Store Connect |
pem |
get_push_certificate |
Download push notification certs |
precheck |
check_app_store_metadata |
Validate metadata before submission |
iOS Workflows
TestFlight Deployment
ruby
lane :beta do
# Sync code signing
match(type: "appstore", readonly: true)
# Increment build number
increment_build_number(
build_number: Time.now.utc.strftime("%y%m%d%H%M")
)
# Build the app
build_app(
scheme: "MyApp",
export_method: "app-store",
output_directory: "./build"
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
uses_non_exempt_encryption: false
)
end
App Store Release
ruby
lane :release do
match(type: "appstore", readonly: true)
build_app(
scheme: "MyApp",
export_method: "app-store"
)
upload_to_app_store(
skip_screenshots: true,
skip_metadata: true,
submit_for_review: false
)
end
App Store Connect API Key
ruby
def api_key
app_store_connect_api_key(
key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'],
issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'],
key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY'],
is_key_content_base64: true
)
end
lane :beta do
upload_to_testflight(api_key: api_key)
end
Android Workflows
Play Store Beta
ruby
platform :android do
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "./android"
)
upload_to_play_store(
track: "beta",
aab: "./android/app/build/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true
)
end
end
Play Store Production
ruby
lane :release do
gradle(task: "bundle", build_type: "Release")
upload_to_play_store(
track: "production",
aab: "./android/app/build/outputs/bundle/release/app-release.aab"
)
end
Code Signing with Match
Initial Setup
bash
# Initialize Match configuration
fastlane match init
# Generate certificates (run once per team)
fastlane match appstore
fastlane match development
Matchfile Configuration
ruby
# fastlane/Matchfile
git_url("git@github.com:your-org/certificates.git")
storage_mode("git")
type("appstore")
app_identifier("com.example.app")
team_id("TEAM_ID")
S3/MinIO Storage (Alternative to Git)
ruby
# Matchfile for S3-compatible storage
storage_mode("s3")
s3_region("us-east-1")
s3_bucket("certificates")
s3_access_key(ENV['AWS_ACCESS_KEY_ID'])
s3_secret_access_key(ENV['AWS_SECRET_ACCESS_KEY'])
# For MinIO, set endpoint
# ENV['AWS_ENDPOINT_URL'] = "https://minio.example.com"
Using Match in Lanes
ruby
lane :beta do
match(
type: "appstore",
readonly: true, # Don't create new certs
keychain_name: ENV['CI'] ? "fastlane_ci" : nil,
keychain_password: ENV['CI'] ? "fastlane_ci_password" : nil
)
end
CI/CD Integration
Keychain Setup for CI
ruby
CI_KEYCHAIN_NAME = "fastlane_ci"
CI_KEYCHAIN_PASSWORD = "fastlane_ci_password"
def setup_ci_keychain
if ENV['CI']
create_keychain(
name: CI_KEYCHAIN_NAME,
password: CI_KEYCHAIN_PASSWORD,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false,
add_to_search_list: true
)
end
end
def cleanup_ci_keychain
if ENV['CI']
delete_keychain(name: CI_KEYCHAIN_NAME)
end
end
lane :beta do
setup_ci_keychain
match(
type: "appstore",
keychain_name: CI_KEYCHAIN_NAME,
keychain_password: CI_KEYCHAIN_PASSWORD
)
# ... build and upload
cleanup_ci_keychain
end
GitHub Actions Example
yaml
# .github/workflows/ios.yml
name: iOS Build
on: [push]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install Fastlane
run: brew install fastlane
- name: Build and Deploy
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
APP_STORE_CONNECT_API_KEY_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_KEY_CONTENT }}
CI: true
run: fastlane ios beta
Environment Variables
iOS (App Store Connect)
| Variable | Description |
|---|---|
APP_STORE_CONNECT_API_KEY_KEY_ID |
API Key ID from App Store Connect |
APP_STORE_CONNECT_API_KEY_ISSUER_ID |
Issuer ID from App Store Connect |
APP_STORE_CONNECT_API_KEY_KEY |
Base64-encoded .p8 key content |
MATCH_PASSWORD |
Encryption password for Match |
MATCH_GIT_URL |
Git repository URL for certificates |
Android (Google Play)
| Variable | Description |
|---|---|
SUPPLY_JSON_KEY_DATA |
Google Play service account JSON (base64) |
SUPPLY_JSON_KEY |
Path to service account JSON file |
Encoding API Keys
bash
# Encode .p8 file to base64
base64 -i AuthKey_XXXXXXXXXX.p8 | tr -d '\n'
# Encode Google Play JSON
base64 -i play-store-key.json | tr -d '\n'
App Store Requirements
Metadata Character Limits
| Field | Limit |
|---|---|
| App Name | 30 characters |
| Subtitle | 30 characters |
| Keywords | 100 characters |
| Description | 4000 characters |
| Release Notes | 4000 characters |
| Promotional Text | 170 characters |
Screenshot Requirements (2024)
| Device | Size | Required |
|---|---|---|
| iPhone 6.7" | 1290 x 2796 | Yes (primary) |
| iPhone 6.5" | 1284 x 2778 | Alternative |
| iPhone 5.5" | 1242 x 2208 | Optional |
| iPad Pro 12.9" | 2048 x 2732 | If iPad supported |
| iPad Pro 11" | 1668 x 2388 | Alternative |
Known Issues Prevention
| Issue | Root Cause | Solution |
|---|---|---|
| "Multiple commands produce" | Duplicate files in build | Remove duplicates from sources |
| Code signing fails in CI | No keychain access | Use create_keychain + Match |
| Build number rejected | Duplicate build number | Use timestamp: Time.now.utc.strftime("%y%m%d%H%M") |
| Profile not found | Wrong Match type | Use appstore for TestFlight/App Store |
| Invalid PEM format | Wrong key encoding | Ensure base64 with is_key_content_base64: true |
| "Missing compliance" | Encryption declaration | Set uses_non_exempt_encryption: false |
| Gradle build fails | Missing SDK/NDK | Set ANDROID_HOME and ANDROID_NDK_HOME |
Tauri Integration
Version from Cargo.toml
ruby
ROOT_DIR = File.expand_path("..", __dir__)
def get_app_version
cargo_toml = File.read("#{ROOT_DIR}/src-tauri/Cargo.toml")
if cargo_toml =~ /^version\s*=\s*"([^"]+)"/
$1
else
"1.0.0"
end
end
def get_next_build_number
Time.now.utc.strftime("%y%m%d%H%M").to_i
end
Update tauri.conf.json
ruby
def update_tauri_config_version
app_version = get_app_version
build_number = get_next_build_number
tauri_conf_path = "#{ROOT_DIR}/src-tauri/tauri.conf.json"
tauri_conf = JSON.parse(File.read(tauri_conf_path))
tauri_conf["version"] = app_version
tauri_conf["bundle"] ||= {}
tauri_conf["bundle"]["iOS"] ||= {}
tauri_conf["bundle"]["iOS"]["bundleVersion"] = build_number.to_s
File.write(tauri_conf_path, JSON.pretty_generate(tauri_conf))
end
Fix Tauri project.yml (Duplicate libapp.a)
ruby
def fix_tauri_project_yml
project_yml_path = "#{ROOT_DIR}/src-tauri/gen/apple/project.yml"
return unless File.exist?(project_yml_path)
content = File.read(project_yml_path)
# Remove "- path: Externals" to prevent duplicate libapp.a
if content.include?("- path: Externals")
content.gsub!(/^\s*- path: Externals\n/, "")
File.write(project_yml_path, content)
sh("cd #{ROOT_DIR}/src-tauri/gen/apple && xcodegen generate")
end
end
Tauri iOS Build Lane
ruby
lane :beta do
match(type: "appstore", readonly: true)
update_tauri_config_version
fix_tauri_project_yml
# Configure signing
update_code_signing_settings(
use_automatic_signing: false,
path: "#{ROOT_DIR}/src-tauri/gen/apple/MyApp.xcodeproj",
team_id: TEAM_ID,
bundle_identifier: APP_IDENTIFIER,
profile_name: "match AppStore #{APP_IDENTIFIER}",
code_sign_identity: "Apple Distribution"
)
# Build with Tauri
sh("cd #{ROOT_DIR}/src-tauri && npx tauri ios build --export-method app-store-connect")
upload_to_testflight(
ipa: "#{ROOT_DIR}/src-tauri/gen/apple/build/arm64/MyApp.ipa",
skip_waiting_for_build_processing: true
)
end
Tauri Android Build Lane
ruby
platform :android do
lane :beta do
ENV['ANDROID_HOME'] = "/opt/homebrew/share/android-commandlinetools"
ENV['ANDROID_NDK_HOME'] = "#{ENV['ANDROID_HOME']}/ndk/28.2.13676358"
# Update version in tauri.conf.json
app_version = get_app_version
build_number = get_next_build_number
tauri_conf_path = "#{ROOT_DIR}/src-tauri/tauri.conf.json"
tauri_conf = JSON.parse(File.read(tauri_conf_path))
tauri_conf["version"] = app_version
tauri_conf["bundle"]["android"] ||= {}
tauri_conf["bundle"]["android"]["versionCode"] = build_number
File.write(tauri_conf_path, JSON.pretty_generate(tauri_conf))
# Build with Tauri
sh("cd #{ROOT_DIR}/src-tauri && npx tauri android build")
upload_to_play_store(
track: "beta",
aab: "#{ROOT_DIR}/src-tauri/gen/android/app/build/outputs/bundle/release/app-release.aab"
)
end
end
Essential Commands
bash
# Initialize Fastlane
fastlane init
# List available actions
fastlane actions
# Run a specific lane
fastlane ios beta
fastlane android release
# Debug with verbose output
fastlane ios beta --verbose
# Run Match commands
fastlane match appstore
fastlane match development
fastlane match nuke distribution # Reset all distribution certs
Sources
Didn't find tool you were looking for?