How Open Source Projects Change HandsHow Open Source Projects Change Hands
How Open Source Projects Change Hands
Andrew Nesbitt
《开源项目的各种花式死法》(Dumb Ways for an Open Source Project to Die)列举了项目最终走向消亡的各种方式,其中大多数条目都描述了这样一个时刻:本应将维护权移交给其他人,却没有这么做。这里是另一份更简短的清单:现有用于项目交接的机制,以及每种机制分别能由谁来触发。
由维护者决定
指定继任者。维护者挑选一个人并移交控制权。这是每个人在谈论继任时都会想到的模式,而且它几乎没有配套的基础设施:只是一次私下沟通,随后更改一些权限。背景调查完全取决于离任维护者想怎么做,event-stream 事件就是这么发生的:“他给我发了封邮件说他想维护这个模块,所以我就交给他了。” xz 事件的接管也是相同的机制,只是操作得更有耐心,通过马甲账号不断制造压力,迫使过度劳累的维护者移交权限。
CPAN 是唯一一个为这个阶段命名的注册中心:如果一个模块的权限列表中包含了伪用户 HANDOFF,就说明该模块的维护者正在寻找接手的人,这与所有真实所有者一样,都记录在同一个机器可读的权限文件中。
继任者设置。自 2020 年起,GitHub 推出了账户继任者功能:你可以在账户设置中指定一个人,在出示死亡证明并等待 7 天后,或者出示讣告并等待 21 天后,他们就可以归档你的公开代码库,或者将其转移到他们控制的账户中。这是本列表中唯一专为维护者去世的情况而设计的条目。它涵盖了代码库,却没有涵盖从中发布代码的注册中心账户,而且没有任何注册中心有类似的功能。
开放认领。维护者不再指定某个人,而是将项目标记为可用并等待。CPAN 同样拥有最古老的版本:将伪用户 ADOPTME 设为所有者,意味着该模块可供认领;而 NEEDHELP 则表示所有者希望寻找联合维护者,自己并不退出。RubyGems 在 2014 年 10 月提出了一个类似方案,其设计初衷是“最理想的情况是实现开发者之间的直接沟通,无需外部介入”,并在 2021 年底将其作为所有权调用和请求功能发布。Debian 的 RFA(Request For Adoption,认领请求)对软件包起着同样的作用:维护者会一直工作,直到出现继任者。在这些注册中心之外,类似的做法就是添加 repostatus 徽章,或在 README 中写上一句“寻找维护者”,不过没有任何工具会去读取这两种信息。
主动终结。维护者也可以拒绝寻找继任者,而各注册中心对这种决定的支持力度,比上述任何一种交接方式都要大。Packagist 的 abandoned(废弃)字段会接收一个替代包的名称,并且 composer 在每次安装时都会打印出“Package X is abandoned, you should avoid using it. Use Y instead.”(包 X 已废弃,应避免使用。请改用 Y。)。NuGet 的弃用功能可以附带一个替代包,而且完全被弃用的旧包可以申请将其搜索排名转移给与其共享所有者的继任包。Maven 则为已迁移的坐标提供了 relocation(重定位)元素。
PyPI 在 2025 年增加了项目归档功能,现在可以通过其 API 提供状态标记。GitHub 归档会使代码库变为只读状态,并显示横幅提示。指向继任包的做法是将用户引导至不同的代码,而不是将代码移交给另一位维护者。
由其他人决定
注册表裁决。第三方听取关于无人维护名称的申请并作出裁决。PyPI 实行了最正式的版本:PEP 541 通过三个并列条件(无法联系到所有者、十二个月内无发布、所有者无活动)定义了“弃置”。申请人必须证明其尝试联系失败,并在其自己的 fork 上做出了改进,同时还要解释为什么重命名 fork 行不通。它在 2025 年处理了超过 500 个请求,并清理了长达九个月的积压。当所有者“完全消失”时,PAUSE 管理员会授予 CPAN 模块的共同维护权,条件是采用者“必须尊重作者的工作和设计”。Hackage 管理员会在你尝试联系原维护者并公开表明意图后,将你添加到该软件包的维护者组中。
npm 曾运行过一个为期四周的调解流程,但在 2021 年 2 月发生一次错误转移后将其暂停;目前的政策是“我们不会仅仅因为另一个用户想要某个名字就转移软件包、组织或用户名的所有权”。crates.io 在 2024 年取消了其转移调解政策,理由是请求随着注册表的增长而增加,而且“通常甚至不会成功”,并援引了 PyPI 团队“无法跟上这些请求”的情况。
孤立软件包池。Linux 发行版将无人维护的软件包视为需要填补的空缺,并为此构建了状态机。Debian 将整个生命周期编码为 WNPP bug:O 代表孤立,RFA 代表请求收养,RFH 代表寻求帮助,ITA 代表有意收养,ITS 代表有意拯救维护者存在但不活跃的软件包。孤立操作会将软件包的 Maintainer 字段设置为 Debian QA Group,而 MIA 团队会通过分阶段的升级措施追踪无响应的维护者,最终将其名下的所有软件包全部孤立。
Fedora 会将被孤立的软件包重新分配给一个名为“orphan”的实体用户,任何打包者只需点击一个按钮即可接管。如果六周内无人认领,该软件包将通过向其仓库提交一个名为 dead.package 的文件而被废弃。如果维护者两周保持沉默,AUR 将批准孤立请求;如果该软件包被标记为过期已满 180 天,则会自动批准。CRAN 直接将 maintainer 字段标记为 ORPHANED,并允许任何人在未经前任维护者同意的情况下接管。所有的语言包注册表都没有类似的机制:发行版软件包是具有记录在案看护者的公共财产,而注册表名称则属于最先发布的人。
2026 年 6 月,一名攻击者接管了四百多个被孤立的 AUR 软件包,并在每个 PKGBUILD 中添加了一行 npm install。获取的 npm 软件包的 preinstall 钩子安装了凭证窃取程序和 eBPF rootkit。第一份报告是一名用户注意到某个 VR 流媒体工具的 PKGBUILD 中出现了 npm install。每次有效载荷包被下架、且安装行被 grep 搜出后,下一批接管就会发生切换:npm install 变成了新软件包上的 bun add,然后变成了 'b''u''n' 'a'"d""d"。在此次事件发生期间,Arch 限制了账户创建和软件包接管。
单体仓库。Homebrew、nixpkgs 和 conda-forge 将每个包的定义都保存在同一个共享仓库中,因此不需要移交任何密钥,权力的交接就是一个 pull request(合并请求),这也是本列表中最接近“由任何人决定并带有审核者参与”的机制。homebrew/core 根本不记录每个 formula 的维护者;维护权相当于 tap 仓库接受其变更提交的任何人。nixpkgs 将包的维护者列为单个文件中的条目,而接手一个被遗弃的包只需在文件中添加你的名字,这是该发行版的“名义看护者”模式,且省去了遗弃流程。
基金会托管。项目可以移交给一个其寿命比任何个人维护者都要长的组织。Apache 项目由 PMC(项目管理委员会)而不是个人运营,因此权力交接是该持久结构内部成员身份的变更;当社区解散时,项目会进入 Attic,这是一种具有自身文档化流程的只读终态。OpenJS 基金会为其托管项目运行着一套晋升与名誉成员(emeritus)的生命周期机制。基金会承担了“公交车因子”(bus factor,即关键人员流失风险),代价是增加治理开销,而任何注册表中的绝大多数包都太小,根本不值得做这种交易。
企业托管。当一家公司掌控项目时,权力交接是一个组织架构上的事件:从内部来看,这是附带雇佣关系的指定继任者机制,而从外部则完全不可见。当 antirez 在 2020 年从 Redis 卸任时,他选择了两位继任者,并拒绝设计后续的治理方式。他和这两位继任者都是 Redis Labs 的员工,而且公司持有该商标。社区的权力交接发生在 2024 年,当时更改许可证(relicense)促使 Madelyn Olson 和大部分外部核心团队在 Linux 基金会下 fork 出了 Valkey。这种安排会一直持续,直到团队被裁撤,项目沦为上一篇文章中提到的“企业孤儿”。
由任何人决定
分叉。上述的所有机制最终都会退回到这一项:PEP 541 要求在 PyPI 考虑转移之前,必须有一个可用的 fork,而 crates.io 对于联系不上的所有者的建议是起个新名字。Fork 只是复制了代码,原项目保留了注册表中的名称,这意味着它也保留了安装计数以及引用它的每个 manifest(清单)和 lockfile(锁文件)。死亡名单中那些处于 fork 僵局和许可证突然撤销(rug-pull)的案例都是这样发生的:开发转移到了新 repo,而安装操作仍然解析到旧 repo。
“留转发地址”的问题在代码托管平台(forge)层只有一种解决方案:GitHub 仓库转移会将旧的 URL 无限期地重定向到新 URL,尽管在旧名称下创建新仓库会永久删除该重定向,这本身就构成了一种攻击面。Go modules 继承了这两个属性,因为模块路径包含了托管它的 repo:没有需要转移的注册表账户,并且一个 fork 位于新路径下,在 go.mod 中留有一条 Deprecated 注释作为转发地址。被转移的 repo 会通过重定向继续解析,而在 manifest 中声明为 Git URL 的 Swift 包,其行为方式相同,但没有等同于 Deprecated 注释的机制来转发到 fork。
陌生人。Avelino 等人研究了 1,932 个流行的 GitHub 项目,发现其中 16% 的项目已被所有核心开发者抛弃;这些项目中有 41% 存活了下来,而 86% 存活下来的项目是由一位新的核心开发者拯救的。最常见的动机是“因为我正在使用该项目”,而救援者报告的障碍并非技术层面的:缺乏时间,以及由于能够授权的人已经离开而无法获得 push 权限。
该研究描述的救援过程是非正式的:需要该 package 的人会去联系仍然拥有访问权限的人,而交接仅仅是一封电子邮件和一些权限更改,没有任何系统对其进行标记或记录。event-stream 易手的过程也是如此,授予访问权限的维护者根本无法区分对方是救援者还是攻击者。当 orphan pool 已经移除了该维护者时,正如上述的四百个 AUR packages 一样,根本没有人处于能够做出区分的位置。
需要完整排版与评论请前往来源站点阅读。