关于 release-and-store-shipping
为 Flutter 应用商店发布提供准备指南,涵盖构建配置、签名、制品检查、隐私声明和分阶段发布。面向负责发布的开发者,并强调发布操作具有不可逆性。
主要功能
- 01校验版本与构建编号的映射
- 02避免将签名凭据提交到源码库
- 03混淆构建并归档符号文件
- 04检查权限和隐私声明
- 05在真实设备验证发布制品
- 06提交前核对商店设置
适用场景
- 01准备 Android 或 iOS 应用商店发布
- 02构建前检查发布卫生状况
- 03测试确切签名制品及升级路径
- 04审查商店隐私和权限声明
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zakariaf/ flutter-skills --skill release-and-store-shipping安装后在 Claude Code 中输入/release-and-store-shipping确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/android-play.mdapp-store-connect-submission.mdios-app-store.mdprivacy-permissions-and-claims.mdscripts/check-ipa-slices.shcheck-release-hygiene.sh详细介绍
发布和商店交付
CI 验证的是代码;发布验证的是构件。这里的一切都关乎陌生人安装的确切软件包——经过 R8、混淆、摇树优化,并移除了发布模式断言,由一把绝不能丢失的密钥签名,还会向应用商店声明一些信息;如果这些信息不实,商店就会下架应用。本技能从 ci-pipeline-and-gates 结束、design-review-workflow 签字通过的地方开始。
构建、签名、上传和打标签都可能产生副作用,且部分操作不可逆(已发布的构建号永远不能重用;iOS 构建永远无法撤回)。绝不要因为任务似乎暗示要发布就执行这些操作——只有开发者明确要求发布时才执行。始终可以阅读并强制执行下方的配置规则。
每次发布构建前(以及在 CI 中)都要运行 scripts/check-release-hygiene.sh:这是本技能的静态检查部分,可检查被跟踪的凭据、格式错误的构建号、调试签名和调试功能,这些问题都不需要构建即可发现。上传前,应先对已构建的 IPA 运行 scripts/check-ipa-slices.sh。平台相关的详细内容见 references/android-play.md、references/ios-app-store.md、references/app-store-connect-submission.md 和 references/privacy-permissions-and-claims.md。
不容妥协的规则
pubspec.yaml中的version: x.y.z+N是唯一的版本来源。x.y.z→versionName/CFBundleShortVersionString,N→versionCode/CFBundleVersion。每次构建只能通过--build-name/--build-number覆盖; 绝不要在build.gradle或Info.plist中硬编码版本。原因:两个来源会悄然出现偏差,导致崩溃报告显示的版本从未发布过。- 构建号只能递增,已发布的构建号永久作废。
Play 会拒绝重复或更低的
versionCode;对于相同的短版本号,App Store Connect 会拒绝重复的CFBundleVersion。即使上传失败,构建号也会被作废——应递增,不要重试。 - 绝不在仓库中存放签名材料。 Keystore/
.jks/.p12、App Store Connect API 密钥(.p8)、Play 服务账号 JSON,以及android/key.properties均已加入 git 忽略,并从机密存储中注入。原因:密钥一旦进入 git 历史记录,就会永久泄露;而轮换 应用签名 密钥需要在商店端办理,你肯定不想在发布时才发现这一点。 - 加入 Play App Signing,并将上传密钥单独保存。 使用 Play App Signing 后,丢失 上传 密钥可通过支持渠道恢复;未加入该计划的应用若丢失 应用签名 密钥,就再也无法更新应用列表——应用会因此停摆。
- 进行混淆并拆分调试信息,然后将符号文件与构建产物一同归档。
每个上传的构建都必须使用
--obfuscate --split-debug-info=<dir>,且<dir>必须按构建版本区分(symbols/<x.y.z>+<N>/)并存储在本机之外。原因:符号文件是读取该版本堆栈跟踪的 唯一 方式;之后重新生成的符号会有所不同,无法解码任何内容。 - 在真实设备上验证发布构建产物本身。 安装准确的
.aab(通过 Play 内部应用分享 /bundletool)或准确的 TestFlight 构建—— 绝不要使用调试或 profile 构建,也绝不要说“在模拟器上能运行就行”。原因:R8、 资源缩减和混淆只会在发布构建中运行,因此反射插件、 缺失的 ProGuard 规则和被剥离的断言会最先在你要发布的构建产物中出问题。 - 根据合并后的清单,完整核对权限集合。 间接依赖的插件会注入你从未编写过的权限。读取合并后的输出(而不是源清单),用
tools:node="remove"移除不需要的权限,并维护一份已提交的预期权限清单,由门禁进行差异比对。在 iOS 上,每个现有的NS*UsageDescription都必须如实描述,每个需要的描述都必须存在——缺少用途说明会导致 App Review 拒绝审核,未使用的说明则是你无法辩护的声明。参见references/privacy-permissions-and-claims.md。 - 商店中的每项声明都必须能在仓库中找到依据。 Play 数据安全表、App
Store 隐私营养标签和
PrivacyInfo.xcprivacy(必需理由 API- 第三方 SDK 隐私清单)描述的是 代码 的实际行为。添加崩溃报告工具、广告 SDK 或分析包时,必须在同一个 PR 中更新声明。原因:虚假声明会导致应用下架,而不只是收到警告。
- 不要做绝对化的隐私声明。 禁止在应用列表文案或引导文案中使用“任何内容都不会离开你的设备”和“完全私密”等说法。应说明具体机制——存储什么、存储在哪里、哪些内容会离开设备,以及何时离开。原因:一次崩溃报告上传、一次通过分享表单导出,或一张地图瓦片,就足以让绝对化表述变成谎言。
- 发布版本中必须无法访问调试功能。 开发菜单、测试数据填充、
eraseDatabaseOnSchemaChange、记录所有内容的日志接收器和预发布环境端点,都必须从构建中排除,或置于商店版本不会使用的 flavor 中——通过 grep 门禁证明,而不是凭记忆判断。原因:这些内容一旦随应用发布,就会清除或泄露真实用户数据。 - 在发布构建上测量应用体积和冷启动时间,并按版本记录。 使用
--analyze-size测量字节数,在真实的低端目标设备上使用 profile 模式下的--trace-startup测量首帧耗时。超过已记录预算的回归是发布阻断项,而不是备注。 - 分阶段发布,并预先书面确定暂停标准。 按阶段百分比依次进行内部 → 封闭测试 → 正式发布,并在开始发布 之前 确定触发暂停的无崩溃会话阈值。原因:Play 发布可以暂停;App Store 上的版本只能由另一个构建取代。
- 只发布一个已打标签的提交。 为准确的提交打标签,附上发布说明,并将构建产物和符号归档与标签一同保存。注明日期的设计评审签核(
design-review-workflow)是前置条件,不属于本轮工作。 - 商店端的门禁操作只能由账号持有人完成,且绝不假定商店端状态已设置。 创建应用记录、填写隐私问卷以及签署付费应用协议都没有 API,必须由人工完成——第一天就应提出这些事项。未生效的付费应用协议会导致 StoreKit 返回 零个商品,表现为购买按钮失效,而不是缺少协议。对于所有 可通过 API 设置 的内容(价格、销售地区、应用内购买状态、屏幕截图、元数据),提交前都必须从商店 读回 核实——上传日志显示成功并不代表服务端状态正确,已提交到仓库的屏幕截图文件夹也不代表截图已上传。原因:这些问题中的每一项都会阻止提交,且错误信息描述的是症状,而非相关设置。
- 首次发布时,必须将应用版本和首个应用内购买项目一同提交。
Apple 要求每种类型的首个购买项目都随应用版本一起提交;若只发布应用版本,App Review 会关闭提交,且不会审核应用
(指南 2.1(b)),解决此问题需要新的二进制文件和完整的审核周期。提交前使用一个请求进行验证——
GET /v1/reviewSubmissions/{id}/items必须返回 2 个项目,而不是 1 个。原因:已配置的购买项目会显示可提交状态,在所有工具和所有人看来,这与已提交状态完全相同。参见references/app-store-connect-submission.md。
有序发布流程
仅在明确请求发布时运行。不要调整顺序或跳过步骤。
- 前置条件。 工作树干净;发布提交上的 CI 全部通过;已取得带日期的设计评审签核;已编写变更日志/发布说明。
- 在
pubspec.yaml中递增version:——面向用户可见的变更使用语义化版本x.y.z,+N则递增至超过历史上传过的最高编号。单独提交此变更。 - 使用混淆和带版本号的符号文件构建制品。
flutter build appbundle --release \ --obfuscate --split-debug-info=build/symbols/1.4.0+42 flutter build ipa --release \ --obfuscate --split-debug-info=build/symbols/1.4.0+42 \ --export-options-plist=ios/ExportOptions.plist - 在上传任何内容之前,将
build/symbols/1.4.0+42/与制品一起归档到设备之外。 这是没人会错过第二次的步骤。 - 在真实硬件上验证制品——安装构建的包,走一遍主要流程,强行停止并重新启动,然后在上一个已发布版本之上进行升级,让真实迁移路径运行(见
run-migration)。 - 测量各项预算,并与上一版的数值并列记录:
flutter build appbundle --release --analyze-size # bytes, by library/asset flutter run --profile --trace-startup # start_up_info.json - 核对各项声明——合并后的权限集合、Data Safety/营养标签/
PrivacyInfo.xcprivacy,以及商店详情中的每一句隐私说明,都要与当前代码的实际行为相符。 - 上传到内部测试轨道/TestFlight,并从应用商店进行冒烟测试,不要使用本地安装版本——商店分发会重新签名并重新压缩制品。
- 通过读取配置来核对商店端设置——价格和销售地区可用性已设置,应用内购买项目已就绪并关联到该版本,每种必需的显示类型都已上传屏幕截图,元数据完整,且只有账户持有人才能完成的项目(隐私问卷、Paid Applications Agreement)均已办妥。查询商店,不要信任写入这些设置的工具(
references/app-store-connect-submission.md)。 - 为提交打标签,发布说明,然后开始分阶段发布,并根据步骤 1 商定的停止标准监控无崩溃率。
版本号与构建编号对应关系
pubspec.yaml | Android | iOS | 规则 |
|---|---|---|---|
x.y.z | versionName | CFBundleShortVersionString | 面向用户可见;同一版本的不同构建可以重复 |
+N | versionCode | CFBundleVersion | 整数,必须严格递增,绝不可重复使用;上传失败也会消耗该编号 |
重新构建时使用 --build-name=1.4.0 --build-number=43 覆盖;绝不要编辑 Gradle 或 Info.plist 中生成的值。在使用风味的应用中,商店风味的 applicationId/bundle id 在该商店详情的整个生命周期内固定不变——更改它会创建一个现有用户永远收不到的新应用。
符号文件与崩溃符号化
经过混淆的发布版本会生成毫无意义的符号堆栈跟踪。只有使用该次构建生成的符号文件才能解码:
flutter symbolize -i crash.txt -d build/symbols/1.4.0+42/app.android-arm64.symbols
每个 version+build 保留一个目录,与制品一起上传到同一持久存储中,并至少保留到该版本仍可能在设备上运行为止。重新构建的二进制文件会生成不同的符号文件——“重新构建一次”什么也恢复不了。
CI 无法证明、但发布流程可以证明的事
CI 会在干净的运行器上运行调试版/JIT 代码。只有发布流程才能证明:R8 和树摇优化没有破坏依赖反射的插件;合并后的权限集合符合预期;经过签名的制品可以安装并在上一个发布版本之上升级;真实低端硬件上的首帧时间和体积符合预算;商店分发的二进制文件可以运行。应如实说明这一点,不要声称流水线通过就意味着可以发布。
反模式
- 在
build.gradle或Info.plist中硬编码版本号——与 pubspec 不一致; 崩溃报告中会出现一个并不存在的构建版本。 - 上传失败后重复使用或降低构建号——应用商店会拒绝;更糟的是,较低的构建号如果被接受,可能会让更新无法显示较新的构建版本。
- 提交密钥库、
key.properties、.p8或服务账号 JSON 文件——密钥会永久泄露;清理历史记录也无法消除泄露。 - 使用
--obfuscate构建,却不归档--split-debug-info输出——一旦构建目录被清除,发布版本中的崩溃就会永久无法读取。 - 只在调试版/配置版中验证就发布构建版本——R8、代码缩减和断言剥离会改变行为;反射式插件故障只会在发布版中出现。
- 审核自己的
AndroidManifest.xml,而不是合并后的清单——用户在应用商店页面上看到的权限来自合并结果,而不是你的文件。 - 在关联了崩溃报告工具或广告 SDK 的情况下声明“我们不收集任何数据”——错误的数据安全/隐私标签声明可能导致应用下架。
- 在应用商店版本中仍可访问开发菜单、预置测试数据或
eraseDatabaseOnSchemaChange——这会破坏或暴露真实用户数据。 - 直接向 100% 用户发布——没有分阶段发布,就无法暂停;只能提交热修复,而热修复本身也要经过一轮审核。
- 通过更改
applicationId/bundle id 来“修复”签名问题——这会创建一个新应用;所有现有用户都会被留在旧应用上。 - 上传从上次为模拟器构建过的代码树生成的 IPA——Apple 会拒绝(90087/91169),二进制瘦身也无法修复;只能进行干净的重新构建。
- 相信配置应用商店的工具,而不读取应用商店中的实际状态——价格、可用性、应用内购买状态和截图通常都只是“已设置”,实际上并未设置。
- 把已提交的截图文件夹当作已上传的截图集——提交会因每种显示类型而被阻止,提示信息会指出设备类别,而不是文件。
- 提交首个应用版本时未包含首个应用内购买项目——App Review 会在未审核应用的情况下关闭提交;修复需要重新提交整个二进制文件。
- 把购买项目的“准备提交”状态理解为“已提交”——它表示已配置,且尚未被任何人处理。
完成定义
- 仅在
pubspec.yaml中递增version: x.y.z+N;N大于曾经上传过的所有编号;平台文件中未硬编码版本号。 - 使用
--release和--obfuscate --split-debug-info=<per-build dir>构建产物;在上传前,将符号文件与产物一并归档到设备之外。 - Git 未跟踪任何密钥库、
key.properties、.p8或服务账号 JSON;已加入 Play App Signing;上传密钥单独保管。 - 已在真实设备上安装确切的发布产物,覆盖升级至该版本之前已发布的版本,并强制停止后重新启动。
- 已将合并后的权限集与已提交的预期列表进行差异对比;每个 iOS
NS*UsageDescription均存在且表述属实。 - Data Safety / 营养标签 /
PrivacyInfo.xcprivacy已根据当前依赖集完成核对;商品详情或引导文案中没有绝对化的隐私声明。 - 已通过门禁证明,商店版本无法访问调试功能。
- 已记录大小(
--analyze-size)和冷启动(--trace-startup)数据,并与上一版本的数字进行对比;没有无法解释的退化。 - 已提供注明日期的设计评审签核;提交已打标签;发布说明已发布。
- 已分阶段推出,并制定书面的停止标准,且有人负责监控。
- 发布提交通过
scripts/check-release-hygiene.sh,上传前的 IPA 通过scripts/check-ipa-slices.sh。 - 提交前已回读商店端状态:价格和地区可用性、应用内购买已就绪并关联到该版本、每种必需显示类型均已提供屏幕截图、元数据完整、仅账户持有人可完成的操作均已完成。
- 首次发布时,提交中同时包含应用版本和首个应用内购买项目(
reviewSubmissions/{id}/items返回 2);每个购买项目均有审核屏幕截图、税务类别、可用性,以及每种应用语言各一项本地化内容。
多 flavor 时
使用各自的 --flavor 和入口点构建每个 flavor,并为每个 flavor 和每次构建各保留一个符号目录(symbols/<flavor>/<x.y.z>+<N>/)。商店 flavor 及其 applicationId 在商品详情的整个生命周期内都必须固定;非商店 flavor(dev、QA)绝不能上传——应在发布脚本中强制执行,不能只靠人的记忆。flavor 组合根目录见 service-boundary-and-native。
相关技能
ci-pipeline-and-gates— 本步骤开始前必须通过的门禁,以及 CI 无法证明内容的如实说明。design-review-workflow— 步骤 1 的前置条件:注明日期的签核。run-migration— 步骤 5 验证的升级至上一发布版本的路径。dependency-hygiene— 新依赖在变更此处的商店声明之前接受审核的流程。service-boundary-and-native— flavor、原生层接缝,以及清单会被合并的插件。flutter-performance— 性能预算背后的 profile 模式测量规范。ads-and-iap-monetization— 决定 App Review 审核人员实际能看到什么的权益、初始化前恢复及预加载或隐藏规则,以及发布门禁检查的广告标识符。
参考资料
- Flutter — Android 构建与发布:https://docs.flutter.dev/deployment/android
- Flutter — iOS 构建与发布:https://docs.flutter.dev/deployment/ios
- Flutter — 混淆 Dart 代码 /
flutter symbolize:https://docs.flutter.dev/deployment/obfuscate - Flutter — 衡量应用大小:https://docs.flutter.dev/perf/app-size
- Android — Play App Signing:https://developer.android.com/studio/publish/app-signing
- Android — 合并多个清单文件:https://developer.android.com/build/manage-manifests
- Google Play — Data safety 表单:https://support.google.com/googleplay/android-developer/answer/10787469
- Apple — 隐私清单文件:https://developer.apple.com/documentation/bundleresources/privacy_manifest_files