一个月前的线上崩溃需要排查,你却发现 Xcode Cloud 的构建产物已经无法下载。

最快解法:不要把 Xcode Cloud 当作长期制品仓库。截至 2026 年 8 月 30 日,Apple 官方说明构建信息与产物最多可访问 30 天;偶尔发版可以当天手动归档,持续发布的项目应在构建完成后用 App Store Connect API 自动下载,并在独立存储或常驻远程 Mac 上做恢复验收。(developer.apple.com)

这篇文章适合依赖 Xcode Cloud 发布 App、却从未单独保存构建产物的独立开发者;也适合需要长期保留符号信息、回溯崩溃报告的小团队。如果你希望集中检查和恢复 xcarchivexcresult,下面这条时间线可以直接照着执行。

先把 30 天窗口换成归档时间表

Xcode Cloud 的问题不在于它不能生成制品,而在于它的默认访问窗口不适合作为长期证据库。Apple 官方列出的构建产物包括详细构建日志、导出的 App Archive、App 二进制文件、Framework,以及测试结果包;这些信息和产物从构建完成时起最多可访问 30 天。(developer.apple.com)

因此,你不需要把每一次普通分支构建都永久保存,而应先按发布价值分类:

产物类别 建议处理 主要用途
正式发布版本的 xcarchive 必须外部归档 重新检查签名、版本、构建环境和崩溃关联
dSYM 与其它符号信息 必须与对应构建绑定保存 将崩溃报告中的地址转换为函数名和行号
构建日志 正式发布建议保存 复盘签名、依赖、脚本和上传问题
xcresult 测试结果包 按项目风险保存 查看测试失败、截图、覆盖率和测试日志
普通分支构建 按需保存 失败复现、代码审查或短期回归验证

Apple 明确要求你保留每个已分发 App 的 Xcode Archive,因为缺少对应 Archive,后续可能无法利用崩溃报告完成诊断。发布版的符号文件也不能只按文件名保存:每个二进制文件可能对应独立的 dSYM,二进制与符号文件通过 Build UUID 关联。(developer.apple.com)

⚠️ “构建已经上传到 App Store Connect”不等于“你的历史归档已经安全保存”。App Store Connect 中的构建记录、Xcode Cloud 工作流产物、你自行上传的附加文件,以及本地导出的 xcarchive,应当分别记录来源。

构建完成当天建立归档基线

第一次备份不要直接写一个“下载成功”脚本。先人工完成一份完整归档,确认你真正需要哪些文件,再把这份结果作为自动化的对照基线。

推荐使用这样的目录结构:

AppName/
  1.8.0/
    build-184/
      archive/
      symbols/
      test-results/
      logs/
      manifest.json
      checksums.txt

目录名应至少绑定以下信息:

  • App 名称或稳定的内部标识;
  • App 版本号;
  • Build Number;
  • Xcode Cloud 工作流名称;
  • 构建 ID;
  • 构建完成时间;
  • 构建结果状态;
  • 下载文件名、文件类型和校验值。

Xcode Cloud 的 API 模型不是“给我一个压缩包”这么简单。一个 Build Run 可以关联多个 Build Actions,而每个 Build Action 又可能关联 Artifacts、问题和测试结果。Apple 的文档将构建运行、构建动作和产物拆成不同资源,因此归档脚本也应沿这个关系定位文件,而不是只根据最新构建名称猜测下载对象。(developer.apple.com)

人工基线至少要完成以下动作:

  1. 在 Xcode 或 App Store Connect 中打开目标构建详情。
  2. 确认它属于正确的 App、工作流、分支或标签。
  3. 下载 Archive、符号信息、测试结果和构建日志。
  4. 解压 Archive,检查是否存在预期的 .xcarchive 结构。
  5. 记录 Build Number、构建 ID、文件名和文件大小。
  6. 为每个文件生成校验记录,并把清单与文件放在同一归档目录。

Apple 官方也说明,Artifacts 资源可提供文件类型、文件名、文件大小和下载地址;单个构建动作的 Artifacts 列表接口最多返回 200 个资源。这里的 200 是接口分页上限,不是你应该保存的文件数量,脚本仍应处理分页和空结果。(developer.apple.com)

第一周接入 App Store Connect API

