EP.02工程品質與 DevOps

CI/CD 入門:GitHub Actions
自動化你的測試與部署流程

每次 Push 都要手動跑測試、手動部署?這不是工程師該做的事。 這篇帶你從零開始設定 GitHub Actions,讓 CI/CD 幫你把守每一道關卡。

Joseph Chen 2026 13 min read CI/CD · GitHub Actions · DevOps · Automation

「如果你的部署流程需要手冊文件,那它本身就是問題所在。 好的 CI/CD 應該讓你合併 PR 就等於部署完成,其他的都是自動的。」

— DevOps 實踐原則

CI/CD 是什麼?為什麼重要?

CI/CD 是兩個概念的組合:Continuous Integration(持續整合) Continuous Delivery/Deployment(持續交付/部署)。 兩者加在一起,目標只有一個:讓程式碼從寫完到上線的距離盡可能短,且過程盡可能安全。

Continuous Integration (CI)

每次有人 Push 或開 PR,自動執行:

  • Lint 與型別檢查
  • 單元測試與整合測試
  • Build 確保編譯不爆炸
目標:及早發現問題,防止壞程式碼合併進主幹

Continuous Deployment (CD)

CI 通過後,自動部署到對應環境:

  • PR 合併 → staging 環境
  • Tag 發布 → production 環境
  • 部署失敗自動 rollback
目標:消除手動部署,讓交付速度與信心同步提升

傳統流程 vs CI/CD 流程

❌ 傳統手動流程

1寫完程式碼
2SSH 連進伺服器
3手動 git pull
4npm install && npm run build
5重啟服務(希望沒忘步驟)
6手動驗證頁面是否正常
7如果壞了,手忙腳亂 rollback

✅ CI/CD 自動化流程

1寫完程式碼
2git push(就這樣)
3→ GitHub Actions 自動跑 CI
4→ 測試通過後自動部署
5→ Slack 通知部署完成
6→ 部署失敗自動通知並保留舊版

為什麼選 GitHub Actions?

CI/CD 工具不只 GitHub Actions 一種。在決定導入之前,了解它和主流替代方案的差異, 幫助你在不同情境做出正確選擇。

工具優勢劣勢最適合
GitHub Actions ⭐與 GitHub 原生整合,YAML 設定,免費額度充足僅限 GitHub 倉庫,複雜邏輯 YAML 難維護個人專案、開源、小型團隊
GitLab CI自帶 GitLab SCM,self-hosted 彈性大學習曲線稍陡,需自建 Runner企業私有部署、GitLab 用戶
CircleCI速度快,平行化能力強,設定簡潔免費額度有限,定價較貴需要高度客製化平行測試的團隊
Jenkins高度彈性,插件生態成熟需自建維運,設定複雜,UI 老舊大型企業、複雜 on-premise 流程

GitHub Actions 適合你的三個訊號

  • • 程式碼已在 GitHub 上:零設定即可使用,PR、Issue、Release 事件直接觸發
  • • 需要快速上手:市場上 Action 生態豐富(actions/checkout、setup-node 等),大多數常見任務有現成解法
  • • 免費額度夠用:公開 repo 無限制;私有 repo 每月 2,000 分鐘(Linux Runner),個人專案通常足夠

GitHub Actions 核心概念

GitHub Actions 是 GitHub 原生提供的 CI/CD 平台,設定全部寫在 YAML 檔案裡, 放在 .github/workflows/ 目錄下。 理解四個核心概念就能上手 90% 的使用情境。

Workflow

整個自動化流程的定義,對應一個 YAML 檔案。

一個 repo 可以有多個 Workflow,例如 ci.yml、deploy.yml、release.yml。

Event(觸發條件)

什麼動作會觸發 Workflow 執行。

常見:push、pull_request、schedule(定時)、workflow_dispatch(手動觸發)。

Job

Workflow 中的一個執行單位,預設平行執行。

每個 Job 在獨立的 Runner 虛擬機上執行,可設定 needs 讓 Job 依序進行。

Step

Job 中的一個步驟,依序執行。

Step 可以執行 shell 命令(run)或使用既有的 Action(uses),例如 actions/checkout@v4。

層級結構

