强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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 ActionsGitea/Forgejo.gitea/workflows/*.ymlGitHub Actions 兼容
GitLab CIGitLab.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 插件

  1. Jenkins → Manage Jenkins → Plugins → Available plugins
  2. 搜索并安装 “Gitea Plugin”
  3. Manage Jenkins → System → Gitea Servers
  4. 添加 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 企业级认证方案。