From f5273f5a587ad2a8b1a9347b87b02d13d39ff00b Mon Sep 17 00:00:00 2001 From: zhangwenjian Date: Thu, 27 Aug 2026 16:19:03 +0800 Subject: [PATCH] =?UTF-8?q?ci=F0=9F=94=A7:=20migrate=20before=20deploying,?= =?UTF-8?q?=20and=20roll=20back=20when=20the=20new=20version=20does=20not?= =?UTF-8?q?=20come=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #871. The deploy did docker rm -f then docker run. Nothing ran migrations, so new code met old tables, and nothing checked the result - a container that exits immediately left the site down with a green deploy. Now, in order: pull the image, run the migration with it, and only then touch what is running. A failed migration stops there, leaving old code with the old schema, which is at least self-consistent. The running container is renamed rather than removed, so it can be started again unchanged if the new one does not become healthy. Healthy means both an HTTP response and a database connection in the log: the captcha endpoint answers without touching the database, so it alone would call a container healthy that cannot reach MySQL. --- .github/workflows/build.yml | 56 ++++++++++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index eb7e051f..0f3c0e8d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,10 +72,58 @@ jobs: # # 路径本身走 secret:它不是凭据,但本仓库公开,没有理由把服务器的 # 目录结构一并公布。DEMO_CONFIG_PATH 指向宿主机上那份配置。 + # + # 顺序是有意的:迁移先跑,跑不过就保持现有版本不动; + # 旧容器改名保留而不是删除,新容器不健康时能原样恢复。 + # 健康检查两条都要过——HTTP 活着不代表数据库通了。 script: | - test -f "${{ secrets.DEMO_CONFIG_PATH }}" || { echo "宿主机配置缺失,中止部署"; exit 1; } - sudo docker rm -f go-admin-api + set -u + CFG="${{ secrets.DEMO_CONFIG_PATH }}" + IMG="${{ env.IMAGE_NAME_TAG }}" + NAME=go-admin-api + PREV="$NAME-prev" + + test -f "$CFG" || { echo "宿主机配置缺失,中止部署"; exit 1; } + sudo docker login --username=${{ secrets.DOCKER_USERNAME }} registry.ap-northeast-1.aliyuncs.com --password=${{ secrets.DOCKER_PASSWORD }} + sudo docker pull "$IMG" || { echo "拉取镜像失败,中止部署"; exit 1; } + + # 迁移用新镜像跑。失败时线上仍是旧版本配旧 schema,是自洽的; + # 硬切过去才会得到代码与表对不上的服务。 + if ! sudo docker run --rm -v "$CFG":/config/settings.yml:ro "$IMG" \ + /main migrate -c /config/settings.yml; then + echo "迁移失败,保持现有版本"; exit 1 + fi + + if sudo docker ps -a --format '{{.Names}}' | grep -qx "$NAME"; then + sudo docker rm -f "$PREV" >/dev/null 2>&1 || true + sudo docker rename "$NAME" "$PREV" + sudo docker stop "$PREV" >/dev/null + fi + sudo docker run -d -p 8000:8000 \ - -v "${{ secrets.DEMO_CONFIG_PATH }}":/config/settings.yml:ro \ - --name go-admin-api ${{ env.IMAGE_NAME_TAG }} + -v "$CFG":/config/settings.yml:ro \ + --name "$NAME" "$IMG" + + ok=0 + for i in $(seq 1 20); do + sleep 3 + code=$(curl -s -o /dev/null -w '%{http_code}' -m 5 http://127.0.0.1:8000/api/v1/captcha 2>/dev/null || true) + if [ "$code" = "200" ] && sudo docker logs "$NAME" 2>&1 | grep -q 'connect success'; then + ok=1; echo "健康检查通过(第 $i 次探测)"; break + fi + done + + if [ "$ok" = "1" ]; then + sudo docker rm -f "$PREV" >/dev/null 2>&1 || true + else + echo "健康检查失败,回滚到上一版本" + sudo docker logs --tail 40 "$NAME" 2>&1 || true + sudo docker rm -f "$NAME" >/dev/null 2>&1 || true + if sudo docker ps -a --format '{{.Names}}' | grep -qx "$PREV"; then + sudo docker rename "$PREV" "$NAME" + sudo docker start "$NAME" >/dev/null + echo "已恢复" + fi + exit 1 + fi