移动端发布 SOP

将 LibreFang 移动端构建发布到 TestFlight (iOS) 与 Play 内部测试 (Android) 的端到端运行手册。与桌面端的生产部署 配套——本文仅涵盖移动端特有的步骤。

本 SOP 假设 移动端 CI 中的 mobile_androidmobile_ios 任务已就绪(PR #3970 关闭)且 .github/SECRETS.md 中的上传所需 secret 已配置完成。


目录


前置条件——每个商店一次性

请在 issue 系统中跟踪以下事项;完成后不再重复。

Apple

  1. Apple Developer Program 注册($99 / 年)。如 LibreFang 有法人实体 则选组织版,否则选个人版。组织团队内部人员变动不需要重新注册。
  2. App Store Connect 应用记录:bundle id ai.librefang.app,主类目 Productivity。尽早占位——bundle id 抢注无法回滚。
  3. 发布证书 + Provisioning Profile(Ad Hoc + App Store)。将证书 导出为 .p12,填入 .github/SECRETS.md 中四个 APPLE_* 签名 secret。
  4. App Store Connect API Key,权限 App Manager。填入三个 APPLE_API_KEY_* 上传 secret。
  5. 隐私政策部署到 librefang.ai/privacy-mobile。草稿模板位于 .github/templates/PRIVACY_MOBILE_TEMPLATE.md ——经法务复核后,发布到 docs/src/app/privacy-mobile/
  6. App Store Connect 中的 App Privacy 问卷。声明:
    • "API key" → 用户输入的联系信息
    • "Agent 对话日志" → 用户内容
    • 全部标记为 Data Not Linked to User(LibreFang 不将数据 绑定到 Apple ID)。
  7. 加密出口合规:iOS 构建流程已自动将 ITSAppUsesNonExemptEncryption = false 写入 Info.plist,并向 TestFlight action 传入 uses-non-exempt-encryption: "false"——除非 未来代码引入非标准加密,否则无需手动填表。

Google

  1. Play Console 账号($25 一次性)。在添加任何制品前,先在开发者 账号上启用两步验证。
  2. 应用记录:包名 ai.librefang.app,默认语言英文。两端 bundle id 按惯例保持一致;技术上不必相同,但保持一致可以简化运维面。
  3. 上传密钥(启用 Play App Signing 时与应用签名密钥分开——推荐)。 上传密钥丢失可在 Play Console 重置;Google 持有的应用签名密钥若 丢失,则永远无法再发版。务必离线备份。
  4. 服务账号 JSON,在该应用上具备 Release Manager 角色。填入 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON
  5. 隐私政策(与 iOS 同 URL)。
  6. 数据安全表单:内容与 Apple 问卷一致。
  7. 内容分级:Productivity / Tools,无敏感内容。

版本号映射方案

两个商店都拒绝 build 号回退的提交。CI 流程按以下规则确定性地派生:

位置字段公式示例
双端display versionCargo.toml[workspace.package].version,去除 -betaN / -rcN 后缀2026.4.28-beta72026.4.28
iOSCFBundleShortVersionStringdisplay version2026.4.28
iOSCFBundleVersionUTC YYYYMMDDNN,NN = run_number % 1002026042901
AndroidversionNamedisplay version2026.4.28
AndroidversionCode同样的 YYYYMMDDNN 整数2026042901

实际硬上限是 Play 的 versionCode 上限 2,100,000,000(int32 有更多 余量但 Play 卡得更紧)。YYYYMMDDNN 方案在 2100-01-01 前都低于该 上限。在此之前需切换到更小值域的公式(例如 (year - 2024) * 1_000_000 + month * 10_000 + day * 100 + nn),并 同步更新 release.yml 中的两个任务。

为什么用 YYYYMMDDNN? 两个商店都要求所有历史上传的 build 号严格 单调递增。基于日期的方案让"这个 build 是哪天切的?"无需查 git 即可 回答——这在 App Review 回滚窗口仅有数小时时尤为重要。


发版操作

前置条件就绪后的端到端流程:

  1. 提升根 Cargo.toml 中的 [workspace.package].version。Conventional commit 规范:chore(release): vYYYY.M.D-betaN
  2. 给该提交打 vYYYY.M.D-betaN 标签并推送。移动端任务仅在匹配 refs/tags/v* 的标签上运行。
  3. 在 Actions 页观察 mobile_androidmobile_ios 任务。两者 continue-on-error: true——单次抖动不会破坏桌面矩阵;这意味着 你必须真的去看 summary,而不是只确认工作流末尾的绿勾。
  4. 成功后,.aab / .apk / .ipa 会附加到 GitHub Release,同时 无人值守上传步骤会分别推到 Play 内部测试 / TestFlight。
  5. 记录 build 号(Mobile / Android / Mobile / iOS 步骤摘要会打印 versionName=… versionCode=…CFBundleShortVersionString=… CFBundleVersion=…)。商店面板上需要 这些值。

如果上传 secret 尚未配置,这些任务会优雅降级:GitHub Release 上传仍会 进行,商店推送步骤打印 ::notice:: 后以 0 退出。


TestFlight 通道

mobile_ios 完成上传步骤后:

  1. App Store Connect → My Apps → LibreFang → TestFlight 标签页。 build 会进入 Processing(干净 build 5–30 分钟,符号上传慢的话 更久)。
  2. 处理期间填写 Test Information 字段(测试凭据见下;要测试什么; 描述)。这些信息在所有 TestFlight build 间共享——只需填写一次, 只在内容变更时编辑。
  3. 将 build 加入 librefang-internal 组(最多 100 邮箱,无需审核)。 内部测试者会在数分钟内收到邀请邮件。
  4. 首个 External Testing build 需提交 Beta App Review。审核 时长约 24 小时;常见拒绝原因:
    • 审核员无法连接到 daemon。在 App Review notes 里给出测试 daemon URL 与 API key。审核窗口结束后请轮换该测试 daemon 上的 API key。
    • "Sign-in wall":连接向导首启即要求填入 daemon URL,Apple 可能 视为登录墙。缓解方式是在 review notes 写一段清晰说明:daemon 连接就是产品本身。
  5. 每个 build 在上传后 90 天过期。设个日历提醒,每月推一个新的 内部 build,避免 beta 通道熄火。

Play 内部测试通道

mobile_android 完成上传步骤后:

  1. Play Console → LibreFang → Testing → Internal testing.aab 会在约 5 分钟内出现在当前 release 中。
  2. Pre-launch report:Play 会在真机集群上运行该 build 并通过邮件 报告崩溃。在升级到 Closed / Open testing 前务必阅读这些报告—— 它经常能暴露模拟器看不到的设备特定布局问题。
  3. 测试者通过邮件列表管理——把内部团队邮箱粘贴到 Testers 标签页。 opt-in URL 可分享。
  4. 升级到 Closed Testing:Play Console → Closed testing → Promote release → 选同一个 .aab。无需重新上传。
  5. Open Testing = 公开 beta。在产品稳定前不要开。Open testing 与 生产共用同一审核界面。

推送到生产

这一步是有意为之的人工卡点——两个商店都有审核延迟,beta 阶段抓到的 回归代价低,自动晋级后才发现的回归代价高。

iOS

  1. App Store Connect → App Store 标签页 → iOS App → 创建目标 CFBundleShortVersionString 的新版本。挑选要送审的 TestFlight build。
  2. App Review 时长:首次 1–3 天,后续更新更快(通常不到 24 小时)。 常见后续询问:审核员要凭据,或要求提供视频演示。
  3. 在版本页选择 Manual release,避免 App Review 通过后立即推给用户。

Android

  1. Play Console → Production → Create new release → 复用现有 AAB (不要重新构建——同一制品必须贯穿所有通道)。
  2. 灰度发布:5% → 20% → 50% → 100%,在 48–72 小时内完成。每一档都看 Vitals 与 ANR 率。当 ANR 率 > 0.47% 或崩溃率 > 1.09%(Play 的 "bad behaviour" 阈值)时停止并回滚。
  3. 首个生产 release:完整审核,约 3 天。后续更新:通常不到 12 小时。

上传失败的恢复

现象可能原因修复
mobile_android 的 "Upload to Play Internal Testing" 步骤报 403服务账号缺少 Release Manager 角色,或该应用还未在内部测试通道首次发布给 SA 授予该角色;若是首次上传,先通过 Play Console 手工上传一次以引导该通道
mobile_ios 的 "Upload to TestFlight" 步骤报 Authentication credentials are missing or invalidAPI key 已撤销,或 APPLE_API_KEY_P8 粘贴时丢了换行重新生成 key;确保 secret 值完整保留 BEGIN/END PEM 标记
Apple build 卡在 Processing 超过 24 小时Apple 端处理延迟(少见),偶有二进制问题等 24 小时,仍卡住则通过 Apple Developer support 提 TSI ticket
Play release 显示 "Internal app sharing only" 而非进入内部测试CI 上传到了内部应用共享通道(未指定 track)检查 r0adkll/upload-google-play action 调用的 track: internal 参数是否正确
versionCode X has already been used同一 UTC 日内 release 重跑次数超过 100 次(NN 槽位回绕)在工作流中手动调高下次运行的 NN 槽位,或等到下一个 UTC 日

周期性检查清单

SOP 依赖的年度或周期性任务——请设置日历提醒:

  • Apple 发布证书:每年轮换(参见 .github/SECRETS.md 中的"Rotation runbook (yearly Apple cert refresh)")。
  • App Store Connect API key:每年轮换;遇人员变动应提前轮换。
  • TestFlight build 续期:每月——在 90 天过期前推一个新的内部 build,确保 beta 通道不熄火。
  • Apple Developer Program:$99 / 年自动续费。默认开启自动续费; 请确认绑定的信用卡未过期。
  • 隐私政策:每年通读一次。商店允许突击审计——声明的与实际的 数据采集行为产生漂移,是触发应用下架最常见的原因。