Tools mentioned in this article
Open the browser-based tool while you read and try the workflow immediately.
needs 是「會合點」,加得越多 CI 可能越慢
GitHub Actions 的 needs 關鍵字會在 job 之間建立依賴(會合點)。這很好用,但也有代價:每寫一個 needs,都是 CI 時間增加的潛在來源。 團隊寫 needs 時往往偏向保守,導致原本可以平行執行的 job,遠比實際需要的更常被串成序列。
這篇文章是以 needs 為核心的模式與反模式對照參考。閱讀時不妨拿自己的 YAML 對照看看符合哪個模式。
模式對照表
| 模式 | 效果 | 適用時機 |
|---|---|---|
| 菱形分支 | 平行執行縮短 CI 時間 | lint/test/typecheck 彼此不互相依賴時 |
concurrency 群組 | 自動取消過時的執行 | 對同一個 PR 連續 push |
fail-fast: false | 一個失敗不會拖垮其他項目 | 想在 matrix 執行中看到所有組合的結果 |
可重複使用工作流程(workflow_call) | 消除重複定義 | 多個工作流程/儲存庫共用同一組 job 序列 |
搭配 if: always() 的通知 job | 失敗時也能確實發出通知 | Slack 通知、狀態回報 |
| 部分等待 matrix | 讓部分組合提早部署 | 類似金絲雀(canary)的先行執行 |
以下逐一以實例說明。
模式 1:用菱形分支打破序列化
最常見的反模式,就是把原本互不相依的 job 排成一直線。
# 反模式:lint -> unit-test -> typecheck -> build 全部序列化
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: unit-test
build:
needs: typecheck
unit-test 和 typecheck 除了「程式碼已經存在」之外,並不共享其他前提。如果兩者都只需要等 lint 完成,就可以把它們都直接掛在 lint 底下,組成菱形結構。
# 改善:lint 之後,unit-test 與 typecheck 平行執行
jobs:
lint:
runs-on: ubuntu-latest
unit-test:
needs: lint
typecheck:
needs: lint
build:
needs: [unit-test, typecheck] # 等待兩者都完成
把 build 的 needs 寫成陣列,就能表達「兩者都完成後才執行」。job 數量相同,但依賴圖的寬度(平行度)能直接縮短 CI 時間。把工作流程貼到 GitHub Actions 視覺化工具,這種「不必要的一直線」馬上就能從圖上看出來。
模式 2:用 concurrency 取消「前一次執行」
就算把 needs 設計最佳化了,如果同一分支連續 push 導致舊的執行還留在佇列中,等待時間依然存在。concurrency 就是用來消除這種浪費的。
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
在 group 中加入 github.ref(分支名稱或 PR 編號),就能做到同一分支有新的 push 進來時,自動取消舊的執行。對同一個 PR 連續 push 三次,理想情況下只有最後一次有意義的執行會跑到底。
:::message
如果 group 沒有同時包含 job 名稱或工作流程名稱,不同的工作流程可能會意外互相取消。用 ${{ github.workflow }}-${{ github.ref }} 這種寫法依工作流程區分群組,會比較安全。
:::
模式 3:善用 fail-fast 的取捨
使用 strategy.matrix 時,一個 job 定義會展開成多個執行(例如 Node 18/20/22 × 3 種作業系統)。這裡的 fail-fast 設定會大幅影響 CI 的行為。
strategy:
fail-fast: false # 預設值為 true
matrix:
node: [18, 20, 22]
os: [ubuntu-latest, windows-latest, macos-latest]
| 設定 | 行為 | 適合的情境 |
|---|---|---|
fail-fast: true(預設) | 只要有一個組合失敗,立即取消其餘組合 | 優先快速回饋,一旦知道失敗就不需要更多資訊 |
fail-fast: false | 即使有組合失敗,其餘組合也會全部跑完 | 想在一次 CI 中掌握「究竟只是 Node 20 的問題,還是跟作業系統有關」 |
在發布新功能前,想一次掌握所有組合的狀況時,false 就有其價值。反之,如果目標是「日常 PR 檢查一定要快」,維持預設的 true、優先快速偵測失敗會更合適。
模式 4:用 needs 等待 matrix job 的陷阱
當 build 依賴(已展開為 matrix 的)test 時,只要寫 needs: test,就能正確等待 matrix 所有組合都完成。到這裡都符合直覺,但有兩點要注意:
- 只要有一個組合失敗,依賴它的 job 也會被跳過(尤其搭配
fail-fast: true時,可能只靠部分結果就讓後續停止) - 如果只想讓部分組合提早繼續(例如先部署 Linux、之後再確認 Windows/macOS 的結果),單靠
needs無法表達,必須拆分 job 本身
# 為了讓 Linux 提早部署而拆分 job 的範例
jobs:
test-linux:
runs-on: ubuntu-latest
test-other-os:
strategy:
matrix:
os: [windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
deploy:
needs: test-linux # 不等待 Windows/macOS 的結果
這個模式的重點在於:「要等全部、還是只等一部分」,取決於如何拆分 job,而不是 needs 怎麼寫。
模式 5:用可重複使用工作流程共享整組依賴圖
如果多個工作流程(甚至多個儲存庫)都在重複使用同一組 job 序列,可以用 workflow_call 把被呼叫的部分集中在一處。
# .github/workflows/ci.yml(呼叫端)
jobs:
call-shared-ci:
uses: ./.github/workflows/shared-test-suite.yml
with:
node-version: '20'
被呼叫端(shared-test-suite.yml)內的 needs 結構會整套被重複使用,因此不需要在儲存庫之間複製貼上「lint 與 test 的依賴關係」。當依賴圖的設計已經模式化之後,下一步就是用這個機制把設計本身變成可重複使用的元件。
通知 job 要設計成「一定會執行」,不受 needs 牽連
由於 needs 的依賴特性,通知型 job 在其依賴的 job 失敗時會自動被跳過。而通知正是少數希望無論如何都執行的例外,因此要明確加上 if: always()。
notify:
needs: [lint, test, build]
if: always() # 無論依賴 job 成功或失敗都一定執行
runs-on: ubuntu-latest
steps:
- run: echo "Result: ${{ needs.build.result }}"
透過 needs.<job>.result 可以取得各個 job 的結果(success / failure / cancelled / skipped),因此能組出包含「究竟是什麼失敗了」的通知訊息。
檢視依賴圖的步驟
- 把工作流程 YAML 貼到 GitHub Actions 視覺化工具,將依賴圖視覺化
- 找出排成一直線的部分,確認是否真的需要這個順序(模式 1)
- 若在意連續 push 造成的重複執行,加入
concurrency(模式 2) - 使用 matrix 的 job,檢查
fail-fast的設定是否符合目的(模式 3) - 若多個工作流程重複出現相同的依賴結構,考慮抽取成可重複使用的工作流程(模式 5)
CI 執行時間的改善,往往調整依賴圖的形狀比刪減步驟內容更有效。不妨把手邊的工作流程畫成圖,檢查看看有沒有不必要的序列化。
常見問題
增加 needs 一定會讓 CI 變慢嗎?
needs 本身不會直接拖慢速度,問題出在寫了不必要等待的依賴。真正等待必要產出物(例如建置好的成品)的 needs 是必要的依賴,不應該移除。該檢討的是像「明明只要等 lint 完成就夠了,卻等了整個前置 job 完成」這種不必要的串接。
concurrency 的 cancel-in-progress 是不是永遠設成 true 比較好?
對於因 PR 持續 push 而觸發的 CI,基本上設為 true 是有效的。但在合併到 main 分支後的部署工作流程中,取消可能會讓部署停在中途的狀態,因此可以考慮設為 cancel-in-progress: false,或乾脆不在部署類工作流程加上 concurrency。
fail-fast 設為 false 會讓 CI 時間變長嗎?
有可能。fail-fast: true(預設值)會在第一個失敗時就中止其餘 job,因此平均 CI 時間通常較短。false 建議只用在真的需要所有組合結果的情境,例如發布前的全環境確認。
用來檢視依賴關係的工作流程 YAML 會被傳送出去嗎?
不會。GitHub Actions 視覺化工具 所有處理都在瀏覽器內完成,即使貼上內部工作流程定義,也不會傳送到伺服器。