🔄 CI/CD:从代码提交到生产上线

每一次 git push 都应该自动验证、构建、部署——手动部署是 Bug 和焦虑的温床
📑 目录

CI/CD 是什么

CI (Continuous Integration):开发者频繁将代码合并到主分支,每次合并自动触发构建和测试,尽早发现集成问题。

CD (Continuous Delivery/Deployment):代码通过所有测试后,自动部署到预发布/生产环境。Delivery 需要人工确认,Deployment 全自动。

📝 代码提交 🔍 代码检查 🏗️ 构建 🧪 测试 📦 打包 🚀 部署
CI vs CD vs CD — 三个概念的区别
概念全称自动化到哪一步需要人工干预
CIContinuous Integration构建 + 测试部署由人做
CD (Delivery)Continuous Delivery到预发布环境一键点确认上线
CD (Deployment)Continuous Deployment到生产环境全程无人值守

💡 对于大多数 SaaS:CI + Continuous Delivery 是最佳平衡。完全自动部署适合高成熟度团队。

为什么你必须有 CI/CD

🚫 没有 CI/CD 的世界
  • ❌ "在我电脑上是好的啊"
  • ❌ 上线后才发现编译错误
  • ❌ 手动 SSH 上去 git pull
  • ❌ 忘记跑测试,引入回归
  • ❌ 凌晨 3 点手动部署出事
  • ❌ 不敢频繁发布,积攒大版本
  • ❌ 上线步骤靠脑子记
✅ 有 CI/CD 的世界
  • ✅ 每个 PR 自动验证
  • ✅ 合并即部署,零手动操作
  • ✅ 10 分钟内发现问题
  • ✅ 一键回滚,安全感拉满
  • ✅ 一天部署 10 次也不怕
  • ✅ 部署步骤版本化管理
  • ✅ 团队任何人都敢上线
核心价值:CI/CD 不是锦上添花——它是你从"能跑"到"能持续交付"的分水岭。没有 CI/CD 的 SaaS,本质上还处于手工作坊阶段。

Pipeline 基础架构

一个成熟的 CI/CD Pipeline 通常包含以下阶段,每个阶段失败都会阻断后续阶段:

1️⃣ Source — 代码获取
触发条件:push / PR / tag / 定时 / 手动。拉取代码,解析变更范围。
必须
2️⃣ Lint & Static Analysis — 静态检查
ESLint/Prettier/Ruff/Clippy、类型检查 (tsc/mypy)、安全扫描 (SAST)。秒级反馈,最便宜的问题发现。
必须
3️⃣ Build — 构建
编译、打包、Docker 镜像构建。缓存是关键——同分支增量构建应 < 2 分钟。
必须
4️⃣ Test — 测试
Unit → Integration → E2E。分层运行,快速失败。并行化提速。
必须
5️⃣ Security Scan — 安全扫描
依赖漏洞检查 (npm audit/safety)、Docker 镜像扫描 (Trivy)、Secrets 泄露检测。
推荐
6️⃣ Deploy Staging — 预发布部署
部署到 staging 环境,运行冒烟测试、E2E 测试。
推荐
7️⃣ Deploy Production — 生产部署
蓝绿/金丝雀/滚动部署。自动回滚条件判断。
进阶
8️⃣ Verify — 上线验证
健康检查、冒烟测试、监控指标对比、错误率告警。
进阶

Pipeline 设计原则

📐 PIPE 管道设计四原则

GitHub Actions — 事实标准

GitHub Actions 已经成为 CI/CD 的事实标准。原因:与 GitHub 深度集成、生态丰富、免费额度充足、YAML 配置简单。

💰 免费额度 (2026)
账户类型免费分钟/月Runner备注
公开仓库无限GitHub-hosted完全免费
私有仓库 (Free)2,000 分钟GitHub-hosted足够小型项目
私有仓库 (Pro/Team)3,000 分钟GitHub-hostedmacOS Runner 10x 计费
自托管 Runner无限Self-hosted零费用,自己维护

基础 Workflow 结构

# .github/workflows/ci.yml
name: CI

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

# 同一 PR 只保留最新一次运行
concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check

  test:
    runs-on: ubuntu-latest
    needs: lint  # lint 通过后才跑测试
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm test:coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

