我專案的文件有規則。
「一份文件,一個職責。」「超過 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 個靜音,繼續前進
第二個選擇很誘人吧?對我來說也是。一個有 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,未修復)。一次是在拆分過程中,我注意到我們過去拒絕提案的原因即將消失,於是恢復了它們。
你無法重建某件事被拒絕的原因。而一旦它消失了,同樣的提案會在六個月後回來。
結語
關於機器強制規則的討論通常是關於偵測:我們如何抓住它。
真正重要的是設計要讓什麼通過。
如果搞錯了,守門會以兩種方式死亡之一:所有東西都紅燈直到人們忽略它,或者忽略清單胖到什麼都保護不了。
註記「之後再移除」的忽略清單,不會在之後被移除。
所以一開始就不要建立它。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.