Workflow(ci.yml)
├─on:push, pull_request ← Event
└─jobs:
├─test:← Job
│ └─steps:← Step list
└─deploy:needs: test ← 等 test 完才跑

實戰 ①:基本 CI Workflow

這是一個 Next.js 專案的標準 CI 設定,每次 PR 開啟或更新時,自動執行 Lint、型別檢查和測試。

.github/workflows/ci.yml
# 這個 Workflow 負責 Continuous Integration
name: CI

# 觸發條件:push 到 main/develop,或任何 PR
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    name: Lint, Type-check & Test
    runs-on: ubuntu-latest   # GitHub 提供的免費 Linux Runner

    strategy:
      matrix:
        node-version: [20.x]  # 可設多個版本同時測試

    steps:
      # 1. 把程式碼 checkout 出來
      - name: Checkout code
        uses: actions/checkout@v4

      # 2. 設定 Node.js 版本
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'  # 快取 node_modules,加速後續執行

      # 3. 安裝依賴
      - name: Install dependencies
        run: npm ci  # 比 npm install 更嚴格,使用 package-lock.json

      # 4. 執行 ESLint
      - name: Run ESLint
        run: npm run lint

      # 5. TypeScript 型別檢查
      - name: Type check
        run: npx tsc --noEmit

      # 6. 跑單元測試,並輸出 coverage 報告
      - name: Run tests
        run: npm test -- --coverage --watchAll=false

      # 7. 上傳 coverage 報告(可選)
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        if: always()  # 就算測試失敗也上傳
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

npm ci vs npm install

npm ci 嚴格按照 package-lock.json 安裝,不會升級版本,確保 CI 環境與本機完全一致。

matrix strategy

可同時在 Node 18、20、22 上測試。每個版本組合是獨立的 Job,平行執行。

actions/cache

cache: npm 會快取 ~/.npm,下次執行直接用快取,從 2 分鐘縮到 30 秒。

實戰 ②:自動部署到 Vercel

CI 通過後,自動部署到 Vercel。這裡用 Vercel CLI 做部署, 比 Vercel 官方 GitHub App 更靈活(可自訂部署邏輯)。

⚙️ 先在 GitHub 設定 Secrets(Settings → Secrets and variables → Actions)

VERCEL_TOKENVercel 帳號設定 → Tokens → Create
VERCEL_ORG_IDvercel link 後查看 .vercel/project.json
VERCEL_PROJECT_ID同上,project.json 裡的 projectId
.github/workflows/deploy.yml
name: Deploy to Vercel

on:
  push:
    branches: [main]  # 只有 main 合併才部署 production

jobs:
  # 先跑 CI(重用前面的概念)
  ci:
    name: Run CI checks
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test -- --watchAll=false

  # CI 通過才執行 deploy
  deploy:
    name: Deploy to Production
    needs: ci          # 等 ci job 成功才執行
    runs-on: ubuntu-latest
    environment: production  # GitHub Environment,可設 reviewers 審核

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      # 安裝 Vercel CLI
      - name: Install Vercel CLI
        run: npm install -g vercel@latest

      # Pull Vercel 專案設定
      - name: Pull Vercel Environment
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      # Build
      - name: Build
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      # Deploy 預建好的產物
      - name: Deploy to Production
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

environment: production 的意義

GitHub Environments 讓你可以設定「部署保護規則」:例如要求至少一位 reviewer 核准才能部署, 或是只允許特定分支觸發。設定路徑:Repository → Settings → Environments。 對個人專案這步可省略,但團隊協作強烈建議加上。

實戰 ③:PR Preview 環境

更進階的做法:每個 PR 自動部署一個獨立的 Preview URL,讓 reviewer 可以在真實環境驗證功能, 而不是只看程式碼。

.github/workflows/preview.yml
name: Preview Deploy

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  preview:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write  # 需要權限才能留 PR 留言

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci

      - name: Install Vercel CLI
        run: npm install -g vercel@latest

      - name: Deploy Preview
        id: deploy
        run: |
          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
          vercel build --token=${{ secrets.VERCEL_TOKEN }}
          PREVIEW_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$PREVIEW_URL" >> $GITHUB_OUTPUT
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

      # 把 Preview URL 自動留言在 PR 上
      - name: Comment Preview URL on PR
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## 🚀 Preview 環境已部署\n\n**URL**: ${{ steps.deploy.outputs.url }}\n\n> 此 Preview 會在 PR 關閉後自動刪除`
            })

