如何搬遷到 Monorepo

前言
分析 monorepo 好壞的文章很多,但如果已經決定要從 polyrepo 搬到 monorepo,反而很難找到在講搬遷過程的教學或討論。最近剛好做了一次,就來分享一下整個過程遇到的問題,還有當初是怎麼一步一步做、中間考慮了哪些東西。
現況與動機
我們團隊用 .NET 開發公司的商業應用,web API、MQ 這些服務都是用 polyrepo 的方式各自管理,而這些服務之間又常常要互相溝通。
第一個碰到的麻煩是這些服務對外的 contract 很難維護,只要 contract 有改動,就得把所有 endpoint 都翻出來確認一遍。
再來就是有大量相似甚至完全一樣的商業邏輯散落在各個 repo 裡,改一次邏輯的成本非常高。而且說到底這些服務都是在支撐同一個產品,用 polyrepo 管理對 domain 邏輯的內聚性其實沒什麼幫助。
挑戰
最重要的前提是產品要盡可能維持可用,所以我們必須在很短的時間內完成切換。偏偏團隊目前用的是 git-flow,feature branch 的結構相當複雜,加上新需求一直進來,也不可能為了換 repo 的架構就把開發停下來。總結來說,我們需要一份能夠平順轉移的計畫,花點時間沒關係,但過程要可預期,出事的時候也要能輕鬆 rollback。
搬遷計畫
整個搬遷過程會拆成幾個階段,這邊先簡單介紹,後面幾節再細講。
- Phase 1:準備一個專用的 repo
第一步是開一個新的 repo 來當 monorepo,再把原本各個 repo 的內容全部搬過去。因為版控用的是 Git,這一步不難,而且我們希望在新的 repo 裡把所有 commit 都保留下來。這段期間所有開發活動還是照舊在原本的 repo 上進行,新的 repo 是拿來準備 CI/CD 用的,所以不需要跟到最新的 commit,同時也可以在上面放整合測試。 - Phase 2:改 CI/CD pipeline
原本每個 repo 都有自己的 CI/CD 設定,要全部搬到新的 repo 上很花時間,理想上還要有一個環境可以拿來測這些 pipeline 搬得對不對。 - Phase 3:在 monorepo 上開發
上面都準備好之後,接著就是把 monorepo 裡的程式碼更新到最新,更新完所有開發活動就可以平順地轉移到新的 repo 上。
Git
整個過程中 Git 幫了很大的忙。要管理多個 repo 的話可以用 submodule 或 subtree。submodule 很好上手,但並沒有真的把程式碼併進 superrepo,彼此之間也沒辦法互相引用,而我們的目標就是要讓各個 repo 的程式碼能夠重用,submodule 顯然不適合。subtree 則是把 subrepo 整個當成 superrepo 的一部分,缺點是 subrepo 有變動的時候同步不太容易。以搬遷這個情境來說,subtree 是比較合適的選擇。
Step 1:把原本的 repo 加成 remote:
用下面這個 Git 指令
git remote add <name> <url> # <name>: 你想取的 repo 別名 # <url>: 要加入的 repo取一個 repo 別名
<name>可以讓後面的 subtree 指令簡單一點。Step 2:把所有 subtree repo 加進來:
git subtree add --prefix=<folder path> <name> <branch> # <folder path>: repo 要拉進來的目標路徑 # <name>: repo 別名 # <branch>: 想拉的 branch有幾件事要注意。原本的 repo 結構可能長這樣:
# projectA |- src |- projectA.sln |... |...全部拉進來之後,新的 repo 結構會變成這樣:
# monorepoProject |- src |- ProjectA | |- src | |- projectA.sln | |... | |... |- ProjectB |- src |- projectB.sln |... |...因為 branch 的狀況很複雜,
<branch>這個參數要特別小心。為了讓搬遷計畫單純一點,我建議不要去動 subtree 資料夾裡的檔案,因為這個階段其他成員還是在原本的 repo 上開發,我不想在這時候改變大家的開發習慣。
關於 CI/CD
改 GitLab CI script 的時候遇到兩個問題。第一個還算合理,每個 repo 本來就有自己的設定,合成一個 repo 之後要把所有 script 整合起來就是要花時間。第二個問題則是因為我們最近改了工作流程,以前是手動觸發 pipeline 去 build 跟 deploy,最近改成 pull request merge 之後自動觸發,搬到 monorepo 之後就變成只要有 merge,每個專案都會重新 build 再 deploy 一次,這顯然不是我們要的行為。第二個問題的解法是用 GitLab 的 rules 搭配 detect changes,針對各個專案資料夾去判斷。
聊一下 monorepo 工具
目前還沒有打算導入 monorepo 工具,我們的專案規模還沒到那個程度。build 跟 deploy 雖然是個麻煩,但改 CI script 就能解決。不過我還是花了一些時間去了解 NX。
monorepo 有個問題是,不是每次改動都會影響 repo 裡的每個專案,而 repo 越長越大之後,我很懷疑光靠一直加 CI script 設定能不能管得動。NX 看起來是個解法,用起來也不難,建一個 workspace 再裝 NX/dotnet plugin 就好,NX 會自動偵測 workspace 資料夾底下哪些專案受到影響,再去處理 build 或 deploy 的設定。
不過站在 .NET 開發者的角度,我覺得 NX 的開發體驗還是比較貼近 JavaScript 開發者。而且如果真的決定拿 NX 當 CI 工具,把所有 CI/CD 都寫成 NX task 才會比較有效率,這樣一來就得放棄目前在用的 GitLab CI script template,轉換成本不低。以現階段來說,還沒有急迫到得要求團隊成員改用 NX。