去年底我花了大概兩週的時間,把我們團隊的 Git 工作流從 Git Flow 整個砍掉重練,換成 GitHub Flow 加上日期版號。過程中踩了不少坑,但換完之後整個團隊的開發節奏順了很多,merge conflict 的頻率也降了一大截。這篇就來聊聊為什麼我們做了這個選擇,以及實際跑起來的感受。
Git Flow 的美好幻想
大概三年前,我剛進公司的時候,團隊用的是最經典的 Git Flow。你知道的,main、develop、feature/*、release/*、hotfix/*,整套搬過來。當時覺得超專業,流程圖畫出來漂漂亮亮的,掛在 Wiki 上感覺很有那麼回事。
gitGraph
commit id: "init"
branch develop
checkout develop
commit id: "feat A"
branch feature/login
checkout feature/login
commit id: "login WIP"
commit id: "login done"
checkout develop
merge feature/login
branch release/1.0
checkout release/1.0
commit id: "bump ver"
checkout main
merge release/1.0 tag: "v1.0"
checkout develop
merge release/1.0
commit id: "feat B"
checkout main
branch hotfix/bug
commit id: "fix bug"
checkout main
merge hotfix/bug tag: "v1.0.1"
checkout develop
merge hotfix/bug
但問題來了——我們是個大約十人的小團隊,做的是內部的 ERP 系統和一些給客戶用的管理後台。這種規模的團隊,搞 Git Flow 根本就是殺雞用牛刀。
實際跑起來的狀況是這樣的:
develop分支永遠跟main差個十幾個 commit,每次 release 都要花時間對齊release分支開出來之後,總有人不小心把新 feature 合進去,然後就炸了- hotfix 的流程最噁心,改完要合回
main又要合回develop,偶爾忘記就會出現詭異的 regression - 新人進來要花半天解釋這套流程,解釋完他還是會搞錯
回頭看覺得很蠢,我們又不是在做 open source 大專案,哪來那麼多平行的 release 要維護?
那 trunk-based 呢?
有同事提議乾脆用 trunk-based development,大家都直接往 main 推。聽起來很潮對吧?Google 和 Facebook 都這樣幹。
gitGraph
commit id: "feat A"
commit id: "feat B"
commit id: "fix C"
commit id: "feat D"
commit id: "feat E"
commit id: "fix F"
commit id: "feat G"
好,但我們不是 Google。
trunk-based 要跑得順,前提是你有夠強的 CI/CD pipeline、完善的自動化測試、還有 feature flag 機制。我們的測試覆蓋率?呃⋯⋯別問,問就是心虛。直接往 main 推的結果就是三不五時有人把半成品推上去,然後部署到測試環境直接白屏,大家一起 debug 到晚上。
試了兩個禮拜就放棄了。
GitHub Flow:剛剛好的甜蜜點
最後我們選了 GitHub Flow,規則超簡單:
- 只有一個長期分支:
main,永遠保持可部署狀態 - 要開發新功能就從
main開feature/xxx分支 - 做完發 Pull Request,至少一個人 review 過才能合併
- 合併回
main後就可以部署
gitGraph
commit id: "init"
branch feature/login
checkout feature/login
commit id: "login WIP"
commit id: "login done"
checkout main
merge feature/login id: "PR #1" tag: "2026.01.15"
branch feature/report
checkout feature/report
commit id: "report WIP"
commit id: "report done"
checkout main
merge feature/report id: "PR #2" tag: "2026.01.18"
branch feature/export
checkout feature/export
commit id: "export"
checkout main
merge feature/export id: "PR #3" tag: "2026.01.20"
就這樣。沒有 develop,沒有 release 分支,沒有那些讓人頭痛的多層合併。
聽起來很簡單對吧?實際跑起來也真的就這麼簡單。新人五分鐘就能理解整套流程,不用再畫那種複雜到爆的分支圖。
當然還是有一些眉角要注意:
- feature branch 的生命週期盡量短,最好不要超過三天。拖太久 merge conflict 會讓你想哭
- PR review 不要堆,每天早上花 15 分鐘把待審的 PR 清一清
main壞了就是最高優先級,所有人停下來一起修
我們搭配 Gitea 的 PR 功能,設了 branch protection rule,強制要求至少一個 approve 才能 merge。一開始有人覺得麻煩,但跑了一個月之後,大家反而養成了寫小 PR 的習慣,review 起來也快很多。
版號:日期版號 vs SemVer
工作流搞定之後,接下來要決定版號格式。這個看起來是小事,但實際上吵了好幾天。
一開始我們很直覺地用了 SemVer(MAJOR.MINOR.PATCH),畢竟這是業界標準嘛。但用了幾個月就發現不太對勁:
- 每次要 release 的時候,大家開始爭論「這次算 minor 還是 patch?」「加了一個新報表算不算 minor?」「改了 API 回傳格式但只有我們自己在用,這算 breaking change 嗎?」
- 我們的產品是內部系統和客戶專案,根本沒有外部開發者會依賴我們的版號語意
- 客戶打電話來回報問題的時候說「我用的是 2 點幾版」,然後你要花五分鐘釐清他到底是 2.3.1 還是 2.3.2
SemVer 的精神是用版號傳達「這次更新會不會 break 你的東西」,這對 open source library 來說超重要。但對我們這種內部產品?完全是多餘的心智負擔。
後來我提議改用日期版號,格式是 YYYY.MM.DD,同一天多次部署就加個序號變成 YYYY.MM.DD.N。例如:
2026.02.05— 2 月 5 號的部署2026.02.05.2— 同一天的第二次部署
好處立刻就感受到了:
- 零爭議。今天部署就是今天的日期,不用開會討論版號
- 好溝通。客戶說「我用 2 月 5 號那版有問題」,馬上就知道在講哪個版本
- 自帶時間資訊。看到版號就知道這個版本多久沒更新了,不用去翻 changelog
- deploy 腳本超好寫,直接抓系統日期就搞定
唯一的小缺點是你沒辦法從版號看出「這次更新幅度多大」,但說真的,對我們的使用情境來說,這個資訊本來就應該去看 changelog 或 PR 列表,而不是靠版號猜。
Gitea Project:用模組切,不用版本切
版號的事情解決之後,還有一個問題:怎麼管理開發進度?
Gitea 有 Project 功能(類似 GitHub Projects),一開始我們很自然地用版本來切 Project,比如「v2026.02 要做的事」。但很快就發現這樣切很彆扭——我們的開發節奏不是「攢一堆功能然後一次 release」,而是持續部署的,feature 做完 review 過就上了。
後來改成用模組或業務領域來切 Project:
- 「庫存模組」
- 「報表系統」
- 「使用者權限」
- 「某客戶專案」
每個 Project 裡面放的是該領域的所有待辦事項,不管它預計哪個版本上線。時間維度的需求則用 Milestone 來補,例如「二月底前要完成」就開一個 Milestone,把相關的 issue 掛上去。再搭配 Label 標記優先級和狀態。
這樣切的好處是:
- Project 的生命週期跟著模組走,不會因為「這個版本 release 了」就要關掉重開
- 團隊成員可以專注在自己負責的領域,不用在一堆跨模組的 issue 裡面翻來翻去
- Milestone 提供了時間壓力,Label 提供了分類和狀態,三個維度各司其職
實際踩過的坑
當然不是一切都那麼美好,分享幾個我們踩過的坑:
feature branch 活太久的問題。 有個同事在做一個比較大的功能,分支開了兩個禮拜都沒合回來。等他終於發 PR 的時候,conflict 多到他花了整整一天在解。後來我們訂了一個規矩:超過三天的 feature branch 必須 rebase main,超過一週的要拆成小 PR 分批合併。
日期版號在 hotfix 的小尷尬。 有一次客戶回報了一個緊急 bug,我們當天修完部署,版號是 2026.01.15。結果隔天又發現另一個相關的 bug,修完部署變成 2026.01.16。客戶就問:「你們昨天不是才更新嗎,怎麼今天又要更新?」這其實不是版號的問題,但日期版號確實讓更新頻率變得很透明。後來我們跟客戶解釋這是持續改善的流程,反而變成一個正面的溝通點。
PR review 的瓶頸。 團隊裡比較資深的就那兩三個人,所有 PR 都要等他們 review,有時候會塞車。我們後來調整成「任何人都可以 review,但涉及核心模組的 PR 需要指定的 reviewer」,這樣大部分簡單的 PR 可以快速通過。
結語
選工作流這件事,沒有標準答案。Git Flow 不是不好,它很適合需要維護多個 release 版本的大型專案。trunk-based 也不是不行,前提是你的基礎建設夠強。但對於像我們這樣的小團隊,做的是內部產品或客戶專案,GitHub Flow 加日期版號就是那個剛剛好的選擇。
不用花時間在流程上較勁,把省下來的精力拿去寫 code 和解決真正的問題,不是很好嗎?
對了,如果你的團隊也在用 Gitea,真的推薦試試用模組切 Project 的方式。一開始可能會覺得不習慣,但跑個一兩個 sprint 之後就會回不去了 😂