Preview 環境的工作流程

開 PR
→ 自動跑 CI
→ 部署 Preview URL
→ Bot 留言附上連結
→ Reviewer 在真實環境驗證
→ 核准 → 合併 → 自動部署 Production

進階技巧

避免並發衝突:concurrency 設定

同一個 PR 快速連推多次,會同時觸發多個 Workflow。用 concurrency 確保同一個 PR 只有最新的 run 在跑,舊的自動取消。

# 加在 jobs: 同層
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # 取消同 group 的舊 run

可重用 Workflow:避免重複 YAML

多個 Workflow 共用相同的 CI 步驟?把它抽成 Reusable Workflow,其他地方 uses 引用即可。

.github/workflows/reusable-ci.yml
# 可被呼叫的 Workflow
name: Reusable CI
on:
  workflow_call:  # 關鍵:這個 Workflow 可被其他 Workflow 呼叫
    inputs:
      node-version:
        required: false
        type: string
        default: '20'

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci && npm run lint && npm test -- --watchAll=false
.github/workflows/deploy.yml(呼叫端)
jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml  # 重用
    with:
      node-version: '20'

  deploy:
    needs: ci
    # ... 部署步驟

環境變數與 Secrets 的層級

層級設定位置適用場景
Organization SecretOrganization → Settings → Secrets多個 repo 共用(例如 NPM_TOKEN)
Repository SecretRepo → Settings → Secrets該 repo 專屬(最常用)
Environment SecretRepo → Settings → EnvironmentsStaging / Production 使用不同的值
Workflow env:YAML 檔案直接寫非敏感設定值(NODE_ENV 等)
原則:所有 API Key、Token、密碼一律用 Secrets,永遠不要直接寫在 YAML 或程式碼裡。 Secrets 在 log 中會被自動遮蔽顯示為 ***。

常見錯誤與除錯方式

Permission denied / EACCES 錯誤

原因:Runner 沒有執行特定指令的權限

解法:在 job 層加上 permissions: contents: read(或需要的權限)。GitHub 有預設權限,某些操作需要明確宣告。

Workflow 不觸發

原因:YAML 縮排錯誤,或 on: 條件設錯

解法:用 yamllint 驗證 YAML 格式。注意 branches 是 list,要用陣列格式:branches: [main],不是 branches: main。

Secrets 讀取到空值

原因:Fork 的 PR 預設無法讀取 parent repo 的 Secrets

解法:這是 GitHub 安全設計。解決方案:用 pull_request_target(注意安全風險),或只針對自己 repo 的 branch 部署。

Job 執行時間太長

原因:每次都重新安裝 node_modules

解法:確認 cache: npm 有設定,且 cache-dependency-path 指向正確的 package-lock.json 路徑。

一張圖理解完整 CI/CD 流程

Developergit push / open PR
↓ triggers
CI WorkflowLint → Type-check → Test → Build
✓ Pass|✗ Fail → PR 被標記,無法合併
↓ on pass
CD Workflow
PR → Preview URL(Vercel Preview)
main merge → Production Deploy(Vercel Prod)
Result用戶看到新版本,工程師收到 Slack 通知

重點整理

CI 的核心價值

讓「壞程式碼」在進入 main 分支前就被攔截,保護主幹的穩定性。

needs: 控制依賴

CI Job 與 Deploy Job 分離,Deploy 等 CI 完成才執行,失敗就不部署。

Secrets 安全存放

所有敏感資訊放 GitHub Secrets,永不寫在 YAML 或程式碼裡。

concurrency 防衝突

同一 PR 多次 push 只跑最新一次,節省 Runner 資源。

Preview 環境

每個 PR 部署獨立預覽 URL,讓 code review 從看程式碼升級為驗證功能。

從小處開始

先設一個簡單的 npm test CI,上線後再逐步加入 CD、Preview、Slack 通知。