Pipeline 設計原則

學習設計高效、可維護的 CI/CD Pipeline:涵蓋 stages 規劃、觸發條件、平行執行、artifact 與 cache 策略,以及 fail-fast 與 timeout 優化技巧。

Stages 設計(lint / test / build / deploy)

Stage 定義了 Pipeline 的執行順序:前一個 stage 的所有 job 全部成功後,才進入下一個 stage。良好的 stage 設計能讓問題在最早的階段被攔截,避免浪費後續的運算資源。每個 stage 代表一個職責明確的質量關卡,從程式碼風格到最終部署,層層把守確保只有通過所有檢查的版本才能進入生產環境。標準四層結構如下:

# 推薦的四層 Stage 設計(GitLab CI 範例)
stages:
  - lint      # 最快(秒級):語法檢查、格式驗證、型別檢查
  - test      # 中速(分鐘):單元測試、整合測試、安全掃描
  - build     # 較慢(分鐘):編譯、打包、建置 Docker 映像
  - deploy    # 需控制(分鐘):部署至測試/正式環境

# 全域預設設定(所有 jobs 繼承,減少重複設定)
default:
  image: node:20-alpine
  before_script:
    - npm ci --cache .npm --prefer-offline
  cache:
    key:
      files:
        - package-lock.json     # lock 檔案變動時自動失效,強制重新安裝
    paths:
      - .npm/
  timeout: 10 minutes           # 全域 job 超時時間,防止 job 卡住耗盡資源

# lint stage:程式碼品質把關(同 stage 內的 job 平行執行)
lint-eslint:
  stage: lint
  script:
    - npm run lint
  allow_failure: false          # lint 失敗則阻止後續所有 stage 執行

typecheck:
  stage: lint                   # 與 lint-eslint 同時平行執行
  script:
    - npm run typecheck

# test stage:測試覆蓋率與報告
unit-test:
  stage: test
  script:
    - npm run test -- --coverage
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'   # 從 log 解析覆蓋率,顯示於 MR 頁面
  artifacts:
    reports:
      junit: test-results.xml   # GitLab 原生支援,在 MR 頁面顯示測試摘要
    paths:
      - coverage/
    expire_in: 3 days

# build stage:產出可部署的產物
build-app:
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/                   # 傳遞給 deploy stage 使用
    expire_in: 1 hour           # 短效期節省儲存空間
lint stage 應永遠排在最前面:Lint 執行速度最快(通常在 30 秒內完成),能在耗時的測試與建置之前就攔截明顯的程式碼問題,節省整體 Pipeline 執行時間。每提早一個 stage 發現問題,就能節省後續所有 stage 的等待時間。

Trigger 條件設定(push / PR / tag / schedule)

不同事件應觸發不同範圍的 Pipeline。過度觸發會浪費資源,觸發不足則可能讓問題流入主線。核心原則是:功能分支執行輕量快速的檢查,主線分支執行完整的 CI,發佈版本觸發完整的建置與部署,排程任務處理定期維護工作。以下示範四種常見觸發場景的設定:

# GitLab CI — 使用 rules 精確控制觸發條件
# (GitLab 14.0+ 推薦 rules 取代舊有的 only/except)

# 1. push 到功能分支時,只執行 lint 與 test(節省建置資源)
feature-check:
  stage: test
  script:
    - npm test
  rules:
    - if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != "main"
      when: on_success

# 2. Merge Request 時執行完整 CI(lint + test + build),確保合併品質
mr-pipeline:
  stage: build
  script:
    - npm run build
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# 3. 推送符合語意化版本格式的 tag 時,觸發正式發佈流程
release-build:
  stage: build
  script:
    - npm run build:production
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/   # 符合 semver(如 v1.2.3)

# 4. 排程觸發:每日定時執行安全掃描(不影響正常開發流程)
security-scan:
  stage: test
  script:
    - npm audit --audit-level=high
    - trivy image $CI_REGISTRY_IMAGE:latest
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
GitLab CI 的 $CI_PIPELINE_SOURCE 變數可辨識觸發來源:push(一般推送)、merge_request_event(MR)、web(手動觸發)、schedule(排程)、trigger(跨專案觸發)。善用此變數搭配 rules 可精確控制每個 job 的執行時機,避免不必要的資源消耗。

平行執行 vs 循序執行

同一個 stage 內的所有 job 預設平行執行,不同 stage 間則循序執行。可透過 needs: 關鍵字建立 job 間的直接依賴關係,打破 stage 邊界,實現更靈活的 DAG(有向無環圖)Pipeline。DAG 模式讓準備好的 job 可以立即執行,不必等待整個 stage 完成,顯著縮短 Pipeline 總執行時間:

# 預設行為:同 stage 內的 job 全部平行執行
test:
  stage: test
  script: npm test            # 與 lint job 同時執行,互不等待

