第 10 章 - CI/CD 集成
第 10 章 - CI/CD 集成
持续集成(CI)和持续部署(CD)是现代软件开发的核心实践。本章介绍如何为自建 Git 服务器配置自动化流水线。
10.1 CI/CD 概述
10.1.1 核心概念
代码提交 → 自动构建 → 自动测试 → 代码质量检查 → 自动部署
│ │ │ │ │
│ │ │ │ ├── staging
│ │ │ │ └── production
│ │ │ └── SonarQube/ESLint
│ │ └── 单元测试/集成测试
│ └── 编译/打包/构建镜像
└── Push/Merge Request 触发
10.1.2 方案对比
| 方案 | 适用平台 | 配置方式 | 生态 | 资源占用 |
|---|---|---|---|---|
| Gitea Actions | Gitea/Forgejo | .gitea/workflows/*.yml | GitHub Actions 兼容 | 中 |
| GitLab CI | GitLab | .gitlab-ci.yml | 成熟完整 | 高 |
| Jenkins | 任意 | Jenkinsfile/GUI | 插件丰富 | 中-高 |
| Drone CI | 任意 | .drone.yml | 轻量 | 低 |
10.2 Gitea Actions 详解
10.2.1 架构
Gitea Server
│
▼
Actions Scheduler (内置)
│
▼
act_runner (独立进程)
├── Docker 执行器 (推荐)
├── 本地执行器
└── 自定义执行器
│
▼
Job 执行容器/进程
10.2.2 Runner 安装与注册
# 下载 act_runner
RUNNER_VERSION="0.2.11"
wget "https://dl.gitea.com/act_runner/${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-amd64"
chmod +x act_runner-*
sudo mv act_runner-* /usr/local/bin/act_runner
# 生成默认配置
act_runner generate-config > config.yaml
编辑配置文件:
# config.yaml
log:
level: info
runner:
file: .runner
capacity: 3
envs:
DOCKER_HOST: unix:///var/run/docker.sock
GOPATH: /home/git/go
cache:
enabled: true
dir: /home/git/runner-cache
host: ""
port: 0
container:
network: "bridge"
privileged: false
options: ""
valid_volumes:
- '**'
workdir_parent: ""
host:
workdir_parent: /home/git/runner-workdir
注册 Runner:
# 方式一:交互式注册
act_runner register \
--instance https://git.example.com \
--config config.yaml
# 方式二:非交互式
# 先在 Gitea Web 界面获取注册 Token:
# Site Administration → Actions → Runners → Create New Runner
TOKEN="your_registration_token"
act_runner register \
--instance https://git.example.com \
--token "$TOKEN" \
--no-interactive \
--config config.yaml \
--name "docker-runner-01" \
--labels "ubuntu-latest:docker://node:20-bookworm"
Systemd 服务:
sudo tee /etc/systemd/system/gitea-runner.service << 'EOF'
[Unit]
Description=Gitea Actions Runner
After=network.target docker.service
[Service]
Type=simple
User=git
Group=git
WorkingDirectory=/home/git/runner
ExecStart=/usr/local/bin/act_runner daemon --config /home/git/runner/config.yaml
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable gitea-runner
sudo systemctl start gitea-runner
sudo systemctl status gitea-runner
10.2.3 工作流语法
# .gitea/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * 1' # 每周一凌晨 2 点
workflow_dispatch: # 手动触发
env:
REGISTRY: git.example.com:5000
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
name: 代码检查
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Check formatting
run: npm run format:check
test:
name: 测试
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- name: Run tests
run: npm test
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Upload coverage
if: matrix.node-version == 20
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
build:
name: 构建镜像
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | \
docker login "$REGISTRY" -u "${{ github.actor }}" --password-stdin
- name: Build and push
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
TAG="${GITHUB_SHA::8}"
docker build -t "${IMAGE}:${TAG}" -t "${IMAGE}:latest" .
docker push "${IMAGE}:${TAG}"
docker push "${IMAGE}:latest"
deploy:
name: 部署
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment:
name: production
url: https://app.example.com
steps:
- name: Deploy via SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh -o StrictHostKeyChecking=no deploy@server \
"cd /opt/app && docker compose pull && docker compose up -d"
10.2.4 矩阵策略
jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, ubuntu-22.04]
node: [18, 20, 22]
include:
- os: ubuntu-latest
node: 20
coverage: true
exclude:
- os: ubuntu-22.04
node: 18
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test
10.3 Jenkins 集成
10.3.1 Jenkins 安装
# docker-compose.yml (Jenkins)
version: '3'
services:
jenkins:
image: jenkins/jenkins:lts
container_name: jenkins
restart: always
ports:
- "8080:8080"
- "50000:50000"
volumes:
- ./jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
environment:
- JAVA_OPTS=-Xmx2g
docker compose up -d
# 获取初始管理员密码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
10.3.2 配置 Gitea 插件
- Jenkins → Manage Jenkins → Plugins → Available plugins
- 搜索并安装 “Gitea Plugin”
- Manage Jenkins → System → Gitea Servers
- 添加 Gitea 服务器 URL 和凭据
10.3.3 Jenkinsfile 示例
// Jenkinsfile (Declarative Pipeline)
pipeline {
agent {
docker {
image 'node:20'
}
}
environment {
REGISTRY = 'git.example.com:5000'
}
options {
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds()
}
stages {
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
}
post {
always {
junit 'test-results/**/*.xml'
publishHTML(target: [
reportName: 'Coverage Report',
reportDir: 'coverage/lcov-report',
reportFiles: 'index.html'
])
}
}
}
stage('Build') {
when {
branch 'main'
}
steps {
sh '''
docker build -t ${REGISTRY}/${JOB_NAME}:${BUILD_NUMBER} .
docker push ${REGISTRY}/${JOB_NAME}:${BUILD_NUMBER}
'''
}
}
stage('Deploy Staging') {
when {
branch 'main'
}
steps {
sshagent(credentials: ['deploy-key']) {
sh '''
ssh deploy@staging-server \
"cd /opt/app && docker compose pull && docker compose up -d"
'''
}
}
}
stage('Deploy Production') {
when {
tag pattern: "v\\d+\\.\\d+\\.\\d+", comparator: "REGEXP"
}
input {
message "Deploy to production?"
ok "Deploy"
}
steps {
sshagent(credentials: ['deploy-key']) {
sh '''
ssh deploy@production-server \
"cd /opt/app && docker compose pull && docker compose up -d"
'''
}
}
}
}
post {
success {
// 发送成功通知
sh '''
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\"✅ Build ${BUILD_NUMBER} succeeded\"}"
'''
}
failure {
// 发送失败通知
sh '''
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"text\":\"❌ Build ${BUILD_NUMBER} failed\"}"
'''
}
}
}
10.3.4 Gitea Webhook 触发 Jenkins
# 在 Gitea 仓库中配置 Webhook
curl -s -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" \
"$GITEA_URL/api/v1/repos/owner/repo/hooks" \
-d '{
"type": "gitea",
"active": true,
"config": {
"url": "https://jenkins.example.com/gitea-webhook/post",
"content_type": "json"
},
"events": ["push", "pull_request"]
}'
10.4 自动化部署模式
10.4.1 Docker Compose 部署
# docker-compose.yml (应用)
version: '3'
services:
app:
image: git.example.com/org/app:latest
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://user:pass@db:5432/app
depends_on:
- db
- redis
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:16-alpine
restart: always
volumes:
- pgdata:/var/lib/postgresql/data
environment:
POSTGRES_DB: app
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
redis:
image: redis:7-alpine
restart: always
volumes:
pgdata:
部署脚本:
#!/bin/bash
# deploy.sh
set -euo pipefail
APP_DIR="/opt/app"
IMAGE="$1"
echo "Deploying: $IMAGE"
cd "$APP_DIR"
# 更新镜像标签
sed -i "s|image: .*|image: ${IMAGE}|" docker-compose.yml
# 拉取新镜像
docker compose pull
# 滚动更新
docker compose up -d --remove-orphans
# 等待健康检查
echo "等待服务就绪..."
for i in $(seq 1 30); do
if curl -sf http://localhost:3000/health > /dev/null 2>&1; then
echo "✅ 部署成功"
exit 0
fi
sleep 2
done
echo "❌ 健康检查超时,回滚..."
docker compose rollback # 或手动回滚
exit 1
10.4.2 蓝绿部署
#!/bin/bash
# blue-green-deploy.sh
set -euo pipefail
IMAGE="$1"
BLUE_PORT=3001
GREEN_PORT=3002
NGINX_UPSTREAM="/etc/nginx/conf.d/app-upstream.conf"
# 确定当前活跃环境
if grep -q "127.0.0.1:$BLUE_PORT" "$NGINX_UPSTREAM" 2>/dev/null; then
CURRENT="blue"
CURRENT_PORT=$BLUE_PORT
TARGET="green"
TARGET_PORT=$GREEN_PORT
else
CURRENT="green"
CURRENT_PORT=$GREEN_PORT
TARGET="blue"
TARGET_PORT=$BLUE_PORT
fi
echo "当前: $CURRENT (端口 $CURRENT_PORT)"
echo "目标: $TARGET (端口 $TARGET_PORT)"
# 启动新环境
echo "启动 $TARGET 环境..."
docker run -d --name "app-$TARGET" \
-p "$TARGET_PORT:3000" \
--network app-network \
"$IMAGE"
# 等待健康检查
echo "等待 $TARGET 就绪..."
for i in $(seq 1 30); do
if curl -sf "http://127.0.0.1:$TARGET_PORT/health" > /dev/null; then
break
fi
sleep 2
done
# 切换 Nginx 上游
echo "切换流量到 $TARGET..."
cat > "$NGINX_UPSTREAM" << EOF
upstream app {
server 127.0.0.1:$TARGET_PORT;
}
EOF
nginx -s reload
# 停止旧环境
echo "停止 $CURRENT 环境..."
docker stop "app-$CURRENT" && docker rm "app-$CURRENT"
echo "✅ 部署完成: $TARGET"
10.5 通知集成
10.5.1 CI/CD 通知工作流
# .gitea/workflows/notify.yml
name: Notify
on:
workflow_run:
workflows: ["CI"]
types: [completed]
jobs:
notify:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: Send notification
run: |
curl -X POST "${{ secrets.WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"msgtype": "markdown",
"markdown": {
"content": "## ❌ CI 构建失败\n> 仓库: ${{ github.repository }}\n> 分支: ${{ github.ref_name }}\n> 提交: ${{ github.sha }}\n> [查看详情](${{ github.event.workflow_run.html_url }})"
}
}'
10.6 自托管 Runner 最佳实践
10.6.1 Runner 安全配置
# config.yaml - 安全加固
container:
privileged: false
options: >-
--read-only
--tmpfs /tmp:rw,noexec,nosuid
valid_volumes:
- '/home/git/runner-cache/**'
- '/var/run/docker.sock'
host:
workdir_parent: /home/git/runner-workdir
10.6.2 多 Runner 策略
Runner 组织策略:
├── docker-runner-01 (标签: ubuntu-latest, docker)
├── docker-runner-02 (标签: ubuntu-latest, docker)
├── shell-runner-01 (标签: native, gpu)
│ └── GPU 相关任务
└── arm64-runner-01 (标签: arm64)
└── ARM 架构构建
10.7 扩展阅读
本章小结
| 学到了什么 | 关键要点 |
|---|---|
| Gitea Actions | 兼容 GitHub Actions 语法,独立 Runner 执行 |
| Runner 配置 | Docker/本地执行器,标签系统,容量控制 |
| Jenkins 集成 | Gitea 插件 + Webhook 触发 + Jenkinsfile |
| 部署模式 | Docker Compose、蓝绿部署、滚动更新 |
| 通知 | Webhook 集成企业微信/钉钉/Slack |
下一章:第 11 章 - 认证集成 — LDAP、OAuth2、SAML 企业级认证方案。