矩阵构建 — 多版本并行

jobs:
  test:
    strategy:
      fail-fast: false  # 一个失败不影响其他
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

缓存最佳实践

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 方式1: 使用 setup-node 内置缓存
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'  # 自动缓存 pnpm store

      # 方式2: 手动缓存 (更灵活)
      - uses: actions/cache@v4
        with:
          path: |
            ~/.pnpm-store
            .next/cache
            node_modules
          key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
          restore-keys: |
            ${{ runner.os }}-pnpm-

      - run: pnpm install --frozen-lockfile
      - run: pnpm build

Docker 构建与推送

jobs:
  docker:
    runs-on: ubuntu-latest
    needs: [test, build]
    permissions:
      contents: read
      packages: write  # 推送到 GHCR
    steps:
      - uses: actions/checkout@v4

      # Docker Buildx — 多平台构建 + 缓存
      - uses: docker/setup-buildx-action@v3

      # QEMU — ARM64 等异构构建
      - uses: docker/setup-qemu-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

部署到 VPS (SSH Deploy)

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: [docker]
    if: github.ref == 'refs/heads/main'
    environment: production  # 需要 GitHub Environment 审批
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            docker pull ghcr.io/myorg/myapp:latest
            docker compose up -d
            sleep 5
            curl -f http://localhost:3000/health || exit 1
⚠️ 安全提醒:永远不要在 Workflow YAML 中硬编码密码。使用 secrets. 引用,SSH Key 用 Deploy Key 而非个人 Key。

GitHub Actions 优劣势

✅ 优势
  • ➤ 与 GitHub 深度集成,零配置
  • ➤ Marketplace 生态 20,000+ Actions
  • ➤ 公开仓库无限免费
  • ➤ 矩阵构建、并发控制
  • ➤ Environment 保护规则 (审批)
  • ➤ 自托管 Runner 支持任意环境
❌ 劣势
  • ➤ 私有仓库免费额度有限
  • ➤ macOS Runner 10x 计费
  • ➤ YAML 写复杂逻辑易失控
  • ➤ 调试体验差 (只有日志)
  • ➤ Runner 冷启动 30-60s
  • ➤ 与 GitHub 绑定,迁移成本高

GitLab CI — 自托管首选

如果你的代码托管在 GitLab(尤其自托管 GitLab),GitLab CI/CD 是自然选择。它比 GitHub Actions 更早成熟,Pipeline 概念更清晰。

# .gitlab-ci.yml
stages:
  - lint
  - test
  - build
  - deploy

variables:
  DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  # 全局缓存 key
  CACHE_KEY: pnpm-${CI_COMMIT_BRANCH}

# 默认配置
default:
  image: node:22
  before_script:
    - pnpm install --frozen-lockfile
  cache:
    key: $CACHE_KEY
    paths:
      - .pnpm-store
      - node_modules

lint:
  stage: lint
  script:
    - pnpm lint
    - pnpm type-check

test:
  stage: test
  script:
    - pnpm test:coverage
  coverage: '/All files[^|]*\|[^|]*\s+([\d.]+)/'  # 自动提取覆盖率
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build:
  stage: build
  script:
    - pnpm build
  artifacts:
    paths:
      - dist/

docker:
  stage: build
  needs: [build]
  image: docker:24
  services:
    - docker:24-dind
  before_script:
    - echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
  script:
    - docker build -t $DOCKER_IMAGE .
    - docker push $DOCKER_IMAGE

deploy:staging:
  stage: deploy
  environment:
    name: staging
    url: https://staging.myapp.com
  script:
    - ssh deploy@staging "docker pull $DOCKER_IMAGE && docker compose up -d"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy:production:
  stage: deploy
  environment:
    name: production
    url: https://myapp.com
  when: manual  # 需手动触发
  script:
    - ssh deploy@prod "docker pull $DOCKER_IMAGE && docker compose up -d"
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

GitLab CI 独有能力

🔮 GitLab CI 独有特性
✅ GitLab CI 优势
  • ➤ Pipeline 概念更成熟
  • ➤ 自托管无限制
  • ➤ 内置安全扫描
  • ➤ Review Apps
  • ➤ 数据完全可控
