name: 构建并部署 on: push: branches: [main] tags: ['v*'] workflow_dispatch: concurrency: group: deploy cancel-in-progress: false env: REGISTRY: git.lzcwy.cn IMAGE: git.lzcwy.cn/zhengchiliu/zt-closet-backend jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: 拉取代码 uses: actions/checkout@v4 - name: 确认 docker 可用 run: docker version --format 'Server: {{.Server.Version}}' - name: 计算镜像标签 id: meta run: | case "$GITEA_REF" in refs/tags/*) TAG="${GITEA_REF#refs/tags/}" ;; *) TAG="sha-$(echo "$GITEA_SHA" | cut -c1-7)" ;; esac echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "镜像标签: $TAG" - name: 登录 Gitea 镜像仓库 env: REGISTRY_USER: ${{ secrets.ZT_CLOSET_REGISTRY_USER }} REGISTRY_TOKEN: ${{ secrets.ZT_CLOSET_REGISTRY_TOKEN }} run: | echo "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin - name: 构建并推送 run: | # 生产机若是 ARM(如甲骨文 ARM 机),去掉下一行注释并改成 linux/arm64 # PLATFORM="--platform linux/arm64" docker build \ --build-arg GOPROXY=https://goproxy.cn,direct \ -f scripts/dockerfile/Dockerfile \ -t "$IMAGE:${{ steps.meta.outputs.tag }}" \ -t "$IMAGE:latest" \ . docker push "$IMAGE:${{ steps.meta.outputs.tag }}" docker push "$IMAGE:latest" - name: SSH 部署到生产机 env: SSH_KEY: ${{ secrets.ZT_CLOSET_PROD_SSH_KEY }} SSH_HOST: ${{ vars.ZT_CLOSET_PROD_SSH_HOST }} SSH_USER: ${{ vars.ZT_CLOSET_PROD_SSH_USER }} run: | mkdir -p ~/.ssh && chmod 700 ~/.ssh printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key SSHOPT="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" ssh $SSHOPT "$SSH_USER@$SSH_HOST" 'bash -s' <<'REMOTE' set -e APP=zt-closet-backend IMAGE=git.lzcwy.cn/zhengchiliu/zt-closet-backend DIR=/opt/zt-closet mkdir -p "$DIR/configs" "$DIR/storage/logs" "$DIR/storage/uploads" # 容器内进程是 uid 10001,日志/上传目录要给它写权限 chown -R 10001:10001 "$DIR/storage" # 记住旧镜像,用于失败回滚 OLD_IMAGE=$(docker inspect -f '{{.Image}}' $APP 2>/dev/null || echo "") docker pull $IMAGE:latest docker rm -f $APP 2>/dev/null || true docker run -d \ --name $APP \ --network host \ --restart unless-stopped \ -v $DIR/configs:/app/configs:ro \ -v $DIR/storage/logs:/app/storage/logs \ -v $DIR/storage/uploads:/app/storage/uploads \ -e TZ=Asia/Shanghai \ --log-opt max-size=50m \ --log-opt max-file=5 \ $IMAGE:latest sleep 5 if (command -v curl >/dev/null && curl -fsS http://127.0.0.1:8080/ping) \ || (command -v wget >/dev/null && wget -qO- http://127.0.0.1:8080/ping); then echo " 健康检查通过" docker image prune -f else echo " 健康检查失败,回滚" docker logs --tail 50 $APP docker rm -f $APP if [ -n "$OLD_IMAGE" ]; then docker run -d \ --name $APP \ --network host \ --restart unless-stopped \ -v $DIR/configs:/app/configs:ro \ -v $DIR/storage/logs:/app/storage/logs \ -v $DIR/storage/uploads:/app/storage/uploads \ -e TZ=Asia/Shanghai \ "$OLD_IMAGE" fi exit 1 fi REMOTE - name: 部署结果 if: always() run: echo "镜像 ${{ env.IMAGE }}:${{ steps.meta.outputs.tag }} 部署流程结束"