Skip to content

CI/CD 持续集成本源代码导览

GitHub Actions 工作流源代码

CI 工作流

yaml
# .github/workflows/ci.yml 核心逻辑
name: CI

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

jobs:
  lint:
    name: 代码检查
    runs-on: ubuntu-latest
    
    steps:
      - name: 检出代码
        uses: actions/checkout@v3
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 运行 ESLint
        run: npm run lint
      
      - name: 运行 Prettier
        run: npm run format:check

  test:
    name: 测试
    runs-on: ubuntu-latest
    
    steps:
      - name: 检出代码
        uses: actions/checkout@v3
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 运行单元测试
        run: npm run test:unit
      
      - name: 运行集成测试
        run: npm run test:integration
      
      - name: 生成覆盖率报告
        run: npm run test:coverage
      
      - name: 上传覆盖率报告
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

  build:
    name: 构建
    runs-on: ubuntu-latest
    needs: [lint, test]
    
    steps:
      - name: 检出代码
        uses: actions/checkout@v3
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 构建应用
        run: npm run build
      
      - name: 上传构建产物
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

CD 工作流

yaml
# .github/workflows/cd.yml 核心逻辑
name: CD

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    name: 部署
    runs-on: ubuntu-latest
    
    steps:
      - name: 检出代码
        uses: actions/checkout@v3
      
      - name: 设置 Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'
      
      - name: 安装依赖
        run: npm ci
      
      - name: 构建应用
        run: npm run build
      
      - name: 登录 Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      - name: 构建 Docker 镜像
        run: |
          docker build -t ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }} .
          docker tag ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }} ${{ secrets.DOCKER_USERNAME }}/myapp:latest
      
      - name: 推送 Docker 镜像
        run: |
          docker push ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}
          docker push ${{ secrets.DOCKER_USERNAME }}/myapp:latest
      
      - name: 部署到服务器
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_USERNAME }}/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 3000:3000 ${{ secrets.DOCKER_USERNAME }}/myapp:latest
      
      - name: 健康检查
        run: |
          sleep 10
          curl -f http://${{ secrets.SERVER_HOST }}:3000/health || exit 1
      
      - name: 通知部署成功
        if: success()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: '部署成功!'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

测试脚本源代码

测试配置

javascript
// jest.config.js 核心逻辑
module.exports = {
  testEnvironment: 'node',
  coverageDirectory: 'coverage',
  collectCoverageFrom: [
    'src/**/*.js',
    '!src/**/*.test.js',
    '!src/**/*.spec.js'
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80
    }
  },
  testMatch: [
    '**/__tests__/**/*.js',
    '**/?(*.)+(spec|test).js'
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  }
};

测试脚本

javascript
// scripts/test.js 核心逻辑
const { execSync } = require('child_process');

function runTest(type) {
  const command = type === 'unit' ? 'jest --testPathPattern=unit' : 'jest --testPathPattern=integration';
  
  try {
    execSync(command, { stdio: 'inherit' });
    console.log(`✅ ${type} tests passed`);
  } catch (error) {
    console.error(`❌ ${type} tests failed`);
    process.exit(1);
  }
}

function runCoverage() {
  try {
    execSync('jest --coverage', { stdio: 'inherit' });
    console.log('✅ Coverage report generated');
  } catch (error) {
    console.error('❌ Coverage generation failed');
    process.exit(1);
  }
}

const type = process.argv[2];

if (type === 'unit') {
  runTest('unit');
} else if (type === 'integration') {
  runTest('integration');
} else if (type === 'coverage') {
  runCoverage();
} else {
  console.error('Unknown test type:', type);
  process.exit(1);
}

部署脚本源代码

部署脚本

javascript
// scripts/deploy.js 核心逻辑
const { execSync } = require('child_process');
const Docker = require('dockerode');

class Deployer {
  constructor(options) {
    this.docker = new Docker({ socketPath: '/var/run/docker.sock' });
    this.imageName = options.imageName;
    this.containerName = options.containerName;
    this.port = options.port;
  }

  async pullImage() {
    console.log(`Pulling image: ${this.imageName}`);
    await new Promise((resolve, reject) => {
      this.docker.pull(this.imageName, (err, stream) => {
        if (err) reject(err);
        this.docker.modem.followProgress(stream, (err) => {
          if (err) reject(err);
          resolve();
        });
      });
    });
    console.log('✅ Image pulled successfully');
  }

  async stopContainer() {
    console.log(`Stopping container: ${this.containerName}`);
    const container = this.docker.getContainer(this.containerName);
    
    try {
      await container.stop();
      await container.remove();
      console.log('✅ Container stopped and removed');
    } catch (error) {
      if (error.statusCode === 404) {
        console.log('Container does not exist');
      } else {
        throw error;
      }
    }
  }

  async startContainer() {
    console.log(`Starting container: ${this.containerName}`);
    const container = await this.docker.createContainer({
      Image: this.imageName,
      name: this.containerName,
      HostConfig: {
        PortBindings: {
          '3000/tcp': [{ HostPort: this.port.toString() }]
        }
      }
    });
    
    await container.start();
    console.log('✅ Container started successfully');
  }

  async healthCheck() {
    console.log('Running health check...');
    await new Promise(resolve => setTimeout(resolve, 5000));
    
    const response = await fetch(`http://localhost:${this.port}/health`);
    if (response.ok) {
      console.log('✅ Health check passed');
    } else {
      throw new Error('Health check failed');
    }
  }

  async deploy() {
    try {
      await this.pullImage();
      await this.stopContainer();
      await this.startContainer();
      await this.healthCheck();
      console.log('✅ Deployment successful');
    } catch (error) {
      console.error('❌ Deployment failed:', error);
      process.exit(1);
    }
  }
}

// 使用示例
const deployer = new Deployer({
  imageName: 'myapp:latest',
  containerName: 'myapp',
  port: 3000
});

deployer.deploy();

总结

CI/CD 的源代码展示了自动化构建、测试和部署的核心机制。理解 GitHub Actions 工作流、测试脚本和部署脚本的实现,有助于我们更好地实现 CI/CD 流程。