昨天結尾說到,這幾天做的 review 都有一個共同問題:全都要我記得做。當步驟越來越多,就容易漏掉,所以今天把檢查搬進 CI。
但在搬之前要先討論
這個檢查,真的檢查得到東西嗎?
前端有一條規矩寫在 CLAUDE.md:vitest 走 esbuild,只剝型別不檢查型別,所以測試綠了之後必須另外跑 npx tsc --noEmit。
我用一個型別錯誤驗證這條規矩,四種寫法的 exit code:
| 指令 | exit | 抓到 TS2339 |
|---|---|---|
npx tsc --noEmit |
0 | ❌ |
npx tsc -b |
2 | ✅ |
npm run typecheck |
2 | ✅ |
npm run build |
2 | ✅ |
第一列就是文件裡寫的那一條。原因在根目錄的 tsconfig.json——它是 Vite 範本給的 project-references 結構,"files": [],所以不帶 -p 的 tsc 沒有輸入。「沒有東西要檢查」不是錯誤,它就安靜地 exit 0。
一個永遠通過的檢查,比沒有檢查更糟。
解法:每加一道閘門,就種一個它應該抓到的錯,確認它真的紅。 這跟 TDD 的 RED 是同一件事,只是對象從程式換成 CI。
前端裝了 oxlint。跑一次,它印出一則違規,然後 exit code 是 0。如果就這樣放進 CI,它會忠實地把訊息印在 log 裡,然後綠燈亮起。第二則也一樣,第十則也一樣。
沒人讀的 warning 是沒有效果的。
解法:一個檢查放進 CI 之前,先回答「它紅的時候我會停下來嗎」。 答案是否,就不要放。
最經典的一種,而且它看起來最有道理。
這個專案的測試跑完會印 999 個 warning,沒有人讀。既然沒人讀,那就讓它擋——pytest -W error。第一次跑:
58 failed, 17 passed, 38 errors
96 個工作單元瞬間壞掉。這時心裡冒出來的第一句話一定是「這條太嚴格了」,而把它調低只要刪一行。
但先看那些 warning 是什麼:999 個全部是同一件事——.env 裡沒設 JWT_SECRET,吃到程式裡的預設值,而那個值只有 21 bytes,低於 HMAC-SHA256 的建議下限。
補一行進 .env,96 變 1。剩下那一條是刻意用假鑰匙簽偽造 token、驗系統會不會拒收的測試——它的 warning 是對的,偽造的鑰匙本來就不該合格。所以放行寫在那一條測試上:
@pytest.mark.filterwarnings("ignore::jwt.warnings.InsecureKeyLengthWarning")
def test_a_token_signed_with_another_secret_is_refused(...):
最後是 113 passed、零 warning。
門檻要放行,只能放行具體的那一個點,而且要寫得出理由,不是回去放寬全域設定。
解法:擋住的時候先問一句——是門檻錯了,還是東西錯了? 這跟昨天那個「這條測試紅掉,是因為需求變了,還是我把需求做壞了」是同一個問題。今天這次,999 個裡面 998 個是東西錯了。
三者的症狀都是綠燈,但驗證方式不一樣:
| 失敗模式 | 怎麼驗 |
|---|---|
| 沒在檢查 | 種一個它該抓到的錯,看它紅不紅 |
| 不會擋 | 看 exit code,不要看輸出 |
| 被調低 | 翻 git log:這條門檻的設定值,歷史上只往上動過嗎 |
第三條之所以要翻歷史,是因為它當下看不出來——一條被調低過的門檻,跟一條本來就訂得很鬆的門檻,設定檔上長得一模一樣。
| 閘門 | 擋什麼 |
|---|---|
alembic upgrade head |
migration 本身壞掉——測試走 create_all,從來不碰 migration |
alembic check |
模型與 migration drift |
pytest(warning = error) |
測試 + 所有 warning |
npm run typecheck |
型別(不是 npx tsc --noEmit) |
oxlint --deny-warnings |
lint |
npm run test · npm run build |
前端行為 + 打包 |
明天:一張工作單元交付完之後文件回填、決策記錄、issue 收尾。