lint:
  stage: test
  script: npm run lint        # 與 test job 同時執行

# 使用 needs: 實現 DAG Pipeline(不受 stage 邊界限制)
# build-app 完成後,deploy-staging 可立即執行,無需等待 security-scan
deploy-staging:
  stage: deploy
  needs:
    - job: build-app          # 只等待 build-app 完成,不等待整個 build stage
      artifacts: true         # 繼承 build-app 上傳的 artifacts(dist/ 目錄)
  script:
    - ./deploy.sh staging

security-scan:
  stage: deploy               # 同 stage,但與 deploy-staging 獨立平行執行
  needs:
    - job: build-app
      artifacts: false        # 只需等待 build-app 完成,不需要其 artifacts
  script:
    - trivy image $CI_REGISTRY_IMAGE:latest

# 矩陣平行:同時在多個環境組合中執行相同的測試
test-matrix:
  stage: test
  parallel:
    matrix:
      - NODE_VERSION: ["18", "20", "22"]
        OS: ["alpine", "bullseye"]        # 共 6 個 job 同時執行
  image: node:${NODE_VERSION}-${OS}
  script:
    - npm test

Artifact 與 Cache 策略差異

Artifact 與 Cache 是兩種不同用途的檔案儲存機制,在 CI/CD 初學者中常被混用,但選擇錯誤會導致 Pipeline 緩慢甚至不穩定。理解兩者的本質差異是設計高效 Pipeline 的關鍵:

# ── Artifact:stage 間傳遞建置產物 ──────────────────────────────────────
# 特性:上傳至 GitLab/GitHub 中央伺服器,可在 UI 下載,跨 job 自動傳遞
# 適合:編譯產物(dist/)、測試報告(coverage/)、Docker 映像 tarball
build:
  stage: build
  script:
    - npm run build
  artifacts:
    name: "$CI_JOB_NAME-$CI_COMMIT_REF_SLUG"   # 人類可讀的命名
    paths:
      - dist/                   # 建置產物,deploy stage 自動下載並使用
      - coverage/               # 測試覆蓋率報告,可在 GitLab UI 瀏覽
    reports:
      junit: test-results.xml   # 特殊類型:在 GitLab MR 頁面顯示測試摘要
    expire_in: 7 days           # 7 天後自動清除(節省儲存空間與費用)
    when: always                # 即使 job 失敗也保存 artifact(便於 debug)

# ── Cache:跨 Pipeline 重用相依套件 ────────────────────────────────────
# 特性:儲存在 Runner 本機磁碟,不保證每次都命中,不應存放建置產物
# 適合:node_modules、pip packages、Maven .m2 等安裝耗時的相依套件
default:
  cache:
    key:
      files:
        - package-lock.json     # 依 lock 檔案內容雜湊值決定 key
                                # lock 檔一旦變動,cache 自動失效重建
    paths:
      - node_modules/           # 快取 node_modules
    policy: pull-push           # pull-push(預設):先讀取舊 cache,完成後更新
                                # pull:唯讀模式,不更新 cache(加速唯讀 job)
核心原則:Artifact 用於「需要傳遞給其他 job 的檔案」(如編譯產物、測試報告);Cache 用於「可以重建但重建很慢的檔案」(如 node_modules、pip packages)。切勿將建置產物放入 cache,因為 cache 不保證存在,且不同 Pipeline 之間的 cache 內容可能不一致,容易產生難以重現的問題。

條件執行(when / rules)

when 關鍵字控制 job 在什麼情況下執行,共有五種值,各有適用場景。搭配 rules 可組合出精細的執行邏輯,讓 Pipeline 根據分支名稱、觸發來源或自訂變數動態決定每個 job 的行為:

# when 的五種值與對應使用情境
deploy-production:
  script: ./deploy.sh production
  when: manual            # 需要人工在 GitLab UI 點擊才執行(適合正式部署審核)

notify-failure:
  script: ./notify-slack.sh "Pipeline 失敗,請立即處理"
  when: on_failure        # 只在前一個 job 失敗時執行(發送告警通知)

cleanup:
  script: ./cleanup.sh
  when: always            # 無論成功或失敗都執行(適合清理臨時資源)

# on_success(預設值):只在前一個 job 成功時執行
# delayed:延遲指定時間後執行(如 start_in: 30 minutes)

# rules 搭配 when 實現複雜的多條件邏輯
deploy:
  stage: deploy
  script: ./deploy.sh
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success                  # main branch push 後自動部署(CD)
    - if: $CI_COMMIT_BRANCH =~ /^release\//
      when: manual                      # release 分支需手動點擊才部署
      allow_failure: false              # 標記為必要步驟,不可略過
    - when: never                       # 其他所有情況完全不執行此 job

