name: Build and Deploy on: push: branches: - main workflow_dispatch: inputs: logLevel: description: 'Log level' required: false default: 'info' type: string env: REGISTRY: gitea.fse-media.group IMAGE_NAME: henry_du/bywcloudreve SERVER_HOST: 192.140.163.199 SERVER_USER: ubuntu CONTAINER_NAME: cloudreve jobs: build-and-deploy: runs-on: - self-hosted - linux permissions: contents: read packages: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Gitea Container Registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ gitea.actor }} password: ${{ secrets.GITEA_TOKEN }} - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=ref,event=branch type=sha,format=long,prefix= type=raw,value=latest,enable={{is_default_branch}} - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile.ci push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} platforms: linux/amd64 cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest cache-to: type=inline build-args: | BUILD_DATE=${{ gitea.sha }} - name: Deploy to server via SSH uses: appleboy/ssh-action@v1.2.0 with: host: ${{ env.SERVER_HOST }} username: ${{ env.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} port: 22 script_stop: true script: | set -euo pipefail sudo -n true 2>/dev/null || SUDO="" [ -z "${SUDO+x}" ] && SUDO="sudo" CONTAINER="${{ env.CONTAINER_NAME }}" NEW_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" echo "[1/6] 登录 Gitea Container Registry" echo "${{ secrets.GITEA_TOKEN }}" | $SUDO docker login "${{ env.REGISTRY }}" -u "${{ gitea.actor }}" --password-stdin echo "[2/6] 拉取最新镜像 ${NEW_IMAGE}" $SUDO docker pull "${NEW_IMAGE}" if ! $SUDO docker inspect "$CONTAINER" > /tmp/old.json 2>/dev/null; then echo "[WARN] 现有容器 $CONTAINER 不存在,按默认参数首次启动(请确认 postgres/redis 已在 172.17.0.1)。" $SUDO docker run -d \ --name "$CONTAINER" \ --restart unless-stopped \ -p 5212:5212 -p 6888:6888 -p 6888:6888/udp \ -v cloudreve_backend_data:/cloudreve/data \ --add-host=postgresql:172.17.0.1 \ --add-host=redis:172.17.0.1 \ -e CR_CONF_Database.Type=postgres \ -e CR_CONF_Database.Host=postgresql \ -e CR_CONF_Database.User=cloudreve \ -e CR_CONF_Database.Name=cloudreve \ -e CR_CONF_Database.Port=5432 \ -e CR_CONF_Redis.Server=redis:6379 \ "$NEW_IMAGE" || true echo "首次启动命令已执行。" exit 0 fi echo "[3/6] 从现有容器提取运行参数 (安装 jq)" which jq >/dev/null 2>&1 || ($SUDO apt-get update -qq && $SUDO apt-get install -y -qq jq >/dev/null) OLD_IMAGE="$(jq -r '.Config.Image' /tmp/old.json)" RESTART_POLICY="$(jq -r '.HostConfig.RestartPolicy.Name' /tmp/old.json)" NETWORK_MODE="$(jq -r '.HostConfig.NetworkMode' /tmp/old.json)" echo "旧镜像: ${OLD_IMAGE} -> 新镜像: ${NEW_IMAGE}" echo "重启策略: ${RESTART_POLICY}, 网络: ${NETWORK_MODE}" # 以数组方式构建参数,避免 eval 转义问题 RUN_ARGS=( -d ) RUN_ARGS+=( --name "${CONTAINER}_new" ) [ "${RESTART_POLICY}" != "null" ] && [ -n "${RESTART_POLICY}" ] && RUN_ARGS+=( --restart "${RESTART_POLICY}" ) [ "${NETWORK_MODE}" != "null" ] && [ -n "${NETWORK_MODE}" ] && RUN_ARGS+=( --network "${NETWORK_MODE}" ) # 环境变量 while IFS= read -r env_line; do [ -z "${env_line}" ] && continue RUN_ARGS+=( -e "${env_line}" ) done < <(jq -r '.Config.Env[]?' /tmp/old.json) # 端口映射 while IFS=$'\t' read -r cport hostip hostport; do [ -z "${cport}" ] && continue proto="" if [[ "${cport}" == */udp ]]; then proto="/udp" cport="${cport%/udp}" elif [[ "${cport}" == */tcp ]]; then cport="${cport%/tcp}" fi if [ -n "${hostport}" ]; then ip_part="" [ -n "${hostip}" ] && ip_part="${hostip}:" RUN_ARGS+=( -p "${ip_part}${hostport}:${cport}${proto}" ) else RUN_ARGS+=( -p "${cport}${proto}" ) fi done < <(jq -r '.HostConfig.PortBindings | to_entries[]? | .key as $k | .value[]? | [$k, .HostIp, .HostPort] | @tsv' /tmp/old.json) # 卷挂载 while IFS= read -r mline; do [ -z "${mline}" ] && continue RUN_ARGS+=( ${mline} ) done < <(jq -r '.Mounts[]? | select(.Type=="bind" or .Type=="volume") as $m | if $m.Type == "bind" then "-v " + $m.Source + ":" + $m.Destination + (if $m.ReadOnly then ":ro" else "" end) else "-v " + $m.Name + ":" + $m.Destination + (if $m.ReadOnly then ":ro" else "" end) end' /tmp/old.json 2>/dev/null || true) # Extra hosts while IFS= read -r h; do [ -z "${h}" ] && continue RUN_ARGS+=( --add-host "${h}" ) done < <(jq -r '.HostConfig.ExtraHosts[]?' /tmp/old.json 2>/dev/null || true) echo "[4/6] 以相同参数启动新容器 (${#RUN_ARGS[@]} args)" NEW_CID=$($SUDO docker run "${RUN_ARGS[@]}" "${NEW_IMAGE}") echo "新容器ID: ${NEW_CID}" sleep 5 NEW_STATUS=$($SUDO docker inspect -f '{{.State.Status}}' "${NEW_CID}" 2>/dev/null || echo "unknown") echo "新容器状态: ${NEW_STATUS}" if [ "${NEW_STATUS}" != "running" ] && [ "${NEW_STATUS}" != "created" ]; then echo "[ERROR] 新容器未正常运行,输出最近日志后回滚。" $SUDO docker logs --tail 60 "${NEW_CID}" || true $SUDO docker rm -f "${NEW_CID}" 2>/dev/null || true exit 1 fi echo "[5/6] 停用旧容器并切换新容器为目标名称" $SUDO docker stop -t 30 "$CONTAINER" 2>/dev/null || true $SUDO docker rm -f "$CONTAINER" 2>/dev/null || true $SUDO docker rename "${CONTAINER}_new" "$CONTAINER" echo "[6/6] 清理旧镜像 / 悬空镜像" $SUDO docker image prune -f >/dev/null 2>&1 || true FINAL_IMG=$($SUDO docker inspect -f '{{.Config.Image}}' "$CONTAINER") echo "✅ 部署完成 @ $(date) 。容器 $CONTAINER 当前镜像: ${FINAL_IMG}"