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