Sourcegraph Batch Changes:批量变更预览、发布与失败退出
一百个仓库需要改一行,真正困难的是控制副作用
平台团队要把一条弃用的构建参数从一百多个仓库移除。脚本在本地目录里跑得很漂亮,于是有人提议直接循环 clone、替换、push。到了第 23 个仓库,受保护分支拒绝写入;第 41 个仓库用了不同目录结构;随后几十条 PR 同时触发 CI,把共享 runner 队列压满。脚本只知道“命令成功过”,却不知道哪些分支已经推送、哪些 PR 已创建、哪些失败应重试、哪些人工提交会被下一次强制更新覆盖。
Sourcegraph Batch Changes 把批量修改描述成期望状态:batch spec 选择仓库和 workspace,步骤生成 diff,changeset spec 保存每个仓库将要发生的变更,controller 再用独立代码宿主凭证协调 branch 与 PR。掌握它的关键不是学会一份 YAML,而是看清 preview、apply、publish、merge、close 和 detach 各自改变了哪一层状态。
先认清三层状态,才不会把 preview 当成零副作用
batch-change 是一组相关变更的长期跟踪对象;batch-spec 是一次期望状态版本;每个 workspace 执行后产生 changeset-spec,其中包含 diff、commit 信息、branch、PR 标题和正文。应用 spec 后,Batch Changes controller 才把这些期望状态与代码宿主上的真实 branch、PR 或 MR 协调。
src batch preview 不是完全不写 Sourcegraph。CLI 会解析 spec、下载仓库归档、执行容器、生成 diff,把 changeset specs 和 batch spec 上传到 Sourcegraph,然后打印预览 URL;它不会因为 preview 就在代码宿主创建 commit、branch 或 PR。src batch apply 比 preview 多最后一步:把该 spec 应用为 batch change 的期望状态。两条链路的官方逐步说明见 How src executes a Batch Spec。
安装、启用与最小权限准备
Batch Changes 是 Enterprise 计划能力,由实例发布线、产品套餐、站点配置和宿主能力共同决定。batch spec 当前推荐 schema version: 2,它在 Sourcegraph 5.5 引入,并把 on.repositoriesMatchingQuery 的默认 pattern type 改为 keyword;从旧 spec 升级时应显式写 patterntype:regexp 或 patterntype:keyword,避免同一字符串换 schema 后选出不同仓库。管理员先连接当前支持的代码宿主、启用 repository permissions,并用 RBAC 收窄谁能创建和管理 batch change。RBAC 本身仍标为 Beta,而且默认所有用户对 Batch Changes 有完整读写权限;上线前必须改默认角色。产品内“能创建 Batch Changes”的权限,不等于代码宿主写权限。
本地执行需要 Sourcegraph CLI、Git、Docker、能读目标仓库归档的 Sourcegraph 身份,以及足够的磁盘和 CPU。安装 src 后用 OAuth 登录,CI 才使用秘密存储注入 PAT:
export SRC_ENDPOINT=https://sourcegraph.example.com
src login
src version
docker version
git --version预期 src version、Docker client/server 和 Git 都能正常响应,src search 'repo:^gitlab\.example\.com/migration-lab/' 只列出操作者有权读取的测试仓库。官方 Requirements 说明了本地并行度、磁盘与 Docker 的关系:所需工作空间大致受最大仓库及依赖体积乘并行任务数影响,生成 patch 还会累计占用磁盘。-j 增大速度,也同步增大下载、容器、磁盘和制品源压力。
接着在 User settings → Batch Changes 为代码宿主添加个人凭证。Sourcegraph 读取仓库的连接凭证与 Batch Changes 写 branch/PR 的凭证分离;后者应属于发起人或受控服务账号,并具有目标仓库所需的最小写权限。不同宿主所需 scope 会变化,按 Configuring Credentials 创建,不在 YAML、命令行历史、容器环境输出或 PR 正文里放 token。
个人凭证会让 PR 以个人身份创建,责任清晰且自然服从该用户仓库权限。全局 service credential 可作为没有个人凭证时的回退,也用于特定导入与同步场景,但它能被更多 Batch Changes 用户间接调用,爆炸半径更大。若启用 fork,分支会进入发布者或全局服务账号名下的 fork。组织若使用 SAML SSO、细粒度 token 审批、受保护分支或只允许 App 写入,还要在代码宿主侧完成授权。
用一个无害改动写出第一份 batch spec
在两个专用测试仓库放置 docs/platform-policy.md,然后创建 add-owner.batch.yaml:
version: 2
name: add-platform-owner
description: Add the platform owner marker to migration labs
on:
- repositoriesMatchingQuery: >-
repo:^gitlab\.example\.com/migration-lab/ file:^docs/platform-policy\.md$
steps:
- run: |
grep -q '^Owner: Platform$' docs/platform-policy.md ||
printf '\nOwner: Platform\n' >> docs/platform-policy.md
changesetTemplate:
title: "chore: add platform owner marker"
body: |
Adds the approved ownership marker to the platform policy.
branch: batch/add-platform-owner
commit:
message: "chore: add platform owner marker"
published: falsename 在 namespace 内标识同一个 batch change;重复 apply 同名 spec 是更新期望状态,不是创建完全无关的新批次。on.repositoriesMatchingQuery 决定候选仓库,它消费 Sourcegraph 搜索结果并受当前用户读权限约束。查询同时限制 repo 与文件,避免只因仓库名匹配就下载无关代码。
steps 在隔离工作区执行。这里的 grep || printf 让文件变换幂等:第二次运行不会重复添加行。Batch Changes 判断是否产生 changeset 的依据是最终 git diff --cached --no-prefix --binary,不是命令打印了多少行。真实迁移还要在步骤中执行格式化、生成器或最小测试,并用明确退出码阻断失败。幂等只覆盖仓库工作树;若 step 还调用工单、制品或通知 API,重跑仍可能产生外部副作用,必须使用外部系统幂等键或把这些动作移到 apply 后的受控流水线。
changesetTemplate.branch 必须使用 Batch Changes 专用且未被人工占用的分支名。发布时 Sourcegraph 会 force-push 该分支;复用已有分支会覆盖提交。published: false 让 apply 后的 changeset 留在 Sourcegraph 内部未发布状态,commit、branch 与 PR 尚未出现在代码宿主。字段完整语义见 Batch Spec YAML Reference。
正向实验:preview、apply 与 publish 分三次取证
先执行预览:
src batch preview -f add-owner.batch.yaml -j 2CLI 应依次解析 schema、解析 namespace、准备容器、解析仓库、下载归档、执行步骤、生成 diff、上传 changeset specs,并打印 Sourcegraph 预览 URL。打开 URL 后逐个检查目标仓库、基线分支、diff、commit message、PR 标题和 body。此时到 GitLab API 或网页搜索 batch/add-platform-owner,预期没有远端分支和 MR。
这一步已经有本地与实例侧副作用:本地缓存可能保留仓库归档和执行结果,Sourcegraph 数据库保存未应用 spec 与 changeset specs;只是代码宿主没有写入。对高度敏感仓库,应把运行主机磁盘、Docker volume、缓存目录和 Sourcegraph 实例都纳入数据驻留评审。
确认 diff 后执行:
src batch apply -f add-owner.batch.yaml -j 2预期 batch change 页面出现两个 Unpublished changeset,代码宿主仍没有 branch/MR。然后只在测试环境把 published 改成 true,再次运行 src batch preview,审查预览中从 unpublished 到 publish 的动作,再 apply。预期 controller 使用配置的个人或服务凭证创建 commit、force-push 专用分支并创建 MR;页面随后同步 MR 的 open/merged/closed、checks 和 review 状态。发布行为见 Publishing changesets。
最后再运行同一 spec。只要 namespace、name、目标仓库、基线 revision、branch 和变换逻辑保持一致,apply 应更新同一个 changeset,而不应创建第二条重复 MR。src 只有在 steps、repository 与 base revision 的缓存键可复用时才直接复用结果;默认分支前移、候选查询变化或 -clear-cache 都会重新执行。因此把首次 preview 的 repository、workspace、base revision、diff hash 保存成清单,再与第二轮逐项比较。若 diff 每轮继续漂移或不断追加内容,先修迁移脚本;批量平台无法替代确定性和幂等变换。
workspace 让 monorepo 的“一个仓库”变成多个项目
默认每个仓库执行一次 steps。在 monorepo 中,根目录可能没有统一 package.json,同一仓库也可能有几十个服务。workspaces 可根据标志文件定位项目根,让步骤在每个项目执行:
version: 2
name: align-node-engine
description: Align the Node engine in monorepo services
on:
- repository: github.example.com/platform/monorepo
workspaces:
- rootAtLocationOf: package.json
in: github.example.com/platform/monorepo
steps:
- run: npm pkg set engines.node=22
changesetTemplate:
branch: ${{ join_if "-" "batch/node-engine" (replace steps.path "/" "-") }}
commit:
message: "build: align Node engine"
title: "build: align Node engine"
body: "Updates one service workspace."
published: falserootAtLocationOf 用 Sourcegraph 搜索定位文件,其所在目录成为 workspace;in 用 glob 限制在哪些仓库发现 workspace,并不匹配仓库内路径。多个 workspace 可能在同一仓库产生多个 changeset,因此 branch 用 steps.path 取得执行目录,再用 replace 把 / 转成适合分支名的 -;根目录为空时,join_if 不会追加空后缀。
workspace 解析预览是第一道证据:目标服务数应与项目清单对得上,排除 examples、fixtures、vendor 和生成目录。第二道证据是每个 workspace 的 step 日志与 diff。第三道证据是同一仓库的 changeset 分组符合代码宿主审查策略。若平台希望一个 monorepo 只发一条 PR,应调整 workspace 与 diff 分组设计,而不是在发布后手工合并分支。
反向实验:把读权限、执行权限和写权限逐一打断
第一轮撤掉操作者对一个测试仓库的 Sourcegraph 读权限,再跑 preview。预期该仓库不进入候选集;这证明 on 的结果受 repository permissions 过滤。它也揭示风险:发起人没有读权时,零命中并不能证明全组织不需要迁移。批量变更 owner 应先拿到经审批的目标仓库清单,再与搜索候选做差集。
第二轮保留读权限,但移除代码宿主 token 的写仓库或建 PR 权限。preview 应仍能生成 diff,因为它读取 Sourcegraph 中的仓库;publish 则在 controller 与代码宿主交互时失败,changeset 页面留下权限错误。不要为消掉 403 直接给 token 组织管理员权限,应核对目标仓库、branch push、PR/MR 和 workflow 文件写入所需 scope。
第三轮把步骤改为确定失败:
steps:
- run: |
test -f docs/platform-policy.md
false预期 workspace 记录非零退出码且不生成可发布 diff。需要快速迭代时可使用 -fail-fast 在首个错误停止;需要盘点所有不兼容仓库时允许其继续并导出失败集合。-skip-errors 会改变“部分失败是否仍生成预览”的语义,只能在负责人明确接受部分成功并能审查遗漏时使用。
第四轮占用 changesetTemplate.branch,放入一条人工提交。发布更新可能 force-push 覆盖该分支,因此实验只能在专用测试仓库进行。预期证据是远端分支提交被 Batch Changes 期望状态替换。由此建立硬规则:批量分支命名空间只归 controller 使用,人工修复进入新分支或回到 batch spec,不能混写。
本地执行与 executor 的信任边界不同
本地 src batch preview/apply 在操作者主机上下载仓库归档并运行容器。bind workspace 通常位于本地缓存目录;volume workspace 使用 Docker volume 并在进程退出时清理。缓存能加速相同 steps、仓库与 revision 的重复执行,但修改迁移逻辑后要确认缓存键是否失效,必要时使用 -clear-cache。官方 执行链路 说明了下载、容器准备、缓存和上传过程。
Server-side execution 把 workspaces 分发给 Sourcegraph executors。它不是本地 preview/apply 自动切换出的执行方式:site admin 要先部署在线 executor 并启用 server-side Batch Changes,用户也可以从实例的 Batch Changes 页面解析 workspace、执行并应用 spec。官方页面对成熟度仍存在 Experimental 与 Beta 两种口径;生产决策按更严格的 Experimental 处理,并逐项核对当前发布线的 namespace、挂载、executor 部署和 API 限制,不能把 Cloud 页面可见或功能可运行写成稳定支持承诺。
executor 需要读取仓库、拉取迁移容器和依赖,并运行来自 batch spec 的命令,本质上是高权限远程代码执行边界。生产上将它放入隔离网络和独立节点池,禁止访问云元数据、生产数据库和内部管理网;容器镜像固定 digest,制品源使用只读凭证,出网走 allowlist,作业结束清理磁盘。
并发由 executor 数量与 EXECUTOR_MAXIMUM_NUM_JOBS 等配置共同决定,单作业 CPU、内存和磁盘配置又决定整机容量。官方 Deploying Sourcegraph executors 提供当前支持的部署方式与资源建议;部署形态和 server-side Batch Changes 限制必须以同一发布线文档为准。先用代表性的最大仓库测量 clone、依赖下载、变换、测试、patch 和上传峰值,再计算并发。
本地模式适合少量仓库、脚本开发和开发者可控凭证;executor 适合集中审计、持续运行和大规模并发,但增加镜像供应链、缓存、网络、隔离与运维成本。两者生成的 changeset specs 应在相同 fixture 上保持等价,切换执行方式前用固定提交比较 diff 摘要。
rollout window 控制写入速率,不替代 CI 容量设计
一次发布几百条 PR 会同时触发 CI、代码所有者通知、安全扫描和依赖下载。管理员可通过站点配置 batchChanges.rolloutWindows 设置不同时段的协调速率。启用后,apply 的 changeset 先进入 Scheduled,controller 按当前窗口的漏桶速率创建、更新或关闭 changeset。配置省略或设为 null 时,系统会尽快在代码宿主限流允许范围内协调。
{
"batchChanges.rolloutWindows": [
{ "rate": 0 },
{
"rate": "1/hour",
"days": ["saturday"],
"start": "06:00",
"end": "08:00"
}
]
}这是一条可验证的试点配置:默认窗口 rate: 0 停止协调,后定义的重叠窗口在 UTC 周六 06:00 到 08:00 以每小时一个 changeset 的演示速率放行。rate 也可写 N/second、N/minute、N/hour 或 unlimited;数组为空同样会让所有协调等待有效窗口,删除字段或设为 null 才恢复按代码宿主允许速度尽快协调。生产团队要测一条 PR 对共享 runner、制品仓库和测试环境的平均与峰值消耗,再扣除正常开发负载,用所得预算替换演示速率。窗口配置影响创建、更新、关闭以及部分导入/分离操作;批量评论、合并和关闭等操作可能不是同样的渐进语义,操作前在 Batch Changes site configuration 核对当前行为。
发布观察至少包括 Scheduled 年龄、controller error、代码宿主 API rate limit、PR 创建速率、CI 排队时间、失败率和制品源流量。若 CI 队列持续增长,先把窗口速率降为零或关闭后续 publish,再处理已创建 PR;增加 executor 只会更快地产生 diff,不会增加代码宿主或 CI 的吞吐。
PR 是受保护的交付单元,不是 Batch Changes 的内部记录
发布后的 changeset 仍服从代码宿主的 branch protection、required checks、CODEOWNERS、合并队列、签名要求和人工审查。Batch Changes 跟踪这些状态,但不应绕过它们。对于高风险迁移,先发布样本仓库,等待构建、测试、静态检查和运行验证完成,再扩大目标集合或提高窗口速率。
更新同名 batch change 时,controller 按新 spec 协调已有 changeset:title/body 可更新;diff 或 commit 属性变化可能重写远端提交;branch 名改变会关闭旧 changeset 并创建新 changeset;某仓库不再产生 diff 时,已发布 changeset 会被关闭并归档。人工直接加到 Batch 分支的 commit 可能在下一次更新中丢失,详细状态变化见 Update a batch change。
因此 batch spec、迁移脚本、容器 digest、目标查询和验证命令都要进入普通 Git 仓库审查。PR body 附迁移批次标识和验证说明,但不暴露 Sourcegraph token、内部查询中的敏感代号或执行日志。组织 namespace 比个人 namespace 更适合长期批次;用 RBAC 限制 Batch Changes 的读写能力,并用 namespace 管理规则限制 apply、close、rename 等管理动作。全局 service credential 由 site admin 单独管理,不能把产品内批次权限误当成凭证管理权或代码宿主写权限。
重试先判断失败层,不能把 apply 当刷新按钮
workspace step 非零退出、changeset spec 上传失败和代码宿主协调失败属于三层不同故障。step 失败时,其他仓库默认如何继续要以当前 src 输出为准;-fail-fast 明确在首错停止,-skip-errors 则允许忽略仓库错误并生成部分结果。无论选哪一种,都要把“目标总数、成功 workspace、失败 workspace、未执行 workspace”保存成守恒清单,不能只看 preview URL 是否生成。
changeset 已进入 controller 后,内部错误和代码宿主 HTTP 5xx 这类瞬态故障通常进入 Retrying,官方当前上限是自动重试十次;凭证缺失、权限不足、分支被另一个 batch change 占用等确定性错误进入 Failed,修复原因后再点 Retry 或重新 apply。HTTP 403 不会因为多试几次变成最小权限,HTTP 429 也应先检查宿主限流与 rollout 速率,而不是并发重放。
手工重试前记录 changeset ID、目标仓库、branch、当前 commit、错误类别和 controller 尝试次数。修复凭证后重试同一 changeset,预期复用同一 branch 并覆盖其期望 commit,不创建第二条 PR;若远端已存在由另一批次管理的同名 branch/PR,则先改专用 branch 或完成 ownership 转移。只有这组证据一致,重试才是幂等协调,而不是重复发布。
失败清理要按“尚未发布、已发布、已合并”分流
preview 阶段失败时,代码宿主没有 branch/PR。停止 src,删除本地 cache 与残留 Docker volume,确认没有容器仍运行;实例中的未应用 spec 会按平台生命周期清理,也可由有权限的 owner 主动处理。若日志含凭证或源码片段,按安全事件流程清理日志副本并轮换泄露 token。
apply 但未发布时,若仍需修正就先更新 batch change;若确定废弃才关闭,并注意关闭后的 batch change 不能更新或重新打开。随后再删除不再需要的 spec;这时主要清理 Sourcegraph 元数据和执行缓存。不要因为代码宿主“看起来没有变化”就忽略实例中保存的 diff,因为它仍可能含敏感源码。
已经发布但未合并时,先暂停 rollout,冻结 spec 更新,然后在 Batch Changes UI 预览 close 操作。关闭 PR/MR、删除 Batch 分支、归档 changeset 是不同动作;是否自动删分支取决于 batchChanges.autoDeleteBranch、代码宿主能力以及关闭动作发生在 Sourcegraph 还是代码宿主。逐项核对远端 branch、open PR、fork 与 controller error,不用一条未经审查的循环删除命令扫所有仓库。
已经合并时,close 不能撤销代码。应为已合并集合生成独立反向 batch spec 或由代码宿主逐 PR revert,并重新运行构建、测试和部署验证。数据库迁移、生成代码、锁文件和发布制品可能有 Git 之外的副作用,必须由对应系统执行补偿。回退 spec 同样先 unpublished preview,再样本发布、分窗 rollout。
从目标查询移除仓库会让 published changeset 关闭并归档;未发布或导入的 changeset 则会跳过归档并直接 detach。手工 detach 会解除 changeset 与 batch change 的关联,后续 controller 不再管理它。归档保留关联历史且可通过重新纳入目标集恢复,detach 适合明确交还人工维护的 PR;退出演练要验证 controller 不再更新 detached PR。
容量、成本与长期治理
批次容量由候选仓库数、每仓 workspace 数、仓库与依赖体积、step 时长、并发、patch 大小、代码宿主 API 限流和 CI 扇出共同决定。先用小样本记录每阶段耗时与峰值磁盘,再分批扩张。成功率不能只看“生成了多少 diff”;还要看目标盘点差集、workspace 失败、unpublished 年龄、scheduled 年龄、controller 重试、PR checks、合并率和退出清理完成率。
成本包括 executor 或开发机计算、镜像与依赖流量、Sourcegraph 存储、代码宿主 API、CI minutes、审查者时间和失败回退。把格式化、编译和测试放进 steps 会增加预览成本,却能在发布前淘汰坏 diff;完全省掉它们只是把成本转移到几百条 PR 的 CI 队列。合理做法是 steps 执行快速确定性门禁,PR CI 执行完整集成验证。
团队为每个批次指定变更 owner、平台 owner、代码宿主 owner、CI owner 和安全审批人。spec 合并前评审目标查询、幂等性、容器 digest、凭证身份、最大仓库、workspace 数、rollout 预算、回退 spec 与退出判据。全局 service credential 定期轮换并审计使用者;离职用户凭证立即删除;executor 镜像、Sourcegraph 发布线和代码宿主 API 变更进入兼容验证。
最后做一次真正的退出演练:停止窗口、关闭未合并 changesets、确认专用分支与 fork 清理、对已合并样本执行反向变更、删除本地与 executor 缓存、撤销临时凭证、保留必要审计记录并关闭 batch change。只有当预览可解释、发布可限速、失败可分型、已经写入的状态也能逐层撤回时,批量变更才比一段循环 push 的脚本更可靠。