# 條件變數賦值:根據分支動態決定建置模式
build:
  stage: build
  variables:
    BUILD_MODE: $([[ "$CI_COMMIT_BRANCH" == "main" ]] && echo "production" || echo "development")
  script:
    - npm run build -- --mode $BUILD_MODE

Reusable Workflows 與 Templates

當多個專案或多條 Pipeline 需要共用相同的 CI 邏輯時(如相同的測試設定、Docker 建置流程),應將共用部分抽取為可重用的模板,集中維護,避免在多處重複相同設定。一旦需要更新(例如升級 Node.js 版本),只需修改模板,所有引用的 Pipeline 自動受益:

# GitLab CI — 使用 include 引入共用模板(三種來源)

# 方式 1:引入同 repo 的本地模板檔案(小型專案推薦)
include:
  - local: '.gitlab/ci-templates/node.yml'

# 方式 2:引入遠端 GitLab 專案的模板(跨專案共用,組織級管理)
include:
  - project: 'my-org/ci-templates'
    ref: main
    file: '/templates/docker-build.yml'

# 方式 3:引入 GitLab 官方維護的安全掃描模板
include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Code-Quality.gitlab-ci.yml

---
# .gitlab/ci-templates/node.yml — 共用基礎模板定義
.node-base:              # 以 . 開頭的 job 不會被直接執行(稱為隱藏 job)
  image: node:20-alpine
  before_script:
    - npm ci --cache .npm --prefer-offline
  cache:
    key:
      files: [package-lock.json]
    paths: [.npm/]

# 在主要設定檔中用 extends 繼承模板(只需定義差異部分)
test:
  extends: .node-base    # 繼承 image、before_script、cache 等所有設定
  stage: test
  script:
    - npm test           # 只需定義此 job 特有的腳本

build:
  extends: .node-base    # 同樣繼承模板,共用相同基礎設定
  stage: build
  script:
    - npm run build
GitHub Actions 對應的機制是 Reusable Workflows(使用 workflow_call 觸發器)與 Composite Actions,可將多個 step 封裝為可呼叫的單元,在不同 workflow 中重複使用。Reusable Workflows 適合封裝完整的工作流程(如完整的 CI 流程),Composite Actions 則適合封裝一組相關的 step(如安裝與設定特定工具)。

Pipeline 優化技巧(fail-fast / timeout / skip)

隨著專案規模增長,Pipeline 執行時間可能從幾分鐘延長至數十分鐘,嚴重影響開發效率。優化 Pipeline 應從「減少不必要的執行」出發,而非一味追求執行速度。以下是五種最有效的優化策略:

# 技巧 1:fail-fast — 矩陣測試中,一個失敗立即取消其他(GitHub Actions)
jobs:
  test:
    strategy:
      fail-fast: true          # 預設 true;設為 false 可讓所有矩陣跑完後再彙整報告
      matrix:
        node-version: [18, 20, 22]

# 技巧 2:timeout — 防止 job 卡住長時間消耗 Runner 資源
test:
  stage: test
  timeout: 5 minutes           # job 超時自動終止(預設全域超時通常是 1 小時)
  script:
    - npm test

# 技巧 3:interruptible — 新 Pipeline 觸發時自動取消舊 Pipeline
build:
  stage: build
  interruptible: true          # 同一分支有新 push 時,自動取消正在執行的舊 build
  script:
    - npm run build

# 技巧 4:resource_group — 限制同一資源同時只有一個 job 執行
deploy-production:
  stage: deploy
  resource_group: production   # 確保不會有兩個部署 job 同時執行(避免競爭條件)
  script:
    - ./deploy.sh production

# 技巧 5:rules changes — 只在相關檔案有變更時才執行對應 job
test-backend:
  stage: test
  script:
    - npm run test:backend
  rules:
    - changes:
        - src/api/**/*          # 只有 api 目錄有變更時才執行後端測試
        - package-lock.json     # 相依套件變動也需要重新測試

# 技巧 6:skip CI — 在 commit message 加入關鍵字跳過 Pipeline
# 在 commit message 末尾加上 [skip ci] 或 [ci skip] 即可略過整個 Pipeline
# 適用於純文件修改(如 README 更新)不需要執行 CI 的情況
最有效的 Pipeline 優化通常是「減少不必要的執行」,而非提升執行速度。善用 rules: changes 只在相關檔案變更時觸發對應 job,搭配 interruptible: true 自動取消過時的 Pipeline,可大幅降低 CI 資源消耗,同時縮短開發者等待反饋的時間,讓 CI/CD 真正服務於開發流程而非成為阻礙。
下一步:學習 Docker 映像建置,將建置結果打包為可重複部署的映像,搭配 Pipeline 實現完整的容器化 CI/CD 流程。良好的 Docker 設計能讓 build stage 的執行時間大幅縮短,並確保部署環境的一致性。