Next.js: CI/CD 与 GitHub Actions
最后更新:2026-08-26
CI/CD 是开发团队的"自动驾驶"——每一次提交都自动经过质量门禁,只有合格的代码才能到达生产环境。
1. 你将学到
- 设计完整的 GitHub Actions 工作流:Lint → Test → Build → Deploy
- 使用并行矩阵策略同时在 Node.js 18/20/22 上运行测试
- 通过
vercel-action自动部署到 Vercel - 编写多阶段 Dockerfile 构建 358MB 的 standalone 镜像
- 管理 GitHub Secrets 环境变量和 PR Preview 预览部署
2. 一个技术主管的真实故事
(1) 痛点:手动部署,凌晨三点还在盯着终端
Bob 是 TaskFlow 团队的技术主管,团队 5 人维护着一个服务 10,000+ 用户的 SaaS 平台。每周五的发布流程是这样的:
"Alice 合并 PR → 通知 Bob → Bob 在本地跑测试 → 测试通过后手动
vercel deploy --prod→ 盯着终端等 10 分钟 → 确认部署成功 → 给团队发消息"
上周,Bob 因为手滑在 npm run build 前忘了 pull 最新代码,部署了一个过时的版本。5 分钟后用户无法登录,回滚又花了 15 分钟。客服收到了 100+ 投诉。
| 痛点 | 影响 |
|---|---|
| 手动部署 | 人为失误率高(30% 的发布存在问题) |
| 无自动测试门禁 | 有问题的代码也能到达生产 |
| 环境不一致 | 本地能跑,生产报错 |
| 无 PR Preview | 合并前看不到效果 |
(2) GitHub Actions 的解法
Bob 设计了三条自动化流水线:
# 每次 PR → 自动 lint + test + build
# 合并到 main → 自动构建 + 部署到 Vercel
# 每天凌晨 → 自动构建 Docker 镜像
(3) 收益
| 维度 | 手动部署 | CI/CD 自动化 |
|---|---|---|
| 发布耗时 | 30 分钟 | 8 分钟 |
| 人为失误 | 30% | < 1% |
| PR 审查效率 | 只看代码 | 代码 + Preview URL |
| 部署频率 | 每周 1 次 | 每天 5 次 |
3. GitHub Actions 基础
Actions 是 GitHub 内置的 CI/CD 平台,通过 YAML 文件定义工作流。
graph LR
A[git push] --> B[GitHub Actions]
B --> C[Events 触发]
C --> D[Jobs 并行/串行]
D --> E[Steps 步骤]
E --> F[Actions 市场组件]
style B fill:#cce5ff
style D fill:#d4edda
| 概念 | 说明 | 示例 |
|---|---|---|
| Workflow | 一个 YAML 文件 = 一个自动化流程 | .github/workflows/test.yml |
| Event | 触发工作流的事件 | push、pull_request、schedule |
| Job | 一组步骤(可并行或依赖) | lint → test → deploy |
| Step | 单个命令或 Action | npm run lint、actions/checkout |
| Runner | 执行环境 | ubuntu-latest、windows-latest |
(1) 工作流文件结构
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run lint
test:
needs: [lint]
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18', '20', '22']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
- run: npm run test:coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
build:
needs: [test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: .next/
4. test.yml:并行矩阵测试
graph TB
A[Git Push / PR] --> B[lint]
B --> C[test 18.x]
B --> D[test 20.x]
B --> E[test 22.x]
C --> F[build]
D --> F
E --> F
F --> G[上传构建产物]
style A fill:#cce5ff
style C fill:#d4edda
style D fill:#d4edda
style E fill:#d4edda
(1) 完整的 test.yml
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: TypeScript type check
run: npx tsc --noEmit
- name: ESLint check
run: npm run lint
- name: Prettier check
run: npx prettier --check "src/**/*.{ts,tsx}"
unit-and-integration:
name: Unit & Integration Tests
needs: [lint]
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
matrix:
node-version: ['18', '20', '22']
fail-fast: false
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Generate Prisma client
run: npx prisma generate
- name: Run database migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Run unit tests
run: npm run test
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
flags: unittests
name: codecov-node-${{ matrix.node-version }}
e2e:
name: E2E Tests
needs: [lint]
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test --project=chromium
env:
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7
build:
name: Build Check
needs: [unit-and-integration]
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: next-build
path: .next/
retention-days: 1
▶ 示例:Matrix 并行测试运行结果
# GitHub Actions 控制台输出
Job: unit-and-integration (node-version: 18.x) ✓ 3m 12s
Job: unit-and-integration (node-version: 20.x) ✓ 2m 58s
Job: unit-and-integration (node-version: 22.x) ✓ 3m 05s
Summary:
✓ lint (1 job) 0m 45s
✓ unit-and-integration (3 jobs) 3m 12s
✓ e2e (1 job) 4m 30s
✓ build (1 job) 1m 20s
5. deploy.yml:自动部署到 Vercel
| 部署环境 | 触发条件 | 目标 |
|---|---|---|
| Preview | PR 创建/更新 | 预览分支部署 |
| Production | 合并到 main | 生产环境更新 |
| Staging | 发布 tag | 预发布验证 |
(1) Vercel Deploy 配置
# .github/workflows/deploy.yml
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
jobs:
deploy-preview:
name: Deploy Preview
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment
run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to Vercel Preview
run: |
vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} > deployment-url.txt
echo "PREVIEW_URL=$(cat deployment-url.txt)" >> $GITHUB_ENV
- name: Comment Preview URL on PR
uses: thollander/actions-comment-pull-request@v2
with:
message: |
🚀 **Preview Deployment Ready**
| Environment | URL |
|:-----------|:----|
| Preview | ${{ env.PREVIEW_URL }} |
| Branch | ${{ github.head_ref }} |
_This comment is automatically updated on each push._
deploy-production:
name: Deploy Production
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: [deploy-preview]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: production
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm install --global vercel@latest
- name: Pull Vercel Environment
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build Project Artifacts
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to Vercel Production
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Notify deployment success
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Production deployment completed: ${{ github.repository }}@${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
6. Docker 镜像构建(多阶段 358MB)
对于自托管部署,需要在 CI 中自动构建 Docker 镜像。
(1) 多阶段 Dockerfile
# Dockerfile — Next.js 16 standalone 生产镜像
# Phase 1: 依赖安装(使用缓存层)
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && \
npm cache clean --force
# Phase 2: 构建(生成 .next 产物)
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# Phase 3: 运行(最小化镜像 ~358MB)
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs
COPY --from=build /app/public ./public
COPY --from=build --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=build --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
(2) Docker Build Action
# .github/workflows/docker.yml
name: Docker Build & Push
on:
push:
tags: ['v*']
schedule:
- cron: '0 2 * * 0' # 每周日凌晨 2 点
jobs:
docker:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
▶ 示例:Docker 镜像构建输出
# Build 日志
#1 [deps 1/1] RUN npm ci --only=production
#1 DONE 15.2s
#2 [build 1/4] COPY --from=deps /app/node_modules ./node_modules
#2 DONE 0.1s
#3 [build 2/4] COPY . .
#3 DONE 0.3s
#4 [build 3/4] RUN npm run build
#4 DONE 28.5s
#5 [runner 1/6] COPY --from=build /app/public ./public
#5 DONE 0.1s
#6 Exporting layers
#6 DONE 3.2s
# 镜像大小分析
ghcr.io/myorg/taskflow:latest 358 MB
7. 环境变量与 Secrets 管理
graph TB
A[GitHub Secrets] --> B[Actions 运行时]
B --> C[Vercel Token]
B --> D[数据库 URL]
B --> E[API 密钥]
B --> F[Slack Webhook]
C --> G[部署到 Vercel]
D --> H[运行测试]
E --> I[构建时注入]
F --> J[部署通知]
style A fill:#cce5ff
style B fill:#d4edda
| 环境变量名 | 用途 | 来源 |
|---|---|---|
VERCEL_TOKEN |
Vercel API 认证 | GitHub Secrets |
VERCEL_ORG_ID |
Vercel 团队 ID | Vercel Dashboard |
VERCEL_PROJECT_ID |
Vercel 项目 ID | Vercel Dashboard |
DATABASE_URL |
数据库连接 | GitHub Secrets |
CODECOV_TOKEN |
覆盖率上传 | Codecov 网站 |
SLACK_WEBHOOK_URL |
部署通知 | Slack 应用配置 |
(1) 设置 GitHub Secrets
# 在 GitHub 仓库 → Settings → Secrets and variables → Actions 中添加
gh secret set VERCEL_TOKEN --body "your-vercel-token"
gh secret set DATABASE_URL --body "postgresql://user:pass@host:5432/db"
gh secret set SLACK_WEBHOOK_URL --body "https://hooks.slack.com/services/..."
(2) 环境隔离策略
# 不同环境的变量覆盖
jobs:
test:
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
NODE_ENV: test
deploy-preview:
environment: preview
env:
DATABASE_URL: ${{ secrets.PREVIEW_DATABASE_URL }}
NEXT_PUBLIC_API_URL: ${{ vars.PREVIEW_API_URL }}
deploy-production:
environment: production
env:
DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
NEXT_PUBLIC_API_URL: https://api.taskflow.io
8. PR Preview 部署工作流
PR Preview 允许开发者在合并前预览改动效果。
graph TB
A[开发者创建 PR] --> B[Actions 触发 deploy-preview]
B --> C[Vercel 创建 Preview Deployment]
C --> D[生成 preview URL]
D --> E[Bot 评论 PR 添加链接]
E --> F[审查者在 Preview 上验证]
F -->|批准| G[合并到 main]
G --> H[Actions 触发 deploy-production]
style A fill:#cce5ff
style F fill:#fff3cd
style H fill:#d4edda
▶ 示例:完整的 PR Preview + 清理工作流
# .github/workflows/preview.yml
name: PR Preview
on:
pull_request:
types: [opened, synchronize, closed]
jobs:
preview:
if: github.event.action != 'closed'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel Preview
id: deploy
run: |
npx vercel --token=${{ secrets.VERCEL_TOKEN }} \
--scope=${{ secrets.VERCEL_ORG_ID }} \
--confirm > preview-url.txt
echo "url=$(cat preview-url.txt)" >> $GITHUB_OUTPUT
- name: Comment URL
uses: actions/github-script@v7
with:
script: |
const url = '${{ steps.deploy.outputs.url }}'
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 **Preview deployed**\n\n${url}\n\n_Commit: ${context.sha}_`
})
cleanup:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- name: Remove Vercel Preview
run: |
npx vercel remove \
taskflow-git-${GITHUB_HEAD_REF//\//-} \
--token=${{ secrets.VERCEL_TOKEN }} \
--yes --scope=${{ secrets.VERCEL_ORG_ID }} || true
9. 完整示例:TaskFlow CI/CD 全套流水线
# .github/workflows/full-pipeline.yml
# ============================================
# TaskFlow 完整 CI/CD 流水线
# ============================================
name: TaskFlow Full Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
release:
types: [published]
env:
NODE_VERSION: '20'
PNPM_VERSION: '9'
jobs:
# === Phase 1: 质量门禁 ===
quality-gate:
name: Quality Gate
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: TypeScript check
run: pnpm typecheck
- name: ESLint + Prettier
run: pnpm lint && pnpm format:check
- name: Unit tests with coverage
run: pnpm test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
# === Phase 2: 集成测试 ===
integration:
name: Integration Tests
needs: [quality-gate]
runs-on: ubuntu-latest
timeout-minutes: 20
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: taskflow
POSTGRES_PASSWORD: taskflow
POSTGRES_DB: taskflow_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: npx prisma generate
- run: npx prisma migrate deploy
env:
DATABASE_URL: postgresql://taskflow:taskflow@localhost:5432/taskflow_test
- name: Integration tests
run: pnpm test:integration
env:
DATABASE_URL: postgresql://taskflow:taskflow@localhost:5432/taskflow_test
- name: E2E tests
run: npx playwright install --with-deps chromium && pnpm test:e2e
env:
DATABASE_URL: postgresql://taskflow:taskflow@localhost:5432/taskflow_test
# === Phase 3: 构建 ===
build:
name: Build Application
needs: [integration]
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Build Docker image
if: github.event_name == 'push'
uses: docker/build-push-action@v5
with:
context: .
load: true
tags: taskflow:ci-${{ github.sha }}
- name: Save build outputs
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
.next/
public/
package.json
# === Phase 4: 部署 ===
deploy:
name: Deploy
needs: [build]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Deploy to Vercel
run: |
npx vercel deploy --prod \
--token=${{ secrets.VERCEL_TOKEN }} \
--scope=${{ secrets.VERCEL_ORG_ID }}
- name: Notify Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ TaskFlow deployed to production\nCommit: ${{ github.sha }}\nBy: ${{ github.actor }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Run Lighthouse CI
run: npx lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_TOKEN }}
❓ 常见问题
vercel-action 和 Vercel CLI 哪个更好?vercel-action(amondnet/vercel-action)封装了 CLI 的常见操作,但更新可能滞后。推荐直接使用 Vercel CLI(npm install -g vercel),在 Actions 中用 run: vercel deploy --prod --token=... 更灵活且版本最新。actions/secrets-scanning 检测意外提交。actions/cache 缓存 node_modules 和 .next/cache;(2) 并行执行独立 Job;(3) 使用 fail-fast: false 避免一个版本失败导致全部取消;(4) 使用 pnpm 替代 npm(更快的安装速度);(5) 自托管 Runner(如果仓库很大)。📖 小节
- GitHub Actions 工作流由 Event → Jobs → Steps 三层结构组成,YAML 文件放在
.github/workflows/下 strategy.matrix实现 Node 18/20/22 并行测试,同时验证多个 LTS 版本的兼容性vercel deploy --prod命令可集成到 Actions 实现自动部署,PR Preview 通过actions/github-script评论 URL- 多阶段 Docker 构建(deps → build → runner)将镜像体积控制在 ~358MB,
output: 'standalone'是必备前提 - GitHub Secrets 加密存储敏感变量,
environment字段实现开发/预览/生产的环境隔离 - CI 优化三大策略:依赖缓存、Job 并行化、使用 pnpm 替代 npm
📝 作业
-
基础题(⭐):创建一个
.github/workflows/ci.yml,包含 Lint、Test、Build 三个串行 Job,并在每次push到main时触发。 -
进阶题(⭐⭐):为你的项目配置完整的 Vercel 自动部署流水线:(1) 在 GitHub Secrets 中设置
VERCEL_TOKEN;(2) 编写deploy.yml实现 PR Preview + 生产部署;(3) 验证 PR 评论中出现 Preview URL。 -
挑战题(⭐⭐⭐):实现一个多环境 CI/CD 流水线(dev → staging → production):(1) 每个环境有不同的数据库和 Secrets;(2) dev 分支自动部署到 dev 环境;(3) main 分支自动部署到 staging;(4) 手动批准后才能部署到 production;(5) 部署成功后通过 Webhook 通知 Slack 频道。