依賴裝完就跑不起來?用三層鎖定終結建置環境漂移
依賴裝完就跑不起來?用三層鎖定終結建置環境漂移 很多工程師把「環境能跑」當成運氣問題:本地好好的,換一台機器、換個 CI runner 就崩潰。其實大多數這類事故,都來自一個習慣——依賴只用 caret 範圍安裝,lockfile 要嘛沒有,要嘛形同虛設。 漂移是怎麼發生的 package.json 裡寫 &qu…
- 入庫時間
- 下載量
- 0
- 人審標記
- 已人工審核
依賴裝完就跑不起來?用三層鎖定終結建置環境漂移
很多工程師把「環境能跑」當成運氣問題:本地好好的,換一台機器、換個 CI runner 就崩潰。其實大多數這類事故,都來自一個習慣——依賴只用 caret 範圍安裝,lockfile 要嘛沒有,要嘛形同虛設。
漂移是怎麼發生的
package.json 裡寫 "foo": "^2.1.0",聽起來只鎖了大版本,實際上 npm 會在 2.x 的任意更新裡挑一個「當時最新」。今天裝到 2.1.3 歲月靜好,下個月新 clone 的仓库裝到 2.7.0,而 2.7.0 裡某個行為微調(預設編碼變了、peer dependency 收緊了、匯出的型別改名了),你的建置就紅了。更糟的是:錯誤發生在別人機器上,你連重現都要花半天。
這類漂移有三層,很多人只鎖了第一層:
- 直接依賴版本漂移——lockfile 能解決的部分。
- 傳遞依賴漂移——A 依賴 B
^1.0,lockfile 同樣要提交,否則每台機器解析結果不同。 - 環境本身漂移——Node/Python 版本、系統函式庫、時區與 locale。lockfile 管不了這一層。
三層各用什麼鎖
- JS:提交 lockfile,CI 用 frozen。 用
npm ci(或pnpm install --frozen-lockfile)而不是npm install。npm ci嚴格照鎖檔案安裝,lockfile 與 package.json 不一致就直接失敗——失敗在 CI 裡,好過失敗在產品線。lockfile 必須進 git,這不是選項。 - Python:鎖定完整解析結果。 用 pip-tools(requirements.txt 由 requirements.in 編譯而來)或
uv lock。只在 requirements.txt 裡寫幾個頂層套件等於沒鎖。 - 環境層:明確宣告 runtime 版本。 Node 用
.nvmrc/ package.json 的engines,Python 用.python-version。Docker base image tag 不要用latest,用node:22.14.1-bookworm這種精確到 patch 的 tag。
升級要主動、批次、可回滾
鎖死不是永不升級。正確節奏是:由你決定何時升級(Renovate/Dependabot 自動開 PR),每次升級都走 CI 全量測試,PR 裡能一眼看到 diff,出問題 revert 一個 commit 就能回退。對比之下,「不鎖版本」的升級是被動發生的——某次重新安裝不小心吃進新版本,測試沒有覆蓋到,使用者先於你發現。安全通告怎麼辦?鎖定檔案不等於放棄修補:通告發布後,針對性地 bump 那一個套件,跑 CI,合入。這比每天頂著未知漂移上線要可控得多。
lockfile 壞了怎麼辦
lockfile 衝突是合併時最常見的惡夢。實用策略:衝突不要手工縫合,直接在目標分支上刪掉 lockfile 重新生成一次,讓解析器重新算一遍完整依賴樹,再把生成結果連同 diff 一起提交。手工改 lockfile 裡的版本號,等於繞過解析器做它的工作,幾乎必然製造出「裝得上但跑不對」的幽靈狀態。monorepo 另一個常見坑是 workspace 內部套件版本與外部依賴交織,pnpm workspace 的 workspace:* 協定能保證內部套件永遠指向本地版本,發布時再替換成真實版本號,這是目前最省心的方案。
自查清單
- 仓库裡有 lockfile 且被 git 追蹤嗎?別在 .gitignore 裡手滑忽略它。
- CI 用的是
npm ci/--frozen-lockfile嗎? - Docker base image tag 精確到 patch 了嗎?
- Node/Python 版本有沒有宣告檔案?
- 升級是走 PR 流程,还是靠某次
npm update碰運氣?
五條裡中了兩條以上,你的「環境能跑」就已經是負債,只是還沒到期。鎖定環境的本質,是把「什麼時候升級、升級到什麼」從隨機事件變成你的決策。這比多寫兩百行業務代碼更能減少半夜告警。