【GitHub Actions CI 實戰】#04 整合實戰:前後端 Monorepo CI 與最佳實踐

測驗:GitHub Actions CI 實戰 – Monorepo CI 與最佳實踐

共 5 題,點選答案後會立即顯示結果

1. 在 Monorepo 專案中,使用 path filters 的主要目的是什麼?

  • A. 限制哪些人可以修改特定目錄
  • B. 只在相關目錄有變動時才觸發對應的 CI
  • C. 自動合併不同分支的程式碼
  • D. 加密敏感檔案的內容

2. 在以下 YAML 設定中,** 代表什麼意思?

paths: – ‘frontend/**’
  • A. 只匹配 frontend 目錄下的檔案,不包含子目錄
  • B. 只匹配名稱中包含兩個星號的檔案
  • C. 匹配 frontend 目錄下任意深度的所有檔案
  • D. 匹配所有目錄,不限於 frontend

3. 在 GitHub Actions 中,如果多個 jobs 沒有使用 needs 關鍵字連接,它們會如何執行?

  • A. 依照 YAML 檔案中定義的順序執行
  • B. 同時並行執行
  • C. 隨機選擇一個執行
  • D. 必須手動觸發才會執行

4. 什麼是 Reusable Workflow?它的觸發方式是什麼?

  • A. 一種只能在本地執行的 workflow,使用 on: local
  • B. 自動重試失敗的 workflow,使用 on: retry
  • C. 定時執行的 workflow,使用 on: schedule
  • D. 可被其他 workflow 呼叫的模板,使用 on: workflow_call

5. 當 path filter 讓某個 job 被跳過(skipped)時,如何確保 Branch Protection 的 Required Check 能正常運作?

  • A. 移除 path filter,讓所有 jobs 都執行
  • B. 將 skipped 的 job 設為 required check
  • C. 加一個使用 if: always() 的總結 job,並將其設為 required check
  • D. 關閉 Branch Protection 功能

一句話說明

把前後端放在同一個 Repo,用 path filters 讓修改哪邊就只跑哪邊的 CI。

這篇你會學到

  • Monorepo 專案的 CI 架構設計
  • 用 path filters 精準觸發 workflow
  • 並行執行加速 CI
  • Workflow 重用技巧
  • CI 除錯與最佳實踐

Monorepo 專案結構

先看典型的前後端 Monorepo 長什麼樣:

my-app/
├── .github/
│   └── workflows/
│       ├── ci.yml           # 主要 CI workflow
│       ├── frontend.yml     # 前端專用(可選)
│       └── backend.yml      # 後端專用(可選)
├── frontend/                # 前端專案
│   ├── package.json
│   ├── src/
│   └── tests/
├── backend/                 # 後端專案
│   ├── requirements.txt
│   ├── src/
│   └── tests/
└── README.md
Code language: PHP (php)

翻譯:前端和後端各自有獨立的目錄,但共用同一個 Git Repo 和 CI 系統。


Path Filters:只跑需要的 CI

最小範例

name: CI

on:
  push:
    paths:
      - 'frontend/**'    # 只有 frontend 目錄變動才觸發
      - 'backend/**'     # 只有 backend 目錄變動才觸發
Code language: PHP (php)

逐行翻譯

on:
  push:
    paths:              # 指定「哪些路徑變動才觸發」
      - 'frontend/**'   # frontend 目錄下任何檔案(** 表示任意深度)
      - 'backend/**'    # backend 目錄下任何檔案
Code language: PHP (php)

常見變化

變化 1:排除某些檔案

on:
  push:
    paths:
      - 'frontend/**'
    paths-ignore:
      - 'frontend/**/*.md'    # 不理 Markdown 檔案
      - 'frontend/docs/**'    # 不理文件目錄
Code language: PHP (php)

翻譯:前端有變動就跑 CI,但改文件不算。

變化 2:同時監控共用檔案

