feat(server,web,desktop,shared,deploy,docs): Linux 一键安装与 classhelper 运维命令、更新检查、图表去 echarts,版本统一 1.1.0
Linux 交付链(新增)
- deploy/install.sh:一键安装器(CLASSHELPER 大字 TUI、native/docker 双形态、向导选安装目录/
端口/数据库/初始管理员密码);识别 apt/yum/dnf 只补缺的依赖、建独立系统用户、下载内置 Node
运行时(glibc < 2.28 的 CentOS 7 自动改用 unofficial-builds 的 glibc-217 构建)、幂等覆盖安装
(保留 config.env 与 data/,不重置密钥)、全程写 /var/log/classhelper/install.log;
`--check` 只体检(不需要 root)、`--yes` 无人值守、`--package` 支持内网离线分发。
- deploy/classhelper:运维命令 —— status / doctor / password / backup(list,restore,schedule) /
upgrade / rollback / start|stop|restart|reload / logs / config / port / key rotate / uninstall /
completion / help。特权操作一律校验 EUID,关键动作写 /var/log/classhelper/audit.log。
- deploy/tools/admin-cli.mjs:离线账号工具(改密、列账号、VACUUM INTO 一致性快照、哈希回滚),
直连数据库 —— 服务停机也能救急,不需要先有一个能登录的账号。
- deploy/{classhelper.service,profile.d,logrotate.d,systemd}/ 模板 + Dockerfile.linux-package。
- scripts/dist-server.mjs 增加 --platform linux(根脚本 `pnpm dist:server:linux`),产物
classhelper-server-linux-x64-<版本>.tar.gz + 同名 .sha256;新增 GitHub Actions workflow 在
ubuntu runner 上构建并挂 Release(@libsql/linux-* 这类平台相关依赖只能在 Linux 上装)。
- docs/linux-deploy.md:安装 / 运维命令 / 升级回滚 / 备份恢复 / 排障 / 安全清单 /
Ubuntu 22.04 与 Rocky 9 的验收清单。deploy/install-linux.sh 改为转发壳,不留两份实现。
三处必须知道的取舍(已写进 AGENTS.md §5 第 59 条)
- 配置真身在 /etc/classhelper/config.env,安装目录里的 .env 是指向它的软链:Web 端
「数据库管理 → 一键切换」改写的是 <安装目录>/.env,而 systemd 注入的是同一份配置 ——
两处不是同一个文件时,切换会「看起来成功、重启后又连回旧库」。classhelper doctor 有专门断言。
- 升级/回滚替换程序文件用「改名 + 就位」而不是 rm+cp:升级时正在运行的就是 bin/classhelper
自己,直接删掉同名文件会让 bash 读到半个脚本。
- 交叉构建(在非 Linux 机器上打 Linux 包)会在包内写 .cross-built 标记、doctor 据此提示:
已核对 @libsql/linux-x64-gnu 与 -musl 就位且无 win32 残留,但 prisma CLI 的原生 schema engine
是宿主平台的,Web 端「数据库一键切换」那一步存疑;正式产物一律走 CI。
更新检查(此前只在工作区,本次一并提交)
- 服务端 modules/update:由服务端转发 GitHub Releases(Web 端 CSP 不允许直连),离线/限流/超时
一律收敛成 ok:false + 一句人话,失败结果同样进缓存;?force=1 只对管理员生效(避免刷爆匿名限流)。
- 版本比较放 @classhelper/shared(compareVersions / isNewerVersion),三端共用;服务端版本号
改由 lib/version.ts 读 package.json,不再在 app.ts 里写字面量。
- Web 端顶栏「检查更新」弹窗、客户端「关于」页的检查更新与诊断信息。
Web 端 / 客户端
- 成绩图表改用自包含的轻量组件(ScoreBarChart / ScoreLineChart),不再全量引入 echarts
(原先 echarts 占桌面端渲染产物 1.12MB、Web 端 1.13MB);柱状图是纯 HTML/CSS,折线图是
viewBox SVG,均无 resize 监听。
- Element Plus 图标改为按需注册(两端各一份 icons.ts),新增 scripts/check-icons.mjs 静态门禁
并挂进两端 build —— 漏注册的后果是界面静默渲染空白,只在控制台一条 warning。
版本号统一 1.1.0
- 5 份 package.json、deploy/package.runtime.json、.env.example 的 APP_VERSION、
docker-compose 与两个 Dockerfile 的镜像标签、Linux 安装包名示例、README 首页版本;
- ClassIsland 插件 manifest.yml 与 BridgeService.PluginVersion 同步(verify:classisland-plugin
有 manifest ↔ code 的一致性断言);
- 刻意保持不变的是**历史性表述与举例**:docs/mysql.md 的「自 v1.0.0 起内置 MySQL 适配器」、
shared/utils.ts 讲版本比较用的 1.0.0 / 1.0.0-fix、README 验收表里的实测记录。
This commit is contained in:
1 parent
aad22df4fd
commit
9a38f86cd3
77 files changed
+8021
-899
No files matched your search
@@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env bash
|
||||
# ---------------------------------------------------------------------------
|
||||
# 班级小助手 · Linux 安装与运维链路验收(在**已安装的服务器上**运行)
|
||||
#
|
||||
# 覆盖:安装结果 → 服务可用性 → API 登录 → 改密(含回滚与审计)→ 备份 →
|
||||
# 配置读写 → key rotate →(可选)升级 / 回滚 / 卸载
|
||||
#
|
||||
# 用法(在服务器上,需要 root):
|
||||
# sudo bash verify-linux.sh # 基础链路
|
||||
# sudo bash verify-linux.sh --full --package /tmp/xxx.tar.gz # 追加升级/回滚
|
||||
# sudo bash verify-linux.sh --admin-password-stdin # 密码从 stdin 读一行(不进 argv/history)
|
||||
#
|
||||
# 说明:
|
||||
# - 本脚本会**真的**改一次管理员密码(换成随机强密码,再改回去),并真的发一条备份;
|
||||
# - 除了 --purge 的卸载测试,不会删除任何数据;
|
||||
# - 全程不需要外网(升级测试用 --package 指向本地包)。
|
||||
# ---------------------------------------------------------------------------
|
||||
set -uo pipefail
|
||||
|
||||
INSTALL_DIR="/opt/classhelper"
|
||||
CONFIG_FILE="/etc/classhelper/config.env"
|
||||
LOG_DIR="/var/log/classhelper"
|
||||
BACKUP_DIR="/var/backups/classhelper"
|
||||
PORT=""
|
||||
ADMIN_USER="admin"
|
||||
ADMIN_PASSWORD=""
|
||||
ADMIN_PASSWORD_FROM_STDIN=0
|
||||
DO_FULL=0
|
||||
PACKAGE=""
|
||||
|
||||
TOTAL=0; PASS=0; FAIL=0
|
||||
FAILED=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dir) INSTALL_DIR="$2"; shift 2 ;;
|
||||
--config) CONFIG_FILE="$2"; shift 2 ;;
|
||||
--port) PORT="$2"; shift 2 ;;
|
||||
--user) ADMIN_USER="$2"; shift 2 ;;
|
||||
--admin-password) ADMIN_PASSWORD="$2"; shift 2 ;;
|
||||
--admin-password-stdin) ADMIN_PASSWORD_FROM_STDIN=1; shift ;;
|
||||
--package) PACKAGE="$2"; shift 2 ;;
|
||||
--full) DO_FULL=1; shift ;;
|
||||
-h|--help) sed -n '2,18p' "$0"; exit 0 ;;
|
||||
*) echo "未知参数:$1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -t 1 ]; then G=$'\033[32m'; R=$'\033[31m'; Y=$'\033[33m'; D=$'\033[2m'; B=$'\033[1m'; N=$'\033[0m'; else G=; R=; Y=; D=; B=; N=; fi
|
||||
|
||||
section() { printf '\n%s▶ %s%s\n' "$B" "$1" "$N"; }
|
||||
check() { # $1=描述 $2=0 通过
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if [ "$2" = "0" ]; then
|
||||
PASS=$((PASS + 1)); printf ' %s✔%s %s\n' "$G" "$N" "$1"
|
||||
else
|
||||
FAIL=$((FAIL + 1)); FAILED+=("$1"); printf ' %s✘%s %s\n' "$R" "$N" "$1"
|
||||
fi
|
||||
}
|
||||
note() { printf ' %s· %s%s\n' "$D" "$1" "$N"; }
|
||||
|
||||
[ "$(id -u)" = "0" ] || { echo "请用 root 运行:sudo bash verify-linux.sh" >&2; exit 1; }
|
||||
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "找不到安装:${CONFIG_FILE} 不存在(用 --config 指定,或先跑 install.sh)" >&2
|
||||
exit 1
|
||||
fi
|
||||
read_cfg() { sed -n "s/^[[:space:]]*$1=//p" "$CONFIG_FILE" | tail -n 1 | tr -d '"'; }
|
||||
[ -n "$PORT" ] || PORT="$(read_cfg PORT)"
|
||||
[ -n "$PORT" ] || PORT=4000
|
||||
CLASSHELPER="${INSTALL_DIR}/bin/classhelper"
|
||||
API="http://127.0.0.1:${PORT}"
|
||||
CURL=(curl -fsS --noproxy '*')
|
||||
|
||||
if [ "$ADMIN_PASSWORD_FROM_STDIN" = "1" ]; then IFS= read -r ADMIN_PASSWORD || true; fi
|
||||
|
||||
printf '%s班级小助手 · Linux 安装验收%s\n' "$B" "$N"
|
||||
note "安装目录 ${INSTALL_DIR}|配置 ${CONFIG_FILE}|端口 ${PORT}"
|
||||
|
||||
# ---------------------------------------------------------------- 1. 安装结果
|
||||
|
||||
section "安装结果"
|
||||
check "安装目录存在且含 server/dist/index.js" "$([ -f "${INSTALL_DIR}/server/dist/index.js" ] && echo 0 || echo 1)"
|
||||
check "版本文件 VERSION 可读" "$([ -s "${INSTALL_DIR}/VERSION" ] && echo 0 || echo 1)"
|
||||
check "安装元数据 .installed.json 存在" "$([ -f "${INSTALL_DIR}/.installed.json" ] && echo 0 || echo 1)"
|
||||
|
||||
cfg_mode="$(stat -c '%a' "$CONFIG_FILE" 2>/dev/null || echo '?')"
|
||||
check "配置文件权限 600(实际 ${cfg_mode})" "$([ "$cfg_mode" = "600" ] && echo 0 || echo 1)"
|
||||
dir_mode="$(stat -c '%a' "$(dirname "$CONFIG_FILE")" 2>/dev/null || echo '?')"
|
||||
check "配置目录权限 700(实际 ${dir_mode})" "$([ "$dir_mode" = "700" ] && echo 0 || echo 1)"
|
||||
|
||||
env_link="${INSTALL_DIR}/.env"
|
||||
check "安装目录 .env 是指向配置真身的软链(Web 端切库依赖它)" \
|
||||
"$([ -L "$env_link" ] && [ "$(readlink "$env_link")" = "$CONFIG_FILE" ] && echo 0 || echo 1)"
|
||||
|
||||
node_bin="${INSTALL_DIR}/runtime/node/bin/node"
|
||||
node_ver="$([ -x "$node_bin" ] && "$node_bin" -v 2>/dev/null || echo '')"
|
||||
check "内置 Node 可用(${node_ver:-未找到})" "$([ -n "$node_ver" ] && echo 0 || echo 1)"
|
||||
|
||||
check "classhelper 命令已装到 /usr/local/bin" "$([ -x /usr/local/bin/classhelper ] && echo 0 || echo 1)"
|
||||
check "安装日志 ${LOG_DIR}/install.log 存在" "$([ -s "${LOG_DIR}/install.log" ] && echo 0 || echo 1)"
|
||||
check "审计日志 ${LOG_DIR}/audit.log 存在" "$([ -f "${LOG_DIR}/audit.log" ] && echo 0 || echo 1)"
|
||||
check "审计/安装日志权限不放行他人(≤640)" \
|
||||
"$([ "$(stat -c '%a' "${LOG_DIR}/audit.log" 2>/dev/null || echo 644)" -le 640 ] && echo 0 || echo 1)"
|
||||
check "/etc/profile.d/classhelper.sh 已写入" "$([ -f /etc/profile.d/classhelper.sh ] && echo 0 || echo 1)"
|
||||
profile_home="$(bash -lc 'echo "${CLASSHELPER_HOME:-}"' 2>/dev/null)"
|
||||
check "新登录 shell 里 CLASSHELPER_HOME 生效(${profile_home:-空})" "$([ -n "$profile_home" ] && echo 0 || echo 1)"
|
||||
|
||||
# ---------------------------------------------------------------- 2. 服务
|
||||
|
||||
section "服务与探针"
|
||||
if command -v systemctl >/dev/null 2>&1 && [ -f /etc/systemd/system/classhelper.service ]; then
|
||||
check "systemd 单元已安装" 0
|
||||
check "服务处于 active" "$(systemctl is-active --quiet classhelper && echo 0 || echo 1)"
|
||||
check "服务开机自启(enabled)" "$(systemctl is-enabled --quiet classhelper 2>/dev/null && echo 0 || echo 1)"
|
||||
else
|
||||
check "systemd 单元已安装" 1
|
||||
fi
|
||||
check "/healthz 返回 200" "$("${CURL[@]}" -o /dev/null "${API}/healthz" 2>/dev/null && echo 0 || echo 1)"
|
||||
|
||||
status_json="$("$CLASSHELPER" status --json 2>/dev/null || true)"
|
||||
check "classhelper status --json 是合法 JSON" "$(printf '%s' "$status_json" | grep -q '"healthy"' && echo 0 || echo 1)"
|
||||
check "status 报告 healthy=yes" "$(printf '%s' "$status_json" | grep -q '"healthy": "yes"' && echo 0 || echo 1)"
|
||||
check "status 报告的端口与实际一致" "$(printf '%s' "$status_json" | grep -q "\"port\": ${PORT}" && echo 0 || echo 1)"
|
||||
check "classhelper help 可执行(非 root 也能看)" "$("$CLASSHELPER" help >/dev/null 2>&1 && echo 0 || echo 1)"
|
||||
check "classhelper completion bash 有输出" "$("$CLASSHELPER" completion bash 2>/dev/null | grep -q 'classhelper' && echo 0 || echo 1)"
|
||||
|
||||
doctor_out="$("$CLASSHELPER" doctor 2>&1 || true)"
|
||||
check "classhelper doctor 无待处理项" "$(printf '%s' "$doctor_out" | grep -q '体检通过' && echo 0 || echo 1)"
|
||||
if ! printf '%s' "$doctor_out" | grep -q '体检通过'; then
|
||||
printf '%s' "$doctor_out" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 3. API
|
||||
|
||||
section "API 与登录"
|
||||
TOKEN=""
|
||||
login() { # $1=密码 → 输出 token(失败则空)
|
||||
"${CURL[@]}" -X POST "${API}/api/auth/login" -H 'content-type: application/json' \
|
||||
-d "{\"username\":\"${ADMIN_USER}\",\"password\":\"$1\"}" 2>/dev/null \
|
||||
| sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'
|
||||
}
|
||||
|
||||
if [ -z "$ADMIN_PASSWORD" ]; then
|
||||
note "未提供 --admin-password(-stdin),跳过登录与改密用例"
|
||||
else
|
||||
TOKEN="$(login "$ADMIN_PASSWORD")"
|
||||
check "管理员用提供的密码能登录并拿到 token" "$([ -n "$TOKEN" ] && echo 0 || echo 1)"
|
||||
if [ -n "$TOKEN" ]; then
|
||||
classes="$("${CURL[@]}" -H "Authorization: Bearer ${TOKEN}" "${API}/api/classes" 2>/dev/null || true)"
|
||||
check "带 token 能读业务接口 /api/classes" "$(printf '%s' "$classes" | grep -q '"data"' && echo 0 || echo 1)"
|
||||
me="$("${CURL[@]}" -H "Authorization: Bearer ${TOKEN}" "${API}/api/auth/me" 2>/dev/null || true)"
|
||||
check "/api/auth/me 返回的是 ADMIN" "$(printf '%s' "$me" | grep -q '"ADMIN"' && echo 0 || echo 1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 4. 改密
|
||||
|
||||
if [ -n "$TOKEN" ]; then
|
||||
section "password 子命令(改密 → 回滚 → 审计)"
|
||||
# 生成一个一定满足强度要求的密码(大小写+数字+符号,且不落在常见口令里)
|
||||
GENERATED="Ch$(head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n')!a9Z"
|
||||
before_count="$(grep -c 'action=password' "${LOG_DIR}/audit.log" 2>/dev/null || echo 0)"
|
||||
|
||||
# 密码经管道进 stdin,不出现在 argv / 环境变量 / shell history
|
||||
out="$(printf '%s\n%s\n' "$GENERATED" "$GENERATED" | "$CLASSHELPER" password "$ADMIN_USER" 2>&1)"
|
||||
check "classhelper password 执行成功" "$(printf '%s' "$out" | grep -q '密码已更新' && echo 0 || echo 1)"
|
||||
|
||||
new_token="$(login "$GENERATED")"
|
||||
check "新密码能登录" "$([ -n "$new_token" ] && echo 0 || echo 1)"
|
||||
old_token="$(login "$ADMIN_PASSWORD")"
|
||||
check "旧密码已失效(登录被拒)" "$([ -z "$old_token" ] && echo 0 || echo 1)"
|
||||
|
||||
hash_backup="$(ls -1t "${BACKUP_DIR}"/password-*.json 2>/dev/null | head -n 1 || true)"
|
||||
check "改密前备份了原哈希(${hash_backup:-无})" "$([ -n "$hash_backup" ] && echo 0 || echo 1)"
|
||||
check "哈希备份权限 600" \
|
||||
"$([ -n "$hash_backup" ] && [ "$(stat -c '%a' "$hash_backup")" = "600" ] && echo 0 || echo 1)"
|
||||
|
||||
after_count="$(grep -c 'action=password' "${LOG_DIR}/audit.log" 2>/dev/null || echo 0)"
|
||||
check "审计日志新增了 password 记录" "$([ "${after_count:-0}" -gt "${before_count:-0}" ] && echo 0 || echo 1)"
|
||||
check "审计日志里没有新密码明文" "$(grep -q -- "$GENERATED" "${LOG_DIR}/audit.log" 2>/dev/null && echo 1 || echo 0)"
|
||||
check "审计日志里没有原密码明文" "$(grep -q -- "$ADMIN_PASSWORD" "${LOG_DIR}/audit.log" 2>/dev/null && echo 1 || echo 0)"
|
||||
|
||||
check "弱密码被拒绝(长度/种数不足)" \
|
||||
"$(printf 'abc\nabc\n' | "$CLASSHELPER" password "$ADMIN_USER" 2>&1 | grep -q '不合规' && echo 0 || echo 1)"
|
||||
check "密码不合规时不会卡死(stdin 结束即退出)" \
|
||||
"$(printf 'abc\n' | timeout 20 "$CLASSHELPER" password "$ADMIN_USER" >/dev/null 2>&1; [ $? -ne 124 ] && echo 0 || echo 1)"
|
||||
check "password --list 能列出账号" \
|
||||
"$("$CLASSHELPER" password --list 2>&1 | grep -q "$ADMIN_USER" && echo 0 || echo 1)"
|
||||
|
||||
# 用改密前保存的哈希把账号恢复原样:比再改一次密码更可靠
|
||||
# (原密码若是 admin123 这类弱口令,改回去会被强度校验挡下 —— 那是产品的正确行为)
|
||||
if [ -n "$hash_backup" ]; then
|
||||
restore_hash_out="$(LOG_LEVEL=error DOTENV_CONFIG_QUIET=true "$node_bin" "${INSTALL_DIR}/tools/admin-cli.mjs" restore-hash "$hash_backup" 2>&1 || true)"
|
||||
check "restore-hash 能把原哈希回滚回来" "$(printf '%s' "$restore_hash_out" | grep -q '已回滚密码哈希' && echo 0 || echo 1)"
|
||||
check "原密码重新可用" "$([ -n "$(login "$ADMIN_PASSWORD")" ] && echo 0 || echo 1)"
|
||||
fi
|
||||
else
|
||||
note "跳过改密用例(没有可用的登录会话)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 5. 备份
|
||||
|
||||
section "backup 子命令"
|
||||
backup_out="$("$CLASSHELPER" backup --keep 5 2>&1)"
|
||||
check "classhelper backup 执行成功" "$(printf '%s' "$backup_out" | grep -q '备份完成' && echo 0 || echo 1)"
|
||||
|
||||
latest="$(ls -1dt "${BACKUP_DIR}"/*/ 2>/dev/null | head -n 1)"
|
||||
check "生成了新的备份目录" "$([ -n "$latest" ] && echo 0 || echo 1)"
|
||||
if [ -n "$latest" ]; then
|
||||
check "备份含 config.env" "$([ -f "${latest}config.env" ] && echo 0 || echo 1)"
|
||||
check "备份 meta.json 带版本号" "$(grep -q '"version"' "${latest}meta.json" 2>/dev/null && echo 0 || echo 1)"
|
||||
db_kind="$(read_cfg DATABASE_PROVIDER)"
|
||||
if [ "$db_kind" = "sqlite" ]; then
|
||||
snap="${latest}classhelper.db"
|
||||
check "备份含 SQLite 快照" "$([ -s "$snap" ] && echo 0 || echo 1)"
|
||||
# 一致性快照必须是完整的 SQLite 文件(VACUUM INTO 产物)
|
||||
check "快照是合法 SQLite 文件(文件头正确)" \
|
||||
"$([ -n "$snap" ] && [ "$(head -c 15 "$snap" 2>/dev/null)" = "SQLite format 3" ] && echo 0 || echo 1)"
|
||||
check "快照权限 600" "$([ -n "$snap" ] && [ "$(stat -c '%a' "$snap")" = "600" ] && echo 0 || echo 1)"
|
||||
else
|
||||
note "数据库是 MySQL,跳过 SQLite 快照用例"
|
||||
fi
|
||||
check "备份目录权限 700" "$([ "$(stat -c '%a' "${latest%/}" 2>/dev/null)" = "700" ] && echo 0 || echo 1)"
|
||||
fi
|
||||
check "backup list 能列出备份" "$("$CLASSHELPER" backup list 2>&1 | grep -qE '^ [0-9]{8}-' && echo 0 || echo 1)"
|
||||
|
||||
# ---------------------------------------------------------------- 6. 配置
|
||||
|
||||
section "config 子命令"
|
||||
check "config get PORT 返回当前端口" "$([ "$("$CLASSHELPER" config get PORT)" = "$PORT" ] && echo 0 || echo 1)"
|
||||
masked="$("$CLASSHELPER" config show 2>/dev/null | grep '^JWT_SECRET=')"
|
||||
check "config show 对 JWT_SECRET 打码" "$(printf '%s' "$masked" | grep -q '\*\*\*\*' && echo 0 || echo 1)"
|
||||
check "config show 没打印出真实密钥" \
|
||||
"$(secret="$(read_cfg JWT_SECRET)"; [ -n "$secret" ] && printf '%s' "$masked" | grep -q -- "$secret" && echo 1 || echo 0)"
|
||||
|
||||
# 用一个无害的键做读写往返(不重启服务)
|
||||
printf 'CH_VERIFY_PROBE=1\n' >>"$CONFIG_FILE"
|
||||
probe="$("$CLASSHELPER" config get CH_VERIFY_PROBE 2>/dev/null)"
|
||||
check "config get 能读到新增键" "$([ "$probe" = "1" ] && echo 0 || echo 1)"
|
||||
sed -i '/^CH_VERIFY_PROBE=/d' "$CONFIG_FILE"
|
||||
|
||||
# ---------------------------------------------------------------- 7. key rotate
|
||||
|
||||
if [ -n "$TOKEN" ]; then
|
||||
section "key rotate"
|
||||
if [ "${CLASSHELPER_VERIFY_SKIP_KEY_ROTATE:-0}" = "1" ]; then
|
||||
note "按环境变量跳过"
|
||||
else
|
||||
old_secret="$(read_cfg JWT_SECRET)"
|
||||
# key rotate 会问一次确认;无 tty 时从 stdin 读,因此用管道喂 yes
|
||||
printf 'yes\n' | CLASSHELPER_NO_TTY=1 "$CLASSHELPER" key rotate >/dev/null 2>&1 || true
|
||||
new_secret="$(read_cfg JWT_SECRET)"
|
||||
check "key rotate 改变了 JWT_SECRET" "$([ -n "$new_secret" ] && [ "$new_secret" != "$old_secret" ] && echo 0 || echo 1)"
|
||||
check "新密钥长度 ≥32" "$([ "${#new_secret}" -ge 32 ] && echo 0 || echo 1)"
|
||||
check "轮换后服务仍可用" "$("${CURL[@]}" -o /dev/null "${API}/healthz" && echo 0 || echo 1)"
|
||||
check "轮换前的 token 已失效(/api/auth/me 返回 401)" \
|
||||
"$(code="$(curl -s --noproxy '*' -o /dev/null -w '%{http_code}' -H "Authorization: Bearer ${TOKEN}" "${API}/api/auth/me")"; [ "$code" = "401" ] && echo 0 || echo 1)"
|
||||
# 轮换后要重新登录,否则后面的用例会带着失效 token 跑
|
||||
TOKEN="$(login "$ADMIN_PASSWORD")"
|
||||
check "轮换后重新登录成功" "$([ -n "$TOKEN" ] && echo 0 || echo 1)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 8. 升级 / 回滚
|
||||
|
||||
if [ "$DO_FULL" = "1" ]; then
|
||||
section "upgrade / rollback(--full)"
|
||||
if [ -z "$PACKAGE" ]; then
|
||||
note "未提供 --package,跳过升级用例"
|
||||
else
|
||||
before_version="$("$CLASSHELPER" version 2>/dev/null | awk '{print $2}')"
|
||||
sha="$(sha256sum "$PACKAGE" | awk '{print $1}')"
|
||||
up_out="$("$CLASSHELPER" upgrade --url "$PACKAGE" --sha256 "$sha" --force 2>&1)"
|
||||
check "upgrade 执行成功" "$(printf '%s' "$up_out" | grep -q '升级完成' && echo 0 || echo 1)"
|
||||
check "升级后服务仍就绪" "$("${CURL[@]}" -o /dev/null "${API}/healthz" && echo 0 || echo 1)"
|
||||
check "升级后配置未被改动(JWT_SECRET 保持)" \
|
||||
"$([ "$(read_cfg JWT_SECRET)" = "${new_secret:-$(read_cfg JWT_SECRET)}" ] && echo 0 || echo 1)"
|
||||
check "产生了程序快照(可回滚)" "$(ls -1 "${BACKUP_DIR}/rollback" 2>/dev/null | grep -q . && echo 0 || echo 1)"
|
||||
check "坏包被拒绝(sha256 不匹配时不改动任何文件)" \
|
||||
"$("$CLASSHELPER" upgrade --url "$PACKAGE" --sha256 0000000000000000000000000000000000000000000000000000000000000000 2>&1 | grep -q '校验失败' && echo 0 || echo 1)"
|
||||
check "校验失败后服务仍在运行" "$("${CURL[@]}" -o /dev/null "${API}/healthz" && echo 0 || echo 1)"
|
||||
|
||||
note "(回滚需要交互确认,这里用 yes 管道)"
|
||||
rb_out="$(printf 'yes\n' | "$CLASSHELPER" rollback 2>&1)"
|
||||
check "rollback 执行成功" "$(printf '%s' "$rb_out" | grep -qE '已回滚到|回滚' && echo 0 || echo 1)"
|
||||
check "回滚后服务仍就绪" "$("${CURL[@]}" -o /dev/null "${API}/healthz" && echo 0 || echo 1)"
|
||||
note "升级前版本 ${before_version}|升级后 $("$CLASSHELPER" version 2>/dev/null | awk '{print $2}')"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------- 结果
|
||||
|
||||
printf '\n'
|
||||
if [ "$FAIL" = "0" ]; then
|
||||
printf '%s=== 结果:%s/%s 项通过 ===%s\n' "$G" "$PASS" "$TOTAL" "$N"
|
||||
else
|
||||
printf '%s=== 结果:%s/%s 项通过,%s 项失败 ===%s\n' "$R" "$PASS" "$TOTAL" "$FAIL" "$N"
|
||||
printf '%s失败项:%s\n' "$Y" "$N"
|
||||
for item in "${FAILED[@]}"; do printf ' · %s\n' "$item"; done
|
||||
fi
|
||||
[ "$FAIL" = "0" ] || exit 1
|
||||
Reference in new issue
Block a user