Skip to content

CI/CD 集成

概述

Git 与 CI/CD(持续集成/持续部署)系统紧密集成,可以自动化构建、测试和部署流程。本章介绍如何将 Git 与主流 CI/CD 平台集成。

Git 与 CI/CD

CI/CD 基本概念

  • 持续集成 (CI):代码提交后自动构建和测试
  • 持续部署 (CD):测试通过后自动部署到生产环境
  • 持续交付 (CD):代码随时可以部署到生产环境

Git 在 CI/CD 中的作用

代码提交 → 触发 CI → 构建测试 → 部署
    │          │          │        │
    └── Git ───┴── Webhook ┴── 流水线 ┘

常见触发方式

触发事件说明
push推送到分支时触发
pull_request创建或更新 PR 时触发
merge合并到主分支时触发
tag创建标签时触发
schedule定时触发
manual手动触发

GitHub Actions

基本配置

yaml
# .github/workflows/ci.yml
name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - name: 检出代码
        uses: actions/checkout@v3
        with:
          fetch-depth: 0  # 获取完整历史
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 代码检查
        run: npm run lint
      
      - name: 运行测试
        run: npm test -- --coverage
      
      - name: 构建项目
        run: npm run build

处理子模块

yaml
# .github/workflows/ci.yml
steps:
  - name: 检出代码(包含子模块)
    uses: actions/checkout@v3
    with:
      submodules: recursive
      token: ${{ secrets.GITHUB_TOKEN }}

矩阵构建

yaml
# .github/workflows/ci.yml
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        node: [16, 18, 20]
    
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

缓存优化

yaml
# .github/workflows/ci.yml
jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: 缓存 Node modules
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      
      - run: npm ci
      - run: npm test

部署配置

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: 构建生产版本
        run: |
          npm ci
          npm run build
      
      - name: 部署到服务器
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ${{ secrets.USERNAME }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            cd /var/www/app
            git pull origin main
            npm install --production
            npm run migrate
            pm2 restart app

发布到 npm

yaml
# .github/workflows/publish.yml
name: Publish to npm

on:
  release:
    types: [created]

jobs:
  publish:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
          registry-url: 'https://registry.npmjs.org'
      
      - run: npm ci
      - run: npm run build
      - run: npm publish
      
      env:
        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

GitLab CI

基本配置

yaml
# .gitlab-ci.yml
stages:
  - lint
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "18"

cache:
  paths:
    - node_modules/

lint:
  stage: lint
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm run lint
  only:
    - merge_requests
    - main

test:
  stage: test
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm test -- --coverage
  coverage: '/Lines\s*:\s*(\d+.\d+)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml
  only:
    - merge_requests
    - main

build:
  stage: build
  image: node:${NODE_VERSION}
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
  script:
    - ssh $SSH_USER@$SSH_HOST "cd /var/www/app && git pull && npm install --production && pm2 restart app"
  only:
    - main
  when: manual

使用 Docker

yaml
# .gitlab-ci.yml
stages:
  - build
  - deploy

build_image:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  variables:
    IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG
  only:
    - main

deploy:
  stage: deploy
  image: alpine:latest
  script:
    - apk add --no-cache curl
    - curl -X POST $WEBHOOK_URL
  only:
    - main

环境部署

yaml
# .gitlab-ci.yml
stages:
  - test
  - deploy_staging
  - deploy_production

deploy_staging:
  stage: deploy_staging
  environment:
    name: staging
    url: https://staging.example.com
  script:
    - ./deploy.sh staging
  only:
    - develop

deploy_production:
  stage: deploy_production
  environment:
    name: production
    url: https://example.com
  script:
    - ./deploy.sh production
  only:
    - main
  when: manual

GitLab CI 变量

yaml
# .gitlab-ci.yml
variables:
  # 全局变量
  APP_NAME: "my-app"
  REGISTRY: "registry.example.com"
  
  # Git 变量(自动提供)
  # CI_COMMIT_SHA - 提交哈希
  # CI_COMMIT_SHORT_SHA - 短提交哈希
  # CI_COMMIT_REF_NAME - 分支或标签名
  # CI_COMMIT_MESSAGE - 提交信息
  # CI_COMMIT_AUTHOR - 提交作者

job:
  script:
    - echo "Building $APP_NAME"
    - echo "Commit: $CI_COMMIT_SHA"
    - echo "Branch: $CI_COMMIT_REF_NAME"
    - echo "Author: $CI_COMMIT_AUTHOR"

Jenkins 集成

Jenkinsfile 基本配置

groovy
// Jenkinsfile
pipeline {
    agent any
    
    tools {
        nodejs 'NodeJS-18'
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log -1 --pretty=format:"%h - %an, %ar : %s"'
            }
        }
        
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }
        
        stage('Lint') {
            steps {
                sh 'npm run lint'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    junit 'test-results/*.xml'
                    publishHTML([
                        allowMissing: false,
                        alwaysLinkToLastBuild: true,
                        keepAll: true,
                        reportDir: 'coverage',
                        reportFiles: 'index.html',
                        reportName: 'Coverage Report'
                    ])
                }
            }
        }
        
        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh './deploy.sh'
            }
        }
    }
    
    post {
        success {
            echo 'Build succeeded!'
            slackSend(color: 'good', message: "Build succeeded: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
        }
        failure {
            echo 'Build failed!'
            slackSend(color: 'danger', message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
        }
    }
}

多分支流水线

groovy
// Jenkinsfile
pipeline {
    agent any
    
    stages {
        stage('Build') {
            steps {
                sh 'npm ci'
                sh 'npm run build'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
        }
        
        stage('Deploy to Staging') {
            when {
                branch 'develop'
            }
            steps {
                sh './deploy.sh staging'
            }
        }
        
        stage('Deploy to Production') {
            when {
                branch 'main'
            }
            input {
                message "Deploy to production?"
                ok "Deploy"
            }
            steps {
                sh './deploy.sh production'
            }
        }
    }
}

Git 触发器配置

groovy
// Jenkinsfile
pipeline {
    agent any
    
    triggers {
        // 轮询 SCM 变化
        pollSCM('H/5 * * * *')
        
        // 或使用 GitLab/GitHub 插件的 webhook
        gitlab(triggerOnPush: true, triggerOnMergeRequest: true)
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }
    }
}

参数化构建

groovy
// Jenkinsfile
pipeline {
    agent any
    
    parameters {
        choice(
            name: 'ENVIRONMENT',
            choices: ['staging', 'production'],
            description: '部署环境'
        )
        string(
            name: 'BRANCH',
            defaultValue: 'main',
            description: '部署分支'
        )
        booleanParam(
            name: 'SKIP_TESTS',
            defaultValue: false,
            description: '跳过测试'
        )
    }
    
    stages {
        stage('Checkout') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: "*/${params.BRANCH}"]],
                    userRemoteConfigs: scm.userRemoteConfigs
                ])
            }
        }
        
        stage('Test') {
            when {
                expression { !params.SKIP_TESTS }
            }
            steps {
                sh 'npm test'
            }
        }
        
        stage('Deploy') {
            steps {
                sh "./deploy.sh ${params.ENVIRONMENT}"
            }
        }
    }
}

