Multibranch 與 PR 觸發:讓每個分支都有自己的 pipeline
· tech
📑 目錄
前面七篇談的都是「一條 pipeline」。但真實的 repo 從來不只有一條路:main 上有人在合、三個 feature branch 在跑、還有兩個 PR 等著 review。
這篇處理的就是這件事——怎麼讓每個分支與 PR 都有自己的 pipeline,以及為什麼這件事做好之後,你才真的擁有第 1 篇說的那個「頻繁合回主幹」。
Multibranch Pipeline:一個 repo,一群 pipeline
Multibranch 做的事很單純:掃描 repo,凡是有 Jenkinsfile 的分支與 PR,自動長出一個 job。
設定本身也該是程式碼(第 6 篇的立場一路延伸到這裡),用 JCasC 寫大概長這樣:
# jenkins.yaml —— Multibranch,連掃描規則與孤兒清理都寫進 git
jobs:
- script: >
multibranchPipelineJob('payment-api') {
branchSources { github { id('payment-api'); repoOwner('acme'); repository('payment-api') } }
orphanedItemStrategy { // 分支刪了,job 也清掉
discardOldItems { numToKeep(10) }
}
factory { workflowBranchProjectFactory { scriptPath('Jenkinsfile') } }
}
如果整個 organization 都用同一套慣例,還可以再上一層用 Organization Folder:掃整個 GitHub org,任何 repo 只要放了 Jenkinsfile 就自動接上 CI——新專案不用「找人幫忙開 job」。
Webhook vs Polling
Jenkins 怎麼知道有新 commit?兩條路:
| Polling(定時去問) | Webhook(它主動通知) | |
|---|---|---|
| 延遲 | 平均是掃描間隔的一半——設 5 分鐘,平均白等 2.5 分鐘 | 幾秒 |
| 成本 | repo 一多就是持續的無效流量,controller 一直在跟 git 講話 | 事件才有動作 |
| 網路 | Jenkins 連得出去就行 | git 服務要連得到 Jenkins(內網要處理) |
能用 webhook 就用 webhook。 polling 是很典型的 toil:重複、可自動化、而且量會隨規模線性長大——一百個 repo 每兩分鐘掃一次,那是三萬次無效請求。
唯一合理留著 polling 的情況,是 Jenkins 在內網、git 服務打不進來。這時折衷做法是把間隔拉長(例如 15 分鐘)當保險,主要靠開發者手動觸發,或改用 GitHub App / 反向代理把 webhook 送進來。
PR check:把「記得跑測試」變成「不需要記得」
Multibranch 會為每個 PR 跑一次 build,並把結果回報成 GitHub 上的 status check。真正的價值在下一步:把它設成 required。
同一份 Jenkinsfile 要能分辨自己現在跑在哪種情境,靠的是分支條件:
stages {
stage('Verify') { // 所有分支與 PR 都跑,要快
steps { sh './scripts/ci-verify.sh' }
}
stage('Full IT') {
when { beforeAgent true; not { changeRequest() } } // PR 不跑重的
steps { sh './scripts/it.sh' }
}
stage('Deploy staging') {
when { beforeAgent true; branch 'main' } // 只有 main 才部署
steps { deployApp(env: 'staging', image: "app:${env.GIT_COMMIT}") }
}
stage('Release') {
when { beforeAgent true; buildingTag() } // 打 tag 才發版
steps { deployApp(env: 'production', image: "app:${env.TAG_NAME}") }
}
}
beforeAgent true 的重要性在第 5 篇講過:沒有它,Jenkins 會先配好 agent、拉完程式碼,才發現這個 stage 不用跑。
但 trunk-based 不是叫工程師勤勞一點
技術設定到這裡就齊了:每個分支有 pipeline、PR 有擋得住的門、main 才部署。但這些都不會自動讓分支變短——因為分支活多久,常常根本不是工程師決定的。
第 1 篇提過這條線,這裡把它講完。關鍵是分清楚兩件被綁在一起的事:
- 合併(merge):程式碼進不進得了主幹——這是工程決定。
- 發布(release):功能給不給使用者看見——這是商業決定。
解開的手段有三階,選最便宜的那個就好:
| 手段 | 什麼時候用 | 代價 |
|---|---|---|
| 不接線 | 新類別、新 API、新頁面——合進去但不掛路由與選單 | 幾乎零 |
| Branch by abstraction | 要替換既有實作:介面與新實作先合,舊的照跑,最後切換 | 多一層抽象,切完要記得拆 |
| Feature flag | 真的需要執行期分流:灰度、A/B、按客戶開關 | 最貴,而且會弄髒程式碼 |
大部分「還不能合」其實只要不接線就解決了。 旗標是最貴的一把,別拿它當萬用解。
而如果真的要用旗標,就要連清理紀律一起買——這三條我在 review 會直接要求:
- 每個旗標上線時就寫下擁有者與預計移除日期,寫在程式碼註解或旗標系統裡都行,但要寫。
- 旗標只做開關,不做業務邏輯的分岔點——一旦
if (flag)底下長出第二套流程,它就不是旗標了,是一個沒人維護的分支。 - 定期盤點,過期的要嘛清掉、要嘛重新說明為什麼還在。
做不到這三條,我不會建議一個團隊為了追求 trunk-based 而大量導入旗標——那真的會換來比長命分支更糟的東西。
但如果改的是「現有功能」呢?
上面那張三階梯有個前提我要講明:它涵蓋的是「新增」。 一旦你改的是既有行為,「不接線」直接失效——那段程式碼合進去就會生效,使用者馬上感受得到。
這時候先做一次分類,因為四種「優化」的答案完全不同:
| 類型 | 例子 | 要不要等商業決策 |
|---|---|---|
| 外部行為不變 | 查詢改寫、加快取、重構、換演算法但輸出一樣 | 不用——這是技術變更,不是產品變更 |
| 行為變但使用者無感 | 錯誤訊息更精準、日誌、後台欄位 | 通常知會就好 |
| 使用者看得到的變更 | 流程三步變兩步、預設值改掉、版面改版 | 要,這是產品變更 |
| 不可逆的變更 | 資料格式、對外 API、計費邏輯 | 要,而且要拆成 expand-contract 分批 |
判準只有一句:這個改動需要跟使用者說明嗎? 需要,它就是產品變更;不需要,它就是技術變更,不該塞進商業排期。
我看過太多團隊把第一類當第三類在等——效能優化排進「下個版本」等一個月。那不是謹慎,是浪費:行為沒變的東西,沒有什麼可以「決策」的。它的風險是技術風險,該用測試與漸進發布去管,不是用審批去管。
真的是行為變更時,有兩個手段比「開個旗標」更適合:
一、kill switch 不等於 feature flag。 差別在預設值,而預設值決定了清理壓力:
| feature flag | kill switch | |
|---|---|---|
| 預設 | off,等商業決定何時打開 | on,新行為直接生效 |
| 舊路徑 | 長期並存,測試矩陣加倍 | 只在事故時走,可以設「兩週後刪」 |
| 適合 | 新功能 | 優化型改動 |
優化型的改動九成只需要一個「壞了可以關掉」的開關,不需要一個「等人決定何時打開」的旗標。
二、把 PR 拆開。 這是我最常用的動作——「優化現有功能」的 PR 通常混了兩種東西:
原本一個 PR:重構 + 改行為 → 整包卡住等決策
拆成:
PR 1 純重構、抽出介面、補測試 → 行為不變,今天就能合
PR 2 切換到新行為 → 薄薄一層,等決策
拆完之後,等待的表面積從整包縮成幾十行。PR 1 立刻進主幹,不會跟別人衝突;PR 2 只剩切換那一刀,review 快、回滾也乾淨。這就是 branch by abstraction 用在「修改」而不是「新增」上。
還有一種閘門,不在使用者身上,在合約上
上面談的閘門都是「使用者何時看得到」。但有一種常見情境完全在另一條軸上:委外與接案。
甲方委託乙方優化一支 SQL,價錢還沒談定;或是乙方自己發現了很好的優化,打算「先做起來再去談」。技術上這是最單純的第一類(輸出一樣、行為不變),照理今天就能合。但這裡的閘門不是曝光,是這段工作何時被承認、被計價。
而它有個殘酷的性質:一旦 merge 進甲方的 repo,議價槓桿就歸零了。 多數委外合約裡,交付物的權利在交付當下就轉移;「先做起來再談價」在工程上很自然,在商務上等於先把牌打出去再開價。
我看過一種很糟的因應方式:程式碼合進去,但用旗標關著,當作「還沒交付」。 三個問題:法律上站不住(它已經在對方的 repo 裡了)、關係上很傷(被發現一次,信任就沒了)、技術上留垃圾(一個永遠不會被清的旗標,連「擁有者與到期日」都寫不出來,前面那三條紀律直接破功)。
閘門在合約,就在合約處理;不要拿旗標當談判籌碼。 具體上:
- 乙方主動發現的優化 → 先給證據,不給程式碼。 對 SQL 優化來說,證據比程式碼更有議價力:執行計畫 before/after、「這支 query 每天跑 4,000 次,P95 從 8 秒降到 300 毫秒」、預估省下的成本。給程式碼是給勞動,給量化的 before/after 是給價值——後者才談得動價錢,而且你既然看得出怎麼改,做這份評估花不了多少時間。
- 已經委託、只是價錢沒定 → 那是報價流程沒走完,不是工程問題。 工程端不該用技術手段去兜商務流程的缺口。
- 現實就是「已經先做了」→ 放在自己的分支,不進對方主幹。 這條分支會活很久,而這正是第 1 篇那句話的極端版:分支活很久是組織層級的題目——只是這次那個「組織」跨越了兩家公司,工程端更沒有話語權。這種時候我不假裝 CI 紀律能解決它,只確保兩件事:定期把主幹合進來(降低最後那次合併的痛),以及一次只押一個未計價的優化,不要越積越多。
順帶一個技術提醒:「SQL 優化不改變行為」這個假設要驗證。加索引通常安全,但改寫 query 很容易踩到 join 造成的重複列、NULL 語意、沒有 ORDER BY 時的隱含順序、DISTINCT 與 GROUP BY 改寫後的邊界。所以就算談定了要合,也該做結果比對:同一批輸入跑新舊兩版,逐列比對輸出。這件事該進 pipeline(下一篇的主題),而且它在委外情境裡一魚兩吃——既是技術保險,也是驗收時最有說服力的交付證據。
反思
Required check 是我做過投報率最高的一次性設定
我在幾個團隊都做過同一件事:把 build、test、lint 設成 required,並開啟「分支落後主幹時要先更新才能合」。設定大概花二十分鐘,之後每一個 PR 都自動享有。
它真正改變的不是品質,是對話的內容。在那之前,review 常常在講「你這個有跑測試嗎」「記得補一下 lint」;之後這些話題完全消失了,因為機器已經講完了,人可以專心討論設計。把機器能檢查的事交給機器,人的注意力才有機會放在機器檢查不了的地方——這是我認為 required check 最被低估的價值。
有一點要提醒:PR build 跑的是外部帶進來的程式碼,所以它不該拿得到任何部署憑證,理由在第 6 篇講過。
我看過 polling 把一台 Jenkins 拖垮
有個環境累積到大約兩百個 job,每個都設 H/2 * * * *(每兩分鐘 poll 一次)。結果 controller 幾乎所有時間都在跟 git 講話,執行緒池被 polling 佔滿,真正的 build 反而排不進去——大家的體感是「Jenkins 好慢」,但沒有人想到慢的原因是它在忙著問「有沒有新東西」。
改成 webhook 之後,那些請求歸零,順帶把觸發延遲從平均一分鐘變成幾秒。這件事讓我學到一個更一般的教訓:輪詢的成本會隨規模線性成長,但它平常完全不痛——它不會壞、不會報錯,只會安靜地吃掉你的容量,直到某天你以為自己需要加機器。
分支保護是要保護主幹,不是為難開發者
反過來的失敗我也看過:某個團隊把六個 check 全設成 required,其中兩個各要跑十幾分鐘。結果不是品質變好,是大家開始想辦法繞過去——小修改直接推 main(因為有管理員權限)、或是把 check 設成 optional 之後忘了改回來。
我現在的原則是:每個 required check 都要能回答「它擋掉過什麼」。擋過真實問題的,留著;從來沒紅過、或紅了大家都直接 re-run 的,那不是關卡,是儀式——該修好它、加快它,或乾脆拿掉。門檻設得比團隊承受能力高,人不會變乖,只會找路繞——而繞過去之後,你連他們繞了都不知道。
下一篇進第三批,談品質關卡本身:測試報告、覆蓋率與靜態分析,怎麼從「有跑」變成「擋得住」,以及為什麼 flaky test 是這道門最大的敵人。