on:
  push:
    paths:
      - 'frontend/**'
      - 'shared/**'           # 共用程式碼
      - 'package.json'        # 根目錄的設定檔
Code language: PHP (php)

翻譯:前端或共用部分有變動都要跑 CI。


完整 Monorepo CI 範例

name: Monorepo CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  # ===== 偵測變動 =====
  changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            frontend:
              - 'frontend/**'
            backend:
              - 'backend/**'

  # ===== 前端 CI =====
  frontend:
    needs: changes
    if: ${{ needs.changes.outputs.frontend == 'true' }}
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: frontend
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: frontend/package-lock.json

      - name: Install
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm test

      - name: Build
        run: npm run build

  # ===== 後端 CI =====
  backend:
    needs: changes
    if: ${{ needs.changes.outputs.backend == 'true' }}
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: backend
    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pip'
          cache-dependency-path: backend/requirements.txt

      - name: Install
        run: pip install -r requirements.txt

      - name: Lint
        run: ruff check .

      - name: Test
        run: pytest
Code language: PHP (php)

逐段翻譯

jobs:
  changes:                    # 第一個 job:偵測哪些目錄有變動
    outputs:
      frontend: ...           # 輸出:前端有沒有變
      backend: ...            # 輸出:後端有沒有變
Code language: PHP (php)

翻譯:先跑一個「偵測 job」,看看這次 commit 改了什麼。

  frontend:
    needs: changes            # 等 changes job 跑完
    if: ${{ needs.changes.outputs.frontend == 'true' }}  # 前端有變才跑
Code language: PHP (php)

翻譯:如果 changes job 說前端有變動,才執行前端 CI。

    defaults:
      run:
        working-directory: frontend    # 所有指令都在 frontend 目錄執行
Code language: PHP (php)

翻譯:不用每個 run 都寫 cd frontend,統一設定工作目錄。


並行執行加速 CI

Matrix 策略

jobs:
  test:
    strategy:
      matrix:
        project: [frontend, backend]    # 同時跑兩個
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test ${{ matrix.project }}
        run: |
          cd ${{ matrix.project }}
          npm test || pytest
Code language: PHP (php)

翻譯:GitHub Actions 會同時啟動兩個 runner,一個跑前端、一個跑後端。

並行 Jobs

jobs:
  lint:           # 這三個 job 會同時開始
    ...
  test:           # 沒有 needs,所以不用等
    ...
  build:
    needs: [lint, test]    # 這個要等前兩個都完成
    ...
Code language: PHP (php)

翻譯:沒寫 needs 的 jobs 會同時執行,寫了才會等。


Workflow 重用

方法 1:Composite Action

把重複的步驟包成可重用的 action:

# .github/actions/setup-node-project/action.yml
name: Setup Node Project
description: 安裝 Node.js 並執行 npm ci

inputs:
  working-directory:
    description: 專案目錄
    required: true

runs:
  using: composite
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '20'
        cache: 'npm'
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json

    - name: Install dependencies
      shell: bash
      run: npm ci
      working-directory: ${{ inputs.working-directory }}
Code language: PHP (php)

使用方式:

steps:
  - uses: actions/checkout@v4
  - uses: ./.github/actions/setup-node-project
    with:
      working-directory: frontend

翻譯:把「安裝 Node + npm ci」包成一個動作,以後一行搞定。

方法 2:Reusable Workflow

把整個 workflow 包成可重用的模板:

# .github/workflows/node-ci.yml
name: Node.js CI Template

on:
  workflow_call:              # 這是關鍵:允許被其他 workflow 呼叫
    inputs:
      working-directory:
        required: true
        type: string

jobs:
  ci:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ inputs.working-directory }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run lint
      - run: npm test
Code language: PHP (php)

使用方式:

# .github/workflows/ci.yml
jobs:
  frontend:
    uses: ./.github/workflows/node-ci.yml
    with:
      working-directory: frontend
Code language: PHP (php)

翻譯:把整套 CI 流程做成模板,不同專案用同一套。


