logo Fonnpo

联系我
2026年5月31日 3 分鐘閱讀 Fonnpo

GitHub Actions 從入門到實戰

核心概念、觸發事件、快取加速 — 以本站 Nuxt 專案為例,實現推送程式碼自動部署。

#DevOps#CI/CD#GitHub

GitHub Actions 是 GitHub 內建的 CI/CD 平台,讓你可以在程式碼儲存庫裡直接定義自動化流程,無需額外的 Jenkins 或 CircleCI。本文以本站(一個 Nuxt 4 + pnpm 專案)為例,從概念到實戰,帶你實現推送程式碼後自動建置並部署到伺服器。

核心概念

在撰寫任何設定之前,先把幾個術語搞清楚,後面不會繞暈。

概念說明
Workflow一個完整的自動化流程,以 YAML 檔案儲存在 .github/workflows/ 目錄下
Event觸發 Workflow 的事件,如 pushpull_requestschedule、手動觸發等
JobWorkflow 裡的一個執行單元,每個 Job 執行在獨立的虛擬機上,預設平行執行
StepJob 裡的單個操作,依序執行,可以是命令列腳本或已封裝好的 Action
Action可重複使用的操作模組,來自 GitHub Marketplace 或自己撰寫,用 uses 引用
Runner執行 Job 的虛擬機,GitHub 提供的叫 hosted runner,也可以自己架 self-hosted
Secret加密的環境變數,在儲存庫 Settings 裡設定,用 ${{ secrets.MY_KEY }} 引用

Workflow 檔案結構

所有 Workflow 檔案放在儲存庫根目錄的 .github/workflows/ 下,檔名隨意,副檔名 .yml.yaml 都可以。

最簡單的結構:

.github/workflows/deploy.yml
        name: Deploy        # Workflow 名稱,顯示在 Actions 頁面

on:                 # 觸發條件
  push:
    branches:
      - main

jobs:               # 包含一個或多個 Job
  build:            # Job ID,自訂命名
    runs-on: ubuntu-latest  # 使用哪種 Runner
    steps:          # 該 Job 的執行步驟
      - name: 取出程式碼
        uses: actions/checkout@v4

      - name: 印出 Node 版本
        run: node -v

    

觸發事件詳解

Yaml
        on:
  # 推送到指定分支時觸發
  push:
    branches:
      - main
      - 'release/**'   # 支援 glob 比對
    paths:
      - 'app/**'       # 只有這些路徑變化才觸發(可選)

  # PR 操作時觸發
  pull_request:
    branches:
      - main
    types: [opened, synchronize, reopened]

  # 定時觸發(cron 語法,UTC 時區)
  schedule:
    - cron: '0 2 * * *'  # 每天 UTC 02:00

  # 手動觸發(在 Actions 頁面點按鈕)
  workflow_dispatch:
    inputs:
      environment:
        description: '部署環境'
        required: true
        default: 'production'
        type: choice
        options:
          - production
          - staging

    

實戰:為本站設定 CI/CD

本站是一個 Nuxt 4 專案,使用 pnpm 管理相依套件,用 PM2 在伺服器上執行。目標:

  1. CI(程式碼檢查):每次 PR 或推送時,自動執行 ESLint 檢查
  2. CD(自動部署):推送到 main 分支時,在 Runner 上完成建置,打包成壓縮檔透過 SCP 上傳到伺服器,再 SSH 解壓並重啟 PM2

第一步:準備 Secrets

在 GitHub 儲存庫的 Settings → Secrets and variables → Actions 裡新增以下變數:

