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 }}"