CI (Continuous Integration):开发者频繁将代码合并到主分支,每次合并自动触发构建和测试,尽早发现集成问题。
CD (Continuous Delivery/Deployment):代码通过所有测试后,自动部署到预发布/生产环境。Delivery 需要人工确认,Deployment 全自动。
| 概念 | 全称 | 自动化到哪一步 | 需要人工干预 |
|---|---|---|---|
| CI | Continuous Integration | 构建 + 测试 | 部署由人做 |
| CD (Delivery) | Continuous Delivery | 到预发布环境 | 一键点确认上线 |
| CD (Deployment) | Continuous Deployment | 到生产环境 | 全程无人值守 |
💡 对于大多数 SaaS:CI + Continuous Delivery 是最佳平衡。完全自动部署适合高成熟度团队。
git pull一个成熟的 CI/CD Pipeline 通常包含以下阶段,每个阶段失败都会阻断后续阶段:
GitHub Actions 已经成为 CI/CD 的事实标准。原因:与 GitHub 深度集成、生态丰富、免费额度充足、YAML 配置简单。
| 账户类型 | 免费分钟/月 | Runner | 备注 |
|---|---|---|---|
| 公开仓库 | 无限 | GitHub-hosted | 完全免费 |
| 私有仓库 (Free) | 2,000 分钟 | GitHub-hosted | 足够小型项目 |
| 私有仓库 (Pro/Team) | 3,000 分钟 | GitHub-hosted | macOS Runner 10x 计费 |
| 自托管 Runner | 无限 | Self-hosted | 零费用,自己维护 |
# .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
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
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
secrets. 引用,SSH Key 用 Deploy Key 而非个人 Key。
如果你的代码托管在 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"
| 平台 | 适合谁 | 免费额度 | 核心特色 | 定价模型 |
|---|---|---|---|---|
| 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 服务深度集成 | 按管道/执行次数 |
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 的隔离,让构建过程可重复、可缓存。
# 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 中风险最高的环节。选择正确的策略,决定了上线事故的影响范围。
停止旧版本 → 部署新版本 → 启动新版本
逐个实例替换,新旧共存过渡期
# Kubernetes 滚动部署配置
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 最多多起 1 个 Pod
maxUnavailable: 0 # 不允许任何 Pod 不可用
维护两套完全相同的环境 (蓝/绿),流量一键切换
# 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
先将新版本发布给一小部分用户 (1-5%),观察指标正常后逐步扩大
# 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
代码已经部署,但功能对用户隐藏。通过开关控制谁能看到新功能。
// 代码中检查功能开关
if (featureFlags.isEnabled('new-dashboard', { userId: user.id })) {
renderNewDashboard();
} else {
renderOldDashboard();
}
| 维度 | 直接 | 滚动 | 蓝绿 | 金丝雀 | 功能开关 |
|---|---|---|---|---|---|
| 停机时间 | 有 | 无 | 无 | 无 | 无 |
| 回滚速度 | 分钟级 | 分钟级 | 秒级 | 秒级 | 毫秒级 |
| 资源成本 | 1x | 1-1.5x | 2x | 1.1-2x | 1x |
| 复杂度 | 极低 | 低 | 中 | 高 | 中 |
| 用户影响 | 全部 | 部分 | 无 | 极少数 | 可控 |
| 适合阶段 | MVP | 成长期 | 关键业务 | 成熟期 | 任何阶段 |
# .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,工具才能正确判断哪些包受影响。
每个 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 }}"
# 两阶段迁移策略
# 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 Pipeline 是攻击者的黄金目标——拿到 Pipeline 权限 = 拿到生产环境。
package-lock.json / pnpm-lock.yaml 必须提交npm audit / safety checkactions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871${{ github.event.issue.title }} 嵌入 shell 命令.github/workflows/ 获取 Secrets# 安全的 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"
node_modules / .pnpm-store / pip cache.next/cache / target/ / Docker 层缓存actions/cache@v4 或内置缓存机制# 只在相关文件变更时运行
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/**'
actions-runner-controller 在 K8s 上弹性伸缩| Runner | 计费倍率 | 适合 |
|---|---|---|
| Linux | 1x | 大部分场景 |
| Windows | 2x | Windows 专用构建 |
| macOS | 10x | iOS/macOS 构建 |
| 自托管 | 0x | 任何场景 (自有硬件) |
一个 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
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 }}"
}}]
}
开发者不会等 15 分钟才知道自己有没有写错。目标:Lint < 1min,Unit < 3min,E2E < 10min。
每个部署步骤都必须有对应的回滚步骤,且验证过回滚路径确实能用。
password: my-secret-123 会永远留在 git 历史里。即使后来删除,git log 里仍然可见。
不稳定的测试比没有测试更糟——它让团队对 CI 失去信任。re-run failed 不应该成为日常操作。
Staging 和 Production 的部署策略应该不同。Staging 可以快速失败,Production 需要金丝雀 + 自动回滚。
Q1: 代码托管在哪里?
Q2: 有特殊需求?
Q3: 预算?
# 零配置!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
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"
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
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
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"
从 Agent 架构研究 中提炼的 CI/CD 相关发现:
Agent 系统的"工具调用循环"本质上就是一个 CI Pipeline:计划 → 执行 → 验证 → 回滚/继续。一个设计良好的 CI/CD Pipeline 就是人类编写的最简单的"Agent"——它遵循固定规则,自动执行,失败时回滚。而 Agent 则是"智能化的 CI/CD"——它能根据执行结果动态调整后续步骤。