129 lines
10 KiB
Bash
Executable File
129 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# Подпись релиза 3X-UI_autoVPS. Запускает только автор, на своём компьютере, перед тегом:
|
||
#
|
||
# tools/release.sh [~/.ssh/3xui_kit_release]
|
||
#
|
||
# Проверяет, что версии сходятся и установщик 3X-UI не подменили, пишет SHA256SUMS,
|
||
# подписывает его закрытым ключом (ssh-keygen спросит пароль ключа) и сразу проверяет
|
||
# подпись открытым ключом, записанным в kit. Сервер 3X-UI_autoVPS ставит обновление, только
|
||
# если эта проверка у него тоже сошлась. Закрытый ключ никуда не отправляется.
|
||
|
||
set -euo pipefail
|
||
cd "$(dirname "$0")/.."
|
||
|
||
die() { printf '✗ %s\n' "$*" >&2; exit 1; }
|
||
ok() { printf '✓ %s\n' "$*"; }
|
||
warn() { printf '! %s\n' "$*" >&2; } # сеть до GitHub отвалилась: проверку пропускаем, релиз не блокируем
|
||
sha() { if command -v sha256sum >/dev/null; then sha256sum "$@"; else shasum -a 256 "$@"; fi; }
|
||
|
||
key=${1:-$HOME/.ssh/3xui_kit_release}
|
||
[[ -f $key ]] || die "Нет закрытого ключа $key. Создать: ssh-keygen -t ed25519 -C \"3X-UI_autoVPS releases\" -f $key"
|
||
NS="3x-ui-kit-release"
|
||
ID="releases@3x-ui-kit"
|
||
FILES=(scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh scripts/kit-sub.py scripts/kit-local.sh scripts/3x-ui-local.sh VERSION)
|
||
|
||
v=$(tr -d '[:space:]' <VERSION)
|
||
[[ $v =~ ^[0-9]+(\.[0-9]+)+$ ]] || die "VERSION: $v"
|
||
for f in scripts/3x-ui.sh scripts/hysteria2.sh scripts/kit.sh; do
|
||
grep -qx "KIT_VERSION=\"$v\"" "$f" || die "$f: KIT_VERSION не $v"
|
||
done
|
||
(grep -q "^## v$v" CHANGELOG.md || grep -q "^## v$v" FORK-CHANGELOG.md) || die "Нет раздела ## v$v ни в CHANGELOG.md, ни в FORK-CHANGELOG.md (форк ведёт свои записи в FORK-CHANGELOG.md)"
|
||
ok "Версия $v во всех скриптах и в CHANGELOG"
|
||
|
||
# Переводы строк. Сервер получает файл из git (там LF), а суммы считаются по рабочей копии:
|
||
# если в рабочей копии окажутся CR, суммы не сойдутся и kit update откажется ставить релиз.
|
||
for f in "${FILES[@]}"; do
|
||
[[ -z $(tr -cd "\r" <"$f") ]] || die "$f: переводы строк Windows (CR). Уберите: tr -d '\r' < \"$f\" > \"$f.lf\" && mv \"$f.lf\" \"$f\""
|
||
done
|
||
ok "Переводы строк во всех файлах релиза — LF"
|
||
|
||
export PY
|
||
bash tools/check-help.sh || die "справка kit help отстала от набора команд"
|
||
|
||
# «Строки-сироты»: строка с флагом без команды (например потерянный "#" в комментарии) не ловится
|
||
# ни bash -n, ни справкой, но при каждом запуске печатает «command not found». Проверяем холостым прогоном.
|
||
err=$(bash scripts/3x-ui-local.sh --dry-run 2>&1 >/dev/null || true)
|
||
if grep -qE "command not found|syntax error|bad substitution" <<<"$err"; then
|
||
printf '%s\n' "$err" | grep -E "command not found|syntax error|bad substitution" | head -3
|
||
die "установщик ругается на строку без команды — поправьте файл (см. выше)"
|
||
fi
|
||
ok "Установщик не ругается на пустые строки и опечатки"
|
||
|
||
# Наши функции в установщике обязаны быть определены РАНЬШЕ первого вызова: `bash -n` и холостой
|
||
# прогон этого не видят, а человек видит «command not found» посреди установки (так и было 13.10).
|
||
for fn in ask_yes ask_ssh_port ssh_ports_now ask_protocols proto_parse proto_menu interactive clean_previous installer_failed; do
|
||
# || true обязателен: без него grep, не найдя совпадений, возвращает 1 и set -e молча обрывает релиз.
|
||
d=$(grep -n "^$fn()" scripts/3x-ui-local.sh | head -1 | cut -d: -f1 || true)
|
||
c=$(grep -n "^[^#]*[^a-z_]$fn[ ;\"]" scripts/3x-ui-local.sh | grep -v "^${d:-0}:" | head -1 | cut -d: -f1 || true)
|
||
[[ -n $d ]] || die "scripts/3x-ui-local.sh: функция $fn не определена, а вызывается"
|
||
[[ -z $c || $c -gt $d ]] || die "scripts/3x-ui-local.sh: $fn вызывается (строка $c) раньше, чем определена ($d)"
|
||
done
|
||
ok "Все наши функции определены раньше своих вызовов"
|
||
|
||
[[ -z $(git status --porcelain -- "${FILES[@]}" CHANGELOG.md FORK-CHANGELOG.md) ]] || die "Есть незакоммиченные изменения в файлах релиза – сначала коммит."
|
||
|
||
# Часть проверок сверяется с GitHub, а он из домашней сети отвечает через раз. Сначала короткая проба:
|
||
# нет связи — сразу предупреждаем и пропускаем внешние проверки (подписи это не мешает), а не висим минутами.
|
||
gh_ok=yes
|
||
curl -fsI --connect-timeout 8 --max-time 12 https://github.com >/dev/null 2>&1 || gh_ok=no
|
||
[[ $gh_ok == yes ]] || warn "GitHub недоступен из вашей сети — установщик, суммы ядра и панели не сверяю. Подписи это не мешает."
|
||
gh_curl() { [[ ${gh_ok:-no} == yes ]] || return 1; curl -fsSL --retry 2 --retry-delay 2 --connect-timeout 15 --max-time 90 "$@"; }
|
||
|
||
# Установщик 3X-UI: хеш в скрипте должен совпадать с тем, что сейчас лежит на GitHub.
|
||
xv=$(sed -n 's/^XUI_VERSION="\(.*\)"/\1/p' scripts/3x-ui.sh)
|
||
xs=$(sed -n 's/^XUI_INSTALL_SHA256="\(.*\)"/\1/p' scripts/3x-ui.sh)
|
||
got=$(gh_curl "https://raw.githubusercontent.com/MHSanaei/3x-ui/$xv/install.sh" | sha | awk '{print $1}') \
|
||
|| { warn "GitHub недоступен — проверку установщика 3X-UI пропускаю (релиз не блокирую)"; got=$xs; }
|
||
[[ $got == "$xs" ]] || die "Установщик 3X-UI $xv изменился: $got вместо $xs. Проверьте, что поменялось, прежде чем выпускать."
|
||
ok "Установщик 3X-UI $xv совпадает с закреплённым SHA256"
|
||
|
||
# Суммы архивов ядра Xray в скрипте должны совпадать с официальными (.dgst релиза Xray-core).
|
||
xc=$(sed -n 's/^XRAY_CORE="\(.*\)"/\1/p' scripts/3x-ui.sh)
|
||
for a in 64 arm64-v8a; do
|
||
pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/3x-ui.sh)
|
||
off=$(gh_curl "https://github.com/XTLS/Xray-core/releases/download/$xc/Xray-linux-$a.zip.dgst" | awk '/^SHA2-256=/ {print $2}' || true)
|
||
[[ -n $off ]] || { warn "GitHub недоступен — суммы Xray для $a пропускаю"; continue; }
|
||
[[ -n $pin && $pin == "$off" ]] || die "Сумма ядра Xray $xc ($a) в 3x-ui.sh не совпадает с официальной: «$pin» вместо «$off»."
|
||
done
|
||
ok "Суммы архивов ядра Xray $xc совпадают с официальными"
|
||
|
||
# Проверенная панель и ядро в kit.sh (kit panel update, kit check): те же версии, что в установщике,
|
||
# а суммы архива панели и скрипта меню x-ui – официальные.
|
||
kp=$(sed -n 's/^XUI_PIN="\(.*\)"/\1/p' scripts/kit.sh)
|
||
[[ $kp == "$xv" ]] || die "XUI_PIN в kit.sh ($kp) не совпадает с XUI_VERSION в 3x-ui.sh ($xv)."
|
||
kx=$(sed -n 's/^XRAY_PIN="\(.*\)"/\1/p' scripts/kit.sh)
|
||
[[ $kx == "$xc" ]] || die "XRAY_PIN в kit.sh ($kx) не совпадает с XRAY_CORE в 3x-ui.sh ($xc)."
|
||
for a in amd64 arm64; do
|
||
pin=$(sed -n "s/^ \[$a\]=\([0-9a-f]\{64\}\)\$/\1/p" scripts/kit.sh)
|
||
off=$(gh_curl "https://github.com/MHSanaei/3x-ui/releases/download/$kp/x-ui-linux-$a.tar.gz.sha256" | awk 'NR == 1 {print $1}' || true)
|
||
[[ -n $off ]] || { warn "GitHub недоступен — сумму панели для $a пропускаю"; continue; }
|
||
[[ -n $pin && $pin == "$off" ]] || die "Сумма архива панели $kp ($a) в kit.sh не совпадает с официальной: «$pin» вместо «$off»."
|
||
done
|
||
shs=$(sed -n 's/^XUI_SH_SHA256=\(.*\)/\1/p' scripts/kit.sh)
|
||
got=$(gh_curl "https://raw.githubusercontent.com/MHSanaei/3x-ui/$kp/x-ui.sh" | sha | awk '{print $1}' || true)
|
||
[[ -n $got ]] || { warn "GitHub недоступен — проверку x-ui.sh $kp пропускаю"; got=$shs; }
|
||
[[ $shs == "$got" ]] || die "Сумма x-ui.sh $kp в kit.sh не совпадает с официальной: «$shs» вместо «$got»."
|
||
ok "Панель $kp и ядро $kx в kit.sh совпадают с установщиком и официальными суммами"
|
||
|
||
# В kit записан открытый ключ, которым подписываем.
|
||
signers() { awk '/^KIT_SIGNERS=\(/ {on = 1; next} on && /^\)/ {exit} on && /^ *"/ {gsub(/^ *"|"$/, ""); print}' "$1"; }
|
||
[[ -n $(signers scripts/kit.sh) ]] || die "В scripts/kit.sh пустой KIT_SIGNERS: впишите туда открытую часть ключа ($key.pub)."
|
||
pub=$(ssh-keygen -y -f "$key" | awk '{print $1, $2}')
|
||
signers scripts/kit.sh | awk '{print $1, $2}' | grep -qxF "$pub" || die "Ключа $key нет в KIT_SIGNERS – серверы не примут такую подпись."
|
||
ok "Ключ подписи записан в kit"
|
||
|
||
{ echo "# 3X-UI KIT $v"; echo "# 3X-UI_autoVPS $v"; sha "${FILES[@]}" | awk '{ sub(/^\*/, "", $2); print $1" "$2 }'; } >SHA256SUMS
|
||
rm -f SHA256SUMS.sig
|
||
ssh-keygen -Y sign -f "$key" -n "$NS" SHA256SUMS >/dev/null
|
||
tmp=$(mktemp)
|
||
trap 'rm -f "$tmp"' EXIT
|
||
signers scripts/kit.sh | while read -r k; do printf '%s namespaces="%s" %s\n' "$ID" "$NS" "$k"; done >"$tmp"
|
||
ssh-keygen -Y verify -f "$tmp" -I "$ID" -n "$NS" -s SHA256SUMS.sig <SHA256SUMS >/dev/null || die "Подпись не прошла проверку."
|
||
ok "SHA256SUMS подписан и проверен так же, как это сделает сервер"
|
||
echo
|
||
cat SHA256SUMS
|
||
echo
|
||
echo "Дальше:"
|
||
echo " git add SHA256SUMS SHA256SUMS.sig && git commit -m \"Релиз v$v: подпись\""
|
||
echo " git tag v$v && git push origin main v$v"
|