GitHub Actions 是 GitHub 內建的 CI/CD 平台,讓你可以在程式碼儲存庫裡直接定義自動化流程,無需額外的 Jenkins 或 CircleCI。本文以本站(一個 Nuxt 4 + pnpm 專案)為例,從概念到實戰,帶你實現推送程式碼後自動建置並部署到伺服器。
核心概念
在撰寫任何設定之前,先把幾個術語搞清楚,後面不會繞暈。
| 概念 | 說明 |
|---|---|
| Workflow | 一個完整的自動化流程,以 YAML 檔案儲存在 .github/workflows/ 目錄下 |
| Event | 觸發 Workflow 的事件,如 push、pull_request、schedule、手動觸發等 |
| Job | Workflow 裡的一個執行單元,每個 Job 執行在獨立的虛擬機上,預設平行執行 |
| Step | Job 裡的單個操作,依序執行,可以是命令列腳本或已封裝好的 Action |
| Action | 可重複使用的操作模組,來自 GitHub Marketplace 或自己撰寫,用 uses 引用 |
| Runner | 執行 Job 的虛擬機,GitHub 提供的叫 hosted runner,也可以自己架 self-hosted |
| Secret | 加密的環境變數,在儲存庫 Settings 裡設定,用 ${{ secrets.MY_KEY }} 引用 |
Workflow 檔案結構
所有 Workflow 檔案放在儲存庫根目錄的 .github/workflows/ 下,檔名隨意,副檔名 .yml 或 .yaml 都可以。
最簡單的結構:
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
觸發事件詳解
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 在伺服器上執行。目標:
- CI(程式碼檢查):每次 PR 或推送時,自動執行 ESLint 檢查
- CD(自動部署):推送到
main分支時,在 Runner 上完成建置,打包成壓縮檔透過 SCP 上傳到伺服器,再 SSH 解壓並重啟 PM2
第一步:準備 Secrets
在 GitHub 儲存庫的 Settings → Secrets and variables → Actions 裡新增以下變數:
| Secret 名稱 | 內容 |
|---|---|
SSH_HOST | 伺服器 IP 或網域,如 123.45.67.89 |
SSH_PORT | SSH 連接埠,預設 22 |
SSH_USER | 登入使用者名稱,如 root 或 deploy |
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(程式碼檢查)
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 建置環境,建置失敗不影響線上服務,伺服器壓力也更小。
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 建置大型專案時 OOMtar打包:只打包.output(Nuxt 建置產出物)、ecosystem.config.cjs、package.json、pnpm-lock.yaml,原始碼不上傳appleboy/scp-action:透過 SCP 協定把壓縮檔傳到伺服器的SSH_PATH目錄pnpm install --prod:伺服器上只裝生產相依套件,不裝建置工具,減少體積pm2 startOrReload:比pm2 reload更健壯,程序不存在時會新建,存在時平滑重載,不會中斷服務;pm2 save儲存程序清單,伺服器重啟後能自動恢復
第四步:合併成一個檔案(先 CI 再 CD)
如果希望只有 CI 通過才執行部署,可以用 needs 讓 Job 串行:
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-node 的 cache: 'pnpm' 可以自動快取 store,大幅減少 pnpm install 時間:
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm # 自動根據 pnpm-lock.yaml 做快取 key
環境變數與 Secrets 的差異
env:
NODE_ENV: production # 明文,會顯示在記錄裡
steps:
- run: echo ${{ secrets.TOKEN }} # Secret,記錄中自動遮蔽為 ***
一般設定用 env,敏感資訊(密碼、Token、私鑰)一定要用 Secrets。
手動觸發部署
有時候不想推程式碼,只想重新部署(例如伺服器重啟後),可以在 Actions 頁面手動觸發:
on:
workflow_dispatch: # 加上這一行,Actions 頁面就會出現 "Run workflow" 按鈕
push:
branches: [main]
跨 Job 傳遞資料
Job 之間是隔離的,透過 outputs 共享資料:
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 }}"
只在特定路徑變更時觸發
前端和後端在同一個儲存庫時,可以只在相關檔案變化時觸發對應的流程:
on:
push:
paths:
- 'app/**'
- nuxt.config.ts
- package.json
- pnpm-lock.yaml
branches: [main]
查看執行結果
推送程式碼後,在 GitHub 儲存庫的 Actions 標籤頁可以看到所有 Workflow 的執行記錄,點進去可以展開每個 Step 查看完整記錄。失敗時記錄會標紅並高亮錯誤訊息,方便排查。
設定好後的完整 Workflow 大概長這樣:
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