我项目的文档有规则。
“一份文档,一项职责。”“超过 45 行的内容要拆分。”
这句话是我前天写下的。
实际情况是这样的。
- 没有 README(没有索引)的文件夹:37 个中有 25 个
- 存放工程规则的文件夹:11 个文件,零索引
- 违反 45 行规则的文档:47 份
- 最严重的违规者:1,203 行
把规则写下来并不意味着它就成了真正的规则。
这道理显而易见。但当你拿到一份 47 份文档的清单,而其中每一份都是你自己亲手违反的规则时,这种感觉在看到一半时就笑不出来了。
(两天前的我确实打算遵守它。)
我重写了规则本身,结果规则文件达到了 150 行
计划已经很清楚了:让机器来强制执行。让 CI 失败。
这意味着首先要把规则写得当。需要 README、文件夹布局、更新义务,以及 CI 实际检查的内容。等全部写完时,规则文件已经超过 150 行。
规则文件本身违反了 45 行规则。
现在,如果你说“规则文件是特殊的,可以豁免”——你刚刚做了什么?
你创造了一个“规则可以豁免”的先例,而且从那天起这个先例就永久存在了。
从此以后,每当有人超过 45 行,他们都可以说“规则文件也这么干”。而他们是对的。
所以我把它拆分了。八个文件,都在 45 行以内。
如果我不能遵守自己的规则,那这个规则就从来不值得写。
CI 上线的那一刻,63 个现有违规暴露了出来
接下来是真正的任务:编写检查器,将其接入 CI。
运行它,结果当然是:
63 violations
Enter fullscreen mode Exit fullscreen mode
一切都失败了。全红。包括我即将修改的文件和几个月没人打开过的文件,都一样红。
人类在这里有两个选择。
- 先修复全部 63 个问题,然后再开启 CI(包括那份 1,203 行的怪物)
- 添加一个忽略列表,静默处理这 63 个问题,然后继续
选项 2 很诱人。至少对我来说是这样的。一个包含 63 行的 .lintignore 文件,附带一句注释“稍后删除”。
那么,你到底什么时候删除那个列表?
想想那个文件实际上是什么。它是第二个真相来源。
规则存在于规则文档中。真正被豁免的内容只存在于忽略列表中。两个文件,越来越背道而驰。
而忽略列表永远不会缩小。没有人有动力从其中删除一行。如果它缩小了,那只是个意外。
(我从未见过一个项目中“稍后”这个承诺兑现过。)
那选项 1 呢,先修复所有问题?也是个陷阱:当你在修复时,CI 是不存在的。你最频繁地修改文档的时期,恰恰是没有门禁的时期。
有人在 1,200 行的文件中修复一个错别字时,是否应该被迫拆分它?
我就是在这里停下来的。
假设你打开一个 PR,只修复那份 1,203 行文档中的一个错别字。如果天真地实施“童子军规则”,这个 PR 就会因为“此文档超过 45 行”而失败。
你刚刚要求一个只改了一个字符的人进行 1,203 行的重构。
这样的 CI 会对团队产生什么影响?
没人再修复错别字了。
视而不见破窗变成了理性的选择。这个规则变成了惩罚改进的机器。
那么,它应该对什么发怒?只针对这个 PR 带来的变化。
- 新文件超过 45 行 → 失败(不要从现在开始增加债务)
- 修改了现有的 1,203 行 → 通过(不是你的错)
- 但将现有的 1,203 行增长到 1,250 行 → 失败(不要让它变得更糟)
不是“新文件还是现有文件”,而是“它是否增长了”。
这不需要忽略列表,因为 git 已经知道。比较文件与合并基线的行数。它变长了吗?这就是全部的判断依据。没有第二个真相来源诞生。
我自己的规则文档被我自己的 lint 标记为失败
我正在把这个设计写入规则文档时,CI 变红了。
LONG docs/rules/doc-ci.md: 46 lines > 45
Enter fullscreen mode Exit fullscreen mode
是我。我就是罪魁祸首。
那个实现了“向现有文件添加行会导致失败”功能的人,向文件添加了行,解释了这个功能本身,然后失败了。
我笑了。然后我修复了它(减到 44 行)。
我没想到,我的设计的证明会来自我自己的脖子。
老实说,我印象深刻。它阻止了应该被阻止的人。它不会因为那个人写下了规则就对他网开一面。我得以用自己验证我不是例外。
移动文件算作添加新文件
还有一个问题。
重组文件夹意味着大量 git mv。在新的路径上,文件在合并基线中不存在。
对 CI 来说,这就是一个新添加的文件。新文件会无情地失败,所以仅仅移动的 1,203 行文件被要求拆分。一次就有十个。
修复方法是教检查器跟踪重命名:对于移动的文件,比较其在旧路径的行数。如果没有增长,就不失败。
修复你的结构会强制进行无关的重构。在运行它之前,我对此一无所知。
我想按路径排除机密(那是错误的)
还有第二个门禁:这个提交是否包含机密——API 密钥、令牌。
它在一个文档 PR 上失败了。两个命中。
-
Authorization: Bearer <TOKEN_NAME>— 尖括号,显然是占位符 - 以固定虚拟值结尾的示例 ID,显然是示例
误报。而且不是我刚刚写的行:那些行已经存在几个月了,因为我拆分了文件而被重新检测为“新添加”。
诱惑来了。
“干脆把整个 docs 文件夹从扫描中排除。”
一行代码。一行代码就能解决。
我很庆幸我没有这么做。
因为在同一次会话中,我发现了一个生产签名密钥以明文形式存在于文档中。是我写的。是我忘记的。
当我发现它时,我脑子里想的是:感谢上帝我没有采取那一行代码的捷径。
如果我排除了这个文件夹,那个密钥就会被悄悄地、永久地置于扫描之外。
排除形状,而不是位置。
只有尖括号占位符的形状被豁免。真正的密钥有不同的形状,所以它总是会被触发。
我测试了两个方向,以确保正确。
- 占位符 → 未检测到(符合预期)
- 放入看起来像真实密钥的内容 → 三个规则同时触发,正确失败
第二个检查比绿色的那个更重要。
你没有看到它失败的门禁就不是门禁。
债务归零的那天,推广只用了三行
从那里我拆分了 47 个。63 → 50 → 36 → 27 → 17 → 0。
文档最终达到 488 个文件,分布在 99 个文件夹中。每个文件夹都有索引,每个文件都在 45 行以内(复制粘贴的完整脚本是记录在案的豁免——15 个)。
债务归零,所以我将 CI 提升为“每个文件,始终,无情”。
差异:
- 在环境中添加
STRICT: '1' - 删除迁移期的“只检查你修改的内容”
三行。
因为我从未创建忽略列表,所以没有第二个真相来源需要拆除。分阶段的推广完全基于 git 中已存在的事实,所以收起它几乎没有成本。
如果我那天写了那 63 行,我现在会逐一检查它们,问“我们还需要这个吗?”那不会是三行。
机器能掌控什么,不能掌控什么
一个诚实的结束语。
这个 CI 保证了结构。没有缺失的索引。没有超过 45 行的内容。没有断开的链接。没有新的机密。
文字是否与实现相符,这不是机器能掌控的。
你可以建立一个门禁,要求在代码更改时更新文档。但那是猜测:它要么让合法的 PR 失败,要么制造一种覆盖的虚假舒适感。两者都不如没有。
所以新鲜度被写入了规则。如果你阅读一份文档并发现差异:只修复你能在真实系统上验证的内容。如果你无法验证它,不要修改文本——打开一个 issue 并标记为可疑。
基于猜测重写是最糟糕的结果,因为那样错误就会戴上“已审查”的标签。
那个规则在工作中触发了两次。一次是当文件夹布局描述与实际情况不符时(已记录,未修复)。一次是在拆分过程中,我注意到我们过去拒绝提案的原因即将消失,于是恢复了它们。
你无法重建某件事被拒绝的原因。一旦它消失了,同样的提案会在六个月后再次出现。
结束语
关于机器强制规则的讨论通常是关于检测的:我们如何抓住它。
真正重要的是设计让什么通过。
如果设计错了,门禁会以两种方式之一死亡:对一切都变红直到人们忽略它,或者一个臃肿的忽略列表什么都不保护。
一个标注“稍后删除”的忽略列表不会稍后删除。
所以一开始就不要创建它。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.