大多数浏览器和
Developer App 均支持流媒体播放。
-
拓展 Xcode Cloud 工作流程
了解 Xcode Cloud 如何适应你的开发需求。我们会介绍如何利用启动条件、自定别名、自定脚本、网页回调以及 App Store Connect API,来简化工作流程并实现测试和分发自动化。
章节
- 0:00 - Introduction
- 1:08 - Essential Workflow Concepts
- 3:00 - Scale your workflows
- 11:38 - Connect other systems
- 12:33 - App Store Connect API
- 16:35 - Webhooks
- 20:33 - Wrap up
资源
- Configuring start conditions
- Configuring webhooks in Xcode Cloud
- Environment variable reference
- Forum: Developer Tools & Services
- Sharing build configurations across Xcode Cloud workflows
- Writing custom build scripts
相关视频
WWDC23
WWDC21
-
下载
大家好 我是 Daniel 稍后我的同事 Colin 也会加入这个讲座 今天 我很高兴与大家探讨 Xcode Cloud 中一些强大 且可扩展的功能 这些功能可以 扩大和扩展你的工作流程 Xcode Cloud 是 Xcode 内置的 持续集成和交付服务 也可在 App Store Connect 中使用 它专为 Apple 开发者而设计 可加快高质量 App 的 开发和交付速度 它汇集了多款基于云的工具 可帮助你构建 App 并行运行多个自动化测试 还能向测试员交付 App 以及查看和管理用户反馈 从而帮助你构建更好的 App
在这个讲座中 我们将探讨 如何扩展 Xcode Cloud 工作流程 我们首先了解 一些基本的工作流程概念 接下来 我们将展示如何随着 App 的发展而扩大工作流程规模 最后 我们将扩展一个工作流程 以便与 Xcode Cloud 之外的系统配合使用
让我们先看看 一些基本的工作流程概念 所有 Apple Developer Program 会员资格 均享有每月 25 小时的构建时间 你将在这个讲座中 掌握必要的工具 无论你是第一次使用 还是想让 工作流程更加灵活 都会很有帮助
我们刚刚说过 Xcode Cloud 可以为你和你的 开发提供很多帮助 但这一切都始于一个简单的工作流程 工作流程由四个元素组成 即:环境、 启动条件、 构建操作、 以及发布操作
在环境中 需要为工作流程定义 环境变量 并决定 工作流程运行时 使用哪个 Xcode 和 macOS 版本 你可以选择一个特定的版本 也可以从一组有用的别名中进行选择 例如 Latest Release (最新版本)
启动条件定义 工作流程什么时候运行 你可以选择 对源代码控制事件作出响应 例如在分支、 拉取请求或 git 标签更新时 你还可以选择设置时间表 以便在特定的日期和时间 运行工作流程 或者 你也可以选择手动启动条件 这个条件只能手动运行
构建操作描述你希望 Xcode Cloud 对源代码执行的操作 工作流程可配置 一个或多个构建操作 你可以选择构建 App、
运行测试、 分析或归档 App 以便为分发做好准备 最后 发布操作定义 构建操作完成运行后 会发生什么 你可能需要在构建完成后 通过 Slack 通知你的团队 或者对已归档的 App 进行一些操作 如进行公证 或分发到 TestFlight 等
大多数情况下 只需一个简单的 工作流程就能完成大量工作 但工作流程很强大 可以随着 App 的发展而扩大规模 以满足你的需求 我们最近在一款正在开发的 App 中添加了一些功能 以便收集一项服务的数据 我们已有一个 Xcode Cloud 工作流程 当代码推送到主分支时 这个流程就会启动 它会构建我们的最新更改 然后运行我们的测试 最后通过 TestFlight 将更改 交付给我们的测试员
我们的测试套件由依赖于模拟数据的 单元测试和 UI 测试组成 这对于测试 App 拥有的 UI 和逻辑非常有用 但现在我们的 App 依赖于一项服务 因此我们添加了一些 与测试服务器通信的集成测试
集成测试是确保 App 与它的依赖项 实现端到端协同工作的有效方法 但对我们的 App 而言 运行集成测试 要比其他测试花费更多的时间和资源 而且可能会受到 App 无法控制的网络条件等问题的影响 我们希望能够配置一个工作流程 来运行我们的集成测试 但由于这些测试依赖于服务器 并会发出真实的网络请求 因此 最好能进一步控制 这个工作流程何时运行 作为 Xcode 15.1 的一项新功能 你可以使用手动启动条件 将工作流程配置为手动启动 我们来看看如何操作 首先 创建一个新的工作流程 以便只运行我的新测试计划
为了节省时间 我将复制现有的工作流程
在 Cloud 报告导航器中 辅助点按我的 App 并选择 Manage Workflows (管理工作流程) 即可查看我的工作流程
在 Manage Workflows (管理工作流程) 视图中 辅助点按现有工作流程 然后选择 Duplicate (复制) 操作 就会在工作流程编辑器中 打开一个新的工作流程 我把工作流程重命名为 Integration Tests 并将 Description (描述) 留空 接下来 我将添加一个手动启动条件 并移除现有的条件
现在 这个工作流程只有 在收到我的指示时才会启动 不会因任何其他事件而启动 我将接受默认选项 以便将启动条件 与任何分支关联起来 但我也可以指定一个分支、 PR 或 git 标签 以便更明确地指明这个启动条件 可与哪些 git 引用关联起来
我会更改测试操作来运行 我的 IntegrationTests 测试计划
现在 由于这个工作流程结束时 不应执行任何操作 因此我还要移除 发布操作 然后点按 Save (保存)
当然 你可以将工作流程配置为 使用 Environment (环境) 标签页中 指定的不同版本 Xcode 和 macOS 来运行 但对于这个示例而言 我们希望确保 两个工作流程始终 使用相同的版本运行
自定别名是 Xcode 15.3 中的新功能 Xcode Cloud 已提供一些别名 例如 大家可能已经看到的 Latest Release (最新版本) 这些别名可确保工作流程 在我们提供的最新版本上运行 现在 你可以为团队定义 自己的别名
当你在一个或多个 工作流程中使用别名时 这些工作流程将使用别名中 指定的 Xcode 或 macOS 版本运行 这意味着 更新别名时 使用这个别名的所有工作流程 都将使用更新后的值运行 让我们在刚刚创建的 新工作流程中使用这一功能
在导航器中 辅助点按某一特定 App 并选择 Manage Custom Aliases (管理自定别名) 就可以看到 这个 App 的所有自定别名
点按加号按钮 并创建一个新的 Xcode 别名 这样会弹出 自定别名编辑器 在这里 我可以对自定别名进行命名 并选择它要解析到的 Xcode 版本 在这个示例中 我将别名命名为 Team Preference 并选择 Xcode 15.3
点按 Save (保存) 然后对 macOS 别名 执行同样的操作
为了将别名应用到我的工作流程 我返回到 Manage Workflows (管理工作流程) 视图 并在 Environment (环境) 标签页中 打开 Integration Tests 工作流程
在 Xcode Version (Xcode 版本) 下拉菜单中 现在可以看到我的自定别名 选择这个别名 并对 macOS 执行同样的操作
你还可以从 这里的版本选择器下拉菜单 或从 Integrations (集成) 菜单项中 快速创建和管理 自定别名 最后 回到第一个工作流程 更新 Xcode 和 macOS 版本 以使用我的新别名
完美!现在 每当我在别名中更新到 一个新版本时 两个工作流程都会得到相应的更改
你可以在文档中进一步了解 如何使用自定别名
我们的集成测试在 Xcode Cloud 中 运行良好 无需任何更改 但我们可以让这些测试更智能一点 让它们只在测试服务器在线时才运行 这时 我们需要利用 Xcode Cloud 的另一项强大功能 即在项目中添加一个自定脚本
你可以在存储库中定义自定脚本 这些脚本将在 构建过程中的特定时刻运行
例如在存储库进行克隆之后、 在 xcodebuild 运行之前 或在 xcodebuild 运行之后
有关自定脚本的更多信息 请观看 “自定义高级 Xcode Cloud 工作流程”
工作流程中定义的 所有环境变量 以及 Xcode Cloud 提供的环境变量 均可在这些脚本中使用 今天 我们将在自定脚本中 使用其中两个环境变量
要查看可以使用的环境变量 的完整列表 请访问文档中的“Environment variable reference”页面
Xcode Cloud 期望所有自定脚本 都位于项目根目录下 名为 ci_scripts 的文件夹中
脚本的文件名 决定脚本在构建过程中的 执行时间点
在这个情景中 我们希望脚本 在测试运行前执行
在 Xcode 中 我会在项目导航器中 辅助点按我的项目 来创建脚本文件夹
我在文件夹中添加一个新的脚本文件 并将它命名为 ci_pre_xcodebuild.sh 以便在我们的测试之前运行
我只想对集成测试执行操作 因此我将添加一些逻辑 来检查构建操作 和 workflowID 的环境变量
在这里 脚本会检查我们是否 正在运行测试构建操作 以及我们的工作流程 是否与标识符相匹配 要获取工作流程的 ID 我只需导航到 Cloud 报告导航器 辅助点按工作流程并选择 Copy Workflow ID (复制工作流程 ID)
现在获得 ID 后 就要回到脚本并将 ID 粘贴进去
在这个代码块中 使用 curl 来调用 服务器的运行状况检查端点 并在失败时打印详细的错误日志
指定 set -e shell 选项后 如果遇到错误 这个脚本将立即退出 工作流程将就此停止 这正是服务器无法访问时 我们所希望的
请注意 由于 Xcode Cloud 构建 在临时任务工作器上运行 因此主机使用的 IP 地址范围 会有所不同 我已经在服务器防火墙的 入站允许列表中 添加所需的 IP 地址范围 从而确保 Xcode Cloud 可以 与我的服务器进行通信
要了解关于允许 哪个 IP 地址的具体信息 请参考“Requirements for using Xcode Cloud”文档页面 我们刚刚使用手动启动条件 为集成测试构建了一个新的工作流程 并使用自定别名确保 工作流程的 macOS 版本和 Xcode 版本保持同步 我们还利用自定脚本的强大功能 使测试更加智能 并在测试服务器 无法访问的情况下提前失败 为了探索 Xcode Cloud 与其他系统连接的方式 并进一步推进工作流程 我将有请 Colin 来为大家讲解
谢谢 Daniel 现在我们已经了解了 工作流程以及它们扩大规模的方式 下面我们将以前面完成的工作为基础 看看如何与 Xcode Cloud 之外的 系统进行连接
正如 Daniel 刚才提到的 我们的 App 现在依赖于一项服务 这种新的依赖性带来了额外的风险 因为有人可能会向服务器 推送代码更改 从而破坏我们的 App
我们可以通过运行 新的 Integration Tests 工作流程 来降低这种风险 这将生成测试结果 告诉我们 App 是否与最新的服务器更改兼容 现在 这个工作流程只能手动运行 但我们可以使用 App Store Connect API 在测试服务器有新更改时 自动启动构建
通过 App Store Connect API 可自动执行各种 App Store Connect 任务 包括与 Xcode Cloud 相关的任务 你可以在文档中进一步了解这些端点 以及它们的调用方式
让我们回过头来看看使用 App Store Connect API 运行我们的 Integration Tests 工作流程需要什么
通过 CiBuildRuns 端点 我们可以创建新的 Xcode Cloud 构建
调用这个端点时 我们必须指定要运行的 工作流程的标识符 和要构建的 gitReference 我们知道 Integration Tests 工作流程的 ID 但不知道要构建的分支的 gitReference 我们可以使用 ScmRepositories 端点 进行查找 我们可以使用 repositoryID 调用这个端点 以获取与这个存储库相关的 所有分支、 标签和拉取请求 但我们首先需要知道 工作流程正在使用的 repositoryID 这是 CiWorkflows 资源的一部分 可使用 workflowID 进行查询
我们的脚本总共要调用三次 API 才能创建我们想要的构建
App Store Connect API 是使用 OpenAPI 指定的 因此我们可以使用代码生成器 为每个端点创建强类型的 Swift 代码 在这个演示中 我们将专注于实现 但你可以在讲座 “认识 Swift OpenAPI Generator”中 进一步了解我使用的开源代码生成器 由于我们需要调用三次 API 来创建我们的构建 为了方便起见 我将在生成的 API 客户端上 编写一些扩展
我们需要使用一个函数 将 workflowID 作为参数 获取与它关联的 CiWorkflows 资源 然后返回 repositoryID
接下来 我们需要通过一种方法 将 repositoryID 转换成我们要构建的 gitReferenceID 这时 我们可以使用 SCMRepositories 端点 获取与我们的存储库相关联的 所有 gitReference 然后返回具有指定名称的分支的 gitReferenceID
最后 我们要开始新的构建 这时 我们需要 workflowID 和 gitReferenceID 并将它们传递给 ciBuildRuns 端点
现在 我们总结一下 首先 我们用 Integration Tests 工作流程的 workflowID 调用 repoID 函数
接下来 用我们要构建的 分支的名称来调用 branchID 在这个示例中 我将使用 main
最后 我们将使用 workflowID 和 gitReferenceID 来调用 startBuild 以完成我们的脚本
请注意 由 App Store Connect API 启动的构建被视为手动构建 因此应确保手动启动条件的设置 与要构建的引用相匹配 在我们的示例中 Daniel 已经用手动启动条件 设置了这个工作流程 并且手动启动条件可以匹配任何分支 因此我们不会遇到问题
脚本完成后 我们就可以在推送 新服务器代码时生成测试结果了 我们可以就此打住 但我们可能希望 对测试结果做些什么
我们希望将目前所做的工作 与生产环境连接起来 这样服务器的更改在测试环境中 得到验证后就可以部署了
我们可以手动检查测试结果 并根据需要手动部署到生产环境 但我们可以做得更好
因此 我们可以利用 Xcode Cloud 的 Web 挂钩功能 将测试结果连接到部署服务 然后在服务器更改通过后 自动推送到生产环境中
Web 挂钩允许服务 在构建事件发生时做出响应 因此你可以将 Xcode Cloud 集成到开发流程依赖的 其他服务和工具中 大家可以在文档中详细了解 Web 挂钩以及如何对它们进行配置
你只需要一个 HTTP 服务器 即可侦听和响应 Web 挂钩 使用 Vapor 或 Hummingbird 框架 你只需几步 就能通过 Swift on Server 建立一个简单的项目 在这个演示中 我希望重点关注实现 因此我设置了一个 Vapor 服务器 它包含一个可以接收 Web 挂钩的端点 配置 Web 挂钩时 Xcode Cloud 将向端点发送请求 其中包含 JSON 有效负载以及 有关不同构建事件的详细信息
在这里 我定义了一个 WebhookPayload 结构 其中包含 我们感兴趣的字段 对于我们的方案 我们需要 workflowID、buildID、 构建执行进度和构建完成状态
我将使用这个结构来解码 Web 挂钩请求 这样就可以轻松地 从有效负载中提取字段
接下来 我将添加一些逻辑 用于将有效负载中的 workflowID 与 Integration Tests 工作流程的 workflowID 进行比较
有两个类似的字段 可描述构建事件的状态 ExecutionProgress 描述 构建是正在运行还是已经完成 而 completionStatus 描述 构建是成功还是失败 我将更新 if 语句 以对两者进行检查 从而确保构建成功完成
在这个语句的正文中 我将告知部署服务 Integration Tests 已通过 并发送 buildID 我们的部署服务可以使用 这些信息以及其他检查 来批准服务器更改 并部署到生产环境中
我们需要做的最后一件事 是告诉 Xcode Cloud 向 Web 挂钩发送构建事件 我们可以在 App Store Connect 的 Xcode Cloud 视图中完成这个操作
选中我的 App 后 前往 Settings (设置) 然后点按 Webhooks (Web 挂钩) 标签页
点按加号按钮 就可以 配置新的 Web 挂钩
进行命名并添加侦听器的 URL
Web 挂钩设置完成后 我们就万事俱备 可通过 Xcode Cloud 连接服务器更改了 让我们回顾一下 到目前为止所做的工作
每当有新的代码更改 部署到服务的测试环境时 我们都会调用 App Store Connect API 来启动 Integration Tests 工作流程的构建 这将运行测试来验证 App 与测试服务器之间的集成
我们的 Web 挂钩侦听器会处理 测试结果 如果所有测试都通过 就会将更改部署到生产环境中
在所有测试都通过的情况下 我们的管道就实现了从代码推送 到生产部署的完全自动化
但是 如果推送的服务器更改 导致 App 出现问题
集成测试就会失败 Web 挂钩侦听器会阻止 将更改部署到生产环境中 这正是我们想要的结果
我们讨论的是服务器代码更改 但推送更改的团队成员可能 甚至没有在他们的机器上安装 Xcode 好消息是 由于 Xcode Cloud 已集成 到 App Store Connect 中 他们仍然可以 通过浏览器查看和调查问题
这是来自 App Store Connect 的 构建结果 其中运行情况与预期不符 从 Actions (操作) 部分可以看出 我们的 IntegrationTests 操作 明显出现问题
点按 Tests (测试) 部分 将打开测试报告
即使我在 App Store Connect 中 我也可以看到并 完成 Xcode 中的所有操作 我可以按测试状态进行筛选 按测试套件进行搜索 还能查看每个测试的详细信息 在这个示例中 我可以看到服务器 返回的数据有问题
如果我需要进一步调查 我可以点按边栏中的 Artifacts (工件) 以下载与构建相关的各种文件 如 Xcode 日志和崩溃报告 我们在这个讲座中介绍了很多内容 但这些只是 Xcode Cloud 工作流程 众多扩展方式中的少数几个
我们使用手动启动条件、 自定别名和自定脚本 来扩大我们团队的工作流程 并使用 App Store Connect API 和 Web 挂钩将它们集成到其他系统中
请观看我们的其他讲座 例如 “在 Xcode Cloud 中创建实用的工作流程” 和“认识 Xcode Cloud” 了解 Xcode Cloud 如何增强团队的 CI 流程
我们希望大家能从中获得一些启发 以便将这些概念应用到 你们自己的工作流程中 感谢观看
-
-
10:02 - Custom Script
#!/bin/sh set -e if [[ $CI_XCODEBUILD_ACTION == "test-without-building" && $CI_WORKFLOW_ID == "82D89C93-B69C-46B5-A794-A2BCFD3EE487" ]] then curl https://example.com/health --fail fi
-
14:01 - App Store Connect API - Client Extension
extension Client { func repoID(workflowID: String) async throws -> String { return try await ciWorkflowsGetInstance( path: .init(id: workflowID), query: .init(include: [.repository]) ).ok.body.json.data.relationships!.repository!.data!.id } func branchID(repoID: String, name: String) async throws -> String { return try await scmRepositoriesGitReferencesGetToManyRelated( path: .init(id: repoID) ) .ok.body.json.data .filter { $0.attributes!.kind == .BRANCH && $0.attributes!.name == name } .first!.id } func startBuild(workflowID: String, gitReferenceID: String) async throws { _ = try await ciBuildRunsCreateInstance( body: .json(.init( data: .init( _type: .ciBuildRuns, relationships: .init( workflow: .init(data: .init( _type: .ciWorkflows, id: workflowID )), sourceBranchOrTag: .init(data: .init( _type: .scmGitReferences, id: gitReferenceID )) ) ) )) ).created } }
-
14:43 - App Store Connect API - Main Function
static func main() async throws { let client = try Client( serverURL: Servers.server1(), configuration: .init(dateTranscoder: .iso8601WithFractionalSeconds), transport: URLSessionTransport(), middlewares: [AuthMiddleware(token: ProcessInfo.processInfo.environment["TOKEN"]!)] ) let workflowID = "82D89C93-B69C-46B5-A794-A2BCFD3EE487" let repoID = try await client.repoID(workflowID: workflowID) let branchName = "main" let branchID = try await client.branchID(repoID: repoID, name: branchName) try await client.startBuild(workflowID: workflowID, gitReferenceID: branchID) }
-
17:09 - Webhook Handler Implementation
struct WebhookPayload: Content { let ciWorkflow: CiWorkflow let ciBuildRun: CiBuildRun struct CiWorkflow: Content { let id: String } struct CiBuildRun: Content { let id: String let executionProgress: String let completionStatus: String } } func routes(_ app: Application) throws { let deploymentService = ExampleDeploymentClient() let workflowID = "82D89C93-B69C-46B5-A794-A2BCFD3EE487" app.post("webhook") { req async throws -> HTTPStatus in return HTTPStatus.ok } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。