❌ GitLab CI 劣势
  • ➤ 自托管运维负担
  • ➤ 社区生态不如 GitHub
  • ➤ SaaS 免费额度更少
  • ➤ 语法比 Actions 更死板
  • ➤ Runner 配置更复杂

其他 CI/CD 平台

平台适合谁免费额度核心特色定价模型
CircleCI 追求速度的团队 6,000 分钟/月 Docker 层缓存、性能优异、配置即代码 按分钟计费
Jenkins 传统企业/Java 栈 自托管免费 1,800+ 插件、完全可定制、Pipeline as Code (Groovy) 自托管零费用
Buildkite 大规模工程团队 试用后付费 Agent 跑在你自己的机器上、极快、弹性 按 Agent 计费
Drone CI Kubernetes 原生团队 开源版免费 每个 Step 一个容器、YAML 简洁、Go 编写 开源免费/企业版
Woodpecker CI Drone 社区分支 开源免费 Drone 的社区 fork,更活跃、轻量 开源免费
Gitea Actions Gitea 用户 自托管免费 兼容 GitHub Actions 语法、无缝迁移 自托管零费用
Forgejo Actions Forgejo 用户 自托管免费 Gitea 的社区 fork,同样兼容 Actions 自托管零费用
Bitbucket Pipelines Atlassian 生态 50 分钟/月 与 Jira/Confluence 集成 按分钟计费
AWS CodePipeline AWS 深度用户 按用量付费 与 AWS 服务深度集成 按管道/执行次数

新兴趋势:CI/CD 即代码的下一代

🚀 Dagger — 用代码写 Pipeline

Dagger 允许你用 Go/Python/TypeScript 写 CI/CD Pipeline,而非 YAML。Pipeline 就是普通的函数调用——可测试、可调试、可复用。

// Dagger: 用 Go 写 CI (不是 YAML!)
package main

import (
    "context"
    "os"
    "dagger.io/dagger"
)

func main() {
    ctx := context.Background()
    client, _ := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
    defer client.Close()

    src := client.Host().Directory()

    // 构建
    builder := client.Container().From("node:22").
        WithMountedDirectory("/app", src).
        WithWorkdir("/app").
        WithExec([]string{"npm", "ci"}).
        WithExec([]string{"npm", "run", "build"})

    // 测试
    builder.WithExec([]string{"npm", "test"})

    // 导出
    builder.Directory("dist").Export(ctx, "./dist")
}

💡 Dagger 的核心价值:同一套 Pipeline 代码可以本地跑、CI 跑、任何平台跑。告别 YAML 地狱。

🐙 Earthly — Makefile + Docker 的融合

Earthly 结合了 Makefile 的简洁和 Docker 的隔离,让构建过程可重复、可缓存。

# Earthfile
VERSION 0.8

build:
    FROM node:22
    COPY package.json pnpm-lock.yaml ./
    RUN pnpm install --frozen-lockfile
    COPY . .
    RUN pnpm build
    SAVE ARTIFACT dist /dist

test:
    FROM +build
    RUN pnpm test

docker:
    FROM +build
    SAVE IMAGE --push myapp:latest

部署策略详解

部署是 CI/CD 中风险最高的环节。选择正确的策略,决定了上线事故的影响范围。

1. 直接部署 (Recreate)

🔴 直接部署 — 停旧启新

停止旧版本 → 部署新版本 → 启动新版本

V1 运行中 V1 停止 V2 启动

2. 滚动部署 (Rolling Update)

🟡 滚动部署 — 逐步替换

逐个实例替换,新旧共存过渡期

V1 V1 V1 V1 V2 V1 V1 V1 V2 V2 V1 V1 V2 V2 V2 V2
# Kubernetes 滚动部署配置
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 最多多起 1 个 Pod
      maxUnavailable: 0   # 不允许任何 Pod 不可用

3. 蓝绿部署 (Blue-Green)

🔵🟢 蓝绿部署 — 两套环境切换

维护两套完全相同的环境 (蓝/绿),流量一键切换

🔵 Blue (V1) ← 流量 🟢 Green (V2) 预热 → 切换 🔵 Blue (V1) 待命 🟢 Green (V2) ← 流量
# Nginx 蓝绿切换
upstream backend {
    server blue:3000;   # 当前活跃
    # server green:3000;  # 切换时注释蓝,取消注释绿
}

