手动部署是什么体验?SSH 登录服务器、拉代码、装依赖、跑迁移、重启服务、祈祷没出错——每次上线都像拆炸弹。CI/CD(持续集成/持续交付)把这些步骤自动化:代码推到仓库后,流水线自动跑测试、构建、部署,全程无需人工干预。本篇以 GitHub Actions 为例,搭建一条从提交到上线的完整自动化流水线。
一、CI:持续集成与自动化测试
CI 的核心是"频繁集成、自动验证"——每次代码推送到仓库,自动运行测试,确保改动没破坏现有功能。这能把"合并后才发现冲突"的痛点前置到"提交时即发现"。CI 流水线通常包含:代码检出、安装依赖、代码规范检查、单元测试。任何一步失败就中止,不允许问题代码进入主分支。
# .github/workflows/ci.yml —— CI 流水线
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
# 1. 检出代码
- uses: actions/checkout@v4
# 2. 设置 PHP 环境
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: pdo_mysql, mbstring, gd, redis
coverage: xdebug
# 3. 安装 Composer 依赖(带缓存加速)
- name: Get Composer Cache Directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- run: composer install --prefer-dist --no-interaction
# 4. 代码规范检查(PSR-12)
- run: ./vendor/bin/phpcs --standard=PSR12 app/
# 5. 单元测试(带覆盖率)
- run: ./vendor/bin/phpunit --coverage-text
env:
DB_HOST: 127.0.0.1
DB_DATABASE: test_db
# 6. 测试通过后通知(可选)
- name: Notify Success
if: success()
run: echo "CI 通过,可以合并"
依赖缓存是加速 CI 的关键——Composer 下载依赖很慢,但依赖版本不变时可以复用缓存。上面配置用 hashFiles('**/composer.lock') 做 cache key,lock 文件没变就直接用缓存,从几分钟降到几秒。尧图项目加上缓存后,CI 平均耗时从 4 分钟降到 90 秒,开发者反馈"推代码马上知道结果"。
二、CD:构建镜像与推送
测试通过后进入 CD 阶段——构建可部署的产物。容器化项目就是构建 Docker 镜像并推送到镜像仓库。这一步只在 main 分支推送时触发(不是每次 PR 都构建),产物打上版本标签,既用于本次部署也留作回滚备份。
# .github/workflows/deploy.yml —— CD 流水线
name: Deploy
on:
push:
branches: [main] # 只有 main 分支触发部署
jobs:
build-and-push:
runs-on: ubuntu-latest
needs: test # 依赖 CI 测试通过(引用 ci.yml 的 job)
steps:
- uses: actions/checkout@v4
# 1. 生成版本号:git 短哈希 + 时间戳
- name: Generate Version
id: version
run: echo "tag=$(date +'%Y%m%d')-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
# 2. 登录镜像仓库(阿里云 ACR)
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: registry.cn-hangzhou.aliyuncs.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASS }}
# 3. 构建并推送镜像
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
registry.cn-hangzhou.aliyuncs.com/ldpk/app:${{ steps.version.outputs.tag }}
registry.cn-hangzhou.aliyuncs.com/ldpk/app:latest
# 4. 把版本号传给部署步骤
- name: Save Version
run: echo ${{ steps.version.outputs.tag }} > version.txt
- uses: actions/upload-artifact@v3
with:
name: version
path: version.txt
注意 secrets.REGISTRY_PASS 这类敏感信息绝不能写在配置文件里,要用 GitHub Secrets 加密存储。尧图所有项目的数据库密码、API 密钥、SSH 私钥都放在 Secrets 里,配置文件只引用 ${{ secrets.XXX }},泄露风险降到零。镜像标签同时打版本号和 latest——版本号用于回滚,latest 供总是拉最新的场景。
三、服务器部署与回滚
镜像推到仓库后,最后一步是登录服务器拉取新镜像并重启。推荐用蓝绿部署或滚动更新实现零停机——新旧版本平滑切换,用户无感知。同时保留快速回滚能力:出问题秒切回上个版本。
# deploy.yml 的部署 job(接上文)
deploy:
runs-on: ubuntu-latest
needs: build-and-push
steps:
# 1. 取版本号
- uses: actions/download-artifact@v3
with: { name: version }
# 2. SSH 登录服务器执行部署
- name: Deploy to Server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/ldpk
TAG=$(cat version.txt)
export TAG=$TAG
# 拉取新镜像
docker-compose -f docker-compose.prod.yml pull
# 滚动重启(Nginx 前置负载均衡,逐个替换容器)
docker-compose -f docker-compose.prod.yml up -d --remove-orphans
# 健康检查:等新容器就绪
sleep 10
curl -sf http://localhost/health || exit 1
# 清理旧镜像
docker image prune -f
echo "部署成功:$TAG"
# 回滚 job(手动触发)
rollback:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
steps:
- name: Rollback
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/ldpk
# 切回上个版本镜像
export TAG=$(cat .last_version)
docker-compose -f docker-compose.prod.yml up -d
echo "已回滚到:$TAG"
健康检查是部署的安全网——新容器启动后先探测 /health 接口,返回正常才算部署成功,否则立即告警。尧图曾因漏配健康检查,新版本启动失败但流水线显示成功,半小时后才发现站点挂了。加上检查后这类问题零发生。回滚要设计成"一键触发"——在 GitHub Actions 界面手动跑 rollback workflow,输入要回滚到的版本号即可。尧图要求每次部署后保留上个版本镜像 7 天,确保随时能回滚。这套 CI/CD 流水线让尧图的部署频率从"一周一次"提升到"一天多次",且每次部署都是按钮一按的事,再也不用半夜 SSH 抢修了。