Cloudflare 发布了 cloudflare/ci,尝试把 CI 流水线从一组一次性脚本,变成运行在 Workers 运行时之上的 TypeScript 工作流。它建立在 Cloudflare Workflows 之上,让每个步骤具备持久化重试和回放能力,默认支持并发执行,并结合 Sandbox 快照缓存减少重复环境准备。
这项能力目前依赖仍处于私有测试阶段的 Artifacts,因此还不能直接当作通用 CI 平台替换方案。更值得借鉴的是它采用的模型:把流水线拆成可恢复、可重试、可观察的 durable steps。
CI 脚本为什么需要持久化步骤
传统 CI 通常把一次 job 看成一个长脚本:拉取代码、安装依赖、运行测试、构建镜像、发布产物。只要中间某个命令失败,系统往往需要从头创建执行环境并重新运行整个 job。
这种方式在步骤少、执行时间短时足够简单,但大型项目会遇到几个实际问题:
- 安装依赖和构建环境准备可能被重复执行。
- 网络抖动导致单个命令失败,却触发整个流水线重跑。
- 并行任务需要开发者手动编排,错误处理和日志关联容易变复杂。
- 调试历史失败时,通常只能查看日志,无法从某个步骤继续回放。
cloudflare/ci 的核心思路是让步骤本身成为持久化执行单元。每个步骤可以保存执行状态,失败后按步骤重试,必要时从历史状态回放。这样,流水线的恢复边界不再是整个 job,而是一个个明确的步骤。
三个关键设计
1. 每个步骤都有 durable retry 和 replay
步骤级重试意味着测试、构建或发布可以拥有不同的重试策略。例如,网络相关的依赖下载适合短时间重试,发布操作则应保持幂等并限制重试次数。
回放能力也改变了排查问题的方式。开发者可以围绕某个步骤检查输入、输出和失败状态,而不是重新执行所有前置动作。这里的前提是步骤需要尽量设计成幂等操作,并明确记录外部副作用。
2. 并发是默认路径
CI 中经常存在这样的依赖图:代码检查、单元测试和文档构建互不依赖,可以同时执行;只有它们都成功后,才进入打包和发布。
在普通脚本里,这通常需要手动启动后台进程、等待多个退出码,再处理失败情况。工作流模型可以直接表达这种并发关系,让每个步骤保留独立状态,同时在汇聚点统一判断结果。
3. Sandbox 快照缓存减少环境成本
许多流水线时间并不花在业务测试上,而是花在创建环境、安装依赖和准备工具链上。Sandbox 快照缓存可以保存已经准备好的执行环境,后续运行从快照恢复,减少重复初始化。
缓存并不是无条件可靠的优化。依赖锁文件、基础镜像、运行时版本和构建工具发生变化时,都应让快照失效。实际采用时,应把快照键绑定到这些输入,避免使用过期环境产生难以解释的构建结果。
一个可改造的 TypeScript 示例
下面是一个表达 CI 工作流结构的最小示例。它使用伪 API 展示 cloudflare/ci 这类 SDK 的使用方式,方法名可能需要根据实际测试版本调整;示例重点是步骤边界、并发关系和重试配置。运行前需要准备 Workers 项目、Workflows、Artifacts 访问权限以及对应的私有测试包。
import { createPipeline } from "cloudflare/ci";
export default createPipeline({
name: "pull-request-checks",
async run(ctx) {
const checkout = await ctx.step("checkout", {
retries: 2,
run: async () => {
return ctx.exec("git", ["checkout", ctx.input.commit]);
},
});
const checks = await ctx.parallel([
ctx.step("lint", {
retries: 1,
run: () => ctx.exec("npm", ["run", "lint"]),
}),
ctx.step("unit-tests", {
retries: 2,
run: () => ctx.exec("npm", ["test", "--", "--runInBand"]),
}),
ctx.step("typecheck", {
retries: 1,
run: () => ctx.exec("npm", ["run", "typecheck"]),
}),
]);
await ctx.step("build", {
dependsOn: [checkout, ...checks],
retries: 1,
run: () => ctx.exec("npm", ["run", "build"]),
});
await ctx.step("publish-artifact", {
retries: 0,
run: () => ctx.artifacts.upload("dist", {
name: `build-${ctx.input.commit}`,
}),
});
},
});
可以这样改造现有流水线:
- 把每个具有清晰输入和输出的命令拆成一个步骤。
- 把可以安全重复执行的网络操作配置为有限重试。
- 把 lint、测试和类型检查放入并发分支。
- 让构建步骤依赖所有检查步骤成功完成。
- 对上传、发布和数据库变更等外部副作用关闭盲目重试,并实现幂等键。
如果只想验证并发和重试的设计,也可以先用本地脚本模拟同样的执行图:
set -euo pipefail
npm ci
npm run lint & lint_pid=$!
npm test & test_pid=$!
npm run typecheck & typecheck_pid=$!
wait "$lint_pid"
wait "$test_pid"
wait "$typecheck_pid"
npm run build
这段 shell 代码没有持久化恢复能力,但能帮助团队先识别步骤边界。迁移到 durable workflow 后,真正需要补充的是状态保存、重试策略、超时控制、日志关联和外部副作用保护。
它还不是通用 CI 替代品
当前方案有明确边界。它面向 Workers 运行时,依赖 Cloudflare Workflows 和 Artifacts;Artifacts 仍处于私有测试阶段,生态成熟度、权限模型、缓存行为以及与现有 Git 提供商的集成能力,都需要在目标环境中单独验证。
因此,选择它时应区分两个问题:
- 如果目标是立刻替换成熟 CI 平台,需要评估触发器、密钥管理、构建机隔离、日志、权限、制品保留和故障排查等完整能力。
- 如果目标是构建长时间运行、容易受网络影响、需要步骤级恢复的自动化流程,durable-step 模型可能更有价值。
尤其要注意幂等性。一个测试步骤通常可以重跑,但“创建发布”“扣减额度”或“向第三方发送通知”可能不能简单重跑。步骤状态持久化不会自动消除外部系统的重复副作用。
采用前的检查清单
- 是否能把流水线拆成边界清楚的步骤?
- 每个步骤的输入、输出和超时是否明确?
- 重试是否只用于临时性错误?
- 发布和写入外部系统的操作是否具备幂等保护?
- 并发步骤是否会争用同一个工作目录或缓存?
- Sandbox 快照是否绑定了锁文件和运行时版本?
- 团队是否能接受 Workers、Workflows 和 Artifacts 带来的平台约束?
cloudflare/ci 最值得关注的地方,不是把所有 CI 功能搬到 Cloudflare,而是把流水线执行从“失败就整段重跑”推进到“步骤可以持久化、恢复和回放”。在私有测试阶段,适合从小型、低副作用的验证流水线开始,先确认步骤语义、并发行为和故障恢复,再决定是否扩大使用范围。