外传:这不是关于已合并PR的流程,而是关于维护处于打开状态的PR的流程。本文中出现的PR在2026-11-13时均处于「进行中」。 实际上,这些内容取自GitHub的PR及检查记录、文档、各项目的贡献规则原文、贡献账目的查询原始数据以及实测记录。时间均为韩国时间。
目录
- 一句话总结
- 1. 为什么要检查:打开的PR放着不管也会过时
- 2. 查询:五种状态值与UNKNOWN
- 3. 三个冲突的原因各不相同
- 4. 无需克隆重新应用:Git Data API与SSH签名
- 5. 未提交的内容:项目各异的大门
- 6. 如何测量在Mac上无法运行的测试
- 7. 收获:维护打开的PR的要领
- 链接
一句话总结
| 对象 | 上游项目中处于打开状态的45个PR。均于10月中旬到月底之间提交且皆非草稿。个人仓库以及所属社区仓库的7个旧PR将单独计算 |
| 查询 | gh pr view --json mergeable,mergeStateStatus,reviewDecision 每个PR一次 |
| 分布 | BLOCKED 24 · UNSTABLE 9 · CLEAN 7 · BEHIND 3 · DIRTY 2。初次查询时为 DIRTY 3、UNKNOWN 4 |
| 处理 | 三个冲突中有一个通过在新的基准之上重新应用而解决。另外两个需要重写,目前正在进行中 |
1. 为什么要检查:打开的PR放着不管也会过时
提交PR的那天既没有冲突也能通过检查。然而,在等待评审的几周时间里,基准分支一直在变动。 其他人修改了相同的文件,文档结构发生了变化,运行检查的Runner环境也变了。我们的PR一行代码未改, 但不知哪天就被贴上了「冲突」标签,或者原本能通过的检查变成了失败。
站在维护者的角度来看,发生冲突的PR是无法进行评审的PR。当轮到评审时如果发现存在冲突,就会直接跳过该顺序。 因此,当打开的PR增加到数十个之后,我们决定不再只等收到通知时才去查看,而是改为一次性查询整体并按状态进行分类。
这篇文章记录了2026-11-13凌晨进行的这次检查。与第1–6篇不同,本文不追踪单个已合并的PR,而是探讨维护多个打开的PR的流程。 由于这些是尚未得出结果的PR,因此不会记录任何PR「已被接受」。
2. 查询:五种状态值与UNKNOWN
首先创建打开的PR列表(gh search prs --author <계정> --state open),并对每个PR执行一次以下命令。
gh pr view <번호> --repo <소유자>/<저장소> --json mergeable,mergeStateStatus,reviewDecision
mergeable 只能告知是否存在冲突(MERGEABLE·CONFLICTING·UNKNOWN), mergeStateStatus 将「现在是否可以点击合并按钮」
进行了更细致的划分。GitHub的GraphQL文档中说明的含义以及此次统计的数量如下。
| 值 | 文档含义(要点) | 数量 | 我们的解读方式 |
|---|---|---|---|
BLOCKED |
合并被阻止 | 24 | 大部分在等待评审。评审判定结果为:“需要评审”17,无判定5,批准1,更改请求1 |
UNSTABLE |
可以合并,但存在未通过状态的检查 | 9 | 打开检查记录,首先确认原因是否由我们的修改引起 |
CLEAN |
可以合并且检查通过 | 7 | 我们无需做任何事。其中获得批准的有1件 |
BEHIND |
PR分支落后于基准 | 3 | 不是冲突。仅基准分支超前 |
DIRTY |
无法干净地创建合并提交 | 2 | 冲突。将原因追溯到具体的PR |
在此学到了两件事。
初次查询的 UNKNOWN 并非最终结果。 在初次查询中,有四项显示为 UNKNOWN 。GitHub的REST文档说明,如果此值为空,则意味着计算是否可合并的后台任务刚开始,建议过一段时间再请求。实际上在大约4分钟后用相同的命令再次查询,发现其中三项变成了 BLOCKED,一项变成了 UNSTABLE 。越是长时间未查询的PR,在初次查询时越容易出现该值,因此批量检查脚本应当将 UNKNOWN 的项目收集起来再次查询。
CLEAN 并不意味着「即将合并」。 这七项仅代表技术上没有阻碍,合并与否由维护者决定。
相反, BLOCKED 的24项大多数也不是因为有什么需要修改,而是处于等待评审顺序的状态。在45项中,需要我们动手的只有
DIRTY 和 UNSTABLE 中原因在我们这一侧的项目。 UNSTABLE 的九项原因各不相同,本文中不再细分。
有时检查失败的原因并非由于我们的修改。在一天前的11-12看到的
containers/kubernetes-mcp-server#1518 在获得一名评审员批准的情况下,浏览器测试变为了失败状态。同样的测试仅在旧基准上失败,原因似乎是检查Runner的镜像发生了变化。
我们保持修改内容不变,将其重新应用到新基准之上。随后获得了第二个批准,在本次查询中显示为 CLEAN 。
该PR也正在进行中。
3. 三个冲突的原因各不相同
初次查询的 DIRTY 有三项。仅凭发生冲突这一事实无法得知应该做什么,因此我们查清了这三项分别是因为哪个PR合并而导致产生了冲突。
原因各不相同,因此处理方式也不同。
| 我们的PR | 造成冲突的合并 | 该合并的规模 | 原因 | 处理方式 |
|---|---|---|---|---|
| ai-dynamo/dynamo#16465 4个文档,+15/−4 | #17229, 11-06 04:34 | 29个文件,+97/−3068 | 我们修改的行的相邻行被修改 | 在新的基准之上重新应用。解决 |
| NVIDIA/dcgm-exporter#760 1个文档,+16/−7 | #768, 11-07 03:45 | 961个文件,+161632/−27808 | 包含被修改段落的文档被整篇重写 | 需要根据新文档重写。进行中 |
| charmbracelet/catwalk#676 3个文件,+63/−0 | #611, 10-29 23:22 | 73个文件,+39578/−18179 | 主版本重构导致模块路径和配置格式发生变化 | 需要用新格式重写。进行中 |
相邻行被修改的情况。 dynamo 的PR是一项为四处已废弃功能的文档补充缺失的废弃标记的修改。在此期间,上游合并了一个移除相关集成代码的PR,修改了同一文档中紧邻的两行。在重新应用之前,我们首先确认的是「我们的修改是否仍然必要」。打开新基准分支的四个文件一看,废弃标记确实仍然缺失。于是我们在新基准的文件中重新应用了相同的修改,修改量保持 +15/−4 不变。再次查询时,冲突解除,并恢复到了等待评审(BLOCKED)状态。
文档被整篇重写的情况。 dcgm-exporter 的PR修改了导出调试转储流程的段落。然而,提交新版本(4.9.0)的一个PR修改了961个文件,并将该文档整篇重写了。由于该PR在创建后仅30分钟便被合并,因此无法提前获知。在新文档中,我们修改的段落已经消失,转储放置的位置也发生了变化。这不是通过重新应用就能解决的冲突。我们必须先测算新文档中残留的一行复制命令是否在新镜像中也会失败,但新版本尚未测算(未实测)。测算后,若将其重写为单行PR或者问题已消失则选择关闭,这才是正确的做法。
主版本被重构的情况。 catwalk 的PR是一项添加提供者配置的修改。在此期间,v3被合并,导致模块路径和配置文件格式发生了变化。无法直接应用旧格式的文件,必须用新格式重写,并尝试用新版本的工具进行构建。这也是正在进行中。
BEHIND 并非冲突。 这三项只是基准分支领先,合并提交可以干净地创建。这次我们没有去动它们。既然没有冲突,我们认为没必要在评审开始前急着重新应用。有些项目对评审开始后的强制推送比较反感,因此最好减少不必要的重新应用。
审视这三项就会发现,冲突并非取决于「我们的修改与他人的修改重叠的程度」,而是取决于「我们的修改所依赖的基础改变了多少」。如果是相邻行级别,重新应用即可;如果段落或格式消失了,则需要重新评估修改本身。
4. 无需克隆重新应用:Git Data API与SSH签名
把几十个仓库全部下载下来再重新应用会带来沉重的负担。为了修复几行文档的PR去下载数GB大小的仓库也是一种浪费。因此,我们不下载仓库,而是通过GitHub的Git Data API来进行重新应用。在本地既不创建提交,也不切换分支。流程如下。
- 将Fork的默认分支与上游保持同步。只有当新的基准提交存在于Fork中时,才能在其上创建提交。
- 仅获取新基准中需要修改的文件,重新应用相同的修改,并运行可以运行的测试。
- 为每个文件创建blob(
POST git/blobs),将新基准的树指定为base_tree来创建树(POST git/trees). - 直接拼装提交对象的主体并用SSH密钥进行签名。
- 附加签名并创建提交(
POST git/commits),确认响应的verification.verified是否为真。 - 仅在为真时,将PR分支的ref移动到新的提交。移动的目标仅限于我自己的Fork分支。
第4步中签名对象正是git在保存提交时使用的原样字符。
tree <새 트리 SHA>
parent <새 기준 커밋 SHA>
author <이름> <메일> <epoch 초> +0900
committer <이름> <메일> <epoch 초> +0900
<커밋 메시지>
ssh-keygen -Y sign -n git -f <개인 키 파일> <위 내용을 담은 파일>
在此记录一下碰到的问题。
- 只要有一个字符不同,签名就会不匹配。 传递给API的创作时间是ISO格式,而提交对象内部的时间是epoch秒和时区。两个值必须指向同一瞬间,消息末尾的换行符也必须相同。因此,在移动分支之前务必确认
verification.verified,若为假则停止。 - 文档虽然只提及PGP签名,但SSH签名也同样得到了验证。 REST文档
signature被解释为提交的 PGP 签名。我们使用在账户中注册为签名密钥的 SSH 密钥进行了签名,所创建的提交显示为 「Verified」。由于这是文档中没有的行为,因此仅作为实测记录记载。 - 确认并移动分支头(branch head)。 工具接收「当前分支头应该是这个 SHA」的期望值,如果不符则什么都不做。因为在此期间维护者可能已经向分支添加了更多提交。
- 大型仓库无法一次性读取整棵树。 递归读取树的调用如果项目过多,响应会被截断并返回
truncated为真。在 vendor 目录很大的仓库中经历过这件事后,我们改为沿路径逐级非递归地读取树。
学到代价最高的一课是关于文件模式(file mode)。最初制作的工具将上传文件的模式固定为 100644 。在 10-31 将
containers/podman-compose#1585 重新变基(rebase)后,原本是可执行文件的脚本变成了普通文件,30 个集成测试以 PermissionError 失败。内容连一个字符都没有不同,因此仅看 diff 的正文是无法发现这种变更的。第二天,我们修复了工具以使其遵循基准树的模式,并用相同的内容重新创建了提交。从那以后,在使用签名工具后,必定会确认以下事项。这个 PR 也在进行中。
gh pr diff <번호> --repo <소유자>/<저장소> | grep -n 'mode'
正常情况下应该没有任何输出。 old mode 100755 和 new mode 100644 如果出现,则是非预期的变更。
5. 未提交的内容:不同项目的门槛
顺便检查了一下,在仅注册了的 Issue 中,有没有可以连结为 PR 的。结论是「现在没有要提交的」。倒不是因为做不出补丁,而是因为项目的规则规定了顺序。
- 只接受贴有批准标签的 Issue 的 PR 的项目。 fake-gpu-operator 的贡献规则规定,所有 PR 都必须连接到一个被维护者贴上
approved标签的开放 Issue,并且检查机制会强制执行这一点。我们注册的两起 Issue 还没有标签。 - 如果没有标签就自动关闭 PR 的项目。 kubeflow/pipelines 要求非组织成员的 PR 连接到带有
ready标签的 Issue。这同样适用于错误修复和文档更改,如果不满足条件,工作流会留下评论并关闭 PR。 - 要求先获得进行批准的项目。 Moby 和 Docker 开源项目共同使用的 AI 政策将“在先打开 Issue 以便维护者确认需求并批准进行之前,提交借助 AI 帮助完成的 PR”作为“虚胖贡献”的例子。虽然我们注册文档 Issue 已经过去了 27 天,但既没有标签也没有评论,所以我们没有提交 PR。
- 其他贡献者已经先打开 PR 的 Issue。 我们注册的五起 Issue 已经由其他人打开了 PR。重复提交相同的修改只会增加维护者的工作量。
也有提交了一个的情况。这是修复了 KubeVirt 的 virt-operator 在不丢弃任何证书的情况下重复打印“丢弃陈旧的 CA”日志这一问题的 kubevirt/kubevirt#19889 。KubeVirt 的 AI 贡献政策要求在提交前审查和理解 AI 生成的代码的所有行,并建议披露工具的使用。因此,我们在提交中附上了 Assisted-by 拖尾(trailer)并作为
草稿 PR 进行了提交。在人工完整阅读 diff 后,将其更改为审查请求状态。这个 PR 也在进行中,合并后会单独写一篇文章。
6. 如何测试无法在 Mac 上运行的测试
上述 KubeVirt 更改的单元测试无法在 Mac 上运行。因为测试所附带的软件包依赖于 Linux 专有软件包。我们没有选择“测试会让 CI 去跑吧”然后直接提交,而是通过以下方法进行了直接测量。
- 在 Mac 上仅交叉编译用于 Linux 的测试二进制文件。由于只使用仓库的 vendor 目录,因此无需下载任何新内容。
- 将该二进制文件放入 TT Lab 集群的临时 Pod 中并仅执行它。Pod 使用节点上已经下载的镜像,丢弃所有 capability,以非 root 用户身份、在没有服务账户令牌、内存上限为 1Gi 的情况下启动。结束后会被删除。
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOTOOLCHAIN=local GOPROXY=off \
GOFLAGS="-mod=vendor -buildvcs=false" go test -c -o <시험 바이너리> <패키지 경로>
| 测试对象 | 结果 |
|---|---|
| 新写的 2 个测试,修改前的代码 | 0 通过,2 失败 |
| 新写的 2 个测试,修改后的代码 | 2 通过 |
| CA 捆绑包相关测试 | 27 个中 27 个通过 |
| 整个软件包(打乱顺序 3 次) | 160 个中 160 个通过,约 0.53 秒 |
邻近的 apply 软件包整体 |
219 个中 219 个通过 |
环境是 Linux 内核 7.0、x86_64、Go 1.26.6。我们还测量了修改前的症状。在安装了 KubeVirt v1.9.0 的 TT Lab 集群中,统计了领导者(leader)operator 的日志 24 分钟,发现相同的行有 711 行。这与每调整 237 次在 CA 捆绑包集中打印一行的次数相吻合。
这里也有一个陷阱。最初编写的测试在 修改前的代码中也通过了。因为测试环境的默认日志记录器过滤掉了信息级别的日志,导致“不应打印日志”的测试由于日志被过滤而通过了。直到修改为在测试内部提高日志详细程度并在结束后恢复后,修改前的代码才出现 2 个全部失败的情况。如果跳过了“必须先确认新测试在修改前的代码中会失败”这一原则,我们差点就提交了一个什么都守护不了的测试。
7. 心得:照看开放 PR 的诀窍
- 定期一次性查询开放的 PR。 通知往往不会告知冲突。只需针对每个 PR 运行一次命令即可。
UNKNOWN需要重新查询。 第一次查询的值意味着“尚未计算”,几分钟后才会出结果。DIRTY才是真正的冲突。BEHIND是落后了,而BLOCKED大多是在等待审查。每种状态要做的事情各不相同。- 冲突要在找出导致冲突的合并之后再进行处理。 如果是相邻行发生了更改,则重新变基(rebase);如果段落或格式消失了,则首先重新判断是否仍需要该更改。在重新变基之前,请确认在新基准中是否仍然存在问题。
- 通过 API 创建提交时,除了内容之外,还要检查是否丢失了其他东西。 文件模式就是这样。每次都要确认签名验证以及模式是否发生了更改。
- 在提交 PR 之前,先阅读该项目的门槛在哪里。 Issue 标签、进行批准、AI 工具标注等,每个项目的顺序都不一样。跳过规则的 PR 会被自动关闭或消耗维护者的精力。
- 新测试首先要确认其在修改前的代码中是否会失败。 只确认通过的测试无法知道它守护了什么。
蒂蒂的一言。 我在图书馆塞了好多张“请买这本书”的小纸条,过了一段时间去看,有些纸条因为书架号变了得重写,还有些纸条说得先去盖馆员老师的章。所以现在我塞了纸条不会忘掉,而是偶尔会一次性拿出来,读一读看它们是不是依然合用。
链接
所有资料均于 2026-11-13 查阅。
- GitHub 文档: REST,查询单笔 PR 与
mergeable, GraphQLMergeStateStatus, REST,创建提交与signature, REST,树与truncated - 已重新变基的 PR: ai-dynamo/dynamo#16465(导致冲突的合并 #17229), containers/kubernetes-mcp-server#1518
- 需要重写的 PR: NVIDIA/dcgm-exporter#760(导致冲突的合并 #768), charmbracelet/catwalk#676(导致冲突的合并 #611)
- 丢失文件模式的 PR: containers/podman-compose#1585
- 以草稿提交的 PR: kubevirt/kubevirt#19889
- 贡献规则: KubeVirt AI 贡献政策, Moby·Docker AI 政策, fake-gpu-operator CONTRIBUTING, kubeflow/pipelines CONTRIBUTING