Secret 名稱內容
SSH_HOST伺服器 IP 或網域,如 123.45.67.89
SSH_PORTSSH 連接埠,預設 22
SSH_USER登入使用者名稱,如 rootdeploy
SSH_KEY伺服器私鑰內容(cat ~/.ssh/id_ed25519
SSH_PATH伺服器部署目錄,如 /www/wwwroot/fonnpo-web

私鑰設定說明:把本機私鑰 ~/.ssh/id_ed25519完整內容(包括 -----BEGIN...-----END... 行)貼到 SSH_KEY 裡,然後把對應公鑰 ~/.ssh/id_ed25519.pub 附加到伺服器的 ~/.ssh/authorized_keys 檔案中。

第二步:CI Workflow(程式碼檢查)

.github/workflows/ci.yml
        name: CI

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

jobs:
  lint:
    name: ESLint 檢查
    runs-on: ubuntu-latest

    steps:
      - name: 取出程式碼
        uses: actions/checkout@v4

      - name: 安裝 pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 10

      - name: 安裝 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm          # 快取 pnpm store,加速後續安裝

      - name: 安裝相依套件
        run: pnpm install --frozen-lockfile

      - name: 執行 ESLint
        run: pnpm eslint .

    
收起

--frozen-lockfile 會確保 CI 環境嚴格按照 pnpm-lock.yaml 安裝,版本一致,不會悄悄升級。

第三步:CD Workflow(自動部署)

本站採用的策略是在 Runner 上完成建置,再把產出物打包傳到伺服器,而不是在伺服器上執行 git pull + build。好處是:伺服器不需要維護 Node.js 建置環境,建置失敗不影響線上服務,伺服器壓力也更小。

.github/workflows/deploy.yml
        name: Deploy to Self-hosted Server

on:
  push:
    branches: [main]
  workflow_dispatch:     # 支援在 Actions 頁面手動觸發

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true  # 強制 Actions 執行期使用 Node 24
      NODE_OPTIONS: --max-old-space-size=4096   # 建置大型專案時防止 OOM

    steps:
      - name: 取出程式碼
        uses: actions/checkout@v4

      - name: 安裝 pnpm
        uses: pnpm/action-setup@v4

      - name: 安裝 Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: 安裝相依套件
        run: pnpm install --frozen-lockfile

      - name: 建置
        run: pnpm build

      - name: 打包產出物
        run: |
          tar -czf release.tgz .output ecosystem.config.cjs package.json pnpm-lock.yaml

      - name: 上傳到伺服器(SCP)
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: release.tgz
          target: ${{ secrets.SSH_PATH }}

      - name: 伺服器端解壓並重啟
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            set -e
            cd ${{ secrets.SSH_PATH }}
            tar -xzf release.tgz
            rm -f release.tgz

            corepack enable
            pnpm install --prod --frozen-lockfile

            pm2 startOrReload ecosystem.config.cjs --update-env
            pm2 save

    
收起

幾個關鍵點:

  • env 區塊FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 讓 Actions 內建執行期對齊 Node 24;NODE_OPTIONS 擴大堆積記憶體上限,防止 Nuxt 建置大型專案時 OOM
  • tar 打包:只打包 .output(Nuxt 建置產出物)、ecosystem.config.cjspackage.jsonpnpm-lock.yaml,原始碼不上傳
  • appleboy/scp-action:透過 SCP 協定把壓縮檔傳到伺服器的 SSH_PATH 目錄
  • pnpm install --prod:伺服器上只裝生產相依套件,不裝建置工具,減少體積
  • pm2 startOrReload:比 pm2 reload 更健壯,程序不存在時會新建,存在時平滑重載,不會中斷服務;pm2 save 儲存程序清單,伺服器重啟後能自動恢復

第四步:合併成一個檔案(先 CI 再 CD)

如果希望只有 CI 通過才執行部署,可以用 needs 讓 Job 串行:

.github/workflows/ci-cd.yml
        name: CI / CD

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

jobs:
  lint:
    name: ESLint 檢查
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 10
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm eslint .

  deploy:
    name: 部署到生產伺服器
    runs-on: ubuntu-latest
    needs: lint                              # ← 等 lint Job 成功後才執行
    if: github.ref == 'refs/heads/main'     # ← PR 不觸發部署,只有推送 main 才部署
    env:
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
      NODE_OPTIONS: --max-old-space-size=4096

    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm build

      - name: 打包產出物
        run: tar -czf release.tgz .output ecosystem.config.cjs package.json pnpm-lock.yaml

      - name: 上傳到伺服器(SCP)
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          source: release.tgz
          target: ${{ secrets.SSH_PATH }}

      - name: 伺服器端解壓並重啟
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_KEY }}
          port: ${{ secrets.SSH_PORT }}
          script: |
            set -e
            cd ${{ secrets.SSH_PATH }}
            tar -xzf release.tgz && rm -f release.tgz
            corepack enable
            pnpm install --prod --frozen-lockfile
            pm2 startOrReload ecosystem.config.cjs --update-env
            pm2 save

    
收起

這樣 PR 只會觸發 lint,合併到 main 後會先跑 lint 再跑 deploy,兩步都綠才算成功。

常用技巧

快取相依套件加速建置

pnpm 搭配 actions/setup-nodecache: 'pnpm' 可以自動快取 store,大幅減少 pnpm install 時間:

Yaml
        - uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm       # 自動根據 pnpm-lock.yaml 做快取 key

    

環境變數與 Secrets 的差異

Yaml
        env:
  NODE_ENV: production       # 明文,會顯示在記錄裡

steps:
  - run: echo ${{ secrets.TOKEN }}  # Secret,記錄中自動遮蔽為 ***

    

一般設定用 env,敏感資訊(密碼、Token、私鑰)一定要用 Secrets。

手動觸發部署

有時候不想推程式碼,只想重新部署(例如伺服器重啟後),可以在 Actions 頁面手動觸發:

Yaml
        on:
  workflow_dispatch:    # 加上這一行,Actions 頁面就會出現 "Run workflow" 按鈕

  push:
    branches: [main]

    

跨 Job 傳遞資料

Job 之間是隔離的,透過 outputs 共享資料:

Yaml
        jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.version }}
    steps:
      - id: get-version
        run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: echo "部署版本 ${{ needs.build.outputs.version }}"

    

只在特定路徑變更時觸發

前端和後端在同一個儲存庫時,可以只在相關檔案變化時觸發對應的流程:

Yaml
        on:
  push:
    paths:
      - 'app/**'
      - nuxt.config.ts
      - package.json
      - pnpm-lock.yaml
    branches: [main]

    

查看執行結果

推送程式碼後,在 GitHub 儲存庫的 Actions 標籤頁可以看到所有 Workflow 的執行記錄,點進去可以展開每個 Step 查看完整記錄。失敗時記錄會標紅並高亮錯誤訊息,方便排查。

設定好後的完整 Workflow 大概長這樣:

Text
        Actions
└── Deploy to Self-hosted Server
    ├── 🟢 lint (20s)
    │   ├── 取出程式碼
    │   ├── 安裝 pnpm / Node.js
    │   ├── pnpm install
    │   └── ESLint 檢查
    └── 🟢 deploy (3min)
        ├── 取出程式碼
        ├── 安裝 pnpm / Node.js
        ├── pnpm install
        ├── pnpm build
        ├── tar 打包產出物
        ├── SCP 上傳到伺服器
        └── SSH 解壓並重啟 PM2
            ├── tar -xzf release.tgz
            ├── pnpm install --prod
            ├── pm2 startOrReload
            └── pm2 save

    

第一次設定好,之後每次 git push 就是全自動,省去手動登入伺服器的麻煩。

Fonnpo