mcpField
R
release-and-store-shipping

release-and-store-shipping

github.com/zakariaf云与 DevOps63150 次安装5 周前更新
代码仓库

关于 release-and-store-shipping

为 Flutter 应用商店发布提供准备指南,涵盖构建配置、签名、制品检查、隐私声明和分阶段发布。面向负责发布的开发者,并强调发布操作具有不可逆性。

主要功能

  1. 01校验版本与构建编号的映射
  2. 02避免将签名凭据提交到源码库
  3. 03混淆构建并归档符号文件
  4. 04检查权限和隐私声明
  5. 05在真实设备验证发布制品
  6. 06提交前核对商店设置

适用场景

  1. 01准备 Android 或 iOS 应用商店发布
  2. 02构建前检查发布卫生状况
  3. 03测试确切签名制品及升级路径
  4. 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.md
app-store-connect-submission.md
ios-app-store.md
privacy-permissions-and-claims.md
scripts/
check-ipa-slices.sh
check-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。

不容妥协的规则

  1. pubspec.yaml 中的 version: x.y.z+N 是唯一的版本来源。 x.y.z → versionName / CFBundleShortVersionString,N → versionCode / CFBundleVersion。每次构建只能通过 --build-name / --build-number 覆盖; 绝不要在 build.gradle 或 Info.plist 中硬编码版本。原因:两个来源会悄然出现偏差,导致崩溃报告显示的版本从未发布过。
  2. 构建号只能递增,已发布的构建号永久作废。 Play 会拒绝重复或更低的 versionCode;对于相同的短版本号,App Store Connect 会拒绝重复的 CFBundleVersion。即使上传失败,构建号也会被作废——应递增,不要重试。
  3. 绝不在仓库中存放签名材料。 Keystore/.jks/.p12、App Store Connect API 密钥(.p8)、Play 服务账号 JSON,以及 android/key.properties 均已加入 git 忽略,并从机密存储中注入。原因:密钥一旦进入 git 历史记录,就会永久泄露;而轮换 应用签名 密钥需要在商店端办理,你肯定不想在发布时才发现这一点。
  4. 加入 Play App Signing,并将上传密钥单独保存。 使用 Play App Signing 后,丢失 上传 密钥可通过支持渠道恢复;未加入该计划的应用若丢失 应用签名 密钥,就再也无法更新应用列表——应用会因此停摆。
  5. 进行混淆并拆分调试信息,然后将符号文件与构建产物一同归档。 每个上传的构建都必须使用 --obfuscate --split-debug-info=<dir>,且 <dir> 必须按构建版本区分(symbols/<x.y.z>+<N>/)并存储在本机之外。原因:符号文件是读取该版本堆栈跟踪的 唯一 方式;之后重新生成的符号会有所不同,无法解码任何内容。
  6. 在真实设备上验证发布构建产物本身。 安装准确的 .aab (通过 Play 内部应用分享 / bundletool)或准确的 TestFlight 构建—— 绝不要使用调试或 profile 构建,也绝不要说“在模拟器上能运行就行”。原因:R8、 资源缩减和混淆只会在发布构建中运行,因此反射插件、 缺失的 ProGuard 规则和被剥离的断言会最先在你要发布的构建产物中出问题。
  7. 根据合并后的清单,完整核对权限集合。 间接依赖的插件会注入你从未编写过的权限。读取合并后的输出(而不是源清单),用 tools:node="remove" 移除不需要的权限,并维护一份已提交的预期权限清单,由门禁进行差异比对。在 iOS 上,每个现有的 NS*UsageDescription 都必须如实描述,每个需要的描述都必须存在——缺少用途说明会导致 App Review 拒绝审核,未使用的说明则是你无法辩护的声明。参见 references/privacy-permissions-and-claims.md。
  8. 商店中的每项声明都必须能在仓库中找到依据。 Play 数据安全表、App Store 隐私营养标签和 PrivacyInfo.xcprivacy(必需理由 API
    • 第三方 SDK 隐私清单)描述的是 代码 的实际行为。添加崩溃报告工具、广告 SDK 或分析包时,必须在同一个 PR 中更新声明。原因:虚假声明会导致应用下架,而不只是收到警告。
  9. 不要做绝对化的隐私声明。 禁止在应用列表文案或引导文案中使用“任何内容都不会离开你的设备”和“完全私密”等说法。应说明具体机制——存储什么、存储在哪里、哪些内容会离开设备,以及何时离开。原因:一次崩溃报告上传、一次通过分享表单导出,或一张地图瓦片,就足以让绝对化表述变成谎言。
  10. 发布版本中必须无法访问调试功能。 开发菜单、测试数据填充、 eraseDatabaseOnSchemaChange、记录所有内容的日志接收器和预发布环境端点,都必须从构建中排除,或置于商店版本不会使用的 flavor 中——通过 grep 门禁证明,而不是凭记忆判断。原因:这些内容一旦随应用发布,就会清除或泄露真实用户数据。
  11. 在发布构建上测量应用体积和冷启动时间,并按版本记录。 使用 --analyze-size 测量字节数,在真实的低端目标设备上使用 profile 模式下的 --trace-startup 测量首帧耗时。超过已记录预算的回归是发布阻断项,而不是备注。
  12. 分阶段发布,并预先书面确定暂停标准。 按阶段百分比依次进行内部 → 封闭测试 → 正式发布,并在开始发布 之前 确定触发暂停的无崩溃会话阈值。原因:Play 发布可以暂停;App Store 上的版本只能由另一个构建取代。
  13. 只发布一个已打标签的提交。 为准确的提交打标签,附上发布说明,并将构建产物和符号归档与标签一同保存。注明日期的设计评审签核(design-review-workflow)是前置条件,不属于本轮工作。
  14. 商店端的门禁操作只能由账号持有人完成,且绝不假定商店端状态已设置。 创建应用记录、填写隐私问卷以及签署付费应用协议都没有 API,必须由人工完成——第一天就应提出这些事项。未生效的付费应用协议会导致 StoreKit 返回 零个商品,表现为购买按钮失效,而不是缺少协议。对于所有 可通过 API 设置 的内容(价格、销售地区、应用内购买状态、屏幕截图、元数据),提交前都必须从商店 读回 核实——上传日志显示成功并不代表服务端状态正确,已提交到仓库的屏幕截图文件夹也不代表截图已上传。原因:这些问题中的每一项都会阻止提交,且错误信息描述的是症状,而非相关设置。
  15. 首次发布时,必须将应用版本和首个应用内购买项目一同提交。 Apple 要求每种类型的首个购买项目都随应用版本一起提交;若只发布应用版本,App Review 会关闭提交,且不会审核应用 (指南 2.1(b)),解决此问题需要新的二进制文件和完整的审核周期。提交前使用一个请求进行验证——GET /v1/reviewSubmissions/{id}/items 必须返回 2 个项目,而不是 1 个。原因:已配置的购买项目会显示可提交状态,在所有工具和所有人看来,这与已提交状态完全相同。参见 references/app-store-connect-submission.md。

