持续集成与持续部署
持续集成(Continuous Integration, CI)和持续部署(Continuous Deployment/Delivery, CD)是现代软件开发的核心实践。CI 确保每次代码提交都经过自动化的构建、测试和检查;CD 确保通过检查的代码能够自动部署到目标环境。本节将介绍 GitHub Actions、GitLab CI、Jenkins 流水线配置以及自动测试和自动部署策略。
基础概念
CI/CD 流水线阶段
text
代码提交 → Lint → 单元测试 → 集成测试 → 代码质量检查 → 构建 → 部署 staging → E2E 测试 → 部署生产
│ │ │ │ │ │ │ │ │
CI 阶段 ──────────────────────────────────────────────────────────────────────────────────→
CD 阶段 ─────────────→GitHub Actions
完整的 PHP CI 配置
yaml
# .github/workflows/ci.yml
name: PHP CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# ==================== 代码质量检查 ====================
quality:
name: Code Quality
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3']
steps:
- uses: actions/checkout@v4
- name: Setup PHP ${{ matrix.php-version }}
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug
tools: composer:v2
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-${{ matrix.php-version }}-
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: PHP CodeSniffer
run: vendor/bin/phpcs --standard=phpcs.xml --report=full src/ tests/
- name: PHP-CS-Fixer
run: vendor/bin/php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php
- name: PHPStan
run: vendor/bin/phpstan analyse --no-progress --error-format=github
# ==================== 单元测试 ====================
test:
name: Unit Tests
runs-on: ubuntu-latest
needs: quality
strategy:
matrix:
php-version: ['8.1', '8.2', '8.3']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: xdebug
tools: composer:v2, phpunit
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: ${{ runner.os }}-php-${{ matrix.php-version }}-${{ hashFiles('**/composer.lock') }}
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction
- name: Run PHPUnit
env:
DB_HOST: 127.0.0.1
DB_DATABASE: test_db
DB_USERNAME: root
DB_PASSWORD: test
REDIS_HOST: 127.0.0.1
run: vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml
- name: Upload coverage
if: matrix.php-version == '8.2'
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
# ==================== 部署到 Staging ====================
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging
uses: easingthemes/ssh-deploy@v4
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.STAGING_HOST }}
REMOTE_USER: ${{ secrets.STAGING_USER }}
ARGS: "-avz --delete --exclude='.git' --exclude='node_modules'"
SOURCE: "./"
TARGET: "/var/www/staging/"
- name: Post-deploy commands
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/staging
composer install --no-dev --optimize-autoloader --no-interaction
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
sudo systemctl reload php-fpm
# ==================== 部署到 Production ====================
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
uses: easingthemes/ssh-deploy@v4
with:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
REMOTE_HOST: ${{ secrets.PRODUCTION_HOST }}
REMOTE_USER: ${{ secrets.PRODUCTION_USER }}
SOURCE: "./"
TARGET: "/var/www/app/releases/$(date +%Y%m%d%H%M%S)/"
- name: Post-deploy
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/app
RELEASE_DIR=$(ls -1dt releases/ | head -1)
ln -sfn "${RELEASE_DIR}" current
cd current
composer install --no-dev --optimize-autoloader --classmap-authoritative
php artisan migrate --force
php artisan config:cache && php artisan route:cache && php artisan view:cache
sudo systemctl reload php-fpm
# 清理旧版本
ls -1dt releases/*/ | tail -n +6 | xargs rm -rf
# 健康检查
for i in {1..10}; do
if curl -sf http://localhost/health > /dev/null; then
echo "Health check passed!"
exit 0
fi
sleep 3
done
echo "Health check failed!"
exit 1GitLab CI
yaml
# .gitlab-ci.yml
stages:
- quality
- test
- deploy-staging
- deploy-production
variables:
COMPOSER_CACHE_DIR: "$CI_PROJECT_DIR/.composer-cache"
DOCKER_DRIVER: overlay2
.cache: &cache
cache:
paths:
- vendor/
- .composer-cache/
# 代码质量检查
code-quality:
stage: quality
image: composer:latest
<<: *cache
script:
- composer install --prefer-dist --no-progress --no-interaction
- vendor/bin/phpcs --standard=phpcs.xml --report=full src/ tests/
- vendor/bin/php-cs-fixer fix --dry-run --diff
- vendor/bin/phpstan analyse --no-progress
artifacts:
reports:
codequality: phpstan-report.json
when: on_failure
# 单元测试
phpunit:
stage: test
image: composer:latest
services:
- name: mysql:8.0
alias: mysql
- name: redis:7-alpine
alias: redis
<<: *cache
variables:
DB_HOST: mysql
DB_DATABASE: test_db
DB_USERNAME: root
DB_PASSWORD: ""
REDIS_HOST: redis
before_script:
- composer install --prefer-dist --no-progress --no-interaction
script:
- vendor/bin/phpunit --coverage-text
coverage: /^\s*Lines:\s*\d+.\d+\%/
# 部署到 Staging
deploy-staging:
stage: deploy-staging
image: alpine:latest
only:
- develop
before_script:
- apk add --no-cache rsync openssh-client
script:
- ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
- rsync -avz --delete --exclude='.git' --exclude='node_modules' ./ deploy@$STAGING_HOST:/var/www/staging/
- ssh deploy@$STAGING_HOST "cd /var/www/staging && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache && sudo systemctl reload php-fpm"
# 部署到 Production
deploy-production:
stage: deploy-production
image: alpine:latest
only:
- main
when: manual
before_script:
- apk add --no-cache rsync openssh-client
script:
- ssh-keyscan $PRODUCTION_HOST >> ~/.ssh/known_hosts
- rsync -avz --delete --exclude='.git' --exclude='node_modules' ./ deploy@$PRODUCTION_HOST:/var/www/app/
- ssh deploy@$PRODUCTION_HOST "cd /var/www/app && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan config:cache && sudo systemctl reload php-fpm"Jenkins 流水线
groovy
// Jenkinsfile
pipeline {
agent any
environment {
PHP_VERSION = '8.2'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh 'composer install --prefer-dist --no-progress --no-interaction'
}
}
stage('Code Quality') {
parallel {
stage('PHP CodeSniffer') {
steps {
sh 'vendor/bin/phpcs --standard=phpcs.xml src/ tests/'
}
}
stage('PHP-CS-Fixer') {
steps {
sh 'vendor/bin/php-cs-fixer fix --dry-run --diff'
}
}
stage('PHPStan') {
steps {
sh 'vendor/bin/phpstan analyse --no-progress'
}
}
}
}
stage('Unit Tests') {
steps {
sh 'vendor/bin/phpunit --coverage-text'
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh './deploy/deploy.sh production'
}
}
}
post {
always {
junit '**/test-results.xml'
}
success {
echo 'Pipeline succeeded!'
}
failure {
echo 'Pipeline failed!'
}
}
}实战示例
自动化质量门禁
yaml
# .github/workflows/quality-gate.yml
name: Quality Gate
on:
pull_request:
branches: [main]
jobs:
quality-gate:
name: Quality Gate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
coverage: xdebug
tools: composer:v2
- name: Install dependencies
run: composer install --prefer-dist --no-progress
# 1. 代码风格
- name: Code Style Check
run: vendor/bin/phpcs --standard=phpcs.xml --report=checkstyle --report-file=checkstyle.xml src/ tests/
# 2. 静态分析
- name: Static Analysis
run: vendor/bin/phpstan analyse --no-progress --error-format=github
# 3. 单元测试 + 覆盖率
- name: Unit Tests with Coverage
run: vendor/bin/phpunit --coverage-clover=coverage.xml --coverage-text
# 4. 覆盖率检查
- name: Coverage Check
run: |
COVERAGE=$(vendor/bin/phpunit --coverage-text 2>&1 | grep 'Lines:' | grep -oP '\d+(?=\.\d+%)')
if [ "$COVERAGE" -lt 80 ]; then
echo "Coverage is ${COVERAGE}%, minimum is 80%"
exit 1
fi
echo "Coverage is ${COVERAGE}%, passed!"最佳实践
- 快速失败:代码质量检查应在最早阶段运行
- 并行执行:独立任务(测试、静态分析)并行运行
- 缓存依赖:缓存 Composer 和 Docker 镜像加速构建
- 自动部署:CD 流水线实现一键发布
- 环境隔离:每个 Job 使用独立环境,避免状态干扰
- 通知集成:构建失败时通知团队(Slack、邮件等)
下一节
继续学习:PHP 8.0 新特性