Appearance
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 流程。