CI/CD 最佳实践

1. 分支策略

yaml
# GitHub Actions 示例
on:
  push:
    branches:
      - main        # 主分支:自动部署生产
      - develop     # 开发分支:自动部署测试
  pull_request:
    branches:
      - main        # PR:运行测试
    types:
      - opened
      - synchronize

2. 环境变量管理

yaml
# GitHub Actions
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production  # 使用 GitHub Environments
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_URL: ${{ secrets.DB_URL }}
        run: ./deploy.sh

3. 构建缓存

yaml
# GitHub Actions
- name: 缓存依赖
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      node_modules
    key: ${{ runner.os }}-deps-${{ hashFiles('**/package-lock.json') }}

4. 安全最佳实践

yaml
# 不要在日志中输出敏感信息
- name: Deploy
  run: |
    echo "::add-mask::${{ secrets.API_KEY }}"
    ./deploy.sh

5. 通知配置

yaml
# GitHub Actions
jobs:
  notify:
    runs-on: ubuntu-latest
    if: always()
    steps:
      - name: Slack Notification
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          fields: repo,message,commit,author
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

总结

平台配置文件特点
GitHub Actions.github/workflows/*.yml与 GitHub 深度集成,市场丰富
GitLab CI.gitlab-ci.yml内置 Docker 支持,环境管理
JenkinsJenkinsfile灵活强大,插件丰富

选择建议

  • GitHub 项目:使用 GitHub Actions
  • GitLab 项目:使用 GitLab CI
  • 企业私有部署:使用 Jenkins
  • 多平台支持:考虑可移植性

最佳实践

  • 使用分支策略控制部署流程
  • 敏感信息使用 Secrets 管理
  • 启用构建缓存提高效率
  • 配置通知及时了解构建状态