Readest 的 Apple App Store 与 TestFlight 提交:fastlane lanes 设计及三个关键工程陷阱
发布时间:2026/9/20 10:19:27 锦皓数字建站

Readest 的 Apple App Store 与 TestFlight 提交fastlane lanes 设计及三个关键工程陷阱【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest导读本文基于 Readest 仓库中的apps/readest-app/.claude/memory/fastlane-apple-appstore-submission.md记忆文档系统梳理其基于 fastlane 的 iOS/macOS App Store 与 TestFlight 提交流程。你将掌握release_ios/release_macos两条 lane 的完整设计、upload_to_app_store与upload_to_testflight的参数含义、从release-notes.json自动生成「新内容」的实现以及仓库实践中踩过的三个陷阱Tauri 构建期误触发 notarization、fastlane 执行期工作目录变化、dotenv 工具链遮蔽。文末给出可直接复用的端到端命令序列。一、发布链路全景构建、上传与提交三阶段分离Readest 的 Apple 发布链路遵循「构建 → 上传 → 提交」三个阶段严格分离的设计。理解这一点是读懂全部 lane 逻辑的前提构建与上传由pnpm run release-ios-appstore/release-macos-universial-appstore驱动内部执行tauri build或tauri ios build产出 IPA / PKG再通过xcrun altool --upload-app上传到 App Store Connect。提交与分发由 fastlane lanes 完成仅针对已经上传好的构建做 App Store 审核提交与 TestFlight 分发绝不构建、也绝不重复上传二进制。这一分工在 fastlane/Fastfile 中体现为两个关键开关upload_to_app_store使用skip_binary_upload: trueupload_to_testflight使用distribute_only: true。提交脚本的 package.json 入口位于 apps/readest-app/package.jsonsubmit-appstore-ios: dotenv -e .env.apple-appstore.local -- bash -c cd ../.. fastlane release_ios, submit-appstore-macos: dotenv -e .env.apple-appstore.local -- bash -c cd ../.. fastlane release_macos,两个入口共用.env.apple-appstore.local作为环境变量来源该文件由开发者本地维护、不提交仓库差别仅在最终调用的 lane 名。二、两条 lanerelease_ios与release_macos两条 lane 分别处理 iOS 与 macOS它们共享同一个 helpersubmit_apple_build(deliver_opts, testflight_opts)只是传入的平台相关参数不同。App Store Connect 上两者是同一个应用记录bundle id 为com.bilingify.readest但由两条 lane 独立提交详见 fastlane/Fastfiledesc Submit the uploaded iOS build for App Store review and to TestFlight lane :release_ios do submit_apple_build( { platform: ios, ipa: repo_path(apps/readest-app/src-tauri/gen/apple/build/arm64/Readest.ipa) }, { app_platform: ios } ) end desc Submit the uploaded macOS build for App Store review and to TestFlight lane :release_macos do submit_apple_build( { platform: osx, pkg: repo_path(target/universal-apple-darwin/release/bundle/macos/Readest.pkg), metadata_path: ./fastlane/metadata-macos, promotional_text: { en-US PROMOTIONAL_TEXT_MACOS }, }, { app_platform: osx } ) end2.1 核心 helpersubmit_apple_buildsubmit_apple_build 是整条提交逻辑的中枢按固定顺序执行两步第一步App Store 审核提交upload_to_app_storeupload_to_app_store({ api_key: api_key, app_identifier: com.bilingify.readest, skip_binary_upload: true, # 复用 altool 上传的构建从二进制读取版本 submit_for_review: true, # 直接进入审核 automatic_release: true, # 审核通过后自动发布 force: true, # 跳过 HTML 元数据预览交互 skip_screenshots: true, # 截图在 App Store Connect 后台手工维护 skip_metadata: false, # 上传 metadata 文件夹中的名称、副标题、关键词、描述 release_notes: { en-US release_notes_text }, promotional_text: { en-US PROMOTIONAL_TEXT }, precheck_include_in_app_purchases: false, submission_information: { add_id_info_uses_idfa: false }, }.merge(deliver_opts))第二步TestFlight 分发upload_to_testflightupload_to_testflight({ api_key: api_key, app_identifier: com.bilingify.readest, distribute_only: true, # 仅分发已上传构建 distribute_external: true, # 允许外部测试员 groups: [Beta Testers], # 分发目标分组 changelog: release_notes_text, }.merge(testflight_opts))2.2 为什么 App Store 提交必须排在 TestFlight 之前lane 内顺序是刻意设计的App Store 提交在前TestFlight 分发在后。原因是upload_to_app_store会等待构建处理build processing完成而upload_to_testflight的分发恰恰依赖这个处理结果。若顺序颠倒TestFlight 可能因构建尚未就绪而失败。这一约束在 Fastfile 的注释中有明确记录。2.3 平台间差异元数据与宣传文本iOS 与 macOS 共享一个 App Store Connect 记录但列表文案并不相同。deliver的metadata_path没有平台维度因此代码通过 lane 参数显式区分release_ios使用默认 metadata 路径即fastlane/metadata/en-USrelease_macos显式传入./fastlane/metadata-macosMac 文案突出多窗口、键盘快捷键、系统词典等 Mac 独有能力宣传文本Promotional Text可在不触发审核的情况下随时修改同样按平台区分iOS 用PROMOTIONAL_TEXTmacOS 用PROMOTIONAL_TEXT_MACOS二者在 fastlane/Fastfile 中定义为常量内联传入后优先于 metadata 文件夹中的promotional_text.txt。此外iOS 与 macOS 的 App Store 截图由人工在 App Store Connect 后台维护lane 不触碰skip_screenshots: true。fastlane/screenshots/ios 与 fastlane/screenshots/osx 下的 PNG 仅作为本地母版参考。注释特别警告若未来要自动化截图上传iOS 与 macOS 共用一条 App Store 记录但不共用截图集deliver 仅凭像素尺寸判断截图类型单一screenshots_path会把 iPhone 截图错误挂到 Mac 列表上。仓库还提供了只读辅助 lanedownload_store_screenshots platform:osx|ios用于拉取当前线上截图与本地母版对比fastlane/Fastfile。2.4 相关的 Android lanesfastlane/Fastfile 将 Android 设为默认平台default_platform(:android)并维护upload_production/upload_internal/upload_beta三条 lane 对接 Google Play。其与 Apple lanes 的关键差异值得注意Play 的列表文案与图片是商店级别而非轨道级别的因此upload_internal/upload_beta故意开启skip_upload_metadata: true等开关只有upload_production才允许触碰线上列表并借助sync_image_upload: true让列表镜像与metadata-play目录严格一致。这是理解 Fastfile 全貌的重要背景但本文聚焦的 Apple 提交逻辑与之完全独立。三、release_notes_text自动生成「新内容」与 TestFlight 变更日志两条 lane 的release_notes与changelog都来自同一个函数release_notes_textfastlane/Fastfile其数据源是仓库中的 apps/readest-app/release-notes.json。该文件以releases对象组织键为版本号值为日期与 notes 数组。解析逻辑分三步def release_notes_text releases JSON.parse(File.read(repo_path(apps/readest-app/release-notes.json)))[releases] latest releases.keys.max_by { |version| Gem::Version.new(version) } releases[latest][notes] .reject { |note| note ~ /\b(?:Android|Windows|Linux)\b/i } .map { |note| – #{note} } .join(\n) end取最新版本用Gem::Version.new做语义化版本比较而非字符串排序避免0.12.8被排到0.12.10之前这类问题过滤平台无关条目丢弃所有匹配/\b(?:Android|Windows|Linux)\b/i的 note——因为 iOS/macOS 商店文案不应出现其它平台的特性描述格式化每条 note 前缀–en dash 空格后换行拼接。同一文本同时供给 App Store 的「Whats New」和 TestFlight 的 changelog保证两处发布说明一致。四、asc_api_keyApp Store Connect API 认证封装所有 Apple 操作都需要 App Store Connect API 密钥。helper asc_api_key 做了两件重要的事1前置校验读取APPLE_API_KEYkey id与APPLE_API_ISSUERissuer id任一为空即UI.user_error!中断并提示先加载apps/readest-app/.env.apple-appstore.local。这避免了裸调用 lane 时出现AuthKey_.p8这类令人困惑的缺文件错误。2密钥路径派生app_store_connect_api_key( key_id: key_id, issuer_id: ENV[APPLE_API_ISSUER], key_filepath: ENV[APPLE_API_KEY_PATH] || repo_path(apps/readest-app/private_keys/AuthKey_#{key_id}.p8), )默认情况下从 key id 派生.p8路径private_keys/AuthKey_KEYID.p8这与xcrun altool的密钥文件命名约定一致仅当显式设置APPLE_API_KEY_PATH时才覆盖例如 iOS 构建环境。至于「为什么 macOS 环境不能设置APPLE_API_KEY_PATH」正是下一节的核心陷阱。五、陷阱一Tauri 构建期误触发 notarization这是整套方案中最隐蔽的问题。tauri build有一个自动行为只要构建环境同时存在完整的 App Store Connect API 密钥三元组APPLE_API_KEYAPPLE_API_ISSUERAPPLE_API_KEY_PATH它就会自动对 macOS App Store 包执行 notarization公证。但 App Store 构建使用的是Apple Distribution 证书而 notarization 要求Developer ID 证书并附带安全时间戳——App Store 应用根本不走 notarization。于是自动公证必然失败报错形如not signed with a valid Developer ID certificate/no secure timestamp导致本应成功的构建直接中断。解决方案在 Fastfile 注释中写得很明确APPLE_API_KEY_PATH必须留在.env.apple-appstore.localmacOS 构建环境之外。只提供 key id 与 issuer 时不会触发 notarization而asc_api_key在提交阶段通过 key id 自行推导.p8路径因此提交环节并不依赖该变量。iOS 构建环境则恰好相反——iOS 不做 notarization且其构建脚本确实需要APPLE_API_KEY_PATH故该变量只在.env.ios-appstore.local中设置。六、陷阱二fastlane 执行期工作目录变化与repo_path修复第二个陷阱源于 fastlane 的行为差异执行 lane 时fastlane 会把当前工作目录切换到./fastlane文件夹此时__dir__只是.因此任何裸写File.read(./apps/...)都会报No such filefastlane lanes时只解析 lane 定义、不执行 lane 体所以这种路径错误在查看 lane 列表时不会被发现——必须实际运行一次路径相关的 lane 才能暴露。仓库的修复方式是统一封装路径解析 helperfastlane/Fastfiledef repo_path(relative) File.expand_path(relative, File.expand_path(.., __dir__)) endrepo_path以仓库根目录fastlane的父目录为锚点解析相对路径。所有路径相关的参数——release-notes.json、.p8密钥、IPA、PKG——都必须经过它而不是直接写相对路径。这样 lane 无论从哪个目录被调用都能正确定位文件。七、陷阱三dotenv 工具链遮蔽第三个陷阱发生在脚本调用层。dotenv这个命令名同时存在于两条工具链裸的dotenvPATH 上的 Ruby gemdotenv使用-f语法指定文件package.json 脚本实际依赖的是 npm 的dotenv-cli从apps/readest-app/node_modules/.bin解析使用-e语法。两个工具共享命令名但参数约定不同混用会静默失败。提交脚本的完整形态是dotenv -e .env.apple-appstore.local -- bash -c cd ../.. fastlane release_*其中cd ../..同样是必要的pnpm 从apps/readest-app执行脚本而fastlane 不会向上查找fastlane/目录必须手动退到仓库根目录才能让 fastlane 找到 Fastfile 与元数据。若缺少这个cdfastlane 会报找不到 Fastfile若误用 Ruby gem 的-f语法则会得到意外的参数错误。八、端到端操作序列综合前述全部内容一次完整的 macOS App Store 发布流程如下iOS 流程对称阶段一构建并上传不使用 fastlane# 在 apps/readest-app 目录下 pnpm run release-macos-universial-appstore内部由 apps/readest-app/scripts/release-mac-appstore.sh 驱动先用jq把src-tauri/tauri.appstore.conf.json的bundle.macOS.bundleVersion更新为当前时间戳该 App Store 专用配置文件由本地维护再执行pnpm run build-macos-universial-appstore构建通用universal二进制随后xcrun productbuild --sign打 PKG最后xcrun altool --upload-app --type macos上传。iOS 一侧对应 apps/readest-app/scripts/release-ios-appstore.sh在tauri ios build --export-method app-store-connect之后还串联了两个守护脚本fix-ios-appstore-appgroup.sh为 widget/分享扩展重新挂接group.com.bilingify.readest的 App Group 授权防止阅读小组件空壳化与verify-ios-appstore-entitlements.sh校验授权未被剥离全部通过后才交给altool。脚本以set -euo pipefail运行任何守护失败都会中止发布。阶段二提交审核与 TestFlight 分发使用 fastlane# 在 apps/readest-app 目录下 pnpm run submit-appstore-macos # 或 submit-appstore-ios脚本会加载.env.apple-appstore.localcd ../..回到仓库根执行fastlane release_macos或release_ioslane 依次完成 App Store 审核提交与 TestFlight 外部测试分发。九、小结Readest 的 fastlane Apple 提交流程是一个「小而精」的工程样本两条 lane 一个共享 helper 就覆盖了 App Store 审核提交与 TestFlight 分发的全部逻辑再辅以repo_path、asc_api_key、release_notes_text三个工具函数解决路径、认证与文案三大横切问题。三个陷阱各有代表性——构建工具链的隐式副作用notarization 误触发、CLI 工具的执行环境差异cwd 切换、同名工具的语义冲突dotenv在任何多工具链的发布流水线中都可能再次出现。理解本文的解法模式比记住具体答案更有迁移价值如需深入源码可继续阅读 fastlane/Fastfile、apps/readest-app/scripts/release-ios-appstore.sh 与 apps/readest-app/scripts/release-mac-appstore.sh。【免费下载链接】readestReadest is a modern, feature-rich ebook reader designed for avid readers offering seamless cross-platform access, powerful tools, and an intuitive interface to elevate your reading experience.项目地址: https://gitcode.com/gh_mirrors/re/readest创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。