有序发布流程

仅在明确请求发布时运行。不要调整顺序或跳过步骤。

  1. 前置条件。 工作树干净;发布提交上的 CI 全部通过;已取得带日期的设计评审签核;已编写变更日志/发布说明。
  2. 在 pubspec.yaml 中递增 version:——面向用户可见的变更使用语义化版本 x.y.z,+N 则递增至超过历史上传过的最高编号。单独提交此变更。
  3. 使用混淆和带版本号的符号文件构建制品。
    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
    
  4. 在上传任何内容之前,将 build/symbols/1.4.0+42/ 与制品一起归档到设备之外。 这是没人会错过第二次的步骤。
  5. 在真实硬件上验证制品——安装构建的包,走一遍主要流程,强行停止并重新启动,然后在上一个已发布版本之上进行升级,让真实迁移路径运行(见 run-migration)。
  6. 测量各项预算,并与上一版的数值并列记录:
    flutter build appbundle --release --analyze-size   # bytes, by library/asset
    flutter run --profile --trace-startup              # start_up_info.json
    
  7. 核对各项声明——合并后的权限集合、Data Safety/营养标签/PrivacyInfo.xcprivacy,以及商店详情中的每一句隐私说明,都要与当前代码的实际行为相符。
  8. 上传到内部测试轨道/TestFlight,并从应用商店进行冒烟测试,不要使用本地安装版本——商店分发会重新签名并重新压缩制品。
  9. 通过读取配置来核对商店端设置——价格和销售地区可用性已设置,应用内购买项目已就绪并关联到该版本,每种必需的显示类型都已上传屏幕截图,元数据完整,且只有账户持有人才能完成的项目(隐私问卷、Paid Applications Agreement)均已办妥。查询商店,不要信任写入这些设置的工具(references/app-store-connect-submission.md)。
  10. 为提交打标签,发布说明,然后开始分阶段发布,并根据步骤 1 商定的停止标准监控无崩溃率。

版本号与构建编号对应关系

pubspec.yamlAndroidiOS规则
x.y.zversionNameCFBundleShortVersionString面向用户可见;同一版本的不同构建可以重复
+NversionCodeCFBundleVersion整数,必须严格递增,绝不可重复使用;上传失败也会消耗该编号

重新构建时使用 --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 审核人员实际能看到什么的权益、初始化前恢复及预加载或隐藏规则,以及发布门禁检查的广告标识符。

参考资料

评论

支持 Markdown