fastlane Spaceship App Store Connect API 完整实战指南:从登录鉴权到审核上架的自动化操作
【免费下载链接】fastlane🚀 The easiest way to automate building and releasing your iOS and Android apps项目地址: https://gitcode.com/GitHub_Trending/fa/fastlane
本指南以 fastlane 仓库中 spaceship/docs/AppStoreConnect.md 为核心,系统讲解 Spaceship 如何通过App Store Connect API以脚本方式完成应用管理、版本元数据修改、TestFlight 分发、审核提交、评分评论获取乃至 Bundle Id 能力配置与 Webhook 管理。读完本文,你将掌握基于 API Key(Auth Key)与 Apple ID 两套登录体系的全部常用操作,并能将Spaceship::ConnectAPI的调用直接复用到自己的 CI/CD 流水线中。
一、背景:Spaceship 与两套 App Store 接口
Spaceship 是 fastlane 体系与 Apple 开发者后台打交道的底层库(仓库位置 spaceship/lib/spaceship)。从文档可以明确看到,Spaceship 实际封装了两代接口:
- App Store Connect API(新版):所有模型类统一挂在
Spaceship::ConnectAPI模块下,例如Spaceship::ConnectAPI::App、Spaceship::ConnectAPI::Build、Spaceship::ConnectAPI::BundleId。它使用 Apple 提供的 API Key(.p8文件)签发 JWT 进行鉴权。 - Tunes(旧版 Web 接口):
Tunes是 "iTunes Connect" 时代的命名遗存。当某些能力(如改价格、拉取评分与评论、App Analytics)尚未在新版 Connect API 中提供时,仍需通过Spaceship::Tunes::Application配合 Apple ID 登录来操作。
文档特别提醒:如果你同时使用 Developer Portal 与 App Store Connect,两处需要分别登录,因为两者可能使用不同的用户凭证(详见仓库内 spaceship/docs/Authentication.md)。
想快速体验,在终端执行
irb后输入require "spaceship"即可开始交互式探索。
二、登录鉴权:两种方式的核心用法
2.1 API Key 方式(Connect API 首选)
token = Spaceship::ConnectAPI::Token.create( key_id: 'the-key-id', issuer_id: 'the-issuer-id', filepath: File.absolute_path("../AuthKey_the-key-id.p8") ) Spaceship::ConnectAPI.token = token创建 token 后,将其赋给Spaceship::ConnectAPI.token,之后所有 ConnectAPI 调用都会自动携带该凭证。
从源码 token.rb 可以看出几个值得注意的实现细节:
- 令牌时长:Apple 支持的最大过期时间为 20 分钟,源码中定义为
MAX_TOKEN_DURATION = 1200(秒),默认值DEFAULT_TOKEN_DURATION = 500秒。可通过duration参数(或环境变量SPACESHIP_CONNECT_API_TOKEN_DURATION)调整。 - JWT 载荷:使用 ES256 椭圆曲线签名,payload 中
aud根据团队类型取appstoreconnect-v1或apple-developer-enterprise-v1;iat会刻意回拨 60 秒,避免本机时钟略快于 Apple 服务器导致令牌被拒。 - 环境变量注入:
key_id、issuer_id、filepath、key、duration、in_house均可通过SPACESHIP_CONNECT_API_KEY_ID、SPACESHIP_CONNECT_API_ISSUER_ID、SPACESHIP_CONNECT_API_KEY_FILEPATH、SPACESHIP_CONNECT_API_KEY、SPACESHIP_CONNECT_API_TOKEN_DURATION、SPACESHIP_CONNECT_API_IN_HOUSE等环境变量提供,非常适合 CI 场景。 - JSON Key 文件:除
.p8路径外,Token.from_json_file(filepath)还支持包含key_id与key(base64 的私钥内容)的 JSON 文件,缺失必填字段时会明确报错。
2.2 Apple ID / 会话方式(Tunes 旧接口)
旧版接口能力(调价、评分评论、Analytics 等)依赖 Apple ID 登录态。仓库提供了对应的 CLI 会话持久化工具,运行spaceship login(通过 commands_generator.rb)即可复用 fastlane 的账号会话。两种方式务必按需选择,切不可混用ConnectAPI模型与 Tunes 模型的鉴权上下文。
三、应用(Applications)管理
3.1 查询与遍历
# Fetch all available applications all_apps = Spaceship::ConnectAPI::App.all # Find a specific app based on the bundle identifier app = Spaceship::ConnectAPI::App.find("com.krausefx.app") app = Spaceship::ConnectAPI.get_app(app_id: 1013943394).first # Access information about the app app.id # => 1013943394 app.name # => "Spaceship App" app.bundle_id # => "com.krausefx.app" app.sku # => "SpaceshipApp01" app.primary_locale # => "en-US" # Show the names of all your apps Spaceship::ConnectAPI::App.all.collect do |app| app.name end对照模型 app.rb 可以看到:
App.all内部调用client.get_apps并自动翻页(all_pages后flat_map(&:to_models)),默认携带ESSENTIAL_INCLUDES = "appStoreVersions"。App.find(bundle_id)其实是"先按bundleId过滤拉全量,再做内存匹配",因此参数就是 Bundle ID 字符串。- 响应解析由 model.rb 中的
Models.parse完成:它根据 JSON 中data的type查找对应模型类,再依据attr_mapping表把 API 的驼峰字段(如bundleId)映射为 Ruby 的蛇形属性(如bundle_id),同时生成别名读写方法。
3.2 创建新应用
# Currently only works with Apple ID login (not API Key) app = Spaceship::ConnectAPI::App.create(name: "App Name", version_string: "1.0", # initial version sku: "123", primary_locale: "en-us", bundle_id: "com.krausefx.app", platforms: ["IOS"], company_name: "krause inc")App.create最终路由到client.post_app(app.rb)。文档明确标注:该操作目前仅支持 Apple ID 登录,不支持纯 API Key,需注意鉴权方式的限制。
3.3 修改非版本级信息与价格
元数据(App 名称、隐私政策 URL 等)以及价格修改走 Tunes 旧接口:
app = Spaceship::Tunes::Application.find("com.krausefx.app") details = app.details details.name['en-US'] = "App Name" details.privacy_url['en-US'] = "https://fastlane.tools" details.save! # To change the price of the app (it's not necessary to call save! when updating the price) app.update_price_tier!("3")注意这里取值是按语言代码的 hash 结构(如details.name['en-US']);改价格时文档特别注明无需调用save!,update_price_tier!会直接提交。
四、App 版本(AppVersions)与元数据修改
4.1 同一时刻的多个版本状态
同一时刻一个 App 最多存在 2 个 App Store 版本:通常是已在商店上架的版本(通过get_live_app_store_version获取)和正在编辑、尚未上架的版本(get_edit_app_store_version)。生产版本部分字段(如应用描述)仍可修改,但大多数选项已被锁定。
app.get_live_app_store_version # the version that's currently available in the App Store app.get_edit_app_store_version # the version that's in `Prepare for Submission`, `Metadata Rejected`, `Rejected`, `Developer Rejected`, `Waiting for Review`, `Invalid Binary` mode app.get_latest_app_store_version # the version that's the latest one app.get_pending_release_app_store_version # the version that's in `Pending Developer Release` or `Pending Apple Release` mode app.get_in_review_app_store_version # the version that is in `In Review` mode上述方法定义位于 app.rb。除get_latest_app_store_version外,其余默认都会带上AppStoreVersion::ESSENTIAL_INCLUDES关系;文档中提到的各种 App Store 状态(PREPARE_FOR_SUBMISSION、WAITING_FOR_REVIEW、IN_REVIEW、PENDING_DEVELOPER_RELEASE、READY_FOR_SALE等)在 app_store_version.rb 的AppStoreState/AppVersionState常量中有完整枚举。
4.2 读取与更新版本元数据
v = app.get_edit_app_store_version # Access information v.app_version_state # => "Waiting for Review" v.version_string # => "0.9.14" # Build is not always available in all app_version_state, e.g. not available in `Prepare for Submission` build_number = v.build.nil? ? nil : v.build.version # Update app metadata copyright = "#{Time.now.year} Felix Krause" v.update(attributes: { "copyright": copyright })注意v.update的attributes会被 model.rb 中的reverse_attr_mapping反转为 API 所需的字段名(蛇形转驼峰),再执行 PATCH 请求,因此你始终用 Ruby 侧命名写即可。
4.3 本地化(Localization)内容
# Get a list of available languages for this app version = app.get_edit_app_store_version(includes: 'appStoreVersionSubmission,build,appStoreVersionLocalizations') localizations = version.appStoreVersionLocalizations localization = localizations.first localization.locale # => "en-GB" localization.description # => "App description" # Update localized app metadata localization.update(attributes: { description: "New Description" })把includes显式传成逗号分隔的关系列表(如appStoreVersionSubmission,build,appStoreVersionLocalizations),就能一次取回审核提交、构建与本地化条目,从而省去多次往返。
4.4 年龄分级(Age Rating)
# fetch_age_rating_declaration with `fetch_live_app_info` or `fetch_edit_app_info` app_info = app.fetch_edit_app_info declaration = app_info.fetch_age_rating_declaration unless app_info.nil? # update age_rating_declaration declaration.update(attributes: { "violenceCartoonOrFantasy": "NONE", "matureOrSuggestiveThemes": "NONE", "unrestrictedWebAccess": false })年龄分级声明的修改路径是AppInfo → AgeRatingDeclaration两级对象。可用的分级取值可参考仓库内的 deliver/assets/example_rating_config.json(如各暴力/主题项的NONE/INFREQUENT/FREQUENT等级及布尔开关),以及 deliver/Reference.md 中关于分级配置的完整说明。
4.5 版本对象可用属性一览
文档整理出的可访问字段可分为三层:
#### # General app store version metadata (app_store_version) #### attr_accessor :platform attr_accessor :version_string attr_accessor :app_store_state attr_accessor :app_version_state attr_accessor :store_icon attr_accessor :watch_store_icon attr_accessor :copyright attr_accessor :release_type attr_accessor :earliest_release_date attr_accessor :is_watch_only attr_accessor :downloadable attr_accessor :created_date attr_accessor :app_store_version_submission attr_accessor :app_store_version_phased_release attr_accessor :app_store_review_detail attr_accessor :app_store_version_localizations #### # App Review Information (app_store_review_detail) #### attr_accessor :contact_first_name attr_accessor :contact_last_name attr_accessor :contact_phone attr_accessor :contact_email attr_accessor :demo_account_name attr_accessor :demo_account_password attr_accessor :demo_account_required attr_accessor :notes attr_accessor :app_store_review_attachments #### # Localized values (app_store_version_localization) #### attr_accessor :description attr_accessor :locale attr_accessor :keywords attr_accessor :marketing_url attr_accessor :promotional_text attr_accessor :support_url attr_accessor :whats_new attr_accessor :app_screenshot_sets attr_accessor :app_preview_sets这些attr_accessor实际上由 model.rb 的attr_mapping根据模型内声明的映射表动态创建,app_store_version.rb、app_store_review_detail.rb、app_store_version_localization.rb等模型文件均位于 spaceship/lib/spaceship/connect_api/models 下,其中保留了每个属性的完整类型、描述与备注。
五、完整审核生命周期:选构建 → 提交 → 发布
5.1 选择用于审核的构建
version = app.get_edit_app_store_version build = Spaceship::ConnectAPI::Build.all(app_id: app.id, platform: platform).first version.select_build(build_id: build.id)select_build在 app_store_version.rb 中定义,底层对应 Connect API 的版本关联构建操作。注意在Prepare for Submission状态下通常还没有可选构建,需要先上传构建并处理完成后才能关联。
5.2 提交审核
version.create_app_store_version_submission提交动作会创建AppStoreVersionSubmission,对应 app_store_version.rb。提交审核前需要把完整的审核信息(联系信息、演示账号、审核备注、附件等,即上文app_store_review_detail那组属性)与所有本地化元数据准备齐全。完整可参考的实现范例在 deliver 的 submit_for_review.rb,fastlane 的deliveraction(对应测试 fastlane/spec/actions_specs/deliver_action_spec.rb)也正是走这套逻辑完成"准备提交 → 提交审核"的端到端流程。
5.3 发布已过审的构建
当版本处于Pending Developer Release或Pending Apple Release(由get_pending_release_app_store_version获取)时,可主动触发发布。文档给出两种等价写法:
version = app.get_pending_release_app_store_version unless version.nil? Spaceship::ConnectAPI.post_app_store_version_release_request(app_store_version_id: version.id) end或直接调用模型实例方法:
version = app.get_pending_release_app_store_version version.create_app_store_version_release_request unless version.nil?两种方式最终都会创建AppStoreVersionReleaseRequest(模型见 app_store_version_release_request.rb),适合"人工点选过审后自动上架"的自动化发布场景。
六、Build Trains 与构建管理(TestFlight)
6.1 理解版本号与构建号
文档先用一张图澄清两个最易混淆的概念:
- version number(版本号):由
CFBundleShortVersionString决定,是 App Store 上对用户展示的版本,如0.9.21; - build number(构建号):由
CFBundleVersion决定,商店页面不可见,上传新构建前必须递增,如99993。
Build Train(构建序列)指同一version number下的全部构建集合;序列内部可以有n个构建,每个构建拥有不同的build number。
6.2 遍历 Build Trains(Tunes 接口)
app = Spaceship::Tunes::Application.find("com.krausefx.app") # Access all build trains for an app app.all_build_train_numbers # => ["0.9.21"] # Access the build train via the version number train = app.build_trains["0.9.21"] # Access all builds for a given train train.count # => 1 build = train.first6.3 读取构建详情并提交 Beta 审核
# Continue from the BuildTrains example build.build_version # => "99993" (the build number) build.train_version # => "0.9.21" (the version number) build.install_count # => 1 build.crash_count # => 0 build.internal_state # => testflight.build.state.testing.ready build.external_state # => testflight.build.state.submit.readybuild.internal_state/build.external_state分别表示内测/外测状态。在设置好全部必要的 TestFlight 元数据后,可以直接把构建提交给外部 Beta 审核:
build.submit_for_testflight_review!构建的底层模型见 spaceship/lib/spaceship/connect_api/models/build.rb,旧版接口实现在 test_flight/build.rb。关于 TestFlight 的深入用法(Beta 组、构建状态流转等),仓库内还有专门的 spaceship/docs/TestFlightTesting.md 可以参考。
6.4 处理长期卡在 Processing 的构建
当构建在 App Store Connect 端长时间停留在Processing状态时,可通过 Connect API 主动轮询取出这些构建:
Spaceship::ConnectAPI::Build.all(app_id: app.id, processing_states: "PROCESSING") # => Array of processing builds for this application配合 CI 做"上传后等待处理完成再提交审核"时,这个过滤条件非常实用。
七、测试员(Testers)管理
文档明确区分了三类测试员:
- External testers(外部测试员):通常不属于你的团队,最多可邀请 10000 名。向外部测试员分发构建前,必须先提交 Beta 审核;
- Internal testers(内部测试员):注册在 App Store Connect 团队中的员工,无需等待审核即可访问所有构建;
- Sandbox testers(沙盒测试员):用于在开发模式下测试应用内购买或 Apple Pay 的虚拟账号。
# Find a tester based on the email address tester = Spaceship::TestFlight::Tester.find(app_id: "some_app_id", email: "felix@krausefx.com") tester = Spaceship::ConnectAPI::BetaTester.find(email: "felix@krausefx.com") # Creating new testers Spaceship::TestFlight::Tester.create_app_level_tester( app_id: "io.myapp", email: "github@krausefx.com", first_name: "Felix", last_name: "Krause" )文档说明当前spaceship 尚不能修改或创建 internal testers。外部测试员既可用旧接口Spaceship::TestFlight::Tester,也可用新版Spaceship::ConnectAPI::BetaTester(模型见 beta_tester.rb)。
沙盒测试员的增删(走 Connect API)示例:
# Load all sandbox testers testers = Spaceship::ConnectAPI::SandboxTester.all # Delete sandbox testers testers.each do |tester| if UI.confirm("Delete #{tester.email}?") tester.delete! end end # Create a sandbox tester Spaceship::ConnectAPI::SandboxTester.create( first_name: "Test", # required last_name: "Three", # required email: "sandbox@test.com", # required password: "Passwordtest1", # required. Must contain >=8 characters, >=1 uppercase, >=1 lowercase, >=1 numeric. confirm_password: "Passwordtest1", # required secret_question: "Question", # required. Must contain >=6 characters secret_answer: "Answer", # required. Must contain >=6 characters birth_date: "1980-03-01", # required app_store_territory: "USA" # required )口令与密保的复杂度约束(长度、大小写、数字)文档均给出明确要求,脚本化创建时应一并校验,避免 API 报错。
八、评分与评论(Ratings & Reviews)
评分评论能力走 Tunes 旧版接口(Apple ID 鉴权):
app = Spaceship::Tunes::Application.find("com.krausefx.app") # Get the rating summary for an application ratings = app.ratings # => Spaceship::Tunes::AppRatings # Get the number of 5 star ratings five_star_count = ratings.five_star_rating_count # Find the average rating across all stores average_rating = ratings.average_rating # Find the average rating for a given store front average_rating = app.ratings(storefront: "US").average_rating # Get reviews for a given store front reviews = ratings.reviews("US") # => Array of hashes representing review data可以通过storefront参数限定地区(如"US")获取某商店前端的平均分与评论数组,用于质量监控与舆情分析。
九、App Analytics(应用分析)
同样基于 Tunes 接口,app.analytics返回Spaceship::Tunes::AppAnalytics,默认覆盖最近 7 天,可拉取下列全部指标(返回值是逐日原始数据的日期数组):
app = Spaceship::Tunes::Application.find("com.krausefx.app") analytics = app.analytics # => Spaceship::Tunes::AppAnalytics units = analytics.app_units # => App units(下载单位) views = analytics.app_views # => App Store page views(商店页浏览量) impressions = analytics.app_impressions # => Impressions(曝光量) sales = analytics.app_sales # => App sales(销售额) users = analytics.paying_users # => Paying users(付费用户数) iap = analytics.app_in_app_purchases # => In app purchases(内购数据) installs = analytics.app_installs # => App installs(安装量) sessions = analytics.app_sessions # => App sessions(会话数) devices = analytics.app_active_devices # => Active devices(活跃设备数) crashes = analytics.app_crashes # => Crashes(崩溃次数)这套接口适合做发布后的数据看板脚本,把这些日报数据按天落库即可生成自己的监控报表。
十、Bundle Id 管理(Auth Key 场景)
10.1 查询与创建 Bundle Identifier
# Fetch all bundle identifiers all_identifiers = Spaceship::ConnectAPI::BundleId.all # Find a specific identifier based on the bundle identifier bundle_id = Spaceship::ConnectAPI::BundleId.find("com.krausefx.app") # Access information about the bundle identifier bundle_id.name bundle_id.platform bundle_id.identifier bundle_id.seed_id # Create a new identifier identifier = Spaceship::ConnectAPI::BundleId.create(name: "Description of the identifier", identifier: "com.krausefx.app")该能力只走 API Key 鉴权(因此标题标注 Auth Key)。实现对应 bundle_id.rb。
文档标注了一个平台相关的事实:无论指定IOS还是MAC_OS,platform都会被设为UNIVERSAL;若不指定,seed_id默认取 team_id。编写依赖平台判断的脚本时务必留意这一行为。
10.2 Bundle Id Capability(能力配置)
# Fetch all capabilities for bundle identifier bundle_id = Spaceship::ConnectAPI::BundleId.find("com.krausefx.app") capabilities = bundle_id.get_capabilities # Create a new capability for bundle identifier bundle_id.create_capability(Spaceship::ConnectAPI::BundleIdCapability::Type::MAPS) # Create a new capability with known bundle identifier id bundle_id_capability = Spaceship::ConnectAPI::BundleIdCapability.create(bundle_id_id: "123456789", capability_type: Spaceship::ConnectAPI::BundleIdCapability::Type::MAPS) # Delete an capability from bundle identifier capabilities.each do |capability| if capability.capability_type == Spaceship::ConnectAPI::BundleIdCapability::Type::MAPS capability.delete! end end能力类型以常量枚举形式组织在 capabilities.rb 与 bundle_id_capability.rb 中(如Type::MAPS)。增删能力支持"已知 bundle_id_id 直接创建"与"通过 BundleId 实例操作"两种方式,删除时先遍历匹配capability_type再调用delete!。
十一、Webhook 管理(Auth Key 场景)
通过 Connect API 可以对单个 App 注册、删除事件通知 Webhook:
app = Spaceship::ConnectAPI::App.find("com.krausefx.app") # Fetch all webhooks webhooks = Spaceship::ConnectAPI::Webhook.all(app_id: app.id) # Create a new webhook new_webhook = Spaceship::ConnectAPI::Webhook.create( app_id: app.id, event_types: [ Spaceship::ConnectAPI::Webhook::EventType::APP_STORE_VERSION_APP_VERSION_STATE_UPDATED, ], name: 'Webhook Name', secret: 'secret1234', url: 'https://webhook.example.com' ) # Delete a webhook webhooks.first.delete! new_webhook.delete!事件类型(EventType)常量集中在 webhook.rb,上例监听的是"App Store 版本状态更新"事件。典型的落地场景是:提交审核后,通过 Webhook 把IN_REVIEW、READY_FOR_SALE、REJECTED等状态变化实时推送到自己的服务,替代 CI 里的轮询逻辑。回调 URL 需要支持 App Store Connect 的签名校验,secret即用于验证消息来源。
十二、许可证与使用边界
仓库在文档末尾对 Spaceship 及整个 fastlane 生态作了如下澄清:本项目与 fastlane 所有工具均与 Apple Inc. 无任何从属关系;项目以 MIT 协议开源,你拥有源码的完整访问权并可自行修改以适配需求;所有 fastlane 工具运行在你自己的电脑或服务器上,因此你的凭证或其他敏感信息永远不会离开你自己的机器——你对自己如何使用 fastlane 工具负责。
延伸阅读
若要继续深入,仓库内的以下文档与源码与本篇主题直接相关,可作为下一步阅读材料:
- spaceship/docs/Authentication.md:两种登录方式的细节与 2FA 处理;
- spaceship/docs/DeveloperPortal.md:开发者门户(证书、描述文件)侧的 API;
- spaceship/docs/TestFlightTesting.md:TestFlight 测试的专项指南;
- spaceship/lib/spaceship/connect_api/models:本篇涉及的全部 ConnectAPI 模型与属性定义;
- deliver/lib/deliver/submit_for_review.rb:把"选构建 + 提交审核"做成真实生产流程的完整实现范例;
- deliver/assets/example_rating_config.json:年龄分级等提交配置的合法取值示例。
【免费下载链接】fastlane🚀 The easiest way to automate building and releasing your iOS and Android apps项目地址: https://gitcode.com/GitHub_Trending/fa/fastlane
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考