2026-09-22 23:17:22 +03:00
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
# desktop-release.sh — раскладывает собранные артефакты desktop-обёртки по
|
|
|
|
|
|
# каталогу обновлений инстанса и генерирует манифест автообновления
|
|
|
|
|
|
# (docs/client-tauri.md §4, desktop/README.md).
|
|
|
|
|
|
#
|
|
|
|
|
|
# Что делает:
|
|
|
|
|
|
# 1. берёт артефакт обновления (`glchat.app.tar.gz` и его `.sig` для macOS,
|
|
|
|
|
|
# `.nsis.zip`/`.msi` для Windows, `.AppImage.tar.gz` для Linux);
|
|
|
|
|
|
# 2. копирует артефакт и подпись в <каталог>/<target>/<arch>/;
|
|
|
|
|
|
# 3. пишет манифест <каталог>/<target>/<arch>/<version>.json и его копию
|
|
|
|
|
|
# latest.json — её инстанс отдаёт в ответ на
|
|
|
|
|
|
# GET /updates/{target}/{arch}/{current_version} (обёртка приходит со
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# своей установленной версией, а «новее ли релиз» решает клиент);
|
|
|
|
|
|
# 4. убирает манифесты прошлых релизов (`<старая версия>.json`): обёртка
|
|
|
|
|
|
# спрашивает манифест своей версии, и оставшийся файл перекрыл бы новый
|
|
|
|
|
|
# релиз — клиент со старой версией не увидел бы обновления вовсе.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Версия релиза берётся из desktop/src-tauri/tauri.conf.json, а та — из файла
|
|
|
|
|
|
# VERSION в корне репозитория (единый источник версии, см. desktop/README.md).
|
2026-09-22 23:17:22 +03:00
|
|
|
|
#
|
|
|
|
|
|
# Приватный ключ подписи не нужен: подпись уже лежит рядом с артефактом (её
|
|
|
|
|
|
# делает `make desktop-build`), сервер только отдаёт файлы, а проверяет
|
|
|
|
|
|
# подпись клиент вшитым публичным ключом.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Примеры:
|
|
|
|
|
|
# # macOS-артефакт из target/release/bundle/macos → /opt/glchat/updates
|
|
|
|
|
|
# sudo bash scripts/desktop-release.sh
|
|
|
|
|
|
# # разложить в свой каталог и посмотреть план
|
|
|
|
|
|
# bash scripts/desktop-release.sh --dir /tmp/updates --dry-run
|
|
|
|
|
|
# # загрузить на стенд (sudo спросит пароль в терминале)
|
|
|
|
|
|
# bash scripts/desktop-release.sh --remote alex@192.168.1.247
|
|
|
|
|
|
# # Windows/Linux-артефакты указываются явно
|
|
|
|
|
|
# bash scripts/desktop-release.sh --artifact bundle/nsis/glchat_0.1.0_x64-setup.nsis.zip
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
|
|
TAURI_DIR="${REPO_DIR}/desktop/src-tauri"
|
|
|
|
|
|
BUNDLE_DIR="${TAURI_DIR}/target/release/bundle"
|
|
|
|
|
|
|
|
|
|
|
|
# Каталог обновлений на инстансе: тот же путь, что в UPDATES_DIR приложения
|
|
|
|
|
|
# (в контейнере смонтирован в /app/updates только для чтения).
|
|
|
|
|
|
UPDATES_DIR="${GLCHAT_UPDATES_DIR:-/opt/glchat/updates}"
|
|
|
|
|
|
# Публичный адрес инстанса: из него собираются ссылки на артефакты в манифесте.
|
|
|
|
|
|
BASE_URL="${GLCHAT_BASE_URL:-https://gl.mhspx.su}"
|
|
|
|
|
|
VERSION=""
|
|
|
|
|
|
TARGET=""
|
|
|
|
|
|
ARCH=""
|
|
|
|
|
|
NOTES=""
|
|
|
|
|
|
REMOTE=""
|
|
|
|
|
|
DRY_RUN=0
|
|
|
|
|
|
ARTIFACTS=()
|
|
|
|
|
|
|
|
|
|
|
|
info() { printf ' %s\n' "$*"; }
|
|
|
|
|
|
ok() { printf ' ✓ %s\n' "$*"; }
|
|
|
|
|
|
warn() { printf ' ! %s\n' "$*" >&2; }
|
|
|
|
|
|
die() {
|
|
|
|
|
|
printf 'ошибка: %s\n' "$*" >&2
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
usage() {
|
|
|
|
|
|
cat <<'EOF'
|
|
|
|
|
|
desktop-release.sh — раскладка артефактов обновления desktop-обёртки на инстансе.
|
|
|
|
|
|
|
|
|
|
|
|
Использование: bash scripts/desktop-release.sh [флаги]
|
|
|
|
|
|
|
|
|
|
|
|
Флаги:
|
|
|
|
|
|
--artifact FILE файл артефакта (можно несколько); по умолчанию берётся
|
|
|
|
|
|
первый найденный артефакт текущей ОС в каталоге бандла
|
|
|
|
|
|
--version V версия релиза (по умолчанию — из desktop/src-tauri/tauri.conf.json)
|
|
|
|
|
|
--target T платформа обновления: darwin|windows|linux (по умолчанию — по имени файла)
|
|
|
|
|
|
--arch A архитектура: aarch64|x86_64|i686|armv7 (по умолчанию — по имени файла)
|
|
|
|
|
|
--dir DIR каталог обновлений (по умолчанию $GLCHAT_UPDATES_DIR или /opt/glchat/updates)
|
|
|
|
|
|
--base-url URL публичный адрес инстанса для ссылок (по умолчанию https://gl.mhspx.su)
|
|
|
|
|
|
--notes TEXT текст «что нового» в манифесте
|
2026-09-26 20:14:25 +03:00
|
|
|
|
--remote HOST разложить в --dir на удалённом хосте: rsync по SSH (через
|
|
|
|
|
|
sudo, если каталог не записываем; опции ssh — GLCHAT_SSH_OPTS)
|
2026-09-22 23:17:22 +03:00
|
|
|
|
--dry-run показать план, ничего не записывать
|
|
|
|
|
|
-h, --help эта справка
|
|
|
|
|
|
EOF
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
while [ $# -gt 0 ]; do
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
--artifact)
|
|
|
|
|
|
[ $# -ge 2 ] || die "--artifact без значения"
|
|
|
|
|
|
ARTIFACTS+=("$2")
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--version)
|
|
|
|
|
|
VERSION="${2:-}"
|
|
|
|
|
|
[ -n "$VERSION" ] || die "--version без значения"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--target)
|
|
|
|
|
|
TARGET="${2:-}"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--arch)
|
|
|
|
|
|
ARCH="${2:-}"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--dir)
|
|
|
|
|
|
UPDATES_DIR="${2:-}"
|
|
|
|
|
|
[ -n "$UPDATES_DIR" ] || die "--dir без значения"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--base-url)
|
|
|
|
|
|
BASE_URL="${2:-}"
|
|
|
|
|
|
[ -n "$BASE_URL" ] || die "--base-url без значения"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--notes)
|
|
|
|
|
|
NOTES="${2:-}"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--remote)
|
|
|
|
|
|
REMOTE="${2:-}"
|
|
|
|
|
|
[ -n "$REMOTE" ] || die "--remote без значения"
|
|
|
|
|
|
shift 2
|
|
|
|
|
|
;;
|
|
|
|
|
|
--dry-run)
|
|
|
|
|
|
DRY_RUN=1
|
|
|
|
|
|
shift
|
|
|
|
|
|
;;
|
|
|
|
|
|
-h | --help)
|
|
|
|
|
|
usage
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
;;
|
|
|
|
|
|
*) die "неизвестный флаг: $1 (см. --help)" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
have() { command -v "$1" >/dev/null 2>&1; }
|
|
|
|
|
|
have jq || die "нужен jq (генерация манифеста)"
|
|
|
|
|
|
BASE_URL="${BASE_URL%/}"
|
|
|
|
|
|
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# newer_version <a> <b> → true, если a новее b. Сравниваем по числам (0.1.10
|
|
|
|
|
|
# новее 0.1.9 — обычное сравнение строк тут ошибается), недостающие части
|
|
|
|
|
|
# считаем нулями.
|
|
|
|
|
|
newer_version() {
|
|
|
|
|
|
jq -n --arg a "$1" --arg b "$2" '
|
|
|
|
|
|
def parts: split(".") | map(tonumber? // 0) | . + [0, 0, 0] | .[0:3];
|
|
|
|
|
|
(($a | parts) > ($b | parts))'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# drop_stale_manifests <каталог> — убирает манифесты прошлых релизов, оставляя
|
|
|
|
|
|
# только манифест публикуемой версии и latest.json.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Зачем: обёртка спрашивает `/updates/{target}/{arch}/{своя_версия}`, сервер
|
|
|
|
|
|
# отдаёт `<версия>.json`, а если его нет — latest.json. Файл прошлого релиза
|
|
|
|
|
|
# (например 0.1.0.json) перекрывал бы новый: клиент со старой версией получал бы
|
|
|
|
|
|
# «обновлений нет» и автообновление не приходило бы никому.
|
|
|
|
|
|
drop_stale_manifests() {
|
|
|
|
|
|
find "$1" -maxdepth 1 -type f -name '*.json' \
|
|
|
|
|
|
! -name "${VERSION}.json" ! -name latest.json -print -delete
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 23:17:22 +03:00
|
|
|
|
# --- сбор артефактов ------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
# Версия релиза: единственный источник правды — tauri.conf.json
|
|
|
|
|
|
# (docs/client-tauri.md §3: версия синхронизирована с сервером).
|
|
|
|
|
|
if [ -z "$VERSION" ]; then
|
|
|
|
|
|
VERSION="$(jq -r '.version' "${TAURI_DIR}/tauri.conf.json")"
|
|
|
|
|
|
fi
|
|
|
|
|
|
[ -n "$VERSION" ] && [ "$VERSION" != "null" ] || die "не удалось определить версию релиза"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "${#ARTIFACTS[@]}" -eq 0 ]; then
|
|
|
|
|
|
shopt -s nullglob
|
|
|
|
|
|
found=("${BUNDLE_DIR}"/macos/*.app.tar.gz "${BUNDLE_DIR}"/nsis/*.nsis.zip \
|
|
|
|
|
|
"${BUNDLE_DIR}"/msi/*.msi "${BUNDLE_DIR}"/appimage/*.AppImage.tar.gz)
|
|
|
|
|
|
shopt -u nullglob
|
|
|
|
|
|
[ "${#found[@]}" -gt 0 ] || die "в ${BUNDLE_DIR} нет артефактов обновления: сначала соберите бандл (make desktop-build)"
|
|
|
|
|
|
ARTIFACTS=("${found[0]}")
|
|
|
|
|
|
if [ "${#found[@]}" -gt 1 ]; then
|
|
|
|
|
|
warn "найдено несколько артефактов, берём ${found[0]}; остальные укажите через --artifact"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# platform_from_name разбирает имя артефакта Tauri: платформа и архитектура
|
|
|
|
|
|
# берутся из имени, если не заданы флагами (glchat_0.1.0_x64-setup.nsis.zip).
|
|
|
|
|
|
platform_from_name() {
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
*.app.tar.gz) printf 'darwin' ;;
|
|
|
|
|
|
*.msi | *.nsis.zip) printf 'windows' ;;
|
|
|
|
|
|
*.AppImage.tar.gz) printf 'linux' ;;
|
|
|
|
|
|
*) die "не удалось определить платформу по имени $1: укажите --target" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
arch_from_name() {
|
|
|
|
|
|
case "$1" in
|
|
|
|
|
|
*aarch64* | *arm64*) printf 'aarch64'; return ;;
|
|
|
|
|
|
*x86_64* | *x64*) printf 'x86_64'; return ;;
|
|
|
|
|
|
*i686* | *i386* | *x86*) printf 'i686'; return ;;
|
|
|
|
|
|
*armv7*) printf 'armv7'; return ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
# Имя macOS-бандла архитектуру не содержит (glchat.app.tar.gz): берём её у
|
|
|
|
|
|
# машины, которая собрала бандл. Явный --arch всегда важнее.
|
|
|
|
|
|
case "$(uname -m)" in
|
|
|
|
|
|
arm64 | aarch64) printf 'aarch64' ;;
|
|
|
|
|
|
x86_64 | amd64) printf 'x86_64' ;;
|
|
|
|
|
|
i686 | i386) printf 'i686' ;;
|
|
|
|
|
|
armv7l) printf 'armv7' ;;
|
|
|
|
|
|
*) die "не удалось определить архитектуру по имени $1: укажите --arch" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Подготовленный каталог раскладки: <target>/<arch>/<файлы>. В него же
|
|
|
|
|
|
# собирается манифест, поэтому локальная запись и загрузка по SSH идут одним
|
|
|
|
|
|
# и тем же кодом.
|
|
|
|
|
|
STAGE="$(mktemp -d)"
|
|
|
|
|
|
trap 'rm -rf "$STAGE"' EXIT
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# mktemp создаёт каталог с режимом 0700, а rsync -a переносит атрибуты корня
|
|
|
|
|
|
# источника на корень назначения: каталог обновлений на инстансе становился
|
|
|
|
|
|
# 0700 и приложение (другой пользователь) переставало читать манифесты —
|
|
|
|
|
|
# /updates/… отвечал 404, хотя файлы лежали на месте.
|
|
|
|
|
|
chmod 755 "$STAGE"
|
2026-09-22 23:17:22 +03:00
|
|
|
|
: >"${STAGE}/layout.txt"
|
|
|
|
|
|
PLATFORMS_FILE="${STAGE}/platforms.json"
|
|
|
|
|
|
printf '{}' >"$PLATFORMS_FILE"
|
|
|
|
|
|
DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
|
|
|
|
|
|
|
|
for artifact in "${ARTIFACTS[@]}"; do
|
|
|
|
|
|
[ -f "$artifact" ] || die "нет файла ${artifact}"
|
|
|
|
|
|
artifact_name="$(basename "$artifact")"
|
|
|
|
|
|
# У .sig-файла артефакт ищется рядом: имя без .sig.
|
|
|
|
|
|
case "$artifact_name" in
|
|
|
|
|
|
*.sig) artifact_name="${artifact_name%.sig}" ;;
|
|
|
|
|
|
esac
|
|
|
|
|
|
artifact_dir="$(cd "$(dirname "$artifact")" && pwd)"
|
|
|
|
|
|
artifact_path="${artifact_dir}/${artifact_name}"
|
|
|
|
|
|
signature_path="${artifact_path}.sig"
|
|
|
|
|
|
|
|
|
|
|
|
target="${TARGET:-$(platform_from_name "$artifact_name")}"
|
|
|
|
|
|
arch="${ARCH:-$(arch_from_name "$artifact_name")}"
|
|
|
|
|
|
[ -f "$artifact_path" ] || die "нет артефакта ${artifact_path}"
|
|
|
|
|
|
[ -f "$signature_path" ] || die "нет подписи ${signature_path}: бандл собран без ключа обновлений (desktop/README.md)"
|
|
|
|
|
|
|
|
|
|
|
|
install_dir="${UPDATES_DIR}/${target}/${arch}"
|
|
|
|
|
|
# В манифесте — только содержимое подписи (одна строка minisign).
|
|
|
|
|
|
signature="$(tr -d '\n' <"$signature_path")"
|
|
|
|
|
|
url="${BASE_URL}/updates/${target}/${arch}/${artifact_name}"
|
|
|
|
|
|
platform_key="${target}-${arch}"
|
|
|
|
|
|
|
|
|
|
|
|
info "артефакт: ${artifact_path}"
|
|
|
|
|
|
info "подпись: ${signature_path}"
|
|
|
|
|
|
info "назначение: ${install_dir}/${artifact_name}"
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "${STAGE}/${target}/${arch}"
|
|
|
|
|
|
cp "$artifact_path" "${STAGE}/${target}/${arch}/${artifact_name}"
|
|
|
|
|
|
cp "$signature_path" "${STAGE}/${target}/${arch}/${artifact_name}.sig"
|
|
|
|
|
|
printf '%s %s %s\n' "$target" "$arch" "$artifact_name" >>"${STAGE}/layout.txt"
|
|
|
|
|
|
|
|
|
|
|
|
# Манифест версии: по записи на платформу. Несколько платформ одной версии
|
|
|
|
|
|
# (macOS + Windows + Linux) складываются в один манифест, а не затирают
|
|
|
|
|
|
# друг друга.
|
|
|
|
|
|
jq --arg key "$platform_key" --arg url "$url" --arg signature "$signature" \
|
|
|
|
|
|
--arg version "$VERSION" --arg notes "$NOTES" --arg date "$DATE" \
|
|
|
|
|
|
'.version = $version
|
|
|
|
|
|
| .notes = (if $notes == "" then (.notes // ("Релиз glchat " + $version)) else $notes end)
|
|
|
|
|
|
| .pub_date = $date
|
|
|
|
|
|
| .platforms = ((.platforms // {}) + {($key): {signature: $signature, url: $url}})' \
|
|
|
|
|
|
"$PLATFORMS_FILE" >"${PLATFORMS_FILE}.tmp"
|
|
|
|
|
|
mv "${PLATFORMS_FILE}.tmp" "$PLATFORMS_FILE"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
while read -r target arch artifact_name; do
|
|
|
|
|
|
install_dir="${UPDATES_DIR}/${target}/${arch}"
|
|
|
|
|
|
mkdir -p "${STAGE}/${target}/${arch}"
|
|
|
|
|
|
cp "$PLATFORMS_FILE" "${STAGE}/${target}/${arch}/${VERSION}.json"
|
|
|
|
|
|
cp "$PLATFORMS_FILE" "${STAGE}/${target}/${arch}/latest.json"
|
|
|
|
|
|
info "манифест: ${install_dir}/${VERSION}.json (+ latest.json)"
|
|
|
|
|
|
done <"${STAGE}/layout.txt"
|
|
|
|
|
|
|
|
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
|
|
|
|
info "dry-run: файлы не записаны (${UPDATES_DIR})"
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# --- раскладка -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
if [ -z "$REMOTE" ]; then
|
|
|
|
|
|
while read -r target arch artifact_name; do
|
|
|
|
|
|
install_dir="${UPDATES_DIR}/${target}/${arch}"
|
|
|
|
|
|
mkdir -p "$install_dir"
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# Понижение версии ничего не сломает (клиент отвергает даунгрейд), но
|
|
|
|
|
|
# публиковать его бессмысленно — предупреждаем.
|
|
|
|
|
|
if [ -f "${install_dir}/latest.json" ]; then
|
|
|
|
|
|
published="$(jq -r '.version // empty' "${install_dir}/latest.json" 2>/dev/null || true)"
|
|
|
|
|
|
if [ -n "$published" ] && [ "$published" != "$VERSION" ] &&
|
|
|
|
|
|
[ "$(newer_version "$published" "$VERSION")" = "true" ]; then
|
|
|
|
|
|
warn "в ${install_dir} уже опубликована версия ${published} — она новее ${VERSION}: обёртки её не увидят"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
2026-09-22 23:17:22 +03:00
|
|
|
|
cp -f "${STAGE}/${target}/${arch}/${artifact_name}" "${install_dir}/${artifact_name}"
|
|
|
|
|
|
cp -f "${STAGE}/${target}/${arch}/${artifact_name}.sig" "${install_dir}/${artifact_name}.sig"
|
|
|
|
|
|
# Манифест этой версии мог уже существовать (другая платформа той же
|
|
|
|
|
|
# версии): записи платформ объединяются.
|
|
|
|
|
|
if [ -f "${install_dir}/${VERSION}.json" ]; then
|
|
|
|
|
|
jq -s '.[0] * .[1]' "${install_dir}/${VERSION}.json" "$PLATFORMS_FILE" >"${install_dir}/${VERSION}.json.tmp"
|
|
|
|
|
|
mv "${install_dir}/${VERSION}.json.tmp" "${install_dir}/${VERSION}.json"
|
|
|
|
|
|
else
|
|
|
|
|
|
cp -f "$PLATFORMS_FILE" "${install_dir}/${VERSION}.json"
|
|
|
|
|
|
fi
|
|
|
|
|
|
# latest.json указывает на последнюю опубликованную версию для этой
|
|
|
|
|
|
# платформы: именно её отдаёт инстанс на запрос обёртки.
|
|
|
|
|
|
cp -f "$PLATFORMS_FILE" "${install_dir}/latest.json"
|
2026-09-26 20:14:25 +03:00
|
|
|
|
drop_stale_manifests "$install_dir" | while read -r stale; do
|
|
|
|
|
|
info "убран манифест прошлого релиза: ${stale}"
|
|
|
|
|
|
done
|
2026-09-22 23:17:22 +03:00
|
|
|
|
chmod 644 "${install_dir}/${artifact_name}" "${install_dir}/${artifact_name}.sig" \
|
|
|
|
|
|
"${install_dir}/${VERSION}.json" "${install_dir}/latest.json"
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# Каталоги — 755: приложение читает их под другим пользователем, а
|
|
|
|
|
|
# манифест прошлого релиза мог прийти с чужого стенда с режимом 700.
|
|
|
|
|
|
find "$install_dir" -type d -exec chmod 755 {} +
|
2026-09-22 23:17:22 +03:00
|
|
|
|
ok "разложено в ${install_dir}"
|
|
|
|
|
|
done <"${STAGE}/layout.txt"
|
|
|
|
|
|
|
|
|
|
|
|
cat <<EOF
|
|
|
|
|
|
|
|
|
|
|
|
Готово. Проверка (подставьте target/arch из вывода выше):
|
|
|
|
|
|
curl -s ${BASE_URL}/updates/<target>/<arch>/${VERSION} | jq .
|
|
|
|
|
|
|
|
|
|
|
|
Обёртка проверяет манифест сама (трей → «Проверить обновления…»); каталог
|
|
|
|
|
|
смонтирован в контейнер только для чтения, перезапуск приложения не нужен.
|
|
|
|
|
|
EOF
|
|
|
|
|
|
exit 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# --- доставка на удалённый инстанс ---------------------------------------
|
|
|
|
|
|
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# Каталог обновлений обычно принадлежит root (его создаёт установщик) — тогда
|
|
|
|
|
|
# rsync на удалённой стороне идёт через sudo и пароль спрашивает ssh -t. Но
|
|
|
|
|
|
# каталог может быть и пользовательским (например, на стенде): если он
|
|
|
|
|
|
# записываем, sudo не нужен вовсе, и загрузка работает без терминала (CI,
|
|
|
|
|
|
# `ssh` без TTY). Дополнительные опции ssh — в GLCHAT_SSH_OPTS.
|
2026-09-22 23:17:22 +03:00
|
|
|
|
have rsync || die "нужен rsync для --remote"
|
2026-09-26 20:14:25 +03:00
|
|
|
|
# Опции ssh задаются строкой (её разбиение по словам здесь намеренное) — так
|
|
|
|
|
|
# скрипт работает и на bash 3.2 из macOS, где пустой массив под `set -u` падает.
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
SSH_EXTRA="${GLCHAT_SSH_OPTS:-}"
|
|
|
|
|
|
|
|
|
|
|
|
if ssh ${SSH_EXTRA} -o BatchMode=yes "$REMOTE" "test -w '${UPDATES_DIR}'" >/dev/null 2>&1; then
|
|
|
|
|
|
info "загружаем на ${REMOTE}:${UPDATES_DIR} (каталог записываем — без sudo)"
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
rsync -az -e "ssh ${SSH_EXTRA}" "${STAGE}/" "${REMOTE}:${UPDATES_DIR}/" ||
|
|
|
|
|
|
die "не удалось загрузить артефакты на ${REMOTE}"
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
ssh ${SSH_EXTRA} "$REMOTE" "find '${UPDATES_DIR}' -type d -exec chmod 755 {} +"
|
|
|
|
|
|
ssh ${SSH_EXTRA} "$REMOTE" "find '${UPDATES_DIR}' -type f -exec chmod 644 {} +"
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
ssh ${SSH_EXTRA} "$REMOTE" "find '${UPDATES_DIR}' -type f -name '*.json' ! -name '${VERSION}.json' ! -name latest.json -print -delete"
|
|
|
|
|
|
else
|
|
|
|
|
|
info "загружаем на ${REMOTE}:${UPDATES_DIR} (sudo спросит пароль)"
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
rsync -az --rsync-path="sudo rsync" -e "ssh -t ${SSH_EXTRA}" "${STAGE}/" "${REMOTE}:${UPDATES_DIR}/" ||
|
|
|
|
|
|
die "не удалось загрузить артефакты на ${REMOTE}"
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
ssh -t ${SSH_EXTRA} "$REMOTE" "sudo find '${UPDATES_DIR}' -type d -exec chmod 755 {} +"
|
|
|
|
|
|
ssh -t ${SSH_EXTRA} "$REMOTE" "sudo find '${UPDATES_DIR}' -type f -exec chmod 644 {} +"
|
|
|
|
|
|
# Манифесты прошлых релизов на удалённой стороне убираются по тому же
|
|
|
|
|
|
# правилу, что и локально: иначе старый <версия>.json перекрывает новый релиз.
|
|
|
|
|
|
# shellcheck disable=SC2086
|
|
|
|
|
|
ssh -t ${SSH_EXTRA} "$REMOTE" "sudo find '${UPDATES_DIR}' -type f -name '*.json' ! -name '${VERSION}.json' ! -name latest.json -print -delete"
|
|
|
|
|
|
fi
|
2026-09-22 23:17:22 +03:00
|
|
|
|
ok "обновления на ${REMOTE}:${UPDATES_DIR}"
|
|
|
|
|
|
cat <<EOF
|
|
|
|
|
|
|
|
|
|
|
|
Проверка:
|
|
|
|
|
|
curl -s ${BASE_URL}/updates/<target>/<arch>/${VERSION} | jq .
|
|
|
|
|
|
EOF
|