Files
life-echo/.github/workflows/docker-build-deploy.yml
penghanyuan 4a331428f7 feat: 优化部署流程和数据库迁移
- 更新 GitHub Actions 工作流,调整部署步骤,先启动数据库服务以确保迁移顺利进行。
- 新增启动所有服务的步骤,确保在迁移完成后启动全部服务。
- 在 SQL 脚本中添加锁定超时设置,提升数据库操作的稳定性。
2026-03-01 11:12:04 +01:00

261 lines
10 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Docker Build and Deploy
on:
push:
branches:
- dev/add-agent
paths:
- 'api/**'
- '.github/workflows/**'
workflow_dispatch:
inputs:
branch:
description: '部署分支'
required: false
type: string
default: ''
env:
IMAGE_NAME: lifecho-api
REGISTRY: crpi-u2903xccyzd6nqnc.cn-shanghai.personal.cr.aliyuncs.com
REGISTRY_NAMESPACE: huaga
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Alibaba Cloud Container Registry
env:
REGISTRY: ${{ env.REGISTRY }}
USERNAME: ${{ secrets.ALIYUN_CR_USERNAME }}
PASSWORD: ${{ secrets.ALIYUN_CR_PASSWORD }}
run: |
echo "正在登录到阿里云容器镜像服务..."
echo "Registry: $REGISTRY"
echo "Username: $USERNAME"
echo "Password length: ${#PASSWORD}"
# 使用 printf 确保密码正确传递(包括特殊字符)
printf '%s\n' "$PASSWORD" | docker login "$REGISTRY" --username="$USERNAME" --password-stdin
echo "✅ 登录成功!"
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./api
file: ./api/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to Remote Server
runs-on: ubuntu-latest
needs: build-and-push
if: github.event_name != 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- name: Set up SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Add server to known hosts
run: |
mkdir -p ~/.ssh
ssh-keyscan -H -p ${{ secrets.SSH_PORT || 22 }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Determine image tag
id: image_tag
run: |
DEPLOY_BRANCH="${{ github.event.inputs.branch || github.ref_name }}"
echo "deploy_branch=$DEPLOY_BRANCH" >> $GITHUB_OUTPUT
if [ "$DEPLOY_BRANCH" == "main" ] || [ "$DEPLOY_BRANCH" == "master" ]; then
echo "tag=latest" >> $GITHUB_OUTPUT
else
BRANCH_TAG=$(echo "$DEPLOY_BRANCH" | sed 's/\//-/g')
echo "tag=$BRANCH_TAG" >> $GITHUB_OUTPUT
fi
- name: Deploy to remote server
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT || 22 }}
IMAGE_TAG: ${{ env.REGISTRY }}/${{ env.REGISTRY_NAMESPACE }}/${{ env.IMAGE_NAME }}:${{ steps.image_tag.outputs.tag }}
COMPOSE_FILE: docker-compose.yml
COMPOSE_DIR: ${{ secrets.DEPLOY_PATH || '/opt/life-echo' }}
REGISTRY: ${{ env.REGISTRY }}
ALIYUN_CR_USERNAME: ${{ secrets.ALIYUN_CR_USERNAME }}
ALIYUN_CR_PASSWORD: ${{ secrets.ALIYUN_CR_PASSWORD }}
run: |
echo "开始部署到远程服务器..."
echo "镜像标签: $IMAGE_TAG"
echo "部署目录: $COMPOSE_DIR/api"
# 登录到阿里云容器仓库
echo "$ALIYUN_CR_PASSWORD" | ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker login $REGISTRY --username=$ALIYUN_CR_USERNAME --password-stdin"
# 创建部署目录(如果不存在)
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"mkdir -p $COMPOSE_DIR/api"
# 第一步:强制停止并删除所有旧容器
echo "停止并删除旧容器..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "
# 先尝试使用 docker-compose down
cd $COMPOSE_DIR/api 2>/dev/null && docker-compose -f '$COMPOSE_FILE' down --remove-orphans 2>/dev/null || true
# 强制停止并删除所有 life-echo 相关容器(按名称匹配)
echo '强制清理所有 life-echo 容器...'
docker ps -a --filter 'name=life-echo' --format '{{.ID}}' | xargs -r docker rm -f 2>/dev/null || true
# 再次确保指定容器被删除
echo '确保指定容器被删除...'
docker rm -f life-echo-api-prod life-echo-celery-worker life-echo-postgres life-echo-redis life-echo-celery-beat life-echo-flower 2>/dev/null || true
# 等待容器完全停止
sleep 3
# 验证容器已删除
echo '验证容器状态...'
docker ps -a --filter 'name=life-echo' || true
"
# 第二步:先删除远程旧配置,再复制仓库中的 docker-compose.yml强制覆盖
echo "删除远程旧 docker-compose 配置以确保使用仓库版本..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"rm -f $COMPOSE_DIR/api/$COMPOSE_FILE $COMPOSE_DIR/api/${COMPOSE_FILE}.bak 2>/dev/null || true"
echo "复制配置文件(覆盖远程 docker-compose.yml..."
scp -P $SSH_PORT ./api/$COMPOSE_FILE $SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/
# 复制 .env.production 到远程服务器(重命名为 .env.prod
echo "复制 .env.production 文件..."
scp -P $SSH_PORT ./api/.env.production $SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.prod
scp -P $SSH_PORT ./api/.env.production $SSH_USER@$SSH_HOST:$COMPOSE_DIR/api/.env.production
# 第三步:准备镜像和配置
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "
set -e
cd $COMPOSE_DIR/api
echo '拉取最新镜像: $IMAGE_TAG'
docker pull '$IMAGE_TAG' || true
echo '备份并更新 docker-compose.yml 中的镜像标签...'
cp '$COMPOSE_FILE' '${COMPOSE_FILE}.bak'
sed -i.tmp 's|image:.*lifecho-api.*|image: $IMAGE_TAG|g' '$COMPOSE_FILE'
sed -i.tmp 's|image:.*life-echo-api.*|image: $IMAGE_TAG|g' '$COMPOSE_FILE'
rm -f '${COMPOSE_FILE}.tmp' 2>/dev/null || true
echo '先只启动数据库(确保迁移不受 API 连接干扰)...'
docker-compose -f '$COMPOSE_FILE' up -d postgres redis
echo '等待数据库就绪...'
sleep 10
"
- name: Run database migration
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT || 22 }}
DB_USER: ${{ secrets.MIGRATION_DB_USER || 'postgres' }}
DB_NAME: ${{ secrets.MIGRATION_DB_NAME || 'life_echo' }}
run: |
echo "执行数据库结构同步迁移(幂等)..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker exec -i life-echo-postgres psql -U $DB_USER -d $DB_NAME" \
< api/migrations/sync_schema_to_models.sql
echo "修复章节 order_index..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker exec -i life-echo-postgres psql -U $DB_USER -d $DB_NAME" \
< api/migrations/fix_chapter_order_index.sql
echo "添加章节 is_active 字段..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker exec -i life-echo-postgres psql -U $DB_USER -d $DB_NAME" \
< api/migrations/add_chapter_is_active.sql
echo "添加用户基础资料字段..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker exec -i life-echo-postgres psql -U $DB_USER -d $DB_NAME" \
< api/migrations/add_user_profile_fields.sql
echo "修正章节排序索引 v2..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"docker exec -i life-echo-postgres psql -U $DB_USER -d $DB_NAME" \
< api/migrations/fix_chapter_order_index_v2.sql
echo "数据库迁移完成"
- name: Start all services
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT || 22 }}
COMPOSE_DIR: ${{ secrets.DEPLOY_PATH || '/opt/life-echo' }}
COMPOSE_FILE: docker-compose.yml
run: |
echo "迁移完成,启动全部服务..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST "
set -e
cd $COMPOSE_DIR/api
docker-compose -f '$COMPOSE_FILE' up -d
echo '等待服务启动...'
sleep 15
echo '清理旧镜像...'
docker image prune -f || true
echo '部署完成!'
docker-compose -f '$COMPOSE_FILE' ps
"
- name: Verify deployment
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_PORT: ${{ secrets.SSH_PORT || 22 }}
COMPOSE_DIR: ${{ secrets.DEPLOY_PATH || '/opt/life-echo' }}
run: |
echo "验证部署状态..."
ssh -p $SSH_PORT $SSH_USER@$SSH_HOST \
"cd $COMPOSE_DIR/api && docker-compose ps && docker-compose logs --tail=50 api"