开发者本地可以拉取私有包,CI 却在同一个提交上失败:先不要删除缓存,也不要把共享密钥复制到构建节点。
最快修复路径是三步:提交并校验 Package.resolved,在真实 CI 服务账号下重建 SSH 上下文,再按信任域拆分只读依赖凭证与生产签名资产。 私有依赖解析节点和正式签名节点默认分离;只有账号、凭证、工作区都完成隔离后,多个项目才适合共享远程 Mac。
这篇文章适合三类人:维护私有 Swift Package 的 iOS / macOS 应用负责人,管理自托管 Mac 构建节点的平台工程团队,以及需要审查仓库凭证、签名资产和远程 Mac 隔离能力的安全与 IT 负责人。
SECTION 01 本周修复时间线
你可以把本周的处理拆成 4 个里程碑,而不是按照“先清缓存、再反复重跑”的经验排查。
- 第 1 个工作日:确认故障阶段。 分别保存依赖解析、Git 连接、二进制下载和编译日志,记录复现命令、执行账号、失败端点与退出状态。
- 第 2 个工作日:固定依赖图。 核查
Package.resolved的位置、版本控制状态和代码评审记录,并用同一提交执行最小解析任务。 - 第 3 个工作日:恢复服务账号上下文。 检查
HOME、known_hosts、SSH 配置、密钥权限、ssh-agent、代理和 URL 映射,不要借用管理员终端的成功结果。 - 第 4 个工作日:完成隔离验收。 在干净节点上执行首次解析、最小编译、重启后复测和换机复测;如果共享节点无法保留边界,就拆分依赖解析池与生产签名池。
Apple 的 CI 文档明确说明,Xcode 会把依赖的确切版本写入 Package.resolved;直接使用 xcodebuild 时,可以通过 -disableAutomaticPackageResolution 让 CI 按锁定文件执行,而不是在构建过程中重新计算依赖。(developer.apple.com)
你还可以对照 Apple 关于 Package.Dependency 的官方说明,确认顶层应用与 Swift Package 的 Package.resolved 作用边界。顶层项目可以利用该文件协调依赖版本,但一个作为其他项目依赖的库,其自身锁定文件并不会替调用方固定完整依赖图。(developer.apple.com)
SECTION 02 故障边界与责任交接
技术负责人:先分清解析失败还是认证失败
“本地成功、CI 失败”不是网络故障的充分证据。至少存在四个不同边界:
- 依赖解析阶段:
Package.swift的版本约束、分支、提交或传递依赖无法形成一致的依赖图。 - Git 连接阶段: URL、SSH 主机校验、密钥、仓库授权或服务账号不匹配。
- 二进制下载阶段: 二进制 Target 或私有包注册表需要另一套凭证,源码仓库凭证并不能自动覆盖。
- 编译阶段: 依赖已经拉取成功,但工具链、架构、构建设置或源代码本身失败。
Swift Package Manager 官方文档指出,依赖解析会综合多个包的版本要求;如果没有可用的 Package.resolved,不同用户或构建系统可能分别解析出不同版本。(github.com)
因此,第一份故障记录不要只保存最后一行错误。你至少要记录:
- 运行的提交哈希与
Package.resolved校验值; - 实际执行用户、
HOME和工作区路径; - 失败的具体仓库或下载端点;
- 使用的 Git URL 形式,是 SSH、HTTPS 还是内部映射;
- 进程退出状态,以及失败发生在解析、连接、下载还是编译阶段。
交接条件: 技术负责人只有在能够证明“依赖版本已锁定、失败端点已定位、实际运行账号已确认”后,才应把问题交给组件负责人或 CI 平台团队。否则,后续每次重跑都可能改变故障条件。
应用团队:把可重复构建入口固定下来
应用团队要确认 Package.resolved 位于正确的项目或工作区路径,并且进入版本控制。对于 Xcode 项目,Apple 文档给出的典型位置是:
App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
如果团队使用工作区,应以实际工程目录和 CI 检出结果为准,不要只在开发者电脑上搜索同名文件。最常见的错误包括:文件在本地生成却没有提交,文件被忽略规则排除,或者项目结构调整后 CI 使用了另一份工作区。
生产构建和依赖升级也要分开。升级流程可以主动执行解析并评审 Package.resolved 的变化;生产 CI 则应使用已审查的锁定结果。Swift Package Manager 官方文档说明,swift package resolve --force-resolved-versions 可要求解析过程对齐 Package.resolved 中记录的版本。(github.com)
最小验证可以保持足够简单:
git status --short
test -f App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
xcodebuild -resolvePackageDependencies \
-project App.xcodeproj \
-scheme App \
-disableAutomaticPackageResolution
如果项目入口是工作区,就替换为 -workspace App.xcworkspace。命令本身不是重点,重点是使用与正式构建相同的提交、账号和工作区,证明开发者本机残留状态没有参与结果。
私有组件负责人:盘点完整依赖图
只检查顶层私有仓库是不够的。你需要从 Package.resolved、Package.swift、二进制 Target 和传递依赖中盘点全部访问端点。
重点检查四类变化:
- 仓库重命名后,锁定文件仍记录旧 URL;
- URL 重写或镜像配置只存在于开发者电脑;
- 顶层包使用一套主机名,传递依赖使用另一套主机名;
- 源码依赖已经授权,但二进制下载端点仍需要独立访问凭证。
Swift Package Manager 的依赖文档说明,远程依赖的地址和版本要求共同参与解析,版本可以是范围、具体版本、分支或提交。(github.com) 这意味着仓库地址变更不能只改一个配置文件;你还要确认锁定状态、镜像规则和所有传递依赖是否一致。
对于源码仓库,企业应统一批准的访问方式。若决定使用 SSH,就不要让部分项目继续使用 HTTPS、另一部分项目使用短主机别名,除非这些差异已经经过平台团队验证并纳入交付配置。
授权边界建议:
- 每个信任域使用独立的只读身份;
- 只授权构建任务实际需要的仓库;
- 禁止依赖解析账号拥有写入、删除或管理权限;
- 记录密钥创建、轮换、撤销和最后使用时间;
- 仓库搬迁后,先更新映射与授权,再修改项目依赖地址。
SECTION 03 CI 服务账号与 SSH 上下文
CI 平台团队:重建真实运行环境
Apple 官方 CI 指南明确指出,直接使用 xcodebuild 时,可以通过 SSH URL 访问私有包,并在执行 CI 任务的 macOS 用户 ~/.ssh 目录中配置 known_hosts;如果需要使用系统 Git 配置、URL 重映射或高级 SSH 配置,应显式传入 -scmProvider system。(developer.apple.com)
最小复现应在真实服务账号下运行,而不是在管理员 Terminal 中运行:
whoami
printf 'HOME=%s\n' "$HOME"
ssh -G git.example.internal | sed -n '1,20p'
ssh -T git@git.example.internal
xcodebuild -resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-disableAutomaticPackageResolution \
-scmProvider system
上面的主机名只是占位符,实际使用时替换为你们批准的源码主机。不要把 ssh -T 的成功当成完整证明,它只能说明当前身份能够完成某种 SSH 握手;你仍需验证目标仓库、只读权限、传递依赖和二进制下载端点。
检查清单应包括:
HOME是否指向服务账号自己的目录;~/.ssh/config是否被实际任务读取;known_hosts是否包含目标主机的可信记录;- 私钥权限是否仅允许服务账号访问;
- 密码保护的私钥是否在任务开始时进入
ssh-agent; - 代理、端口和 URL 映射是否只存在于交互式环境;
xcodebuild是否需要-scmProvider system才能使用系统 Git 配置。
如果 SSH 密钥受密码保护,Apple 文档建议在调用 xcodebuild 前配置 SSH agent。不要把私钥密码写入脚本、项目变量或长期保留的构建日志。
缓存处理:证据优先于清理
缓存能够掩盖身份错误,也能够制造“某台节点正常、另一台节点失败”的假象。你应先在现有节点保留失败日志,再用同一提交和干净工作区执行最小解析。
只有出现以下证据时,才进入定向清理:
- 缓存记录的提交与
Package.resolved不一致; - 工作区中残留了已删除或已重命名的依赖;
- 干净账号可以成功,原服务账号只能读取旧内容;
- 节点重启后结果变化,说明环境依赖没有交付完整;
- 换到新节点后稳定复现同一认证错误,说明问题不在缓存。
Swift Package Manager 的官方注册表文档还区分了注册表凭证与 Git 凭证:注册表访问可使用 Keychain、.netrc 或环境变量,但这并不改变 git clone 和 git fetch 使用自身凭证系统的事实。(github.com) 因此,清理注册表缓存不能修复 SSH 仓库授权错误。
如果团队同时使用源码依赖、二进制下载和包注册表,必须分别记录它们的认证方式。官方注册表规范也说明,依赖图可能包含传递依赖,服务端还可能返回未授权、找不到或限流等不同状态;不要把所有非成功响应都归类成“网络不稳定”。(github.com)
提醒: 不要把“删除全部缓存后构建成功”视为修复完成。你还需要在干净账号、重启后的节点和另一台节点上重复最小解析,否则无法判断是身份修复,还是临时绕过了残留状态。
SECTION 04 安全隔离与节点决策
安全发布团队:依赖凭证和签名资产分层
私有源码读取凭证不能与 Apple 签名私钥、发布 API 凭证或共享管理员账号放在同一长期环境中。原因不是形式上的合规要求,而是权限用途完全不同:
- 私有依赖凭证需要读取代码;
- 签名私钥可以影响产物身份;
- 发布凭证可能触发外部交付动作;
- 管理员账号还可能修改节点和凭证配置。
非可信分支、外部贡献代码和普通拉取请求,不应获得生产私有依赖与签名节点的完整权限。更稳妥的边界是:
- 私有依赖解析使用独立服务账号;
- 依赖仓库授权限定为只读;
- 正式签名使用独立节点、独立临时 Keychain 和审批后的工作区;
- 任务结束后删除临时凭证、清理工作区和构建日志中的敏感内容;
- 轮换、撤销和异常使用都保留审计记录。
如果你需要补齐远程节点的账号、SSH 和数据处理边界,可先查看 VPSNIX 的隐私政策 与 帮助中心,再把企业自身的凭证注入和清理要求写成交付验收项,而不是只依赖服务商默认配置。
基础设施负责人:干净节点验收
干净节点验收不是“换一台 Mac 再跑一次”这么简单。你要验证方案是否依赖某台长期节点的残留状态,至少保留以下证据:
- 新节点交付后,确认系统版本、工具链、工作区和服务账号;
- 在没有历史包缓存的条件下执行依赖解析;
- 使用同一提交完成最小编译;
- 节点重启后重复解析和编译;
- 销毁或替换节点后,用新节点再次执行;
- 对比每次运行的依赖版本、失败端点、账号和退出状态。
如果现有共享节点能够做到项目级工作区、服务账号、只读凭证和任务后清理,可以继续修复并保留共享模式。若无法做到其中任何一项,就应回退到隔离节点。
决策条件:
- 若满足账号隔离、凭证只读、工作区隔离、重启可恢复, 可继续使用共享远程 Mac 承担私有依赖解析。
- 若依赖解析与生产签名使用同一账号或同一长期 Keychain, 应立即拆分资源池,不要继续扩大共享节点权限。
- 若干净节点失败、旧节点成功, 优先修复节点交付和凭证注入流程,而不是继续增加缓存。
- 若同一私有依赖需要多个信任域访问, 为每个信任域配置独立身份,并建立轮换与撤销记录。
- 若项目需要物理接口、长期满负载或严格固定硬件, 应评估自购 Mac;若只是临时 CI 容量、隔离复测或弹性构建,则远程 Mac 更适合按需扩容。
当前方案最大的隐性缺点,通常不是“能不能跑”,而是共享 Mac 上的管理员环境、长期缓存和混用凭证会让问题难以复现;同时,采购实体设备还会把硬件折旧、交付、替换和闲置容量一起纳入 TCO。完成服务账号与 Package.resolved 修复后,你可以用 VPSNIX 的远程 Mac 做一次干净环境复测;如果现有节点无法做到凭证、账号和工作区分离,再通过 VPSNIX 的企业 Mac 方案入口评估隔离节点,而不是继续给共享节点叠加权限。
SECTION 05 FAQ:企业 CI 私有依赖故障
本地能拉取,为什么 CI 失败?
因为本地用户和 CI 服务账号通常拥有不同的 HOME、SSH 配置、known_hosts、密钥代理、Git URL 映射和仓库授权。你必须在实际执行 xcodebuild 的账号下复现,并分别记录依赖解析、Git 连接、二进制下载和编译阶段的证据。
生产构建是否必须提交 Package.resolved?
对于作为顶层项目构建的应用,生产 CI 通常应提交并评审 Package.resolved。依赖升级可以使用单独工作流主动解析;正式构建则应关闭非预期的自动解析,并验证锁定文件位于实际使用的项目或工作区路径。
怎样让 xcodebuild 使用 SSH Key?
统一私有包的 SSH URL,在实际 CI 用户的 ~/.ssh 下配置 known_hosts 和 SSH 配置,必要时在任务开始前启动 ssh-agent。如果依赖系统 Git 的代理、URL 重写或高级 SSH 设置,再显式传入 -scmProvider system,不要假设管理员环境会被继承。
多项目共享 Mac 怎样隔离凭证?
至少按信任域拆分服务账号、只读密钥和工作区,禁止多个项目共享管理员身份和长期生产密钥。任务完成后清理临时凭证;生产签名还应使用独立节点和独立临时 Keychain,避免私有源码访问权限扩大为产物签名权限。
Swift Package Manager 缓存要不要每次清理?
不建议每次清理。先保存锁定文件、失败端点、执行账号和退出状态,再用干净账号或干净节点复现;只有确认缓存与提交不一致,或缓存残留改变结果时,才进行定向清理。全量清理应作为验收步骤,而不是默认故障处理方法。