Sourcegraph:跨宿主 Code Search、SCIP 与 Batch Changes
Sourcegraph 的读取链和写入链必须一起治理
Sourcegraph 先把多个代码宿主中的仓库、修订、全文索引、SCIP 语义索引和权限同步收进一个查询面;Batch Changes 再把搜索候选变成 batch spec、workspace、changeset 和代码宿主 Pull Request。二者属于同一平台,但权限风险完全不同:搜索读取源码,批量变更会执行命令并写入分支。
因此主文先建立“当前身份究竟看见了什么”的搜索证据,再进入 preview、apply 和 publish。把两项能力拆成两篇会重复安装、代码宿主连接、权限、容量和退出治理,也容易让读者误以为 Batch Changes 是另一套独立产品。
先把搜索链路拆成六个对象
用户输入查询后,Sourcegraph 并不是实时调用 GitHub、GitLab 或 Bitbucket 的搜索 API。代码宿主连接先同步仓库元数据,gitserver 再克隆并维护 Git 数据;默认分支等被选中的修订由 Zoekt 建立全文索引。前端接收查询,搜索服务将它拆到索引分片或 Git 数据路径,最后再按 repository authorization 过滤当前用户可见仓库。SCIP 索引走另一条上传和存储链路,为定义、引用和实现关系提供编译器级语义。
这六类状态不能互相替代:仓库出现在管理页,只证明元数据同步;镜像完成,不证明默认分支索引新鲜;全文搜索命中,不证明 SCIP 已上传;管理员能看到仓库,也不证明普通用户权限正确。排障时沿对象逐层取证,比反复改查询更快。
选择托管还是自托管,再开始安装
Sourcegraph Cloud 是单租户托管实例,平台负责升级、监控和备份,但实例必须能连接代码宿主。官方列出的云区域、私网连接方式、日志留存和可选能力会变化,采购时应在 Sourcegraph Cloud 重新确认区域、网络、审计导出和合同条件。源码、仓库元数据、代码宿主凭证和用户元数据都会进入托管边界,因此“网页只展示搜索结果”不等于“源码不驻留”。
自托管把 Git 镜像、全文索引、SCIP 数据库、应用数据库和日志放进组织控制的基础设施,但不等于开源免费版。当前官方将 self-hosted 标为 Enterprise 支持面,Docker Compose 文档还明确要求超过 10 个用户的实例取得 Sourcegraph license;采购必须分别确认订阅、席位、功能和支持条款,不能从“镜像可以拉取”推导出生产使用许可。官方 部署选择 将 Helm 作为健壮、可扩展的多节点方案,将 Docker Compose 作为单节点入口;Kustomize 已进入计划弃用,Machine Images 的 sunset 边界标为 Sourcegraph 7.0.0,ARM/ARM64 也不属于受支持的生产部署。部署方法之间迁移涉及完整重部署和数据库迁移,试点不要用待退出形态或临时单容器形态假装生产拓扑。
版本基线要同时固定服务端发布标签与 src CLI。官方 Docker Compose 安装页当前以 v7.5.4453 展示 7.x 标签形式,但这个示例不是组织自动追新的理由;升级文档要求按受支持路径处理数据库 schema 和 out-of-band migration,跨多个 minor 的 multiversion upgrade 还会引入停机。src 最新版若与实例不兼容,应回到与实例发布线兼容的 CLI,而不是只升级客户端。
一个可审计的单节点试点可以从官方 Compose 部署仓库开始:
git clone https://github.com/sourcegraph/deploy-sourcegraph-docker.git
cd deploy-sourcegraph-docker
git checkout <经过组织批准的-v7.x.y-发布标签> -b release
cd docker-compose
docker compose up -d
docker ps --filter="name=sourcegraph-frontend-0"预期证据是 sourcegraph-frontend-0 进入 healthy,浏览器能打开实例,Site admin 的服务状态没有持续失败。<经过组织批准的发布标签> 必须固定为团队验证过的发布标签;追随浮动分支会让升级、回滚和数据迁移失去共同基线。生产配置使用 docker-compose.override.yaml 保存资源、外部数据库、证书和环境变量覆盖,不直接改官方基础文件,具体结构见 Docker Compose 部署。
首次登录后立即接企业身份源并关闭不需要的内建注册。Sourcegraph 的登录认证与仓库授权是两件事:OIDC、SAML 或 OAuth 解决“你是谁”,repository permissions 解决“你能看哪些仓库”。不要因为 SSO 成功就把搜索开放给全员。
连接多个代码宿主时,配置字段就是搜索边界
站点管理员在 Site admin → Code hosts 添加连接。不同宿主的 JSON 模式不同,不能把一份 PAT 粘给所有连接;优先使用宿主支持的 App 或服务身份,并把连接拆到可独立轮换、可独立停用的组织边界。官方 代码宿主连接 会列出各宿主的同步、认证和权限能力。
以 GitHub 连接为例,管理员通常要决定这些字段:
url 决定 API 与克隆所指向的 GitHub.com 或 GHES 实例;写错主机可能连接到同名但不同信任域的服务。token 或 GitHub App 凭证决定 Sourcegraph 能枚举和克隆哪些仓库;它是平台采集身份,不是最终用户的可见权限。repositoryQuery 或连接支持的仓库选择字段决定纳入哪些组织、用户或仓库。选择过宽增加镜像、索引、API 限流和数据驻留成本;选择过窄会产生静默漏检。
exclude 在同步入口排除仓库,适合隔离依法不得复制的仓库、超大生成物仓库或已退役项目。排除后应等待仓库状态页消失,并按组织数据删除流程确认副本与备份生命周期。authorization 相关配置决定是否从代码宿主同步用户与仓库关系。未建立 repository authorization 之前,不应导入私有仓库并开放普通用户访问。
配置后先点 Test connection,再到 Site admin → Repositories 观察三个阶段:仓库被发现、克隆完成、索引完成。若仓库已出现但不更新,检查 worker 的宿主 API 日志与 gitserver 克隆日志;若连接测试出现 DNS、TLS 或超时错误,先修通实例到代码宿主的网络和 CA 信任,不要用全局跳过 TLS 验证作为长期解法。官方 添加仓库与排障 给出了这些证据入口。
项目接入不应靠管理员手工记忆。平台团队维护一份连接清单,至少记录代码宿主、组织选择表达式、排除理由、采集身份 owner、授权机制、数据分类、预计仓库数与退役日期。新项目只要进入受管组织并符合选择表达式就自动被发现;敏感项目则通过明确排除和审批进入独立实例或不采集路径。
第一个正向实验:跨宿主命中必须能解释
准备两个测试仓库,分别放在不同代码宿主。两个仓库都提交同一无敏感信息的标记:
SOURCEGRAPH_CROSS_HOST_PROBE_7F3A等待仓库状态页显示克隆与索引完成,然后用 CLI 做可保存的查询。src 是单文件客户端,安装和登录方式见 src quickstart:
export SRC_ENDPOINT=https://sourcegraph.example.com
src login
src search 'SOURCEGRAPH_CROSS_HOST_PROBE_7F3A count:all'预期输出包含两个不同宿主前缀的仓库路径,并指向标记所在文件。count:all 要求搜索等待更多结果,适合校验候选集;它仍受实例查询上限、超时、当前用户权限、索引跳过规则和仓库选择面影响,不能升级成数学意义上的全库证明。持续时间很长或结果量巨大时,应使用官方 Search Jobs,并检查任务日志中每个 repository-revision 子任务是否成功。
接着把查询收敛成项目日常使用的 search context。Search context 保存仓库与修订集合,团队可以用它表示“支付域”“移动端 SDK”或“一组受支持发布线”,而不是每次复制长 repo: 正则。context: 改变默认目标集合;它不会绕过仓库权限,也不会把尚未同步的仓库变出来。配置静态仓库清单时用 HEAD 表示默认分支,使用查询型 context 时要重新确认相应索引能力是否仍为实验配置,见 Search Contexts。
先用关键词与正则收敛候选,再决定是否碰结构化搜索
Sourcegraph 5.4 起默认 pattern type 是 keyword;空格分隔的词是同一文档中的关键词交集,双引号是精确短语,/.../ 才把局部模式交给 RE2。若切换到 patterntype:regexp,整段模式按正则解释。RE2 不支持回溯与反向引用,因此面对嵌套括号、跨行调用和“两个捕获必须相同”时,不要把 PCRE 经验直接搬过来。
先在测试仓库放入下面两段调用,一段是真调用,一段只出现在注释里:
client.Do(ctx, legacy.Request{ID: id})
// client.Do(ctx, legacy.Request{ID: sample})第一轮用正则建立可审计的宽候选集:
lang:go repo:^gitlab\.example\.com/payments/ /client\.Do\([^\n]+legacy\.Request/ count:all预期两行都可能命中,因为全文索引只证明字节模式满足 RE2,并不理解注释或 Go 调用表达式。把“正则命中数”直接当迁移工作量会制造假阳性,但这个结果适合做候选盘点:查询稳定、可保存、能通过 Search Jobs 跑大集合,也符合官方当前推荐路径。
结构化搜索使用 Comby hole 与平衡分隔符匹配,更接近语法树形状,却不是编译器 AST,也不消费 SCIP 的符号解析。Sourcegraph 5.3 起默认关闭它,管理员必须显式设置:
{
"experimentalFeatures": {
"structuralSearch": "enabled"
}
}重载配置后,在专用测试仓库运行:
lang:go repo:^gitlab\.example\.com/payments/ client.Do(:[args]) patterntype:structural预期它能跨行并按平衡的 () 捕获参数;lang:go 会改变注释、字符串和分隔符的解释,不能省略后再宣称语言精确。反向实验是在未被 Zoekt 索引的功能分支运行同一结构化查询:即使动态全文搜索能读取该 revision,结构化搜索仍只支持 indexed repositories,因而可能零命中。更重要的是,官方明确说明它存在性能限制、没有积极开发,并建议改用正则或 Search Jobs 加自有脚本;Comby rule、缩进敏感代码块和 saved search 也有支持缺口。生产重构因此采用“关键词/正则盘点候选,语言 parser 或编译器在 workspace 内确认 AST,再生成 diff”的两阶段链路,不把 patterntype:structural 设成发布门禁。
关闭实验时从 site configuration 删除 experimentalFeatures.structuralSearch,重跑固定正则探针,确认团队保存的查询和自动化不再依赖结构化模式。配置回滚不会自动删除已经导出的结果或下游脚本,owner 还要清点这些衍生物。
索引搜索与动态搜索解决不同时间尺度
Zoekt 通常索引每个仓库的默认分支,从而让跨大量仓库的交互式搜索足够快。显式指定未索引的分支、标签或提交时,Sourcegraph 可以基于 Git 数据执行动态搜索:
repo:^gitlab\.example\.com/payments/ledger$ rev:release-next LegacyCharge这条查询的 repo: 锚定唯一仓库,rev: 改变搜索修订。若省略 rev:,默认搜索默认分支;所以默认分支零命中不能证明维护分支不存在调用。要检索多个修订,可以使用官方查询语法支持的修订列表或 glob,但跨所有分支的查询会扩大 Git 读取、任务数量与响应时间,应进入 Search Job 或离线影响分析,而不是塞进每次键入的全局查询。语法与当前限制以 Search Query Syntax 为准。
索引也有明确的语料缺口。官方 搜索配置 说明,默认会跳过二进制、无效 UTF-8、过大文件和超过 trigram 条件的文件;仓库根目录的 .sourcegraph/ignore 还可按提交排除路径。管理员可以在仓库 Settings → Indexing 查看跳过文件。search.largeFiles 能让特定大文本进入索引,但会增加索引大小、CPU、磁盘 I/O 和查询成本,且不能让无效 UTF-8 变成可索引文本。
做一个稳定的反向实验:在测试仓库功能分支加入唯一标记但不合并,先搜索默认分支,预期零命中;再加 rev:<功能分支>,预期命中。若第二步也为零,依次确认分支已被 gitserver 拉取、修订名正确、文件未被 .sourcegraph/ignore 排除、内容可作为文本读取。这个实验把“真的不存在”和“没有搜索那个修订”分开了。
需要把长期发布分支也纳入低延迟索引时,可在站点配置 experimentalFeatures.search.index.branches 中设置额外 indexed branches。该能力仍标为 Experimental,接入前要核对当前发布线;它会直接放大索引分片、更新工作和磁盘占用。先统计每个候选分支的代码体积、提交频率与实际查询量,再决定保留哪些长期分支。短命 feature branch 更适合动态搜索。
SCIP 把文本位置升级为符号关系
文本搜索能找到字符串,却不知道两个同名 Client 是否是同一个符号。Sourcegraph 的 Search-based Code Navigation 使用文本与语法启发式,可以开箱提供一部分跳转;Precise Code Navigation 则消费 SCIP 索引中的 document、occurrence、symbol 和 symbol information,把定义、引用、实现以及跨仓依赖关系绑定到具体提交。
SCIP 索引必须和仓库、提交、项目根目录匹配。最可靠的路径是在已有构建环境中运行语言索引器,因为那里已经具备依赖、生成代码、编译选项和私有包凭证。生成 index.scip 后安装 src 并上传:
export SRC_ENDPOINT=https://sourcegraph.example.com
export SRC_ACCESS_TOKEN="<CI_SECRET_STORE_INJECTS_TOKEN>"
<语言对应的 SCIP 索引器命令>
src code-intel upload \
-repo=gitlab.example.com/payments/ledger \
-commit="$CI_COMMIT_SHA" \
-root=services/ledger \
-file=index.scip-repo 必须等于 Sourcegraph 中的规范仓库名;-commit 绑定完整提交身份;-root 对齐 SCIP projectRoot,monorepo 子项目错一个目录就可能出现定义跳错或引用缺失;-file 指向生成物。不要在生产 CI 使用 -ignore-upload-failure 掩盖失败,否则流水线会绿色通过而导航悄悄退回搜索启发式。命令细节见 src code-intel upload。
预期证据不是“上传命令退出 0”这么简单。到同一提交打开符号,确认导航界面标识精确结果;先选择一个同仓跨文件引用。只有当语言索引器确实写出了外部符号关系、依赖仓库也有可关联的精确索引时,再选择一个跨仓依赖引用,核对仓库、提交和路径;单次上传不构成跨仓导航承诺。随后故意把 -commit 改为一个实例中不存在的提交或把 -root 改错,上传可能被拒绝、进入未关联状态,或无法在目标提交提供精确导航。这个反例证明 SCIP 数据不是脱离 Git 图独立生效的符号数据库。
Auto-indexing 适合标准构建;涉及私有依赖、复杂生成步骤或受控密钥时,官方建议在 CI 中索引。支持语言、索引器状态和功能矩阵会持续变化,接入时在 Precise Code Navigation 与 Indexer 列表 核对,不把 Beta 或 Experimental 状态写进生产承诺。
权限同步必须做“同查询、不同身份”实验
Sourcegraph 会缓存仓库代码,因此代码宿主在查询时临时拒绝访问并不能保护已经进入实例的副本。repository permissions 必须在 Sourcegraph 内部执行。官方 Permission syncing 描述了两条轮询链:以用户为中心同步其可读仓库,以仓库为中心同步可读用户;同步结果保存在内部数据库,调度器和任务队列并行更新。
这会产生一个真实窗口:代码宿主刚撤权,Sourcegraph 的权限同步尚未刷新。这里没有一个统一 TTL 开关;站点侧由 permissions.syncScheduleInterval、permissions.syncOldestUsers、permissions.syncOldestRepos、permissions.syncUsersBackoffSeconds、permissions.syncReposBackoffSeconds、permissions.syncUsersMaxConcurrency 与 permissions.syncReposMaxConcurrency 共同决定调度,代码宿主连接的 requestsPerHour 和外部 API 限流还会限制实际吞吐。大规模用户与仓库关系下,官方明确提醒同步可能耗时数小时。支持权限 webhook 的宿主应同时配置 webhook 来缩短撤权延迟。生产值来自离职回收 SLO、用户/仓库关系规模、代码宿主限流预算和完整同步周期实测,而不是复制示例数字。
上线前创建三个测试身份:
reader-a 同时能读公开测试仓库与私有测试仓库。reader-b 只能读公开测试仓库。site admin 仅用于管理验证,不参与普通用户基线,因为管理员会绕过仓库权限检查。
两名普通用户运行完全相同的唯一标记查询。预期 reader-a 得到两个仓库,reader-b 只得到公开仓库。随后在代码宿主撤销 reader-a 的私有仓库权限,触发或等待权限同步,再重复查询;预期私有结果消失。GraphQL 的 permissionsInfo.syncedAt 表示该用户或仓库自身方向最近一次同步,updatedAt 表示另一方向同步对它产生的最近更新,两者不能被误读成一个统一“权限已新鲜”时间。若普通用户仍能看到私有代码,立即停止开放,检查 authorization provider、用户身份映射、同步错误、连接 token 与用户 token 不一致,以及管理员误用。
在多宿主环境里,同一个邮箱并不天然等于同一个外部身份。SSO 登录、代码宿主账号关联、用户名规范化与授权 provider 必须形成唯一映射。使用 Explicit Permissions API 时,它会成为独立授权机制;初始空授权、站点管理员绕过和子仓路径规则都需要单独实验,见 Explicit permissions API。
数据驻留、敏感信息与审计不能留到采购最后一页
进入 Sourcegraph 边界的数据至少包括 Git 对象与工作树内容、仓库元数据、全文索引、SCIP 索引、代码宿主连接凭证、用户身份、权限关系、搜索查询和运行日志。搜索词本身可能包含尚未公开的漏洞编号、客户名、密钥片段或项目代号,因此日志平台的访问级别不能低于代码平台。
Cloud 方案要把实例区域、备份区域与保留、支持人员访问审批、日志导出、私网连接和删除证明写入数据处理评审;自托管方案要把 Git、Zoekt、PostgreSQL、Redis/队列、对象存储、备份和灾备副本逐一落到允许区域。网络上只允许实例访问批准的代码宿主 API、Git 端点、身份源、制品源和必要更新源,禁止把采集 token 放进部署仓库或 Compose 文件。
仓库排除不是立即擦除证明。退出一个敏感仓库时,先从所有代码宿主连接排除,等待 Repository Status 页面消失,再按部署形态确认 Git 镜像、索引、SCIP 上传、数据库元数据、日志和备份的保留策略。官方 移除仓库 提供了排除和损坏副本清理入口;生产操作要先备份并由平台 owner 执行,不能让应用团队直接删除 gitserver 数据目录。
容量与成本由“仓库数”之外的变量决定
容量估算至少输入仓库数、Git 数据体积、默认分支可索引文本量、额外 indexed branches、提交频率、并发用户、查询形状、权限关系规模、SCIP 索引量和 Search Job 并发。同样一万个仓库,许多小仓库与少数超大 monorepo 对分片、克隆、增量更新和尾延迟的影响完全不同。
Sourcegraph 的 Resource Estimator 只能作为初始估算。试运行要记录这些趋势:仓库同步积压是否回落、Zoekt 索引延迟是否稳定、索引磁盘是否随代码增长符合预测、查询 P95/P99 是否在代表性查询集上稳定、权限同步队列是否持续增长、SCIP 上传与处理是否堆积。扩容时分清瓶颈属于 gitserver 磁盘与网络、Zoekt 分片与页缓存、数据库、搜索编排,还是代码宿主 API 限流。
成本同样分层:托管或许可费用、计算、SSD 与备份、跨区流量、代码宿主 API 配额、SCIP 在 CI 中消耗的分钟数、平台值班与升级演练。扩大 search.largeFiles、索引所有发布分支、提高 Search Job 并发或提高权限同步频率都会改变其中至少一项。配置评审应要求“为什么增加、用什么指标证明有效、怎样撤回”。
从试点接入到长期治理
先选一组跨两个代码宿主、权限层次清晰、代码量可控的仓库。冻结唯一标记查询、非默认分支查询、忽略文件、SCIP 跨仓引用和双身份权限对照五组探针。每次版本升级、代码宿主 App 权限调整、索引配置变更和身份源迁移都重跑;失败时保留查询、用户、仓库、修订、结果数、任务 ID 与组件日志关联,不保存命中源码到低权限系统。
平台 owner 管部署、升级、备份、权限同步和容量;代码宿主 owner 管采集 App 与 API 限流;安全团队审批数据域和审计;语言平台 owner 维护 SCIP indexer 与构建镜像;应用 owner 维护 search context 和仓库内 .sourcegraph/ignore。任何一方都不能用“搜索能用”替代自己的证据。
升级采用固定发布线、小规模实例或隔离租户验证、备份恢复演练、再分批切流。索引可重建,不代表所有状态都可丢弃:站点配置、身份映射、授权状态、Batch Changes 元数据和审计证据要按官方升级与备份说明分类。回滚前确认数据库迁移是否支持目标版本,不能只回退容器镜像。
退出时先停止新增代码宿主连接与 SCIP 上传,导出必要的配置和审计证据,撤销采集 App、PAT 与 CI token,再按数据分类删除实例内副本、索引、数据库、对象存储和备份。最后用原来的跨宿主探针确认旧端点不可访问、代码宿主不再有活跃集成、CI 不再上传。这样 Sourcegraph 才从“方便的搜索框”变成一套可证明接入、可限制扩张、也可完整退出的代码基础设施。
从搜索候选进入 Batch Changes
搜索命中只是迁移范围的起点。Batch Changes 使用声明式 batch spec 描述仓库选择、执行步骤与 changeset 模板;preview 保留站内候选,publish 才把分支和 Pull Request 写回代码宿主。
先认清三层状态,才不会把 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 的脚本更可靠。