当你已经完成一份人工基线,第二阶段才是让 App Store Connect API 接管重复下载。API 使用 JSON Web Token 进行授权,相关密钥应在 App Store Connect 的团队账户中创建和管理。(developer.apple.com)

自动化流程可以拆成下面这条链:

读取成功构建
→ 定位 Build Run
→ 获取 Actions
→ 筛选 Archive / 发布相关 Action
→ 获取 Artifacts
→ 下载文件
→ 写入 manifest
→ 生成校验记录
→ 标记成功或告警

脚本中的变量必须使用占位符,不要把真实凭据提交到仓库:

ASC_KEY_ID=<你的_API_Key_ID>
ASC_ISSUER_ID=<你的_Issuer_ID>
ASC_PRIVATE_KEY_PATH=<本地私钥路径>
APP_ID=<你的_App_ID>
WORKFLOW_ID=<你的_Workflow_ID>

这几类权限不要混在一起:

凭据或权限 用途 不应承担的职责
App Store Connect API 凭据 查询构建、动作、产物和测试结果 不能替代代码签名资产
代码签名证书与私钥 签名和发布 不应放入普通制品归档目录
Provisioning Profile 指定签名与能力配置 不等于完整发布归档
远程 Mac 登录权限 执行解压、读取、符号化和恢复脚本 不应直接暴露 API 私钥

查询层面,先读取 Build Run 的状态、构建编号、工作流和完成时间,再读取关联的 Actions。只有当构建结果成功,并且 Action 类型符合你的发布规则时,才进入 Artifacts 下载阶段。这样可以避免把失败构建、Pull Request 构建或普通测试构建误归档为正式版本。

下载完成后,脚本必须把执行结果写入状态文件,例如:

{
  "buildId": "<BUILD_ID>",
  "workflow": "<WORKFLOW_NAME>",
  "status": "success",
  "artifacts": [
    {
      "fileName": "<FILE_NAME>",
      "fileType": "<FILE_TYPE>",
      "downloaded": true,
      "checksum": "<CHECKSUM>"
    }
  ]
}

如果同一个构建再次触发任务,脚本应先读取 buildId 和文件校验记录。已成功归档的对象直接跳过,只有下载中断、校验失败或清单不完整时才重试。这就是幂等;没有幂等控制,定时任务很容易生成大量重复文件。

构建事件与定时扫描的取舍

Xcode Cloud webhook 可以在构建创建、开始和完成时向 HTTPS 端点发送请求;请求中包含 App、工作流、构建和代码仓库等信息。Webhook 的作用是通知外部任务,不是替你保存制品,因此收到事件后仍要调用 App Store Connect API 查询 Artifacts 并执行下载。(developer.apple.com)

你可以按发布频率选择触发方式:

  • 低频发版:使用定时任务扫描最近完成的构建,部署简单,适合每周或每月才发布一次的独立项目。
  • 高频发版:使用 BUILD_COMPLETED 事件触发归档,减少等待窗口。
  • 关键发布流程:事件触发后仍保留补偿扫描,避免网络故障、接口超时或任务进程退出造成漏归档。

Webhook 处理器要先验证请求来源和签名,再从事件中提取构建 ID。Apple 的 webhook 文档指出,如果服务端返回可重试的错误,或在 30 秒 内没有返回响应,Xcode Cloud 会重新发送请求。你的处理器不应在 HTTP 请求内同步执行大文件下载,更稳妥的做法是快速返回成功,将构建 ID 放入后台队列,再由归档任务完成下载。(developer.apple.com)

事件处理还要记录三种状态:

received:已收到事件
processing:已开始查询和下载
archived:文件已下载并通过验收

如果下载失败,状态应明确写成 failed,并带上错误原因、重试次数和下一次扫描时间。不要只记录“任务运行过”,因为任务运行成功不代表制品已经完整落盘。

FAQ:保留窗口、下载与恢复

见元数据中的 FAQ。下面再补充一个容易被忽略的判断:App Store Connect 中的构建存在,不代表你仍然拥有可用于诊断的完整 Archive 和符号信息。正式版本的恢复目标应是“可以重新定位、读取、分析”,而不是“网页上还能看到一个构建编号”。

发版验收不止检查下载成功

正式发布时,建议把验收拆成“文件层、版本层、分析层”三组。Apple 的 Artifacts 文档将 App Archive、测试结果包和构建日志都视为构建动作产生的文件输出;测试结果包还可以包含会话结果、代码覆盖率和其它日志。(developer.apple.com)