# 自动化脚本
# sed -i 's/server blue/server green/' /etc/nginx/conf.d/app.conf
# nginx -s reload

4. 金丝雀部署 (Canary)

🐦 金丝雀部署 — 小范围试水

先将新版本发布给一小部分用户 (1-5%),观察指标正常后逐步扩大

V1: 100% V2: 5% / V1: 95% V2: 25% / V1: 75% V2: 100%
# Argo Rollouts 金丝雀配置
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
  strategy:
    canary:
      steps:
      - setWeight: 5
      - pause: {duration: 5m}    # 观察 5 分钟
      - setWeight: 25
      - pause: {duration: 5m}
      - setWeight: 50
      - pause: {duration: 10m}
      - setWeight: 100
      canaryService: myapp-canary
      stableService: myapp-stable

5. 功能开关部署 (Feature Flag)

🚩 功能开关 — 部署与发布解耦

代码已经部署,但功能对用户隐藏。通过开关控制谁能看到新功能。

// 代码中检查功能开关
if (featureFlags.isEnabled('new-dashboard', { userId: user.id })) {
  renderNewDashboard();
} else {
  renderOldDashboard();
}

策略选择矩阵

维度直接滚动蓝绿金丝雀功能开关
停机时间
回滚速度分钟级分钟级秒级秒级毫秒级
资源成本1x1-1.5x2x1.1-2x1x
复杂度极低
用户影响全部部分极少数可控
适合阶段MVP成长期关键业务成熟期任何阶段

高级模式

Monorepo CI/CD

📂 Monorepo 增量构建 — 只构建变更的包
# .github/workflows/ci.yml — 使用 Turborepo
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 需要完整 git 历史计算 diff

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      # Turborepo 自动检测变更的包,只构建/测试受影响的
      - run: pnpm turbo build --filter=...[HEAD^1]
      - run: pnpm turbo test --filter=...[HEAD^1]

      # Nx 的等价写法
      # - run: npx nx affected --target=build
      # - run: npx nx affected --target=test

💡 关键:fetch-depth: 0 让 CI 能看到完整 diff,工具才能正确判断哪些包受影响。

Preview Deployments

👁️ Preview 部署 — 每个 PR 一个临时环境

每个 Pull Request 自动创建一个独立部署,让审查者直接体验变更效果。

# Vercel Preview 自动完成,零配置
# Cloudflare Pages 也原生支持

# 自托管方案:Docker + 子域名
jobs:
  preview:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - run: |
          PREVIEW_DOMAIN="pr-${{ github.event.number }}.preview.myapp.com"
          docker build -t myapp:pr-${{ github.event.number }} .
          # 部署到预览集群
          ssh deploy@preview "docker run -d --name pr-${{ github.event.number }} \
            -e VIRTUAL_HOST=$PREVIEW_DOMAIN myapp:pr-${{ github.event.number }}"
          echo "Preview: https://$PREVIEW_DOMAIN"

  # PR 关闭时清理
  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - run: ssh deploy@preview "docker rm -f pr-${{ github.event.number }}"

数据库迁移在 CI/CD 中

🗄️ 数据库迁移 — 最容易出事的环节
铁律:迁移必须向后兼容。新代码必须能跑在旧 schema 上,旧代码必须能跑在新 schema 上(至少在过渡期)。
# 两阶段迁移策略
# Phase 1: 添加新列(不删旧列)
jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - run: |
          # 1. 先跑迁移
          npx prisma migrate deploy
          # 2. 验证迁移成功
          npx prisma migrate status

  deploy:
    needs: migrate
    steps:
      - run: |
          # 3. 部署新代码(兼容新旧 schema)
          docker compose up -d

# Phase 2: 下一个部署周期删除旧列
# 删除列的迁移要在新代码运行稳定至少一个周期后才做

推荐工具:prisma migrate / alembic / flyway / golang-migrate。核心是迁移文件纳入版本控制,CI 中执行。

自动回滚

