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-testtypecheck 除了「程式碼已經存在」之外,並不共享其他前提。如果兩者都只需要等 lint 完成,就可以把它們都直接掛在 lint 底下,組成菱形結構。

# 改善:lint 之後,unit-test 與 typecheck 平行執行
jobs:
  lint:
    runs-on: ubuntu-latest
  unit-test:
    needs: lint
  typecheck:
    needs: lint
  build:
    needs: [unit-test, typecheck]   # 等待兩者都完成

buildneeds 寫成陣列,就能表達「兩者都完成後才執行」。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),因此能組出包含「究竟是什麼失敗了」的通知訊息。

檢視依賴圖的步驟

  1. 把工作流程 YAML 貼到 GitHub Actions 視覺化工具,將依賴圖視覺化
  2. 找出排成一直線的部分,確認是否真的需要這個順序(模式 1)
  3. 若在意連續 push 造成的重複執行,加入 concurrency(模式 2)
  4. 使用 matrix 的 job,檢查 fail-fast 的設定是否符合目的(模式 3)
  5. 若多個工作流程重複出現相同的依賴結構,考慮抽取成可重複使用的工作流程(模式 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 視覺化工具 所有處理都在瀏覽器內完成,即使貼上內部工作流程定義,也不會傳送到伺服器。