文件层

  • 压缩包可以完整下载并解压;
  • Archive 目录结构没有损坏;
  • dSYM 文件没有被清理或重命名到无法追踪;
  • xcresult 能被 Xcode 打开;
  • 构建日志文件可以读取;
  • 校验值与 checksums.txt 一致。

版本层

  • App 名称或 Bundle Identifier 对应正确项目;
  • Version 与 Build Number 对应 App Store Connect 中的目标构建;
  • 工作流、分支或标签信息能够回溯;
  • 构建状态确实是成功,而不是仅仅完成了上传;
  • 发布产物和测试产物没有被不同 Build Number 混放。

分析层

  • 用归档中的符号信息检查一次脱敏崩溃报告;
  • 在 Xcode 中打开 xcresult,确认测试会话和失败信息可读取;
  • 检查 Archive 中的应用、扩展和 Framework 是否都有对应符号;
  • 验证旧产物能够按 App、版本和 Build Number 被重新定位。

Apple 的测试文档说明,使用 xcodebuild 执行测试时会生成 .xcresult 测试结果包,其中可以包含会话结果、代码覆盖率和其它日志;这个文件可以在 Xcode 中打开或分享。(developer.apple.com)

✅ 恢复验收的最低标准不是“文件存在”,而是“你能用它解释一次历史失败”。如果 Archive 能解压,却找不到对应 Build Number 或符号 UUID,这份备份仍然不合格。

长期运行的清理与恢复规则

不同类型的构建不应使用一套固定保存期限。你可以根据发布风险制定内部规则:

  • 内部测试构建:优先保留最近一段开发周期内仍可能复现问题的版本;
  • 候选发布构建:保留到正式版本稳定、审核和上线风险关闭;
  • 正式发布构建:至少保留到项目维护和崩溃排查不再需要它;
  • 失败构建:如果包含关键回归证据,单独标记,不要被普通清理规则误删。

存储职责也应分层:

  • 本地磁盘:适合短期缓存和快速下载,不适合作为唯一副本;
  • 对象存储或备份盘:适合保存压缩包、清单、校验记录和日志;
  • 常驻远程 Mac:适合定期打开 Xcode Archive、读取 xcresult、运行符号化脚本和执行恢复演练。

如果你需要一个可持续运行的 Mac 环境来检查历史制品,可以先了解 VMSPIN 的远程 Mac 使用方式,再判断是采用短期租赁节点,还是为持续发布任务保留常驻环境。对于只是偶尔发版的项目,手动下载加独立存储通常已经足够;对于频繁发布、需要自动符号化或多人共享恢复环境的团队,常驻远程 Mac 更容易把检查脚本固定下来。

每月一次的恢复抽查

  • [ ] 随机选择一个历史正式 Build Number;
  • [ ] 从独立存储下载对应归档;
  • [ ] 校验 checksums.txt
  • [ ] 解压并读取 xcarchive
  • [ ] 检查 App、扩展和 Framework 的符号文件;
  • [ ] 用脱敏崩溃报告执行一次符号化;
  • [ ] 打开对应 xcresult,确认测试结果可读;
  • [ ] 把恢复结果、耗时和失败原因写入审计记录;
  • [ ] 检查自动任务最近一次运行状态,而不是只看文件数量。

如果归档任务还需要定期用 Xcode 打开历史产物、执行符号化或持续运行下载脚本,你可以进一步查看 VMSPIN 的 Mac 租赁方案,再根据发布频率选择短期环境或长期节点。

当前方案与 Mac 归档方案的最后判断

只依赖 Xcode Cloud 本身,优点是接入快、无需维护主机;但它有三个现实缺点:构建信息和产物访问窗口只有官方确认的 30 天,Webhook 只负责通知而不是存储,恢复操作还依赖你提前保留 Archive 和符号信息。只把构建编号留在 App Store Connect,也无法替代一份经过校验、能够被 Xcode 重新读取的外部归档。

如果你只是偶尔发布,构建完成当天手动下载即可;如果你持续发版,App Store Connect API 加独立存储是更稳妥的基础方案;如果还要长期执行 Xcode 读取、符号化、测试结果检查和恢复脚本,租赁 VMSPIN 的真实 Mac 环境会比临时找一台个人电脑更适合形成稳定流程。