🔙 自动回滚 — 当部署出问题时
# 健康检查 + 自动回滚
deploy:
  steps:
    - name: Deploy
      run: docker compose up -d

    - name: Health Check (60s grace)
      run: |
        for i in $(seq 1 12); do
          if curl -f http://localhost:3000/health; then
            echo "✅ Healthy"
            exit 0
          fi
          sleep 5
        done
        echo "❌ Health check failed"
        exit 1

    - name: Rollback on Failure
      if: failure()
      run: |
        docker compose down
        docker compose up -d --build  # 重新用上一个镜像
        echo "⚠️ Rolled back due to health check failure"

CI/CD 安全

CI/CD Pipeline 是攻击者的黄金目标——拿到 Pipeline 权限 = 拿到生产环境。

🛡️ CI/CD 安全清单

1. Secrets 管理

2. 供应链安全

3. Pipeline 注入防护

⚠️ 常见 CI/CD 攻击向量:
# 安全的 GitHub Actions 配置示例
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read       # 最小权限
      packages: write      # 只写包注册表
      id-token: write      # OIDC 需要
    environment:
      name: production
      # 保护规则:需要 1 人审批 + 等待 5 分钟
    steps:
      # ✅ 用 commit SHA 而非 tag
      - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4

      # ✅ 用 OIDC 而非 Access Key
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/deploy-role
          aws-region: ap-northeast-1

      # ✅ 避免注入:使用环境变量而非直接插值
      - name: Deploy
        env:
          BRANCH: ${{ github.head_ref }}
        run: |
          # 不要: echo "Deploying ${{ github.head_ref }}"
          # 要用: 
          echo "Deploying $BRANCH"

成本优化

💸 CI/CD 成本优化策略

1. 缓存一切

2. 并行化

3. 跳过不必要的运行

# 只在相关文件变更时运行
on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - 'pnpm-lock.yaml'
    paths-ignore:
      - '**.md'
      - 'docs/**'

# 或者用 path-filtering Action
jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      backend: ${{ steps.filter.outputs.backend }}
      frontend: ${{ steps.filter.outputs.frontend }}
    steps:
      - uses: actions/checkout@v4
      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            backend:
              - 'backend/**'
            frontend:
              - 'frontend/**'

4. 自托管 Runner

5. 选择合适的 Runner

Runner计费倍率适合
Linux1x大部分场景
Windows2xWindows 专用构建
macOS10xiOS/macOS 构建
自托管0x任何场景 (自有硬件)

常见反模式

🚫 CI/CD 反模式 — 你可能正在犯的错

❌ 反模式 1:Pipeline 写成脚本大杂烩

一个 Job 里 run 几十行脚本。应该拆分为有语义的 Steps,每步有 name。

# 🚫 Bad
- run: |
    npm ci
    npm run lint
    npm run test
    npm run build
    docker build .
    docker push ...

# ✅ Good
- name: Install dependencies
  run: npm ci
- name: Lint
  run: npm run lint
- name: Test
  run: npm run test
- name: Build
  run: npm run build

❌ 反模式 2:没有失败通知

Pipeline 悄悄失败没人知道。必须配置 Slack/Discord/Telegram 通知。

# 失败通知
- name: Notify on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "❌ Pipeline failed: ${{ github.workflow }}",
        "blocks": [{"type": "section", "text": {"type": "mrkdwn",
          "text": "*Failed:* ${{ github.workflow }}\n*Branch:* ${{ github.ref }}\n*Commit:* ${{ github.sha }}"
        }}]
      }

❌ 反模式 3:CI 时长超过 15 分钟

开发者不会等 15 分钟才知道自己有没有写错。目标:Lint < 1min,Unit < 3min,E2E < 10min。

❌ 反模式 4:生产部署没有回滚计划

每个部署步骤都必须有对应的回滚步骤,且验证过回滚路径确实能用。

❌ 反模式 5:Secrets 写在 YAML 里

password: my-secret-123 会永远留在 git 历史里。即使后来删除,git log 里仍然可见。

❌ 反模式 6:忽略 Flaky Tests

不稳定的测试比没有测试更糟——它让团队对 CI 失去信任。re-run failed 不应该成为日常操作。

❌ 反模式 7:所有环境用同一个配置

Staging 和 Production 的部署策略应该不同。Staging 可以快速失败,Production 需要金丝雀 + 自动回滚。