Branch Protection 與 Required Checks

在 GitHub 設定中保護主分支:

Settings > Branches > Add rule

Branch name pattern: main

[v] Require a pull request before merging
[v] Require status checks to pass before merging
    - frontend
    - backend
[v] Require branches to be up to date before merging
Code language: CSS (css)

常見問題:Skipped Jobs 無法滿足 Required Check

當 path filter 讓某個 job 被跳過時,會顯示 skipped 狀態,GitHub 預設不會把它當成「通過」。

解法:加一個總結 job

jobs:
  changes:
    ...
  frontend:
    needs: changes
    if: ${{ needs.changes.outputs.frontend == 'true' }}
    ...
  backend:
    needs: changes
    if: ${{ needs.changes.outputs.backend == 'true' }}
    ...

  # 總結 job:永遠都會跑
  ci-success:
    needs: [frontend, backend]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Check results
        run: |
          if [[ "${{ needs.frontend.result }}" == "failure" ]] || \
             [[ "${{ needs.backend.result }}" == "failure" ]]; then
            echo "CI failed"
            exit 1
          fi
          echo "CI passed"
Code language: PHP (php)

翻譯:加一個「收尾 job」,它永遠會跑,然後檢查前面的 jobs 有沒有失敗。把這個 job 設成 required check 就行了。


CI 失敗的除錯技巧

1. 看 Job Summary

每個 workflow run 頁面最上面有 Summary,會顯示:

  • 哪些 jobs 成功/失敗
  • 執行時間
  • 產出的 artifacts

2. 展開失敗的步驟

點進失敗的 job,紅色的步驟就是出錯的地方。展開看完整 log。

3. 常見錯誤對照表

你看到的錯誤 可能原因
Permission denied 沒有執行權限,加 chmod +x
Command not found 套件沒裝或路徑不對
ENOENT: no such file 工作目錄不對或檔案不存在
npm ci 失敗 package-lock.jsonpackage.json 不同步
Timeout 執行太久,考慮拆分或加快

4. 本地重現

# 用 act 在本地跑 GitHub Actions
brew install act
act -j frontend
Code language: PHP (php)

翻譯act 工具可以在本地模擬 GitHub Actions 環境。


CI 最佳實踐清單

效能優化

  • [x] Cache 依賴:用 cache 選項避免每次重新下載
  • [x] 並行執行:獨立的 jobs 不要用 needs 串起來
  • [x] Path filters:只在相關檔案變動時觸發
  • [x] 選對 runner:簡單任務用 ubuntu-latest,需要特定環境才用其他

可維護性

  • [x] 重用 workflow:用 composite action 或 reusable workflow
  • [x] 統一版本:把 Node/Python 版本寫在一個地方
  • [x] 有意義的 job 名稱:讓人一眼看出在做什麼
  • [x] 加註解:特別是 if 條件和複雜邏輯

安全性

  • [x] Secrets 管理:敏感資料用 ${{ secrets.XXX }}
  • [x] Pin action 版本:用 @v4 而非 @main
  • [x] 限制 permissions:只給需要的權限
permissions:
  contents: read    # 只能讀,不能寫
  pull-requests: write
Code language: PHP (php)

Vibe Coder 檢查點

看到 Monorepo CI 設定時確認:

  • [ ] Path filters 有設對嗎? 改前端會觸發後端 CI 嗎?
  • [ ] 工作目錄對嗎? working-directorycd 有沒有寫
  • [ ] Cache 有設嗎?cachecache-dependency-path
  • [ ] 需要 required check 嗎? 是否有總結 job 處理 skipped 狀況
  • [ ] Secrets 安全嗎? 有沒有把 API Key 寫死在 yaml 裡

本系列回顧

恭喜你完成 GitHub Actions CI 實戰系列!來回顧一下你學到了什麼:

篇數 主題 你學會的
#01 第一個 Workflow onjobssteps 基本結構
#02 環境與 Secrets 環境變數、Secrets、環境設定
#03 真實 CI 流程 Lint、Test、Build 完整流程
#04 Monorepo 整合 Path filters、並行、重用、最佳實踐

下一步建議

  • 在自己的專案套用這些設定
  • 嘗試加入 CD(自動部署)流程
  • 探索 GitHub Actions Marketplace 找更多好用的 action

總結

這篇教了你:

  1. Monorepo 結構:前後端各自目錄,共用 CI
  2. Path filters:只在對應目錄變動時觸發
  3. 並行執行:用 matrix 或獨立 jobs 加速
  4. Workflow 重用:composite action 和 reusable workflow
  5. Required checks:用總結 job 解決 skipped 問題
  6. 除錯技巧:看 summary、展開 log、本地用 act 測試

現在你已經具備設計和維護 CI 系統的能力了!

進階測驗:GitHub Actions CI 實戰 – Monorepo CI 與最佳實踐

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你的團隊有一個前後端 Monorepo 專案,目前每次 push 都會跑前端和後端的完整 CI,即使只改了一行 README。你想優化這個流程,只在對應目錄有變動時才跑 CI。最佳做法是什麼? 情境題

  • A. 把前端和後端拆成兩個獨立的 Repo
  • B. 使用 dorny/paths-filter action 偵測變動,配合 if 條件控制 job 執行
  • C. 在每個 job 的第一步加上手動判斷變動的 shell script
  • D. 建立多個 workflow 檔案,每個都用不同的 branch trigger

2. 你設定了 Branch Protection,要求 frontendbackend jobs 都必須通過才能合併 PR。但團隊回報說:當只修改後端程式碼時,PR 一直顯示「等待 frontend check」無法合併。問題出在哪裡? 錯誤診斷

jobs: frontend: if: ${{ needs.changes.outputs.frontend == ‘true’ }} … backend: if: ${{ needs.changes.outputs.backend == ‘true’ }} …
  • A. if 條件的語法寫錯了
  • B. needs.changes.outputs 沒有正確輸出值
  • C. 當 job 被跳過時顯示 skipped 狀態,GitHub 不會將其視為「通過」,需要加一個總結 job
  • D. Branch Protection 設定錯誤,應該只設定 backend 為 required

3. 你的 CI 跑得很慢,發現 lint、test、build 三個步驟是串行執行的。你想讓它們在可能的情況下並行執行。以下哪種設計最合理? 情境題

  • A. 把三個步驟都放在同一個 job 的 steps 裡,並行無法加速
  • B. 將 lint 和 test 拆成獨立的 jobs(不加 needs),build 使用 needs: [lint, test]
  • C. 使用 matrix 策略讓三個步驟同時在不同機器上跑
  • D. 購買更快的 runner 來加速執行

4. 小明在本地執行 npm test 正常通過,但 GitHub Actions 上卻失敗了,錯誤訊息如下。最可能的原因是什麼? 錯誤診斷

npm ci npm ERR! code ENOENT npm ERR! syscall open npm ERR! path /home/runner/work/my-app/frontend/package-lock.json npm ERR! errno -2
  • A. GitHub Actions 的 Node.js 版本太舊
  • B. npm registry 暫時無法連線
  • C. 測試程式碼有 bug,需要修正
  • D. 工作目錄設定錯誤,或是 package-lock.json 沒有 commit 到 Repo

5. 你的團隊有三個 Node.js 前端專案,它們的 CI 流程幾乎相同(安裝、lint、test、build)。為了減少重複並方便統一維護,最佳做法是什麼? 情境題

  • A. 把三個專案的 workflow 檔案內容複製貼上,分別維護
  • B. 建立一個 shell script 來處理所有邏輯,workflow 只呼叫這個 script
  • C. 建立 Reusable Workflow(使用 on: workflow_call),讓各專案用 uses: 呼叫
  • D. 使用 GitHub Actions Marketplace 上的第三方 action 來取代所有步驟

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *