Fastlane
Fastlane 是专为 iOS 和 Android 移动应用自动化设计的开源工具集,用 Ruby 编写。它将截图生成、签名管理、打包构建、上传发布等繁琐的手动操作统一封装成可重复执行的自动化流程,是移动端 CI/CD 的事实标准。
核心概念:
| 概念 | 说明 |
|---|---|
| Fastfile | 核心配置文件,用 Ruby DSL 定义所有 lane |
| lane | 一条自动化流水线,由多个 action 顺序组成 |
| action | 内置功能单元,如 gym(构建)、pilot(上传 TestFlight) |
| plugin | 社区扩展 action,通过 fastlane-plugin-xxx 安装 |
| Appfile | 存储 App ID、Team ID 等应用元信息 |
| Matchfile | match 工具的配置,管理证书和 Provisioning Profile |
| Deliverfile | deliver 工具的配置,管理 App Store 元数据 |
| Gymfile | gym 工具的配置,管理构建参数 |
| Screengrabfile | Android 截图自动化配置 |
| Snapfile | iOS 截图自动化配置 |
安装
前置要求
| 平台 | 要求 |
|---|---|
| iOS | macOS + Xcode(最新稳定版) |
| Android | JDK 8+、Android SDK |
| 通用 | Ruby 2.6+(建议用 rbenv/rvm 管理) |
安装 Fastlane
# 方式一:Homebrew(推荐 macOS)
brew install fastlane
# 方式二:RubyGems
gem install fastlane
# 方式三:Bundler(推荐团队协作,锁定版本)
# 在项目根目录创建 Gemfile
bundle init
echo 'gem "fastlane"' >> Gemfile
bundle install
# 验证安装
fastlane --version
fastlane actions # 查看所有内置 action
使用 Bundler 管理版本(推荐)
# Gemfile
source "https://rubygems.org"
gem "fastlane", "~> 2.220"
gem "cocoapods" # iOS 依赖管理
# 插件
gem "fastlane-plugin-firebase_app_distribution"
gem "fastlane-plugin-versioning"
bundle install
bundle exec fastlane <lane> # 通过 Bundler 运行,保证版本一致
初始化项目
# 进入项目根目录
cd your-project
# iOS 初始化
fastlane init
# Android 初始化
fastlane init
# 交互式向导,选择用途:
# 1. Automate screenshots
# 2. Automate beta distribution to TestFlight
# 3. Automate App Store distribution
# 4. Manual setup
初始化后生成的目录结构:
your-project/
├── fastlane/
│ ├── Fastfile # 核心:lane 定义
│ ├── Appfile # 应用信息(Bundle ID、Apple ID 等)
│ ├── Matchfile # 证书管理配置(iOS)
│ ├── Deliverfile # App Store 上传配置(iOS)
│ ├── Gymfile # 构建配置(iOS)
│ ├── Screengrabfile # Android 截图配置
│ ├── Snapfile # iOS 截图配置
│ └── metadata/ # App Store 文字描述、截图等
│ ├── zh-Hans/
│ └── en-US/
├── Gemfile
└── Gemfile.lock
Fastfile 语法详解
基本结构
# fastlane/Fastfile
# 设置最低 Fastlane 版本要求
fastlane_version "2.200.0"
# 默认平台(ios / android)
default_platform(:ios)
platform :ios do
# ── 生命周期钩子 ────────────────────────
before_all do |lane|
ensure_git_status_clean # 确保工作目录干净
git_pull # 拉取最新代码
end
after_all do |lane|
notify_team(lane: lane) # 自定义通知
end
error do |lane, exception|
slack(
message: "Lane #{lane} 失败:#{exception.message}",
success: false
)
end
# ── Lane 定义 ────────────────────────────
desc "运行所有测试"
lane :test do
run_tests(scheme: "MyApp")
end
desc "打包并上传到 TestFlight"
lane :beta do
increment_build_number
build_app(scheme: "MyApp")
upload_to_testflight(skip_waiting_for_build_processing: true)
end
desc "发布到 App Store"
lane :release do
capture_screenshots
build_app(scheme: "MyApp")
upload_to_app_store(
force: true,
submit_for_review: true,
automatic_release: false
)
end
end
platform :android do
desc "打包 Release APK"
lane :build do
gradle(task: "clean assembleRelease")
end
desc "上传到 Google Play 内测"
lane :beta do
gradle(task: "bundle", build_type: "Release")
upload_to_play_store(track: "internal")
end
end
lane 基础语法
# 带参数的 lane
lane :deploy do |options|
env = options[:env] || "staging"
version = options[:version] || get_version_number
puts "部署 #{version} 到 #{env}"
build_app(
scheme: "MyApp-#{env.capitalize}",
configuration: env == "prod" ? "Release" : "Staging"
)
end
# 调用带参数的 lane
# fastlane deploy env:prod version:2.0.0
# lane 调用另一个 lane
lane :ci do
test
beta
end
# 私有 lane(不暴露在命令行,只能被其他 lane 调用)
private_lane :notify_team do |options|
slack(message: "Lane #{options[:lane]} 完成")
end
变量与环境
# 读取环境变量
api_key = ENV["APP_STORE_CONNECT_API_KEY"]
# .env 文件(fastlane 自动加载 fastlane/.env)
# fastlane/.env
# SLACK_URL=https://hooks.slack.com/...
# MATCH_PASSWORD=your_password
# 指定 .env 文件运行
# fastlane beta --env staging
# fastlane/.env.staging
# APP_IDENTIFIER=com.example.app.staging
iOS 核心 Actions
代码签名(match)
match 是 Fastlane 推荐的证书管理方案(Code Signing Guide 的实现),将证书和 Provisioning Profile 存储到 Git 仓库或 S3/GCS,团队共享同一套签名配置。
# fastlane/Matchfile
git_url("git@github.com:your-org/certificates.git")
storage_mode("git") # git | s3 | google_cloud
type("development") # development | adhoc | appstore | enterprise
app_identifier(["com.example.app", "com.example.app.extension"])
username("apple@example.com")
# 初始化证书仓库(首次)
fastlane match init
# 生成并存储证书
fastlane match development
fastlane match adhoc
fastlane match appstore
# 团队成员同步证书
fastlane match development --readonly
# 撤销并重新生成
fastlane match nuke development # 危险:撤销所有 development 证书
fastlane match development
# 在 Fastfile 中使用 match
lane :beta do
match(
type: "adhoc",
readonly: is_ci, # CI 上只读,不自动生成
app_identifier: "com.example.app"
)
build_app(scheme: "MyApp")
distribute_via_firebase
end
构建应用(gym / build_app)
# fastlane/Gymfile(全局默认配置)
scheme("MyApp")
workspace("MyApp.xcworkspace")
output_directory("./build")
output_name("MyApp")
clean(true)
export_method("app-store") # app-store | ad-hoc | development | enterprise
# Fastfile 中使用
lane :build_release do
build_app(
scheme: "MyApp",
workspace: "MyApp.xcworkspace",
configuration: "Release",
export_method: "app-store",
output_directory: "./build",
output_name: "MyApp.ipa",
clean: true,
include_bitcode: false,
export_options: {
provisioningProfiles: {
"com.example.app" => "match AppStore com.example.app",
"com.example.app.extension" => "match AppStore com.example.app.extension"
}
}
)
end
版本号管理
# 读取当前版本
version = get_version_number(xcodeproj: "MyApp.xcodeproj", target: "MyApp")
build = get_build_number(xcodeproj: "MyApp.xcodeproj")
# 递增 Build Number(常用于 CI)
increment_build_number(
build_number: ENV["CI_BUILD_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
)
# 递增版本号
increment_version_number(bump_type: "patch") # 1.0.0 → 1.0.1
increment_version_number(bump_type: "minor") # 1.0.0 → 1.1.0
increment_version_number(bump_type: "major") # 1.0.0 → 2.0.0
increment_version_number(version_number: "2.0.0")
# 使用 agvtool(更精准)
increment_build_number(build_number: latest_testflight_build_number + 1)
测试(scan / run_tests)
lane :test do
run_tests(
workspace: "MyApp.xcworkspace",
scheme: "MyAppTests",
devices: ["iPhone 15", "iPad Pro (12.9-inch)"],
clean: true,
code_coverage: true,
output_directory: "fastlane/test_output",
output_types: "html,junit", # 生成 HTML 和 JUnit 报告
fail_build: true
)
end
上传 TestFlight(pilot / upload_to_testflight)
lane :beta do
# 使用 App Store Connect API Key(推荐,不依赖 2FA)
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_CONTENT"], # Base64 编码的 .p8 内容
in_house: false
)
upload_to_testflight(
api_key: api_key,
ipa: "./build/MyApp.ipa",
changelog: "修复若干 Bug",
beta_app_review_info: {
contact_email: "dev@example.com",
contact_first_name: "Dev",
contact_last_name: "Team",
contact_phone: "+86-000-0000-0000"
},
groups: ["内测组", "QA 组"],
skip_waiting_for_build_processing: true # 不等待处理完成(CI 推荐)
)
end
上传 App Store(deliver / upload_to_app_store)
lane :release do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_CONTENT"]
)
upload_to_app_store(
api_key: api_key,
ipa: "./build/MyApp.ipa",
app_version: get_version_number,
# 元数据(也可以放在 fastlane/metadata/ 目录下的文件中)
name: {
"zh-Hans" => "我的应用",
"en-US" => "My App"
},
release_notes: {
"zh-Hans" => "· 修复已知问题\n· 性能优化",
"en-US" => "· Bug fixes\n· Performance improvements"
},
submit_for_review: true,
automatic_release: false,
force: true, # 不需要人工确认
skip_screenshots: true, # 跳过截图上传
skip_metadata: false,
# 审核信息
submission_information: {
add_id_info_uses_idfa: false,
export_compliance_uses_encryption: false
}
)
end
自动截图(snapshot / capture_screenshots)
# fastlane/Snapfile
devices([
"iPhone 15 Pro Max",
"iPhone SE (3rd generation)",
"iPad Pro (12.9-inch) (6th generation)"
])
languages(["zh-Hans", "en-US"])
scheme("MyAppUITests")
output_directory("./fastlane/screenshots")
clear_previous_screenshots(true)
headless(true)
lane :screenshots do
capture_screenshots(scheme: "MyAppUITests")
# 生成美化的截图框架(需要 frameit)
frame_screenshots(
white: true,
path: "./fastlane/screenshots"
)
end
App Store Connect API Key 配置
# 在 App Store Connect → Users and Access → Keys 生成 .p8 文件
# 记录 Key ID 和 Issuer ID
# 将 .p8 内容转为 Base64 存入 CI 环境变量
base64 -i AuthKey_XXXXXXXX.p8 | tr -d '\n'
Android 核心 Actions
构建应用(gradle)
# fastlane/Fastfile (Android)
platform :android do
desc "构建 Debug APK"
lane :build_debug do
gradle(
task: "assemble",
build_type: "Debug",
print_command: true
)
end
desc "构建 Release AAB"
lane :build_release do
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"android.injected.signing.store.password" => ENV["KEYSTORE_PASS"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASS"]
}
)
end
desc "执行 Android 单元测试"
lane :test do
gradle(task: "test")
end
end
版本号管理(Android)
# 读取 versionName 和 versionCode
version_name = get_android_version_name # 需要 fastlane-plugin-versioning
version_code = get_android_version_code
# 递增 versionCode
increment_version_code(
gradle_file_path: "app/build.gradle",
version_code: ENV["CI_BUILD_NUMBER"].to_i
)
# 设置 versionName
increment_version_name(
gradle_file_path: "app/build.gradle",
bump_type: "patch" # major | minor | patch
)
# 也可以直接用 Android Gradle 属性
lane :set_version do |options|
android_set_version_name(
version_name: options[:version],
gradle_file_path: "app/build.gradle"
)
android_set_version_code(
version_code: options[:build_number].to_i,
gradle_file_path: "app/build.gradle"
)
end
代码签名(Android)
# 方式一:通过 Gradle 属性传入(推荐)
lane :sign do
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
"android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
}
)
end
# 方式二:使用 sign_apk action
lane :sign_apk do
sign_apk(
apk_path: "app/build/outputs/apk/release/app-release-unsigned.apk",
signed_apk_path: "app/build/outputs/apk/release/app-release.apk",
keystore_path: ENV["KEYSTORE_FILE"],
alias: ENV["KEY_ALIAS"],
storepass: ENV["STORE_PASSWORD"],
keypass: ENV["KEY_PASSWORD"]
)
end
上传 Google Play(supply / upload_to_play_store)
# fastlane/Supplyfile
package_name("com.example.app")
track("internal")
json_key(ENV["GOOGLE_PLAY_JSON_KEY"]) # 服务账号 JSON 文件路径
# Fastfile 中使用
lane :deploy_play_store do |options|
track = options[:track] || "internal" # internal | alpha | beta | production
upload_to_play_store(
track: track,
aab: "app/build/outputs/bundle/release/app-release.aab",
json_key: ENV["GOOGLE_PLAY_JSON_KEY"],
release_status: "draft", # draft | completed | halted | inProgress
rollout: "0.1", # 10% 灰度(production track 用)
skip_upload_apk: true,
skip_upload_metadata: false,
skip_upload_screenshots: true,
version_codes_to_retain: [last_version_code, last_version_code - 1]
)
end
# 仅晋升 track(不重新上传)
lane :promote_to_production do
upload_to_play_store(
track: "internal",
track_promote_to: "production",
rollout: "1.0"
)
end
# 获取 Google Play JSON Key:
# Google Play Console → Setup → API access → Create service account
# 在 Google Cloud Console 下载 JSON Key
# 在 Google Play Console 赋予该账号"发布管理员"权限
上传 Firebase App Distribution
# 需要插件:fastlane-plugin-firebase_app_distribution
lane :distribute_firebase do
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID"],
firebase_cli_token: ENV["FIREBASE_TOKEN"], # firebase login:ci 生成
# 或使用服务账号
service_credentials_file: "google-service-account.json",
ipa_path: "./build/MyApp.ipa", # iOS
# apk_path: "./app/build/outputs/apk/release/app-release.apk", # Android
groups: "qa-team, beta-testers",
release_notes: "Build #{ENV['CI_BUILD_NUMBER']}: #{last_git_commit[:message]}",
release_notes_file: "RELEASE_NOTES.txt" # 或从文件读取
)
end
Android 自动截图(screengrab)
# fastlane/Screengrabfile
locales(["zh-CN", "en-US"])
clear_previous_screenshots(true)
app_apk_path("app/build/outputs/apk/debug/app-debug.apk")
tests_apk_path("app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk")
output_directory("fastlane/screenshots")
# 运行
lane :screenshots do
capture_android_screenshots
end
常用通用 Actions
Git 操作
# 确保 Git 干净
ensure_git_status_clean
# 拉取最新代码
git_pull
# 提交版本号变更
git_commit(
path: ["./MyApp/Info.plist", "./MyApp.xcodeproj"],
message: "Bump version to #{get_version_number} (#{get_build_number})"
)
# 打 Tag
add_git_tag(
tag: "v#{get_version_number}-#{get_build_number}",
message: "Release #{get_version_number}"
)
# 推送 commit 和 tag
push_to_git_remote(
remote: "origin",
local_branch: "main",
tags: true
)
# 获取最近一次 commit 信息
commit = last_git_commit
puts commit[:message] # commit 信息
puts commit[:author] # 作者
puts commit[:commit_hash] # SHA
通知
# Slack 通知
slack(
message: "#{lane_context[SharedValues::LANE_NAME]} 构建成功 🎉",
success: true,
slack_url: ENV["SLACK_WEBHOOK_URL"],
payload: {
"版本" => get_version_number,
"Build" => get_build_number,
"分支" => git_branch,
"提交" => last_git_commit[:message]
},
default_payloads: [:git_branch, :git_author]
)
# 钉钉通知(需插件 fastlane-plugin-dingtalk)
dingtalk(
webhook: ENV["DINGTALK_WEBHOOK"],
message: "iOS 打包成功,版本 #{get_version_number}"
)
工具类
# 读取 JSON 文件
data = load_json(json_path: "config.json")
puts data["version"]
# 在 CI 环境中检测
if is_ci
puts "running on CI"
end
# 获取当前 Git 分支
branch = git_branch
# 确保当前在正确分支
ensure_git_branch(branch: "main")
# 清理构建目录
clean_build_artifacts
# 执行 Shell 命令
sh("echo Hello World")
sh("pod install --repo-update")
# 获取命令输出
output = sh("git rev-parse --short HEAD").strip
puts "当前 commit: #{output}"
环境与配置管理
多环境配置
fastlane/
├── .env # 通用配置(所有环境)
├── .env.default # 默认配置
├── .env.staging # Staging 环境
└── .env.production # Production 环境
# fastlane/.env
SLACK_WEBHOOK_URL=https://hooks.slack.com/xxx
MATCH_GIT_URL=git@github.com:org/certificates.git
# fastlane/.env.staging
APP_IDENTIFIER=com.example.app.staging
APP_STORE_CONNECT_TEAM_ID=XXXXXXXXXX
MATCH_TYPE=adhoc
# fastlane/.env.production
APP_IDENTIFIER=com.example.app
MATCH_TYPE=appstore
# 指定环境运行
fastlane beta --env staging
fastlane release --env production
# Fastfile 中引用
lane :build do
match(
type: ENV["MATCH_TYPE"],
app_identifier: ENV["APP_IDENTIFIER"]
)
end
Appfile
# fastlane/Appfile
# iOS
app_identifier ENV["APP_IDENTIFIER"] || "com.example.app"
apple_id ENV["APPLE_ID"] || "apple@example.com"
team_id ENV["TEAM_ID"] # Developer Team ID
itc_team_id ENV["ITC_TEAM_ID"] # App Store Connect Team ID
# Android
json_key_file ENV["GOOGLE_PLAY_JSON_KEY"]
package_name ENV["PACKAGE_NAME"] || "com.example.app"
插件系统
安装与管理插件
# 搜索插件
fastlane search_plugins firebase
# 安装插件(自动更新 Pluginfile 和 Gemfile)
fastlane add_plugin firebase_app_distribution
fastlane add_plugin versioning
fastlane add_plugin pgyer # 蒲公英分发
fastlane add_plugin fir_cli # fir.im 分发
# 安装项目已有插件
fastlane install_plugins
# 查看已安装插件
cat fastlane/Pluginfile
常用插件推荐
| 插件 | 用途 |
|---|---|
firebase_app_distribution |
Firebase 内测分发 |
versioning |
Android 版本号管理 |
pgyer |
蒲公英内测分发 |
fir_cli |
fir.im 内测分发 |
appicon |
自动生成各尺寸应用图标 |
increment_build_number_in_plist |
plist 版本管理 |
sonar |
SonarQube 代码扫描 |
slack_upload |
上传文件到 Slack |
badge |
在应用图标上添加 Badge(版本号/环境标识) |
changelog |
自动生成 changelog |
jira |
更新 Jira 票据状态 |
与 CI/CD 平台集成
GitHub Actions + Fastlane(iOS)
# .github/workflows/ios-release.yml
name: iOS Release
on:
push:
tags: ['v*.*.*']
jobs:
release:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true # 自动 bundle install 并缓存
- name: Install CocoaPods
run: bundle exec pod install --repo-update
- name: Setup SSH for Match
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.MATCH_SSH_PRIVATE_KEY }}
- name: Run Fastlane
run: bundle exec fastlane release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
APP_IDENTIFIER: com.example.app
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
GitHub Actions + Fastlane(Android)
# .github/workflows/android-release.yml
name: Android Release
on:
push:
tags: ['v*.*.*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > app/release.jks
- name: Run Fastlane
run: bundle exec fastlane android deploy_play_store
env:
KEYSTORE_FILE: app/release.jks
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
FIREBASE_APP_ID: ${{ secrets.FIREBASE_APP_ID }}
FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}
GitLab CI + Fastlane(iOS)
# .gitlab-ci.yml
stages: [build, distribute]
variables:
LC_ALL: "en_US.UTF-8"
LANG: "en_US.UTF-8"
.ios-base:
tags: [macos, xcode] # 需要 macOS Self-hosted Runner
before_script:
- bundle install --path vendor/bundle
- bundle exec pod install
ios-beta:
extends: .ios-base
stage: distribute
script:
- bundle exec fastlane beta
environment:
name: testflight
rules:
- if: '$CI_COMMIT_BRANCH == "develop"'
ios-release:
extends: .ios-base
stage: distribute
script:
- bundle exec fastlane release
environment:
name: app-store
when: manual
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
完整示例
iOS 完整 Fastfile
# fastlane/Fastfile
fastlane_version "2.200.0"
default_platform(:ios)
APP_ID = ENV["APP_IDENTIFIER"] || "com.example.app"
OUTPUT_DIR = "./build"
platform :ios do
before_all do
setup_ci if is_ci # CI 环境下自动配置 Keychain
end
# ── 私有辅助 Lane ─────────────────────────────
private_lane :prepare_signing do |opts|
match(
type: opts[:type] || "adhoc",
app_identifier: APP_ID,
readonly: is_ci
)
end
private_lane :build do |opts|
prepare_signing(type: opts[:match_type])
increment_build_number(
build_number: is_ci ? ENV["CI_PIPELINE_IID"] : Time.now.strftime("%Y%m%d%H%M")
)
build_app(
scheme: "MyApp",
workspace: "MyApp.xcworkspace",
configuration: opts[:configuration] || "Release",
export_method: opts[:export_method] || "ad-hoc",
output_directory: OUTPUT_DIR,
output_name: "MyApp.ipa",
clean: true
)
end
private_lane :notify do |opts|
slack(
message: opts[:message],
success: opts.fetch(:success, true),
slack_url: ENV["SLACK_WEBHOOK_URL"],
payload: {
"版本" => "#{get_version_number} (#{get_build_number})",
"分支" => git_branch,
"提交" => last_git_commit[:message]
}
) if ENV["SLACK_WEBHOOK_URL"]
end
# ── 测试 ───────────────────────────────────────
desc "运行单元测试和 UI 测试"
lane :test do
run_tests(
workspace: "MyApp.xcworkspace",
scheme: "MyAppTests",
devices: ["iPhone 15 Pro"],
code_coverage: true,
output_directory: "fastlane/test_output",
output_types: "html,junit"
)
end
# ── 内测分发(Firebase) ────────────────────────
desc "打包并上传到 Firebase App Distribution"
lane :firebase do
build(match_type: "adhoc", export_method: "ad-hoc")
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID"],
firebase_cli_token: ENV["FIREBASE_TOKEN"],
ipa_path: "#{OUTPUT_DIR}/MyApp.ipa",
groups: "qa-team",
release_notes: "[#{git_branch}] #{last_git_commit[:message]}"
)
notify(message: "iOS 内测包已上传到 Firebase 🚀")
end
# ── TestFlight ─────────────────────────────────
desc "打包并上传到 TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_CONTENT"]
)
build(match_type: "appstore", export_method: "app-store")
upload_to_testflight(
api_key: api_key,
ipa: "#{OUTPUT_DIR}/MyApp.ipa",
changelog: last_git_commit[:message],
skip_waiting_for_build_processing: true
)
notify(message: "iOS TestFlight 包已上传 ✅")
end
# ── App Store 发布 ─────────────────────────────
desc "构建并提交到 App Store 审核"
lane :release do
ensure_git_branch(branch: "main")
ensure_git_status_clean
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"],
issuer_id: ENV["ASC_ISSUER_ID"],
key_content: ENV["ASC_KEY_CONTENT"]
)
build(match_type: "appstore", export_method: "app-store")
version = get_version_number
upload_to_app_store(
api_key: api_key,
ipa: "#{OUTPUT_DIR}/MyApp.ipa",
app_version: version,
submit_for_review: true,
automatic_release: false,
force: true,
skip_screenshots: true
)
add_git_tag(tag: "v#{version}-#{get_build_number}")
push_to_git_remote(tags: true)
notify(message: "iOS v#{version} 已提交 App Store 审核 🎉")
end
error do |lane, exception|
notify(message: "Lane [#{lane}] 失败:#{exception.message}", success: false)
end
end
Android 完整 Fastfile
# fastlane/Fastfile (Android)
fastlane_version "2.200.0"
default_platform(:android)
platform :android do
# ── 测试 ───────────────────────────────────────
desc "运行单元测试"
lane :test do
gradle(task: "test")
end
# ── 构建 ───────────────────────────────────────
desc "构建 Release AAB"
lane :build do |opts|
version_code = (ENV["CI_PIPELINE_IID"] || Time.now.strftime("%Y%m%d%H%M")).to_i
gradle(
task: "bundle",
build_type: "Release",
properties: {
"versionCode" => version_code,
"android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
"android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
}
)
end
# ── Firebase 内测分发 ──────────────────────────
desc "打包并上传到 Firebase App Distribution"
lane :firebase do
gradle(task: "assemble", build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
"android.injected.signing.store.password" => ENV["STORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
})
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID"],
firebase_cli_token: ENV["FIREBASE_TOKEN"],
apk_path: lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH],
groups: "qa-team",
release_notes: last_git_commit[:message]
)
end
# ── Google Play 发布 ───────────────────────────
desc "上传到 Google Play"
lane :deploy do |opts|
track = opts[:track] || "internal"
build
upload_to_play_store(
track: track,
aab: lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH],
json_key: ENV["GOOGLE_PLAY_JSON_KEY"],
skip_upload_apk: true,
skip_upload_metadata: true,
skip_upload_screenshots: true
)
slack(
message: "Android 已上传到 Google Play [#{track}] 🚀",
slack_url: ENV["SLACK_WEBHOOK_URL"]
) if ENV["SLACK_WEBHOOK_URL"]
end
error do |lane, exception|
slack(
message: "Android Lane [#{lane}] 失败:#{exception.message}",
success: false,
slack_url: ENV["SLACK_WEBHOOK_URL"]
) if ENV["SLACK_WEBHOOK_URL"]
end
end
常用命令
# 列出所有可用 lane
fastlane lanes
fastlane list
# 运行指定 lane
fastlane ios beta
fastlane android deploy track:internal
# 运行并传参
fastlane deploy env:production version:2.0.0
# 在 CI 环境运行(跳过交互,输出更简洁)
fastlane ios release --env production
# 指定日志级别
fastlane beta --verbose
# 查看某个 action 的文档
fastlane action gym
fastlane action upload_to_testflight
# 更新 Fastlane
bundle update fastlane # Bundler 方式
最佳实践
-
用 Bundler 锁定版本:项目中始终使用
Gemfile+Gemfile.lock固定 Fastlane 和插件版本,避免升级导致的兼容性问题,CI 上用bundle exec fastlane执行。 -
敏感信息环境变量化:证书密码、API Key、Webhook URL 一律通过环境变量注入,
.env文件加入.gitignore,CI 中使用平台提供的 Secret 管理。 -
使用 App Store Connect API Key:替代 Apple ID + 2FA 登录,CI 更稳定,不受双因素认证干扰。Key 内容 Base64 编码后存为 Secret。
-
match 统一管理签名:团队共用同一套证书,CI 上使用
readonly: true只拉取不生成,避免证书冲突。 -
lane 职责单一:每条 lane 只做一件事,通过
private_lane封装公共逻辑,lane之间组合调用,提高可读性和复用性。 -
CI 上设置
setup_ci:在before_all中调用setup_ci if is_ci,它会自动创建临时 Keychain,防止签名时弹出密码对话框。 -
构建编号与 CI 流水线号绑定:用 CI 的 Pipeline/Build Number 作为 Build Number,保证每次构建唯一且可追溯。
-
提取公共配置到 Appfile:Bundle ID、Team ID、Apple ID 等写入 Appfile,lane 中无需重复声明。
-
善用
error钩子:在error回调中统一处理失败通知,避免在每条 lane 中写begin/rescue。 -
本地测试与 CI 保持一致:开发者本地运行
bundle exec fastlane test应与 CI 结果完全一致,通过.env文件模拟 CI 变量调试。