我專案的文件有規則。

「一份文件,一個職責。」「超過 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

全部失敗。全部紅燈。包含我即將修改的檔案,以及好幾個月沒人碰過的檔案,通通亮紅燈。

人類在這裡有兩個選擇。

  1. 先修好全部 63 個,再開啟 CI(包含那個 1,203 行的怪物)
  2. 加上忽略清單,把 63 個靜音,繼續前進

第二個選擇很誘人吧?對我來說也是。一個有 63 行的 .lintignore,再加一行註解說「之後再移除」。

那你到底什麼時候要移除那個清單?

想想那個檔案實際上是什麼。它是第二個真相來源。

規則存在於規則文件裡。真正例外的東西只存在於忽略清單。兩個檔案,逐漸分歧。

而且忽略清單永遠不會縮小。沒有人有動機從裡面刪除一行。如果它縮小了,那只是意外。

(我從沒看過哪個專案的「之後再移除」真的發生過。)

那第一個選擇呢,先修好所有東西?也是個陷阱:你在修的時候,CI 是不存在的。你最常碰文件的時期,恰恰是沒有守門人的時期。

有人修 1,200 行文件裡的一個錯字,應該被迫拆分它嗎?

我就是卡在這裡。

假設你開了一個 PR,只修那個 1,203 行文件裡的一個錯字。如果你天真地實踐「童子軍規則」,那個 PR 會因為「這個文件超過 45 行」而失敗。

你剛剛要求一個只改一個字元的人,去做 1,203 行的重構。

如果團隊有這種 CI,會發生什麼事?

沒人再修錯字了。

走過破掉的窗戶變成理性的選擇。規則變成一台懲罰改進的機器。

那它到底應該對什麼生氣?只對這個 PR 帶來的東西生氣。

  • 新檔案超過 45 行 → 失敗(從現在開始不要新增債務)
  • 碰了既有的 1,203 行 → 通過(不是你的錯)
  • 把既有的 1,203 行增加到 1,250 行 → 失敗(不要讓它變更糟)

不是「新或舊」,而是「它有沒有變長?」

而且這不需要忽略清單,因為 git 已經知道。把檔案跟 merge base 做比較。它有變長嗎?這就是全部的判斷。不會產生第二個真相來源。

我自己的規則文件被我自己的 lint 打槍

我在把這個設計寫進規則文件時,CI 亮紅燈了。

LONG  docs/rules/doc-ci.md: 46 lines > 45

Enter fullscreen mode Exit fullscreen mode

是我。我就是犯人。

實作「對既有檔案增加行數會失敗」的人,增加了行數,解釋的就是這個功能,然後失敗了。

我笑了。然後我修好它(降到 44 行)。

我沒想到,我的設計有效的證明,會從我自己脖子上出現。

老實說,我很 impressed。它阻止了應該被阻止的人。它沒有因為他寫了這個規則,就輕易放過他。我用自己驗證了我不是例外。

移動檔案算作新增

還有一件事。

重新組織資料夾意味著大量的 git mv。在新路徑上,merge base 裡沒有這個檔案。

對 CI 來說,這是新加入的檔案。新檔案會毫不留情地失敗,所以只是被移動的 1,203 行檔案被要求拆分。一次十個。

修正方式是教檢查器追蹤重命名:對於移動的檔案,跟它在舊路徑的行數做比較。沒有變長,就不失敗。

整理結構會強制進行無關的重構。我跑了之後才知道。

我想依路徑排除機密(而這是錯的)

還有第二道守門:這個 commit 是否包含機密——API 金鑰、token。

它在一個文件 PR 上失敗了。兩個命中。

  • Authorization: Bearer <TOKEN_NAME> —— 角括號,顯然是佔位符
  • 一個以固定假值結尾的範例 id,顯然是範例

假陽性。而且不是我剛寫的行:是已經存在好幾個月的行,因為我拆分檔案而被重新偵測為「新加入」。

誘惑來了。

「只要把整個 docs 資料夾從掃描中排除就好。」

一行。一行就解決了。

我很慶幸我沒有這樣做。

因為在同一次工作裡,我發現一個生產環境的簽名金鑰以明文形式存在於文件裡。我寫的。我忘記的。

當我發現它時,我腦中浮現的是:感謝老天我沒有選擇那一行的逃脫方式。

如果我排除了這個資料夾,那個金鑰就會被靜靜地、永久地放在掃描範圍之外。

排除形狀,而不是位置。

只有角括號佔位符的形狀是豁免的。真正的金鑰有不同的形狀,所以它總是會觸發。

我測試了兩個方向,以確保正確。

  • 佔位符 → 未偵測到(符合預期)
  • 放入看起來像真金鑰的東西 → 三個規則同時觸發,正確失敗

第二個檢查比綠燈更重要。

你沒看過它失敗的守門,就不是守門。

債務歸零的那天,推廣只花了三行

從那裡開始,我拆分了那 47 個。63 → 50 → 36 → 27 → 17 → 0。

文件最後變成 99 個資料夾裡的 488 個檔案。每個資料夾都有索引,每個檔案都低於 45 行(整段複製的腳本有文件化的例外——15 個)。

債務歸零,所以我把 CI 升級為「每個檔案,永遠,毫不留情」。

差異:

  • 在環境中加入 STRICT: '1'
  • 刪除遷移時期的「只檢查你碰過的」

三行。

因為我從來沒有建立忽略清單,所以沒有第二個真相來源需要拆除。分階段推出完全基於 git 裡已經存在的事實,所以收攏它不花任何代價。

如果我那天寫了那 63 行,我現在會一個一個看它們問「我們還需要這個嗎?」那就不會是三行了。

機器能掌握什麼,以及不能掌握什麼

以一個誠實的註記作為結尾。

這個 CI 保證了結構。沒有遺漏的索引。沒有超過 45 行的東西。沒有壞掉的連結。沒有新的機密。

文字是否符合實作,不是機器能掌握的。

你可以用建立一個守門,要求文件在程式碼變更時也要更新。但那是猜測:它不是失敗合法的 PR,就是製造出一種有覆蓋的舒適感。兩者都不如沒有。

所以新鮮度被寫進規則。如果你讀了一份文件並發現差異:只修復你能在真實系統上驗證的東西。如果你無法驗證它,不要碰文字——開一個 issue 並標記它可疑

根據猜測重寫是最糟糕的結果,因為錯誤會戴上「已審閱」的徽章。

這個規則在工作期間觸發了兩次。一次是資料夾佈局描述與實際情況不符(已開 issue,未修復)。一次是在拆分過程中,我注意到我們過去拒絕提案的原因即將消失,於是恢復了它們。

你無法重建某件事被拒絕的原因。而一旦它消失了,同樣的提案會在六個月後回來。

結語

關於機器強制規則的討論通常是關於偵測:我們如何抓住它。

真正重要的是設計要讓什麼通過

如果搞錯了,守門會以兩種方式死亡之一:所有東西都紅燈直到人們忽略它,或者忽略清單胖到什麼都保護不了。

註記「之後再移除」的忽略清單,不會在之後被移除。

所以一開始就不要建立它。