跳到主要内容

CI/CD 实践

本章通过实际案例演示如何使用 GitHub Actions 构建完整的 CI/CD 流水线。

CI/CD 基本概念

持续集成(CI)

持续集成是一种开发实践,开发者频繁地将代码集成到主分支。每次集成都通过自动化构建和测试来验证,从而尽早发现集成错误。

CI 的核心目标:

  • 尽早发现和修复问题
  • 减少集成问题
  • 提高代码质量
  • 加快开发速度

持续部署(CD)

持续部署是持续集成的延伸,将代码自动部署到生产环境。每次通过测试的代码都会自动部署。

CD 的核心目标:

  • 自动化部署流程
  • 减少人为错误
  • 快速交付价值
  • 快速回滚能力

Node.js 项目 CI/CD

项目结构

project/
├── src/
├── tests/
├── package.json
└── .github/
└── workflows/
├── ci.yml
└── deploy.yml

CI 工作流

name: CI

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

permissions:
contents: read
pull-requests: write

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

- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: 安装依赖
run: npm ci

- name: 代码检查
run: npm run lint

test:
runs-on: ubuntu-latest
needs: lint

strategy:
matrix:
node: [18, 20, 22]

steps:
- uses: actions/checkout@v4

- name: 设置 Node.js ${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'

- name: 安装依赖
run: npm ci

- name: 运行测试
run: npm test -- --coverage

- name: 上传覆盖率
if: matrix.node == '20'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info

build:
runs-on: ubuntu-latest
needs: test

steps:
- uses: actions/checkout@v4

- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: 安装依赖
run: npm ci

- name: 构建
run: npm run build

- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: build
path: dist/
retention-days: 7

部署工作流

name: Deploy

on:
push:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production

concurrency:
group: deploy-${{ github.event.inputs.environment || 'staging' }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.build.outputs.id }}

steps:
- uses: actions/checkout@v4

- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: 安装依赖
run: npm ci

- name: 构建
id: build
run: |
npm run build
echo "id=$(date +%s)" >> $GITHUB_OUTPUT

- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: deploy-build
path: dist/

deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
if: github.event.inputs.environment == 'staging' || github.event_name == 'push'

steps:
- name: 下载构建产物
uses: actions/download-artifact@v4
with:
name: deploy-build
path: dist/

- name: 部署到 Staging
run: |
echo "部署到 Staging 环境"
echo "构建 ID: ${{ needs.build.outputs.artifact-id }}"

deploy-production:
needs: build
runs-on: ubuntu-latest
environment: production
if: github.event.inputs.environment == 'production'

steps:
- name: 下载构建产物
uses: actions/download-artifact@v4
with:
name: deploy-build
path: dist/

- name: 部署到 Production
run: |
echo "部署到 Production 环境"
echo "构建 ID: ${{ needs.build.outputs.artifact-id }}"

Docker 镜像构建与推送

构建并推送到 GitHub Container Registry

name: Docker Build

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: 检出代码
uses: actions/checkout@v4

- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 登录 GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 提取 Docker 元数据
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- name: 构建并推送
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

多平台构建

- name: 设置 QEMU
uses: docker/setup-qemu-action@v3

- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3

- name: 构建并推送多平台镜像
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}

Java 项目 CI/CD

Maven 项目

name: Java CI

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

jobs:
build:
runs-on: ubuntu-latest

strategy:
matrix:
java: ['17', '21']

steps:
- uses: actions/checkout@v4

- name: 设置 JDK ${{ matrix.java }}
uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
cache: maven

- name: 构建与测试
run: mvn -B verify --file pom.xml

- name: 上传测试报告
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports-java-${{ matrix.java }}
path: target/surefire-reports/

deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'

steps:
- uses: actions/checkout@v4

- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: maven

- name: 发布到 GitHub Packages
run: mvn -B deploy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Gradle 项目

name: Gradle CI

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

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: 设置 JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'

- name: 设置 Gradle
uses: gradle/actions/setup-gradle@v3

- name: 构建与测试
run: ./gradlew build

- name: 运行代码检查
run: ./gradlew check

Python 项目 CI/CD

基础 CI

name: Python CI

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

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

- name: 设置 Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: 安装依赖
run: |
pip install --upgrade pip
pip install ruff black mypy

- name: 运行 Ruff
run: ruff check .

- name: 运行 Black
run: black --check .

- name: 运行 MyPy
run: mypy .

test:
runs-on: ubuntu-latest
needs: lint

strategy:
matrix:
python: ['3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v4

- name: 设置 Python ${{ matrix.python }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: 'pip'

- name: 安装依赖
run: |
pip install -r requirements.txt
pip install pytest pytest-cov

- name: 运行测试
run: pytest --cov=src --cov-report=xml

- name: 上传覆盖率
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

发布到 PyPI

name: Publish

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
id-token: write

steps:
- uses: actions/checkout@v4

- name: 设置 Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: 安装构建工具
run: pip install build

- name: 构建
run: python -m build

- name: 发布到 PyPI
uses: pypa/gh-action-pypi-publish@release/v1

部署到云服务

部署到 AWS

name: Deploy to AWS

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4

- name: 配置 AWS 凭证
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1

- name: 登录 ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2

- name: 构建并推送镜像
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: my-app
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

- name: 部署到 ECS
run: |
aws ecs update-service --cluster my-cluster --service my-service --force-new-deployment

部署到 Azure

name: Deploy to Azure

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4

- name: 登录 Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: 部署到 Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: 'my-app'
package: ./dist

部署到 Vercel

name: Deploy to Vercel

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: 部署到 Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'

SSH 部署到服务器

name: SSH Deploy

on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
environment: production

steps:
- uses: actions/checkout@v4

- name: 设置 Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: 安装依赖并构建
run: |
npm ci
npm run build

- name: 部署到服务器
uses: appleboy/ssh-[email protected]
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: 22
script: |
cd /var/www/app
git pull origin main
npm install --production
pm2 restart app

通知和报告

Slack 通知

jobs:
notify:
runs-on: ubuntu-latest
if: always()
needs: [build, deploy]

steps:
- name: 发送 Slack 通知
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()

邮件通知

- name: 发送邮件
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: 构建完成 - ${{ github.repository }}
to: [email protected]
from: GitHub Actions
body: |
构建状态: ${{ job.status }}
仓库: ${{ github.repository }}
分支: ${{ github.ref }}
提交: ${{ github.sha }}

安全最佳实践

最小权限原则

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

使用 OIDC 替代密钥

- name: 配置 AWS 凭证
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/my-github-actions-role
aws-region: us-east-1

扫描漏洞

- name: 运行 Trivy 漏洞扫描
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'

依赖检查

- name: 依赖检查
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'My Project'
path: '.'
format: 'HTML'
out: 'reports'

完整流水线示例

name: Full Pipeline

on:
push:
branches: [main, develop]
pull_request:
branches: [main]
release:
types: [published]

env:
NODE_VERSION: '20'

concurrency:
group: ${{ github.workflow }}-${{ 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: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint

test:
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}

build:
runs-on: ubuntu-latest
needs: test
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- id: version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: build
path: dist/

docker:
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
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@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ needs.build.outputs.version }}

deploy:
runs-on: ubuntu-latest
needs: [build, docker]
if: github.event_name == 'release'
environment: production
steps:
- name: 部署
run: echo "部署版本 ${{ needs.build.outputs.version }}"

参考资源