deploy: 一键更新 / 卸载脚本,安装脚本自动选择 IPv4/IPv6 站点与最新版本
- 新增 deploy/update.sh:只替换二进制并重启服务,保留配置;已是最新时跳过、不自动降级; 重启由独立进程执行(通过 bolt 自身运行也安全),新版本起不来时自动回滚 - 新增 deploy/uninstall.sh:删除服务、二进制与备份,默认同时删除配置,--keep-config 保留 - install.sh:先试 IPv4 站点(git.jocayn.qzz.io)再试 IPv6 站点(git.draws.qzz.io), 通过 Release API 获取最新版本,支持 SHA256SUMS 校验与 --version; 已安装时拒绝覆盖配置(--reinstall 强制);macOS 缺少签名时补 ad-hoc 签名 - .gitattributes 强制 *.sh 使用 LF - README / CLAUDE.md 更新部署说明与发布流程 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+94
-31
@@ -2,12 +2,14 @@
|
||||
# bolt 一键安装脚本(POSIX sh,需 root; Linux 使用 systemd,macOS 使用 launchd)
|
||||
#
|
||||
# 用法(在目标设备上):
|
||||
# 方式 A - 从 Gitea Release 下载二进制并安装:
|
||||
# curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sh -s -- --http 0.0.0.0:8080 --token 你的令牌
|
||||
# (私有仓库下载附件需要认证:先 export BOLT_DL_TOKEN=xxx,与 --token 分开配置)
|
||||
# 方式 A - 从 Gitea Release 下载最新版二进制并安装:
|
||||
# IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌
|
||||
# IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/install.sh | sudo sh -s -- --http 0.0.0.0:8080 --token 你的令牌
|
||||
# 方式 B - 已把二进制拷到目标设备(如 scp):
|
||||
# ./install.sh --binary ./bolt --http 0.0.0.0:8080 --token xxx --allow "git,docker"
|
||||
#
|
||||
# 已安装的设备升级请用 update.sh(保留配置);卸载用 uninstall.sh。
|
||||
#
|
||||
# 选项:
|
||||
# --http <addr> 监听地址,默认 127.0.0.1:8080
|
||||
# --token <t> HTTP Bearer 认证令牌(监听公网时必填)
|
||||
@@ -17,8 +19,15 @@
|
||||
# --file-root <d1,d2> 文件工具与 /files 只能访问这些目录(配置 --allow 时必须设置,否则文件功能禁用)
|
||||
# --max-upload <n> PUT /files 单次上传上限,支持 K/M/G(默认 2G)
|
||||
# --binary <src> 二进制来源:本地路径 或 http(s) URL(默认从 Gitea Release 下载)
|
||||
# --version <tag> 安装指定版本(默认 Release 上的最新版本)
|
||||
# --reinstall 已安装时按本次参数重写配置(不加则拒绝覆盖,避免丢失令牌等配置)
|
||||
# --no-start 只安装不启动
|
||||
# --no-enable 不设置开机自启
|
||||
#
|
||||
# 环境变量:
|
||||
# BOLT_GIT_HOST 指定下载站点(如 https://git.jocayn.qzz.io),默认先试 IPv4 站点再试 IPv6 站点
|
||||
# BOLT_DL_URL 直接指定二进制下载地址
|
||||
# BOLT_DL_TOKEN 仓库为私有时的下载令牌(与 --token 无关)
|
||||
set -eu
|
||||
|
||||
HTTP_ADDR="${HTTP_ADDR:-127.0.0.1:8080}"
|
||||
@@ -29,6 +38,8 @@ TIMEOUT_MS="${BOLT_TIMEOUT_MS:-30000}"
|
||||
FILE_ROOT=""
|
||||
MAX_UPLOAD=""
|
||||
BINARY_SRC=""
|
||||
VERSION="${BOLT_VERSION:-}"
|
||||
REINSTALL=0
|
||||
NO_START=0
|
||||
NO_ENABLE=0
|
||||
|
||||
@@ -42,12 +53,17 @@ while [ $# -gt 0 ]; do
|
||||
--file-root) FILE_ROOT="${2:-}"; shift 2 ;;
|
||||
--max-upload) MAX_UPLOAD="${2:-}"; shift 2 ;;
|
||||
--binary) BINARY_SRC="${2:-}"; shift 2 ;;
|
||||
--version) VERSION="${2:-}"; shift 2 ;;
|
||||
--reinstall) REINSTALL=1; shift ;;
|
||||
--no-start) NO_START=1; shift ;;
|
||||
--no-enable) NO_ENABLE=1; shift ;;
|
||||
*) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPO=jocay/bolt
|
||||
HOST4=https://git.jocayn.qzz.io
|
||||
HOST6=https://git.draws.qzz.io
|
||||
BIN_PATH=/usr/local/bin/bolt
|
||||
ENV_FILE=/etc/bolt.env
|
||||
UNIT_FILE=/etc/systemd/system/bolt.service
|
||||
@@ -78,50 +94,97 @@ case "$ARCH" in
|
||||
aarch64|arm64) ARCH=arm64 ;;
|
||||
*) die "不支持的架构: $ARCH" ;;
|
||||
esac
|
||||
DL_URL="${BOLT_DL_URL:-https://git.draws.qzz.io/jocay/bolt/releases/download/${BOLT_VERSION:-v0.1.0}/bolt-$PLATFORM-$ARCH}"
|
||||
say "目标平台: $PLATFORM/$ARCH"
|
||||
|
||||
# 已安装时默认拒绝:重写配置会丢掉原来的令牌、白名单等
|
||||
if [ "$REINSTALL" -eq 0 ] && [ -x "$BIN_PATH" ] && { [ -e "$ENV_FILE" ] || [ -e "/Library/Application Support/bolt/io.jocay.bolt.plist" ]; }; then
|
||||
die "检测到已安装的 bolt。升级请用 update.sh(保留配置);确实要按本次参数重写配置请加 --reinstall"
|
||||
fi
|
||||
|
||||
# fetch <url> <输出文件> [-4|-6]
|
||||
fetch() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
curl -fsSL --retry 3 ${3:-} -H "Authorization: token $BOLT_DL_TOKEN" -o "$2" "$1"
|
||||
else
|
||||
curl -fsSL --retry 3 ${3:-} -o "$2" "$1"
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
wget -q ${3:-} --header="Authorization: token $BOLT_DL_TOKEN" -O "$2" "$1"
|
||||
else
|
||||
wget -q ${3:-} -O "$2" "$1"
|
||||
fi
|
||||
else
|
||||
die "需要 curl 或 wget"
|
||||
fi
|
||||
}
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
mkdir -p /usr/local/bin
|
||||
|
||||
# ---------- 1. 获取二进制 ----------
|
||||
if [ -n "$BINARY_SRC" ]; then
|
||||
if [ -f "$BINARY_SRC" ]; then
|
||||
say "使用本地二进制: $BINARY_SRC"
|
||||
install -m 0755 "$BINARY_SRC" "$BIN_PATH"
|
||||
cp "$BINARY_SRC" "$TMP/bolt"
|
||||
elif echo "$BINARY_SRC" | grep -q '^https\?://'; then
|
||||
say "下载二进制: $BINARY_SRC"
|
||||
rm -f /tmp/bolt-dl.$$
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
curl -fsSL --retry 3 -H "Authorization:token ${BOLT_DL_TOKEN:-}" -o /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败"
|
||||
else
|
||||
curl -fsSL --retry 3 -o /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败"
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO /tmp/bolt-dl.$$ "$BINARY_SRC" || die "下载失败"
|
||||
else
|
||||
die "需要 curl 或 wget"
|
||||
fi
|
||||
install -m 0755 /tmp/bolt-dl.$$ "$BIN_PATH" && rm -f /tmp/bolt-dl.$$
|
||||
fetch "$BINARY_SRC" "$TMP/bolt" || die "下载失败"
|
||||
else
|
||||
die "二进制来源不存在: $BINARY_SRC"
|
||||
fi
|
||||
elif [ -n "${BOLT_DL_URL:-}" ]; then
|
||||
say "下载二进制: $BOLT_DL_URL"
|
||||
fetch "$BOLT_DL_URL" "$TMP/bolt" || die "下载失败"
|
||||
else
|
||||
say "从 Release 下载: $DL_URL"
|
||||
rm -f /tmp/bolt-dl.$$
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
curl -fsSL --retry 3 -H "Authorization:token ${BOLT_DL_TOKEN:-}" -o /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败,请确认 Release 附件存在且 BOLT_DL_TOKEN 有效"
|
||||
else
|
||||
curl -fsSL --retry 3 -o /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败,请确认 Release 附件存在;私有仓库需设置 BOLT_DL_TOKEN"
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO /tmp/bolt-dl.$$ "$DL_URL" || die "下载失败"
|
||||
# 选择站点:BOLT_GIT_HOST 优先,否则先试 IPv4 站点再试 IPv6 站点
|
||||
HOST=""
|
||||
IPFLAG=""
|
||||
if [ -n "${BOLT_GIT_HOST:-}" ]; then
|
||||
HOST="${BOLT_GIT_HOST%/}"
|
||||
else
|
||||
die "需要 curl 或 wget"
|
||||
for cand in "$HOST4 -4" "$HOST6 -6"; do
|
||||
if fetch "${cand% *}/api/v1/version" "$TMP/probe" "${cand#* }" 2>/dev/null; then
|
||||
HOST=${cand% *}
|
||||
IPFLAG=${cand#* }
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ -n "$HOST" ] || die "无法连接 $HOST4(IPv4)或 $HOST6(IPv6),可用 BOLT_GIT_HOST 指定站点"
|
||||
fi
|
||||
say "下载站点: $HOST"
|
||||
if [ -z "$VERSION" ]; then
|
||||
fetch "$HOST/api/v1/repos/$REPO/releases/latest" "$TMP/latest.json" "$IPFLAG" || die "查询最新版本失败"
|
||||
VERSION=$(sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' "$TMP/latest.json")
|
||||
[ -n "$VERSION" ] || die "无法解析最新版本号"
|
||||
fi
|
||||
BASE="$HOST/$REPO/releases/download/$VERSION"
|
||||
say "从 Release 下载: $BASE/bolt-$PLATFORM-$ARCH"
|
||||
fetch "$BASE/bolt-$PLATFORM-$ARCH" "$TMP/bolt" "$IPFLAG" || die "下载失败,请确认 Release $VERSION 有 bolt-$PLATFORM-$ARCH 附件"
|
||||
# 有 SHA256SUMS 附件时校验
|
||||
if fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" "$IPFLAG" 2>/dev/null; then
|
||||
want=$(awk -v f="bolt-$PLATFORM-$ARCH" '$2 == f || $2 == "*" f {print $1}' "$TMP/SHA256SUMS")
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
got=$(sha256sum "$TMP/bolt" | awk '{print $1}')
|
||||
else
|
||||
got=$(shasum -a 256 "$TMP/bolt" | awk '{print $1}')
|
||||
fi
|
||||
[ -n "$want" ] && [ "$want" = "$got" ] || die "SHA-256 校验失败(期望 $want,实际 $got)"
|
||||
say "SHA-256 校验通过"
|
||||
fi
|
||||
install -m 0755 /tmp/bolt-dl.$$ "$BIN_PATH" && rm -f /tmp/bolt-dl.$$
|
||||
fi
|
||||
"$BIN_PATH" --help >/dev/null 2>&1 || die "二进制不可执行: $BIN_PATH"
|
||||
chmod 0755 "$TMP/bolt"
|
||||
if [ "$PLATFORM" = darwin ]; then
|
||||
# Apple Silicon 要求可执行文件带签名;没有签名时补 ad-hoc 签名
|
||||
codesign -v "$TMP/bolt" 2>/dev/null || codesign -s - -f "$TMP/bolt" >/dev/null 2>&1 || true
|
||||
fi
|
||||
"$TMP/bolt" --help >/dev/null 2>&1 || die "二进制无法在本机运行"
|
||||
# 先写临时文件再 rename,替换正在运行的旧版本也安全
|
||||
cp "$TMP/bolt" "$BIN_PATH.new"
|
||||
chmod 0755 "$BIN_PATH.new"
|
||||
mv -f "$BIN_PATH.new" "$BIN_PATH"
|
||||
say "二进制就绪: $BIN_PATH ($("$BIN_PATH" --help | head -1))"
|
||||
|
||||
# macOS 将环境配置直接写入 plist,不通过 shell 解释令牌或命令规则。
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/bin/sh
|
||||
# bolt 卸载脚本(POSIX sh,需 root;Linux 使用 systemd,macOS 使用 launchd)
|
||||
#
|
||||
# 用法(在目标设备上):
|
||||
# IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh
|
||||
# IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/uninstall.sh | sudo sh
|
||||
# 保留配置: ... | sudo sh -s -- --keep-config
|
||||
#
|
||||
# 选项:
|
||||
# --keep-config 保留配置(Linux: /etc/bolt.env;macOS: /Library/Application Support/bolt 与 /var/log/bolt)
|
||||
#
|
||||
# 注意:如果是通过 bolt 自己执行本脚本,最后停止服务时连接会断开,这是正常现象。
|
||||
set -eu
|
||||
|
||||
BIN_PATH=/usr/local/bin/bolt
|
||||
LABEL=io.jocay.bolt
|
||||
KEEP_CONFIG=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--keep-config) KEEP_CONFIG=1; shift ;;
|
||||
*) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '\033[1;32m[bolt]\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[1;31m[bolt] 错误:\033[0m %s\n' "$*"; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "需要 root 权限(请用 sudo 运行)"
|
||||
|
||||
# 二进制及更新脚本留下的备份(正在运行的进程不受影响,最后再停服务)
|
||||
rm_bins() {
|
||||
for f in "$BIN_PATH" "$BIN_PATH.bak" "$BIN_PATH.new" "$BIN_PATH.rollback" "$BIN_PATH".v*.bak; do
|
||||
if [ -e "$f" ]; then
|
||||
rm -f "$f"
|
||||
say "已删除 $f"
|
||||
fi
|
||||
done
|
||||
rm -rf /tmp/bolt-update.* /tmp/bolt-upgrade 2>/dev/null || true
|
||||
}
|
||||
|
||||
case "$(uname -s)" in
|
||||
Linux)
|
||||
UNIT=/etc/systemd/system/bolt.service
|
||||
HAS_SYSTEMD=0
|
||||
command -v systemctl >/dev/null 2>&1 && HAS_SYSTEMD=1
|
||||
if [ "$HAS_SYSTEMD" -eq 1 ]; then
|
||||
systemctl disable bolt >/dev/null 2>&1 && say "已取消开机自启" || true
|
||||
fi
|
||||
rm_bins
|
||||
if [ -e "$UNIT" ] || [ -d "$UNIT.d" ]; then
|
||||
rm -rf "$UNIT" "$UNIT.d"
|
||||
say "已删除服务单元 $UNIT"
|
||||
fi
|
||||
if [ "$KEEP_CONFIG" -eq 0 ]; then
|
||||
if [ -e /etc/bolt.env ]; then
|
||||
rm -f /etc/bolt.env
|
||||
say "已删除配置 /etc/bolt.env"
|
||||
fi
|
||||
else
|
||||
[ -e /etc/bolt.env ] && say "保留配置 /etc/bolt.env"
|
||||
fi
|
||||
say "卸载完成,正在停止服务…"
|
||||
if [ "$HAS_SYSTEMD" -eq 1 ]; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl stop bolt >/dev/null 2>&1 || true
|
||||
systemctl reset-failed bolt >/dev/null 2>&1 || true
|
||||
fi
|
||||
pkill -x bolt 2>/dev/null || true
|
||||
;;
|
||||
Darwin)
|
||||
AUTO_PLIST="/Library/LaunchDaemons/$LABEL.plist"
|
||||
PLIST_DIR="/Library/Application Support/bolt"
|
||||
rm_bins
|
||||
if [ -e "$AUTO_PLIST" ]; then
|
||||
rm -f "$AUTO_PLIST"
|
||||
say "已删除开机配置 $AUTO_PLIST"
|
||||
fi
|
||||
if [ "$KEEP_CONFIG" -eq 0 ]; then
|
||||
for d in "$PLIST_DIR" /var/log/bolt; do
|
||||
if [ -e "$d" ]; then
|
||||
rm -rf "$d"
|
||||
say "已删除 $d"
|
||||
fi
|
||||
done
|
||||
else
|
||||
say "保留配置 $PLIST_DIR 与日志 /var/log/bolt"
|
||||
fi
|
||||
say "卸载完成,正在停止服务…"
|
||||
launchctl bootout "system/$LABEL" 2>/dev/null || true
|
||||
launchctl disable "system/$LABEL" 2>/dev/null || true
|
||||
;;
|
||||
*) die "不支持的操作系统: $(uname -s)" ;;
|
||||
esac
|
||||
@@ -0,0 +1,263 @@
|
||||
#!/bin/sh
|
||||
# bolt 在线更新脚本(POSIX sh,需 root;Linux 使用 systemd,macOS 使用 launchd)
|
||||
#
|
||||
# 只替换二进制并重启服务,保留现有配置(/etc/bolt.env 或 launchd plist);
|
||||
# 新版本启动失败时自动回滚到旧版本。
|
||||
#
|
||||
# 用法(在目标设备上):
|
||||
# IPv4: curl -fsSL https://git.jocayn.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh
|
||||
# IPv6: curl -fsSL https://git.draws.qzz.io/jocay/bolt/raw/branch/main/deploy/update.sh | sudo sh
|
||||
# 传参: ... | sudo sh -s -- --version v0.2.0
|
||||
#
|
||||
# 选项:
|
||||
# --version <tag> 安装指定版本(默认 Release 上的最新版本)
|
||||
# --binary <src> 使用本地二进制或 http(s) URL,跳过 Release 下载
|
||||
# --force 版本相同或更旧也照样安装
|
||||
#
|
||||
# 环境变量:
|
||||
# BOLT_GIT_HOST 指定下载站点(如 https://git.jocayn.qzz.io),默认先试 IPv4 站点再试 IPv6 站点
|
||||
# BOLT_DL_TOKEN 仓库为私有时的下载令牌
|
||||
set -eu
|
||||
|
||||
REPO=jocay/bolt
|
||||
HOST4=https://git.jocayn.qzz.io
|
||||
HOST6=https://git.draws.qzz.io
|
||||
BIN_PATH=/usr/local/bin/bolt
|
||||
LABEL=io.jocay.bolt
|
||||
|
||||
VERSION="${BOLT_VERSION:-}"
|
||||
BINARY_SRC=""
|
||||
FORCE=0
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) VERSION="${2:-}"; shift 2 ;;
|
||||
--binary) BINARY_SRC="${2:-}"; shift 2 ;;
|
||||
--force) FORCE=1; shift ;;
|
||||
*) echo "未知参数: $1"; echo "用法见脚本头部注释"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
say() { printf '\033[1;32m[bolt]\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m[bolt]\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[1;31m[bolt] 错误:\033[0m %s\n' "$*"; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "需要 root 权限(请用 sudo 运行)"
|
||||
[ -x "$BIN_PATH" ] || die "未找到已安装的 $BIN_PATH,请先用 install.sh 安装"
|
||||
|
||||
# ---------- 平台 ----------
|
||||
OS=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
case "$OS" in
|
||||
Linux) PLATFORM=linux ;;
|
||||
Darwin)
|
||||
PLATFORM=darwin
|
||||
# Rosetta 下 uname 可能返回 x86_64,优先识别实际的 Apple Silicon 硬件
|
||||
if [ "$(sysctl -n hw.optional.arm64 2>/dev/null || true)" = 1 ]; then
|
||||
ARCH=arm64
|
||||
fi
|
||||
;;
|
||||
*) die "不支持的操作系统: $OS" ;;
|
||||
esac
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) ARCH=amd64 ;;
|
||||
aarch64|arm64) ARCH=arm64 ;;
|
||||
*) die "不支持的架构: $ARCH" ;;
|
||||
esac
|
||||
|
||||
# ---------- 下载工具 ----------
|
||||
# fetch <url> <输出文件> [-4|-6]
|
||||
fetch() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
curl -fsSL --retry 3 ${3:-} -H "Authorization: token $BOLT_DL_TOKEN" -o "$2" "$1"
|
||||
else
|
||||
curl -fsSL --retry 3 ${3:-} -o "$2" "$1"
|
||||
fi
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
if [ -n "${BOLT_DL_TOKEN:-}" ]; then
|
||||
wget -q ${3:-} --header="Authorization: token $BOLT_DL_TOKEN" -O "$2" "$1"
|
||||
else
|
||||
wget -q ${3:-} -O "$2" "$1"
|
||||
fi
|
||||
else
|
||||
die "需要 curl 或 wget"
|
||||
fi
|
||||
}
|
||||
|
||||
# 探测可用站点:先 IPv4 站点,再 IPv6 站点
|
||||
IPFLAG=""
|
||||
pick_host() {
|
||||
if [ -n "${BOLT_GIT_HOST:-}" ]; then
|
||||
HOST="${BOLT_GIT_HOST%/}"
|
||||
return
|
||||
fi
|
||||
for cand in "$HOST4 -4" "$HOST6 -6"; do
|
||||
h=${cand% *}
|
||||
f=${cand#* }
|
||||
if fetch "$h/api/v1/version" "$TMP/probe" "$f" 2>/dev/null; then
|
||||
HOST=$h
|
||||
IPFLAG=$f
|
||||
return
|
||||
fi
|
||||
done
|
||||
die "无法连接 $HOST4(IPv4)或 $HOST6(IPv6),可用 BOLT_GIT_HOST 指定站点"
|
||||
}
|
||||
|
||||
# ver_cmp a b:输出 1 / 0 / -1
|
||||
ver_cmp() {
|
||||
awk -v a="$1" -v b="$2" 'BEGIN {
|
||||
n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m
|
||||
for (i = 1; i <= k; i++) {
|
||||
if (x[i] + 0 > y[i] + 0) { print 1; exit }
|
||||
if (x[i] + 0 < y[i] + 0) { print -1; exit }
|
||||
}
|
||||
print 0
|
||||
}'
|
||||
}
|
||||
|
||||
bin_version() { "$1" --help 2>/dev/null | head -1 | awk '{print $2}'; }
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
CUR=$(bin_version "$BIN_PATH")
|
||||
[ -n "$CUR" ] || CUR=unknown
|
||||
say "当前版本: $CUR 平台: $PLATFORM/$ARCH"
|
||||
|
||||
# ---------- 1. 获取新二进制 ----------
|
||||
NEW_BIN="$TMP/bolt"
|
||||
if [ -n "$BINARY_SRC" ]; then
|
||||
if [ -f "$BINARY_SRC" ]; then
|
||||
cp "$BINARY_SRC" "$NEW_BIN"
|
||||
elif echo "$BINARY_SRC" | grep -q '^https\?://'; then
|
||||
say "下载: $BINARY_SRC"
|
||||
fetch "$BINARY_SRC" "$NEW_BIN" || die "下载失败"
|
||||
else
|
||||
die "二进制来源不存在: $BINARY_SRC"
|
||||
fi
|
||||
else
|
||||
pick_host
|
||||
say "下载站点: $HOST"
|
||||
if [ -z "$VERSION" ]; then
|
||||
fetch "$HOST/api/v1/repos/$REPO/releases/latest" "$TMP/latest.json" "$IPFLAG" || die "查询最新版本失败"
|
||||
VERSION=$(sed -n 's/.*"tag_name":"\([^"]*\)".*/\1/p' "$TMP/latest.json")
|
||||
[ -n "$VERSION" ] || die "无法解析最新版本号"
|
||||
EXPLICIT=0
|
||||
else
|
||||
EXPLICIT=1
|
||||
fi
|
||||
TARGET=${VERSION#v}
|
||||
if [ "$FORCE" -eq 0 ] && [ "$CUR" != unknown ]; then
|
||||
c=$(ver_cmp "$TARGET" "$CUR")
|
||||
if [ "$c" = 0 ]; then
|
||||
say "已是最新版本 $CUR,无需更新(--force 可强制重装)"
|
||||
exit 0
|
||||
fi
|
||||
if [ "$c" = -1 ] && [ "$EXPLICIT" -eq 0 ]; then
|
||||
say "Release 上最新的是 $VERSION,比已安装的 $CUR 旧,不降级(需要降级请用 --version 指定)"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
BASE="$HOST/$REPO/releases/download/$VERSION"
|
||||
say "下载: $BASE/bolt-$PLATFORM-$ARCH"
|
||||
fetch "$BASE/bolt-$PLATFORM-$ARCH" "$NEW_BIN" "$IPFLAG" || die "下载失败,请确认 Release $VERSION 有 bolt-$PLATFORM-$ARCH 附件"
|
||||
# 有 SHA256SUMS 附件时校验
|
||||
if fetch "$BASE/SHA256SUMS" "$TMP/SHA256SUMS" "$IPFLAG" 2>/dev/null; then
|
||||
want=$(awk -v f="bolt-$PLATFORM-$ARCH" '$2 == f || $2 == "*" f {print $1}' "$TMP/SHA256SUMS")
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
got=$(sha256sum "$NEW_BIN" | awk '{print $1}')
|
||||
else
|
||||
got=$(shasum -a 256 "$NEW_BIN" | awk '{print $1}')
|
||||
fi
|
||||
[ -n "$want" ] && [ "$want" = "$got" ] || die "SHA-256 校验失败(期望 $want,实际 $got)"
|
||||
say "SHA-256 校验通过"
|
||||
fi
|
||||
fi
|
||||
chmod 0755 "$NEW_BIN"
|
||||
if [ "$PLATFORM" = darwin ]; then
|
||||
# Apple Silicon 要求可执行文件带签名;没有签名时补 ad-hoc 签名
|
||||
codesign -v "$NEW_BIN" 2>/dev/null || codesign -s - -f "$NEW_BIN" >/dev/null 2>&1 || true
|
||||
fi
|
||||
NEW_VER=$(bin_version "$NEW_BIN")
|
||||
[ -n "$NEW_VER" ] || die "新二进制无法在本机运行"
|
||||
if [ "$FORCE" -eq 0 ] && [ "$NEW_VER" = "$CUR" ]; then
|
||||
say "已是版本 $CUR,无需更新(--force 可强制重装)"
|
||||
exit 0
|
||||
fi
|
||||
say "新版本: $NEW_VER"
|
||||
|
||||
# ---------- 2. 备份并原子替换 ----------
|
||||
cp -p "$BIN_PATH" "$BIN_PATH.bak"
|
||||
cp "$NEW_BIN" "$BIN_PATH.new"
|
||||
chmod 0755 "$BIN_PATH.new"
|
||||
mv -f "$BIN_PATH.new" "$BIN_PATH"
|
||||
say "已替换 $BIN_PATH(旧版本备份为 $BIN_PATH.bak)"
|
||||
|
||||
# ---------- 3. 重启服务(独立进程执行,失败自动回滚) ----------
|
||||
if [ "$PLATFORM" = linux ] && command -v systemctl >/dev/null 2>&1 && systemctl cat bolt >/dev/null 2>&1; then
|
||||
RESTART="systemctl restart bolt"
|
||||
MAINPID="systemctl show -p MainPID --value bolt"
|
||||
ACTIVE="systemctl is-active --quiet bolt"
|
||||
elif [ "$PLATFORM" = darwin ] && launchctl print "system/$LABEL" >/dev/null 2>&1; then
|
||||
RESTART="launchctl kickstart -k system/$LABEL"
|
||||
MAINPID="launchctl print system/$LABEL | awk '/^[[:space:]]*pid = /{print \$3; exit}'"
|
||||
ACTIVE="launchctl print system/$LABEL | grep -q 'state = running'"
|
||||
else
|
||||
say "未发现已注册的 bolt 服务,只替换了二进制(stdio 模式下由 MCP 客户端重新拉起即可)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 重启必须由独立进程执行:如果本脚本是通过 bolt 自己运行的,重启 bolt 会连带结束本脚本
|
||||
WORK=$(mktemp -d /tmp/bolt-update.XXXXXX)
|
||||
STATUS="$WORK/status"
|
||||
cat > "$WORK/restart.sh" <<EOF
|
||||
#!/bin/sh
|
||||
healthy() {
|
||||
$ACTIVE || return 1
|
||||
p1=\$($MAINPID)
|
||||
sleep 3
|
||||
$ACTIVE || return 1
|
||||
p2=\$($MAINPID)
|
||||
[ -n "\$p1" ] && [ "\$p1" != 0 ] && [ "\$p1" = "\$p2" ]
|
||||
}
|
||||
sleep 1
|
||||
$RESTART
|
||||
sleep 2
|
||||
if healthy; then
|
||||
echo ok > "$STATUS"
|
||||
else
|
||||
cp -p "$BIN_PATH.bak" "$BIN_PATH.rollback" && mv -f "$BIN_PATH.rollback" "$BIN_PATH"
|
||||
$RESTART
|
||||
echo rolled-back > "$STATUS"
|
||||
fi
|
||||
EOF
|
||||
if [ "$PLATFORM" = linux ] && command -v systemd-run >/dev/null 2>&1; then
|
||||
# 临时服务不在 bolt 的 cgroup 里,停止 bolt 时不会被一起杀掉
|
||||
systemd-run --quiet --collect --unit="bolt-update-$$" /bin/sh "$WORK/restart.sh"
|
||||
else
|
||||
(trap '' HUP; sh "$WORK/restart.sh" </dev/null >/dev/null 2>&1 &)
|
||||
fi
|
||||
say "正在重启服务…"
|
||||
|
||||
i=0
|
||||
while [ $i -lt 30 ]; do
|
||||
if [ -f "$STATUS" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
case "$(cat "$STATUS" 2>/dev/null || echo timeout)" in
|
||||
ok)
|
||||
say "更新完成: $CUR -> $(bin_version "$BIN_PATH"),服务运行正常"
|
||||
rm -rf "$WORK"
|
||||
;;
|
||||
rolled-back)
|
||||
rm -rf "$WORK"
|
||||
die "新版本服务启动失败,已回滚到 $CUR。查看日志: $([ "$PLATFORM" = linux ] && echo 'journalctl -u bolt -n 50' || echo '/var/log/bolt/stderr.log')"
|
||||
;;
|
||||
*)
|
||||
warn "等待超时,重启结果写在 $STATUS(ok / rolled-back)"
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user