Files
server-deploy/gitea/upgrade.sh
2026-04-07 17:06:12 +08:00

618 lines
20 KiB
Bash
Executable File
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.
#!/usr/bin/env bash
set -euo pipefail
# ============================================
# Gitea 服务组件升级脚本
# 支持升级Gitea / MySQL / Nginx / Certbot / Docker
# 用法bash upgrade.sh [组件名]
# bash upgrade.sh # 交互式选择
# bash upgrade.sh gitea # 仅升级 Gitea
# bash upgrade.sh mysql # 仅升级 MySQL
# bash upgrade.sh nginx # 仅升级 Nginx
# bash upgrade.sh certbot # 仅升级 Certbot
# bash upgrade.sh docker # 仅升级 Docker
# bash upgrade.sh all # 升级全部组件
# ============================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
step() { echo -e "\n${CYAN}========== $* ==========${NC}"; }
confirm() {
echo -en "${YELLOW}[确认]${NC} $* [y/N]: "
read -r reply
[[ "$reply" =~ ^[Yy]$ ]]
}
# ===== 前置检查 =====
preflight() {
if [ "$(id -u)" -ne 0 ]; then
error "请使用 root 用户运行: sudo bash upgrade.sh"
exit 1
fi
if [ ! -f .env ]; then
error ".env 文件不存在,请先完成部署: bash deploy.sh"
exit 1
fi
set -a
source .env
set +a
# 检测包管理器
if command -v apt-get &> /dev/null; then
PKG_MGR="apt"
elif command -v dnf &> /dev/null; then
PKG_MGR="dnf"
elif command -v yum &> /dev/null; then
PKG_MGR="yum"
else
error "不支持的系统"
exit 1
fi
}
# ===== 获取当前版本信息 =====
show_versions() {
step "当前组件版本"
# Gitea
local gitea_ver="未运行"
if docker compose ps --format json 2>/dev/null | grep -q '"server"' || \
docker compose ps 2>/dev/null | grep -q "gitea.*Up"; then
gitea_ver=$(curl -sf http://127.0.0.1:3000/api/v1/version 2>/dev/null | grep -o '"version":"[^"]*"' | cut -d'"' -f4 || echo "未知")
fi
local gitea_image
gitea_image=$(docker compose images server 2>/dev/null | tail -1 | awk '{print $2":"$3}' || echo "未知")
echo -e " Gitea: ${CYAN}${gitea_ver}${NC} (镜像: ${gitea_image})"
# MySQL
local mysql_ver="未运行"
if docker compose ps --format json 2>/dev/null | grep -q '"db"' || \
docker compose ps 2>/dev/null | grep -q "gitea-db.*Up"; then
mysql_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知")
fi
echo -e " MySQL: ${CYAN}${mysql_ver}${NC}"
# Nginx
local nginx_ver
nginx_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装")
echo -e " Nginx: ${CYAN}${nginx_ver}${NC}"
# Certbot
local certbot_ver
certbot_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装")
echo -e " Certbot: ${CYAN}${certbot_ver}${NC}"
# Docker
local docker_ver
docker_ver=$(docker --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装")
local compose_ver
compose_ver=$(docker compose version --short 2>/dev/null || echo "未安装")
echo -e " Docker: ${CYAN}${docker_ver}${NC} (Compose: ${compose_ver})"
# SSL 证书
local domain="${GITEA_DOMAIN:-}"
if [ -n "$domain" ] && [ -d "/etc/letsencrypt/live/${domain}" ]; then
local cert_expiry
cert_expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" 2>/dev/null | cut -d= -f2 || echo "未知")
echo -e " SSL证书: 到期 ${CYAN}${cert_expiry}${NC}"
fi
echo ""
}
# ===== 备份 =====
do_backup() {
step "升级前备份"
if [ -f "$SCRIPT_DIR/backup.sh" ]; then
log "正在执行完整备份..."
bash "$SCRIPT_DIR/backup.sh"
log "备份完成"
else
warn "backup.sh 不存在,跳过自动备份"
if ! confirm "继续升级(未备份)?"; then
error "已取消"
exit 1
fi
fi
}
# ============================================================
# 升级 GiteaDocker 容器)
# ============================================================
upgrade_gitea() {
step "升级 Gitea"
local current_image
current_image=$(grep -E '^\s*image:' docker-compose.yml | head -1 | awk '{print $2}' | envsubst || echo "unknown")
log "当前镜像配置: $(grep -E 'GITEA_IMAGE' .env 2>/dev/null | head -1 || echo '使用默认')"
# 获取远程最新 tag
log "检查最新版本..."
local latest_tag=""
# 尝试从 Docker Hub API 获取最新稳定版
latest_tag=$(curl -sf "https://registry.hub.docker.com/v2/repositories/gitea/gitea/tags?page_size=50&ordering=last_updated" 2>/dev/null \
| grep -o '"name":"[0-9]\+\.[0-9]\+\.[0-9]\+"' \
| head -1 \
| cut -d'"' -f4 || echo "")
if [ -n "$latest_tag" ]; then
log "Docker Hub 最新稳定版: $latest_tag"
else
warn "无法查询最新版本,请手动确认目标版本"
fi
echo ""
echo -e " ${YELLOW}升级方式说明:${NC}"
echo " ┌──────────────────────────────────────────────────────┐"
echo " │ 方式 A推荐修改 .env 中的 GITEA_IMAGE 后拉取 │"
echo " │ 方式 B直接拉取当前配置的镜像获取 tag 内最新构建) │"
echo " └──────────────────────────────────────────────────────┘"
echo ""
local do_change_version="n"
if [ -n "$latest_tag" ]; then
echo -en " 是否更新 GITEA_IMAGE 到 ${CYAN}gitea/gitea:${latest_tag}${NC}[y/N]: "
read -r do_change_version
fi
if [[ "$do_change_version" =~ ^[Yy]$ ]] && [ -n "$latest_tag" ]; then
# 更新 .env 中的 GITEA_IMAGE
if grep -q "^GITEA_IMAGE=" .env; then
sed -i "s|^GITEA_IMAGE=.*|GITEA_IMAGE=gitea/gitea:${latest_tag}|" .env
else
echo "GITEA_IMAGE=gitea/gitea:${latest_tag}" >> .env
fi
log "已更新 .env: GITEA_IMAGE=gitea/gitea:${latest_tag}"
# 重新加载
set -a; source .env; set +a
fi
log "正在拉取 Gitea 镜像..."
docker compose pull server
log "正在重启 Gitea 容器(数据库迁移会自动执行)..."
docker compose up -d server
# 等待 Gitea 就绪
log "等待 Gitea 启动..."
local ready=0
for i in $(seq 1 60); do
if curl -sf http://127.0.0.1:3000/api/v1/version &> /dev/null; then
ready=1
break
fi
sleep 2
done
if [ "$ready" -eq 1 ]; then
local new_ver
new_ver=$(curl -sf http://127.0.0.1:3000/api/v1/version | grep -o '"version":"[^"]*"' | cut -d'"' -f4)
log "Gitea 升级成功!当前版本: $new_ver"
else
warn "Gitea 可能仍在启动,请检查日志: docker compose logs -f server"
fi
echo ""
warn "请浏览器访问 https://${GITEA_DOMAIN:-你的域名} 确认功能正常"
}
# ============================================================
# 升级 MySQLDocker 容器 — 小版本升级)
# ============================================================
upgrade_mysql() {
step "升级 MySQL"
local current_ver
current_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知")
log "当前 MySQL 版本: $current_ver"
local current_image
current_image=$(grep -E 'image:\s*mysql' docker-compose.yml | awk '{print $2}' | tr -d '"' || echo "mysql:8.0")
log "当前镜像: $current_image"
echo ""
echo -e " ${YELLOW}MySQL 升级须知:${NC}"
echo " ┌──────────────────────────────────────────────────────────┐"
echo " │ ● 小版本升级8.0.x → 8.0.y拉取新镜像重启即可 │"
echo " │ ● 大版本升级8.0 → 8.4/9.0):需要额外迁移步骤 │"
echo " │ 大版本升级建议mysqldump 导出 → 新版本容器 → 导入 │"
echo " │ ● MySQL 仅支持相邻大版本升级,不可跨版本 │"
echo " └──────────────────────────────────────────────────────────┘"
echo ""
# 选择升级类型
echo " 选择升级类型:"
echo " 1) 小版本升级 — 拉取 mysql:8.0 最新补丁(推荐)"
echo " 2) 大版本升级 — 升级到 MySQL 8.4 LTS"
echo " 3) 大版本升级 — 升级到 MySQL 9.x创新版本"
echo " 0) 跳过 MySQL 升级"
echo ""
echo -en " 请选择 [0-3]: "
read -r mysql_choice
case "$mysql_choice" in
1)
_mysql_minor_upgrade
;;
2)
_mysql_major_upgrade "mysql:8.4" "8.4"
;;
3)
_mysql_major_upgrade "mysql:9.0" "9.0"
;;
*)
log "跳过 MySQL 升级"
return
;;
esac
}
_mysql_minor_upgrade() {
log "正在拉取 MySQL 8.0 最新镜像..."
docker compose pull db
log "正在重启 MySQL 容器..."
docker compose up -d db
# 等待就绪
log "等待 MySQL 就绪..."
for i in $(seq 1 60); do
if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent &> /dev/null; then
break
fi
sleep 2
done
local new_ver
new_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知")
log "MySQL 小版本升级完成!当前版本: $new_ver"
# 重启 Gitea 确保数据库连接正常
log "重启 Gitea 以重新连接数据库..."
docker compose restart server
}
_mysql_major_upgrade() {
local target_image="$1"
local target_ver="$2"
warn "⚠️ MySQL 大版本升级有风险,确保已完成备份!"
echo ""
if ! confirm "确认要将 MySQL 升级到 ${target_ver}"; then
log "已取消"
return
fi
log "第 1 步:导出当前数据库..."
local dump_file="/tmp/gitea_mysql_upgrade_$(date +%Y%m%d_%H%M%S).sql"
docker compose exec -T db mysqldump \
-u root -p"${DB_ROOT_PASSWORD}" \
--single-transaction \
--routines \
--triggers \
--databases gitea \
> "$dump_file"
local dump_size
dump_size=$(du -h "$dump_file" | cut -f1)
log "数据库导出完成: $dump_file ($dump_size)"
log "第 2 步:停止服务..."
docker compose down
log "第 3 步:备份 MySQL 数据目录..."
local mysql_data="${MYSQL_DATA_DIR:-/var/lib/mysql/gitea}"
local backup_mysql="${mysql_data}.bak.$(date +%Y%m%d_%H%M%S)"
cp -a "$mysql_data" "$backup_mysql"
log "数据目录已备份至: $backup_mysql"
log "第 4 步:清空 MySQL 数据目录(新版本将重新初始化)..."
rm -rf "${mysql_data:?}"/*
log "第 5 步:更新 docker-compose.yml 中的 MySQL 镜像..."
sed -i "s|image: mysql:8\.0|image: ${target_image}|" docker-compose.yml
# MySQL 8.4+ 不再需要 --mysql-native-password
if [[ "$target_ver" != "8.0" ]]; then
warn "MySQL ${target_ver} 默认使用 caching_sha2_password"
warn "移除 --mysql-native-password=ON 参数(如有兼容问题可恢复)"
sed -i '/--mysql-native-password=ON/d' docker-compose.yml
fi
log "第 6 步:启动新版本 MySQL..."
docker compose up -d db
log "等待 MySQL ${target_ver} 初始化..."
for i in $(seq 1 90); do
if docker compose exec -T db mysqladmin ping -h localhost -u root -p"${DB_ROOT_PASSWORD}" --silent &> /dev/null; then
break
fi
if [ "$i" -eq 90 ]; then
error "MySQL 启动超时!请检查日志: docker compose logs db"
error "如需回滚cp -a $backup_mysql/* $mysql_data/ 并恢复 docker-compose.yml"
exit 1
fi
sleep 2
done
log "第 7 步:导入数据库..."
docker compose exec -T db mysql -u root -p"${DB_ROOT_PASSWORD}" < "$dump_file"
log "数据库导入完成"
log "第 8 步:启动 Gitea..."
docker compose up -d
local new_ver
new_ver=$(docker compose exec -T db mysql --version 2>/dev/null | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1 || echo "未知")
log "MySQL 大版本升级完成!当前版本: $new_ver"
echo ""
warn "导出文件保留在: $dump_file"
warn "旧数据目录保留在: $backup_mysql"
warn "确认运行正常后可手动删除以上文件"
}
# ============================================================
# 升级 Nginx系统包
# ============================================================
upgrade_nginx() {
step "升级 Nginx"
local current_ver
current_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知")
log "当前 Nginx 版本: $current_ver"
log "正在更新 Nginx..."
case "$PKG_MGR" in
apt)
apt-get update -qq
apt-get install -y --only-upgrade nginx
;;
dnf)
dnf upgrade -y nginx
;;
yum)
yum update -y nginx
;;
esac
local new_ver
new_ver=$(nginx -v 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知")
if [ "$current_ver" = "$new_ver" ]; then
log "Nginx 已是最新版: $new_ver"
else
log "Nginx 已更新: $current_ver$new_ver"
fi
# 验证配置
log "验证 Nginx 配置..."
if nginx -t 2>&1; then
systemctl reload nginx
log "Nginx 配置验证通过并已重载"
else
error "Nginx 配置验证失败!请手动检查"
error " nginx -t"
error " vi /etc/nginx/sites-available/gitea"
fi
}
# ============================================================
# 升级 Certbot系统包
# ============================================================
upgrade_certbot() {
step "升级 Certbot"
local current_ver
current_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未安装")
log "当前 Certbot 版本: $current_ver"
log "正在更新 Certbot..."
case "$PKG_MGR" in
apt)
apt-get update -qq
apt-get install -y --only-upgrade certbot python3-certbot-nginx
;;
dnf)
dnf upgrade -y certbot python3-certbot-nginx
;;
yum)
yum update -y certbot python3-certbot-nginx
;;
esac
local new_ver
new_ver=$(certbot --version 2>&1 | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知")
if [ "$current_ver" = "$new_ver" ]; then
log "Certbot 已是最新版: $new_ver"
else
log "Certbot 已更新: $current_ver$new_ver"
fi
# 测试证书续期
log "测试证书续期dry-run..."
if certbot renew --dry-run 2>&1 | tail -3; then
log "证书续期测试通过"
else
warn "证书续期测试失败,请检查 certbot 配置"
fi
# 检查证书有效期
local domain="${GITEA_DOMAIN:-}"
if [ -n "$domain" ] && [ -f "/etc/letsencrypt/live/${domain}/fullchain.pem" ]; then
local expiry
expiry=$(openssl x509 -enddate -noout -in "/etc/letsencrypt/live/${domain}/fullchain.pem" | cut -d= -f2)
log "当前证书到期时间: $expiry"
# 检查是否30天内到期
local expiry_epoch
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || echo 0)
local now_epoch
now_epoch=$(date +%s)
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt 30 ] && [ "$days_left" -gt 0 ]; then
warn "证书将在 ${days_left} 天后到期"
if confirm "是否立即续期?"; then
certbot renew --force-renewal --post-hook 'systemctl reload nginx'
log "证书已续期"
fi
elif [ "$days_left" -le 0 ]; then
error "证书已过期!正在强制续期..."
certbot renew --force-renewal --post-hook 'systemctl reload nginx'
else
log "证书有效期剩余: ${days_left}"
fi
fi
}
# ============================================================
# 升级 Docker系统包
# ============================================================
upgrade_docker() {
step "升级 Docker"
local current_ver
current_ver=$(docker --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知")
log "当前 Docker 版本: $current_ver"
log "正在更新 Docker..."
case "$PKG_MGR" in
apt)
apt-get update -qq
apt-get install -y --only-upgrade docker-ce docker-ce-cli containerd.io docker-compose-plugin
;;
dnf)
dnf upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
;;
yum)
yum update -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
;;
esac
local new_ver
new_ver=$(docker --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' || echo "未知")
if [ "$current_ver" = "$new_ver" ]; then
log "Docker 已是最新版: $new_ver"
else
log "Docker 已更新: $current_ver$new_ver"
warn "Docker 已更新,容器将自动重启"
fi
local compose_ver
compose_ver=$(docker compose version --short 2>/dev/null || echo "未知")
log "Docker Compose 版本: $compose_ver"
# 确保服务正常运行
log "确认容器运行状态..."
docker compose ps
}
# ============================================================
# 升级全部
# ============================================================
upgrade_all() {
step "升级全部组件"
echo ""
warn "将依次升级: Docker → Nginx → Certbot → MySQL → Gitea"
warn "升级前会自动执行完整备份"
echo ""
if ! confirm "确认升级全部组件?"; then
log "已取消"
exit 0
fi
do_backup
upgrade_docker
upgrade_nginx
upgrade_certbot
upgrade_mysql
upgrade_gitea
step "全部组件升级完成"
show_versions
}
# ============================================================
# 交互菜单
# ============================================================
interactive_menu() {
echo ""
echo -e " ${CYAN}可升级组件:${NC}"
echo " 1) Gitea — Docker 容器镜像升级"
echo " 2) MySQL — Docker 容器镜像升级(支持大/小版本)"
echo " 3) Nginx — 系统包升级"
echo " 4) Certbot — 系统包升级 + 证书检查"
echo " 5) Docker — Docker Engine + Compose 升级"
echo " 6) 全部升级 — 依次升级所有组件"
echo " 0) 退出"
echo ""
echo -en " 请选择 [0-6]: "
read -r choice
case "$choice" in
1) do_backup; upgrade_gitea ;;
2) do_backup; upgrade_mysql ;;
3) upgrade_nginx ;;
4) upgrade_certbot ;;
5) do_backup; upgrade_docker ;;
6) upgrade_all ;;
0) log "退出"; exit 0 ;;
*) error "无效选择"; exit 1 ;;
esac
}
# ============================================================
# 主入口
# ============================================================
main() {
echo -e "${CYAN}"
echo " ____ _ _"
echo " / ___|(_) |_ ___ __ _"
echo "| | _ | | __/ _ \\/ _\` |"
echo "| |_| || | || __/ (_| |"
echo " \\____|_|\\__\\___|\\__,_| Upgrade Script"
echo -e "${NC}"
preflight
show_versions
local target="${1:-}"
case "$target" in
gitea) do_backup; upgrade_gitea ;;
mysql) do_backup; upgrade_mysql ;;
nginx) upgrade_nginx ;;
certbot) upgrade_certbot ;;
docker) do_backup; upgrade_docker ;;
all) upgrade_all ;;
"") interactive_menu ;;
*)
error "未知组件: $target"
echo " 用法: bash upgrade.sh [gitea|mysql|nginx|certbot|docker|all]"
exit 1
;;
esac
echo ""
step "升级后版本信息"
show_versions
log "升级完成!请检查服务是否正常: docker compose ps"
}
main "$@"