在混合技术栈的公司里演示 Xcode Cloud,第一个问题永远一样:"我们后端是 Kotlin,有 Android 团队,一切都跑在 GitHub Actions 上。这些也要搬过去吗?"简短回答:不用。完整回答就是这篇文章:Xcode Cloud 不是用来取代公共流水线的,但只要掌握三座桥 —— 入口的 App Store Connect API、出口的 webhook、按需获取的工件 —— 它可以作为单一角色的执行者,很好地嵌进去。
服务本身的基础 —— workflow、价格、优缺点 —— 在系列第一篇。这里只讲集成。
边界:它能构建什么,不能构建什么#
Xcode Cloud 的环境是 Apple silicon 上的 macOS。没有 Linux,没有 Docker,不能自带运行器。Android 应用在那里构建不了 —— 不是因为被禁止,而是因为环境根本不存在:没有 Android SDK,也没有可以把它塞进去的容器。后端就更不用说了。
于是典型公司的布局是这样的:后端和 Android 留在原地 —— GitHub Actions、GitLab CI、Jenkins。Xcode Cloud 接管 iOS/macOS 部分:构建、测试、签名、TestFlight。任务就简化成一件事:让两个系统互相看见。
入口:从外部触发构建#
App Store Connect API 可以完整驱动 Xcode Cloud:读取产品和 workflow、查看结果,以及最关键的 —— 触发构建:POST /v1/ciBuildRuns。
真实场景:后端刚把新版 API 契约部署到 staging,你希望 iOS 应用立刻对这个 staging 跑一遍集成测试。触发脚本就是普通的 Python,依赖只有 pyjwt 和 requests:
"""trigger_xcode_cloud.py — start a workflow via the App Store Connect API."""
import os
import time
import jwt
import requests
ISSUER_ID = os.environ["ASC_ISSUER_ID"] # App Store Connect → Users and Access → Integrations
KEY_ID = os.environ["ASC_KEY_ID"]
PRIVATE_KEY = os.environ["ASC_PRIVATE_KEY"] # contents of AuthKey_XXXX.p8
WORKFLOW_ID = os.environ["XC_WORKFLOW_ID"] # GET /v1/ciProducts/{id}/workflows — once, by hand
token = jwt.encode(
{"iss": ISSUER_ID, "aud": "appstoreconnect-v1", "exp": int(time.time()) + 600},
PRIVATE_KEY,
algorithm="ES256",
headers={"kid": KEY_ID},
)
resp = requests.post(
"https://api.appstoreconnect.apple.com/v1/ciBuildRuns",
json={"data": {
"type": "ciBuildRuns",
"relationships": {
"workflow": {"data": {"type": "ciWorkflows", "id": WORKFLOW_ID}},
},
}},
headers={"Authorization": f"Bearer {token}"},
timeout=30,
)
resp.raise_for_status()
print("Started build run:", resp.json()["data"]["id"])令牌是有效期最长 20 分钟的 ES256 JWT;密钥在 App Store Connect 里签发一次即可,触发构建有 Developer 角色就够了。WORKFLOW_ID 最省事的办法是手动 GET 一次,然后固定进密钥配置。
在 GitHub Actions 里,这变成 staging 部署末尾的一个廉价 Linux job:
ios-contract-tests:
needs: deploy-staging
runs-on: ubuntu-latest # Linux at $0.008/min — we only poke an API
steps:
- uses: actions/checkout@v4
- run: pip install "pyjwt[crypto]" requests
- run: python ci/trigger_xcode_cloud.py
env:
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
XC_WORKFLOW_ID: ${{ secrets.XC_WORKFLOW_ID }}注意一点:Xcode Cloud 构建的是 git 仓库里的内容,不是你发给它的内容:构建从 workflow 所配置分支的 HEAD 开始。没 push 的东西不会被构建。Jenkins 有官方插件 xcode-cloud-for-pipeline,原理相同。
出口:webhook 回到流水线#
反方向由 webhook 负责:在 workflow 设置里填一个 URL,Xcode Cloud 会在 build run 创建和完成时发 POST —— 带着构建的 JSON 描述、状态和各种链接。Slack 集成是开箱即用的,所以如果只需要频道通知,webhook 服务器可以完全不写。
如果公共流水线确实要等 iOS 构建的结果(比如把后端、Android、iOS 按同一版本编成发布列车),有两个选项。第一个:用自己的端点接收 webhook,从那里继续流水线。苹果文档没有描述请求签名机制,所以端点应该放在保密路径上,并且对收到的构建 id 用反向 GET 到 API 去核实 —— 不要信任 webhook 的请求体。第二个:笨但可靠 —— POST /v1/ciBuildRuns 之后,每分钟轮询一次 GET /v1/ciBuildRuns/{id},直到状态变成 completed。相对于构建本身的时长,轮询不会破坏任何东西。
工件与状态#
在 GitHub 仓库内部,Xcode Cloud 会自己给 pull request 打状态,还可以设为合并的必要条件 —— 这部分零代码就能工作。
工件通过同一个 API 获取:build run 下面有 build actions,后者挂着带下载链接的 artifacts。也就是你的 .ipa、.xcresult 和日志。典型的夜间场景:定时 workflow → 完成 webhook → 你的 job 取走 .ipa 和 .xcresult,归档到 QA 和安全团队能看到的公司存档里。
Monorepo 与共享契约#
Workflow 的启动条件支持按文件和文件夹过滤:如果 iOS 应用住在 monorepo 的 ios/ 里,只有这个文件夹的变更才会触发构建 —— Android 团队的提交不会烧掉你的小时数。
与后端共享的代码 —— protobuf schema、OpenAPI 契约 —— 在 ci_scripts/ci_post_clone.sh 里生成:用 Homebrew 装好生成器,跑一遍,构建就带着最新的类型继续。环境变量可以在多个 workflow 之间共享,staging 地址不会靠复制粘贴扩散。
依然别扭的地方#
有三件事集成治不了。集成测试只能访问外部 staging —— 没法在构建旁边起一个带 mock 的 docker-compose。Workflow 配置住在 App Store Connect 而不是 git:可以通过 API 导出,但这里的"基础设施即代码"是你自己的脚本,不是平台功能。构建排期被摊在两个系统里,"什么时候构建了什么"再也无法只看一个仓库回答 —— 在 wiki 里建个页面吧,认真的。
决策清单#
"公共 CI + Xcode Cloud"的混合方案,在以下条件多数成立时才有意义:
- 产品是多平台的,但 iOS 团队真正的痛是签名和 Mac 构建机。
- 公共流水线已经在 GitHub Actions / GitLab / Jenkins 上,没人会让你推倒重来。
- 集成测试通过网络访问 staging 环境就够了。
- iOS 构建的密钥放在 App Store Connect 里可以接受。
- 构建量在每月 25–250 小时以内。
如果都对上了 —— 从小处开始:先给 iOS 文件夹配一个 PR 检查的 workflow,状态进 GitHub,通知进 Slack。API 和 webhook 的桥,等发布列车真的需要等 iOS 构建时再架 —— 而不是因为架构图上少了它们显得不完整。