决策树:选哪个 CI/CD?

🌳 CI/CD 平台选择决策树

Q1: 代码托管在哪里?

  • GitHub → GitHub Actions (除非有特殊原因)
  • GitLab → GitLab CI (自然选择)
  • 自建 Gitea/Forgejo → Gitea Actions / Forgejo Actions
  • Bitbucket → Bitbucket Pipelines

Q2: 有特殊需求?

  • 需要极致性能 → Buildkite
  • 需要完全控制 → Jenkins / 自托管 GitLab + Runner
  • 讨厌 YAML → Dagger / Earthly
  • K8s 原生 → Drone CI / Tekton / Argo Workflows

Q3: 预算?

  • 零预算 (公开仓库) → GitHub Actions (无限免费)
  • 零预算 (私有) → 自托管 Runner + GitHub/GitLab
  • 有预算 → 看团队偏好,都行
💡 实用建议:不要花超过 1 天选择 CI/CD 平台。对于 90% 的团队,GitHub Actions 就是正确答案。只有当 GitHub Actions 明显不满足时,才考虑其他选项。先跑起来,再优化。

实战模板合集

模板 1: Next.js + Vercel 全自动

# 零配置!Vercel 原生集成 GitHub
# Push to main → 自动部署
# PR → 自动 Preview
# 只需在 Vercel Dashboard 连接仓库

# 如果需要额外检查:
name: CI
on: [push, pull_request]
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm lint
      - run: pnpm type-check
      - run: pnpm test

模板 2: Docker + VPS 部署

name: Build & Deploy
on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: 'pnpm' }
      - run: pnpm install --frozen-lockfile
      - run: pnpm test

  docker:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.sha }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: docker
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: |
            docker compose pull
            docker compose up -d --remove-orphans
            sleep 10
            curl -f http://localhost:3000/health || (docker compose rollback && exit 1)

      - name: Notify Success
        if: success()
        run: echo "✅ Deployed ${{ github.sha }} to production"

      - name: Notify Failure
        if: failure()
        run: echo "❌ Deploy failed, rolled back"

模板 3: Python FastAPI + Celery

name: Python CI/CD
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install ruff mypy
      - run: ruff check .
      - run: mypy src/

  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env: { POSTGRES_DB: test, POSTGRES_USER: test, POSTGRES_PASSWORD: test }
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      redis:
        image: redis:7
        ports: ['6379:6379']
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install -e ".[dev]"
      - run: pytest --cov=src -v
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/test
          REDIS_URL: redis://localhost:6379/0

模板 4: 语义化版本自动发布

name: Release
on:
  push:
    branches: [main]

jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      packages: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      # Conventional Commits → 自动计算版本号
      - uses: python-semantic-release/python-semantic-release@v9
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

      # 或者用 release-please (Google 出品)
      # - uses: google-github-actions/release-please-action@v4
      #   with:
      #     release-type: node

模板 5: 多环境部署 (Dev → Staging → Prod)

name: Deploy Pipeline

on:
  push:
    branches: [main, develop]

jobs:
  deploy-dev:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    environment: development
    steps:
      - run: echo "Deploy to dev"

  deploy-staging:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - run: echo "Deploy to staging"

  deploy-prod:
    needs: deploy-staging
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment:
      name: production
      # 需要 1 人审批 + 等待 5 分钟
    steps:
      - run: echo "Deploy to production"

  # Tag 触发直接部署生产 (热修复)
  deploy-hotfix:
    if: startsWith(github.ref, 'refs/tags/hotfix-')
    runs-on: ubuntu-latest
    environment: production
    steps:
      - run: echo "Hotfix deploy to production"
🤖 CI/CD 在 Agent 系统中的位置

Agent 架构研究 中提炼的 CI/CD 相关发现:

🔑 核心洞察

Agent 系统的"工具调用循环"本质上就是一个 CI Pipeline:计划 → 执行 → 验证 → 回滚/继续。一个设计良好的 CI/CD Pipeline 就是人类编写的最简单的"Agent"——它遵循固定规则,自动执行,失败时回滚。而 Agent 则是"智能化的 CI/CD"——它能根据执行结果动态调整后续步骤。

📚 延伸阅读