Files
mindfulness/.gitea/workflows/server-deploy.yml
2026-02-03 00:26:13 +08:00

354 lines
14 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: Deploy Server (SSH + Nginx 蓝绿)
# 手动触发选择部署环境dev/pro并输入要部署的镜像 tag
on:
workflow_dispatch:
inputs:
env:
description: '部署环境'
type: choice
required: true
options:
- dev
- pro
tag:
description: '要部署的镜像 Tag例如v1.2.7'
required: true
# 同一环境同一时间只允许一个部署在跑,避免互相覆盖
concurrency:
group: deploy-server-${{ github.event.inputs.env }}
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
env:
# 统一在 env 中注入变量,减少 runner 差异带来的兼容性问题
DEPLOY_ENV: ${{ github.event.inputs.env }}
DEPLOY_TAG: ${{ github.event.inputs.tag }}
# 镜像名(优先 vars.DOCKER_IMAGE未配置则步骤里兜底
DOCKER_IMAGE: ${{ vars.DOCKER_IMAGE }}
# 可选:明确 registry私有仓库用。未配置会从 DOCKER_IMAGE 推断
DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY }}
# SSHdev 用 secretspro 用 vars按你现有用法
SSH_HOST: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_HOST || vars.PRO_SSH_HOST }}
SSH_USER: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_USER || vars.PRO_SSH_USER }}
SSH_PORT: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_PORT || vars.PRO_SSH_PORT }}
SSH_KEY: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_KEY || secrets.PRO_SSH_KEY }}
# 推荐把私钥做成 base64单行存到 Secrets避免多行变量丢换行
SSH_KEY_B64: ${{ github.event.inputs.env == 'dev' && secrets.DEV_SSH_KEY_B64 || secrets.PRO_SSH_KEY_B64 }}
# Nginx upstream 配置(建议放到 vars
NGINX_UPSTREAM_FILE: ${{ vars.NGINX_UPSTREAM_FILE }}
NGINX_UPSTREAM_NAME: ${{ vars.NGINX_UPSTREAM_NAME }}
# 蓝绿端口(宿主机端口,建议放到 vars未配置则脚本内有默认值
BLUE_PORT: ${{ vars.BLUE_PORT }}
GREEN_PORT: ${{ vars.GREEN_PORT }}
# 容器内监听端口FastAPI 常用 8000未配置则默认 8000
CONTAINER_PORT: ${{ vars.CONTAINER_PORT }}
# 健康检查路径(未配置则默认 /health如果你没有 health 接口,可改为 /docs 或 /
HEALTHCHECK_PATH: ${{ vars.HEALTHCHECK_PATH }}
# 可选:远端 env 文件路径(例如 /opt/mindfulness-server/.env.prod存在则 docker run --env-file
REMOTE_ENV_FILE: ${{ vars.REMOTE_ENV_FILE }}
steps:
- name: 配置 SSH Key密钥登陆
shell: bash
run: |
set -euo pipefail
# 镜像名兜底(与 .gitea/workflows/server-build.yml 的默认一致)
if [[ -z "${DOCKER_IMAGE:-}" ]]; then
DOCKER_IMAGE="docker.damer.fun/damer/mindfulness-server"
fi
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITHUB_ENV"
fi
if [[ -n "${GITEA_ENV:-}" ]]; then
echo "DOCKER_IMAGE=${DOCKER_IMAGE}" >> "$GITEA_ENV"
fi
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# 写入私钥
if [[ -n "${SSH_KEY_B64:-}" ]]; then
# 去掉粘贴过程中可能混入的空白字符(换行/回车/空格/Tab
# 避免出现base64: invalid input
CLEAN_B64="$(printf '%s' "${SSH_KEY_B64}" | tr -d '\r\n\t ')"
if ! printf '%s' "${CLEAN_B64}" | base64 -d | tr -d '\r' > ~/.ssh/id_rsa; then
echo "私钥 base64 解码失败:请确认你配置的 *_SSH_KEY_B64 是对私钥文件做的 base64单行且未被截断。"
exit 1
fi
else
printf '%s' "${SSH_KEY}" | tr -d '\r' > ~/.ssh/id_rsa
fi
chmod 600 ~/.ssh/id_rsa
# 快速校验私钥是否可解析(不会输出私钥内容)
if ! ssh-keygen -y -f ~/.ssh/id_rsa >/dev/null 2>&1; then
echo "SSH 私钥无法解析(常见原因:内容复制丢换行/截断、格式不兼容、或 base64 不正确)。"
echo "推荐:生成一把“无口令”的部署专用私钥,并用 base64 存储到 Secrets。"
exit 1
fi
SSH_PORT="${SSH_PORT:-22}"
if [[ -n "${GITHUB_ENV:-}" ]]; then
echo "SSH_PORT=${SSH_PORT}" >> "$GITHUB_ENV"
fi
if [[ -n "${GITEA_ENV:-}" ]]; then
echo "SSH_PORT=${SSH_PORT}" >> "$GITEA_ENV"
fi
# 预写 known_hosts避免交互
ssh-keyscan -p "${SSH_PORT}" -H "${SSH_HOST}" >> ~/.ssh/known_hosts 2>/dev/null
- name: 验证 SSH 连接(快速定位公钥/用户/端口问题)
shell: bash
run: |
set -euo pipefail
SSH_PORT="${SSH_PORT:-22}"
ssh -p "${SSH_PORT}" \
-i ~/.ssh/id_rsa \
-o StrictHostKeyChecking=yes \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
"${SSH_USER}@${SSH_HOST}" 'echo "SSH_OK $(whoami)@$(hostname)"'
- name: 远程登录 Docker Registry如需私有镜像
shell: bash
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
run: |
set -euo pipefail
# 如果镜像是公开的,可不配 DOCKER_USERNAME/DOCKER_TOKEN此步骤会自动跳过
if [[ -z "${DOCKER_USERNAME:-}" || -z "${DOCKER_TOKEN:-}" ]]; then
echo "未配置 DOCKER_USERNAME/DOCKER_TOKEN跳过远程 docker login。"
exit 0
fi
SSH_PORT="${SSH_PORT:-22}"
# 推断 registry优先使用 DOCKER_REGISTRY否则从 DOCKER_IMAGE 的第一个段推断
REG="${DOCKER_REGISTRY:-}"
if [[ -z "${REG}" ]]; then
FIRST_SEG="${DOCKER_IMAGE%%/*}"
if [[ "${FIRST_SEG}" == *.* || "${FIRST_SEG}" == *:* || "${FIRST_SEG}" == "localhost" ]]; then
REG="${FIRST_SEG}"
fi
fi
if [[ -z "${REG}" ]]; then
echo "无法推断 registry看起来像 Docker Hub 公有镜像),跳过 docker login。"
exit 0
fi
ssh -p "${SSH_PORT}" -i ~/.ssh/id_rsa -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes \
"${SSH_USER}@${SSH_HOST}" bash -s -- "${DOCKER_TOKEN}" "${DOCKER_USERNAME}" "${REG}" <<'REMOTE'
set -euo pipefail
TOKEN="$1"
USERNAME="$2"
REGISTRY="$3"
SUDO=""
if [[ "$(id -u)" -ne 0 ]] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
printf '%s' "$TOKEN" | ${SUDO} docker login "${REGISTRY}" -u "$USERNAME" --password-stdin
REMOTE
- name: SSH 部署Nginx 蓝绿切换)
shell: bash
run: |
set -euo pipefail
SSH_PORT="${SSH_PORT:-22}"
NGINX_UPSTREAM_NAME="${NGINX_UPSTREAM_NAME:-mindfulness_backend}"
NGINX_UPSTREAM_FILE="${NGINX_UPSTREAM_FILE:-/etc/nginx/conf.d/mindfulness-server.conf}"
BLUE_PORT="${BLUE_PORT:-8001}"
GREEN_PORT="${GREEN_PORT:-8002}"
CONTAINER_PORT="${CONTAINER_PORT:-8000}"
HEALTHCHECK_PATH="${HEALTHCHECK_PATH:-/health}"
echo "准备部署:${DOCKER_IMAGE}:${DEPLOY_TAG} -> ${DEPLOY_ENV} (${SSH_USER}@${SSH_HOST}:${SSH_PORT})"
ssh -p "${SSH_PORT}" -i ~/.ssh/id_rsa -o StrictHostKeyChecking=yes -o IdentitiesOnly=yes "${SSH_USER}@${SSH_HOST}" bash -s -- \
"${DOCKER_IMAGE}" "${DEPLOY_TAG}" "${NGINX_UPSTREAM_FILE}" "${NGINX_UPSTREAM_NAME}" "${BLUE_PORT}" "${GREEN_PORT}" "${CONTAINER_PORT}" "${HEALTHCHECK_PATH}" "${REMOTE_ENV_FILE:-}" <<'REMOTE'
set -euo pipefail
IMAGE="$1"
TAG="$2"
UPSTREAM_FILE="$3"
UPSTREAM_NAME="$4"
BLUE_PORT="$5"
GREEN_PORT="$6"
CONTAINER_PORT="$7"
HEALTHCHECK_PATH="$8"
REMOTE_ENV_FILE="$9"
APP_DIR="/opt/mindfulness-server"
ACTIVE_FILE="${APP_DIR}/active_color"
mkdir -p "${APP_DIR}"
# 判断 sudo如果非 root 且存在 sudo则使用 sudo
SUDO=""
if [[ "$(id -u)" -ne 0 ]] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
ACTIVE_COLOR="blue"
if [[ -f "${ACTIVE_FILE}" ]]; then
ACTIVE_COLOR="$(cat "${ACTIVE_FILE}" || echo blue)"
fi
if [[ "${ACTIVE_COLOR}" == "blue" ]]; then
NEW_COLOR="green"
NEW_PORT="${GREEN_PORT}"
OLD_COLOR="blue"
OLD_PORT="${BLUE_PORT}"
else
NEW_COLOR="blue"
NEW_PORT="${BLUE_PORT}"
OLD_COLOR="green"
OLD_PORT="${GREEN_PORT}"
fi
echo "当前在线:${ACTIVE_COLOR}${OLD_PORT}),准备发布:${NEW_COLOR}${NEW_PORT}"
# 拉取镜像
${SUDO} docker pull "${IMAGE}:${TAG}"
# 启动新颜色容器
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" >/dev/null 2>&1 || true
ENV_FILE_ARGS=()
if [[ -n "${REMOTE_ENV_FILE}" && -f "${REMOTE_ENV_FILE}" ]]; then
ENV_FILE_ARGS=(--env-file "${REMOTE_ENV_FILE}")
echo "将使用远端 env 文件:${REMOTE_ENV_FILE}"
elif [[ -n "${REMOTE_ENV_FILE}" ]]; then
echo "提示REMOTE_ENV_FILE 已配置但文件不存在:${REMOTE_ENV_FILE}(将忽略 env-file"
fi
${SUDO} docker run -d \
--name "mindfulness-server-${NEW_COLOR}" \
--restart=always \
-p "${NEW_PORT}:${CONTAINER_PORT}" \
"${ENV_FILE_ARGS[@]}" \
"${IMAGE}:${TAG}"
# 健康检查
if [[ "${HEALTHCHECK_PATH}" != /* ]]; then
HEALTHCHECK_PATH="/${HEALTHCHECK_PATH}"
fi
HEALTH_URL="http://127.0.0.1:${NEW_PORT}${HEALTHCHECK_PATH}"
echo "健康检查:${HEALTH_URL}"
for i in $(seq 1 30); do
if command -v curl >/dev/null 2>&1; then
if curl -fsS "${HEALTH_URL}" >/dev/null; then
echo "健康检查通过"
break
fi
elif command -v wget >/dev/null 2>&1; then
if wget -q -O /dev/null "${HEALTH_URL}"; then
echo "健康检查通过"
break
fi
else
echo "远端缺少 curl/wget跳过 HTTP 健康检查"
break
fi
if [[ "$i" -eq 30 ]]; then
echo "健康检查失败:新版本未就绪,回滚并退出"
${SUDO} docker logs --tail 200 "mindfulness-server-${NEW_COLOR}" || true
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
sleep 2
done
# 切换 Nginx upstream在同一个 conf 文件中通过 backup 做主备切换)
if [[ ! -f "${UPSTREAM_FILE}" ]]; then
echo "未找到 Nginx upstream 配置文件:${UPSTREAM_FILE}"
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
if ! ${SUDO} grep -qE "upstream[[:space:]]+${UPSTREAM_NAME}[[:space:]]*\\{" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 upstream${UPSTREAM_NAME}"
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
if ! ${SUDO} grep -qE "server[[:space:]]+127\\.0\\.0\\.1:${BLUE_PORT}" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 server 127.0.0.1:${BLUE_PORT}(请先按参考配置写入 upstream"
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
if ! ${SUDO} grep -qE "server[[:space:]]+127\\.0\\.0\\.1:${GREEN_PORT}" "${UPSTREAM_FILE}"; then
echo "在 ${UPSTREAM_FILE} 中未找到 server 127.0.0.1:${GREEN_PORT}(请先按参考配置写入 upstream"
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
# 备份(回滚用)
BACKUP_FILE="${UPSTREAM_FILE}.bak.$(date +%s)"
${SUDO} cp -f "${UPSTREAM_FILE}" "${BACKUP_FILE}"
echo "切换 upstream${UPSTREAM_NAME}${OLD_PORT} -> ${NEW_PORT})通过 backup 切换"
TMP_FILE="$(mktemp)"
${SUDO} awk -v name="${UPSTREAM_NAME}" -v old="${OLD_PORT}" -v new="${NEW_PORT}" '
BEGIN { in_up = 0 }
$0 ~ ("^[[:space:]]*upstream[[:space:]]+" name "[[:space:]]*\\{[[:space:]]*$") { in_up = 1 }
in_up == 1 {
# 新端口:主(去掉 backup
if ($0 ~ ("server[[:space:]]+127\\.0\\.0\\.1:" new)) {
gsub(/[[:space:]]+backup[[:space:]]*;/, ";")
}
# 旧端口:备(确保有 backup;
if ($0 ~ ("server[[:space:]]+127\\.0\\.0\\.1:" old)) {
gsub(/[[:space:]]+backup[[:space:]]*;/, ";")
sub(/;[[:space:]]*$/, " backup;")
}
}
in_up == 1 && $0 ~ /^[[:space:]]*\}[[:space:]]*$/ { in_up = 0 }
{ print }
' "${UPSTREAM_FILE}" > "${TMP_FILE}"
${SUDO} cp -f "${TMP_FILE}" "${UPSTREAM_FILE}"
rm -f "${TMP_FILE}"
# 校验并 reload nginx失败则回滚并退出
if ${SUDO} nginx -t; then
${SUDO} nginx -s reload
else
echo "Nginx 配置校验失败,回滚 upstream 配置并退出"
${SUDO} cp -f "${BACKUP_FILE}" "${UPSTREAM_FILE}" || true
${SUDO} nginx -t && ${SUDO} nginx -s reload || true
${SUDO} docker rm -f "mindfulness-server-${NEW_COLOR}" || true
exit 1
fi
# 记录当前在线颜色
echo "${NEW_COLOR}" | ${SUDO} tee "${ACTIVE_FILE}" >/dev/null
# 下线旧容器(切流后再停旧的)
${SUDO} docker rm -f "mindfulness-server-${OLD_COLOR}" >/dev/null 2>&1 || true
echo "部署完成:${NEW_COLOR} 已上线"
REMOTE