#!/usr/bin/env bash # Установка / обновление / откат из клонированного репозитория: образы собираются локально. # ./update.sh — последний релизный тег (без -rc) # ./update.sh v0.2.0 — конкретная версия (можно без «v») # ./update.sh main — текущая ветка main (для тестов, не для продакшена) set -euo pipefail # git checkout может заменить этот файл прямо во время работы — выполняемся из временной копии. if [ -z "${PIPU_UPDATE_REEXEC:-}" ]; then tmp="$(mktemp)" cp "$0" "$tmp" PIPU_UPDATE_REEXEC=1 PIPU_DEPLOY_DIR="$(cd "$(dirname "$0")" && pwd)" exec bash "$tmp" "$@" fi trap 'rm -f "$0"' EXIT cd "$PIPU_DEPLOY_DIR" # Share a lock with manual deployments and config application. if [ ! -f .management.lock ]; then (umask 022; : > .management.lock); fi exec 9<.management.lock flock -n 9 || { echo 'Another deployment is running' >&2; exit 1; } touch config.local chmod 600 config.local compose() { docker compose --env-file config.local --env-file .env "$@"; } [ -f .env ] || { echo "нет .env — сначала sudo ./install.sh" >&2; exit 1; } # Ключи подписи (токены — M1, ссылки на обложки и аудио — M2): создаются один раз, # в том числе в .env, сделанном до их появления. for key in JWT_PRIVATE_KEY STREAM_SIGNING_KEY; do if ! grep -q "^$key=." .env; then sed -i "/^$key=\$/d" .env echo "$key=$(head -c 32 /dev/urandom | base64)" >> .env echo "→ в .env добавлен $key" fi done # Самообновление Android: адрес Gitea и репозиторий — из origin этого клона, если в .env пусто # или осталась заглушка из .env.example (https://host/owner/repo, git@host:owner/repo, ssh://git@host:port/owner/repo). fill_gitea() { local url repo origin host path url="$(sed -n 's/^GITEA_URL=//p' .env)" repo="$(sed -n 's/^GITEA_REPO=//p' .env)" case "$url" in "" | *example.*) url="" ;; esac case "$repo" in "" | owner/*) repo="" ;; esac [ -n "$url" ] && [ -n "$repo" ] && return 0 origin="$(git -c safe.directory='*' remote get-url origin 2>/dev/null || true)" [ -n "$origin" ] || return 0 case "$origin" in http://* | https://*) host="$(printf '%s' "$origin" | sed -E 's#^(https?://)([^@/]*@)?([^/]+)/.*#\1\3#')" path="$(printf '%s' "$origin" | sed -E 's#^https?://[^/]+/##')" ;; ssh://*) host="https://$(printf '%s' "$origin" | sed -E 's#^ssh://([^@/]*@)?([^:/]+).*#\2#')" path="$(printf '%s' "$origin" | sed -E 's#^ssh://[^/]+/##')" ;; *:*) host="https://$(printf '%s' "$origin" | sed -E 's#^([^@]*@)?([^:]+):.*#\2#')" path="${origin#*:}" ;; *) return 0 ;; esac path="${path%.git}" [ -n "$url" ] || url="$host" [ -n "$repo" ] || repo="$path" for kv in "GITEA_URL=$url" "GITEA_REPO=$repo"; do if grep -q "^${kv%%=*}=" .env; then sed -i "s|^${kv%%=*}=.*|$kv|" .env; else echo "$kv" >> .env; fi done echo "→ самообновление Android: $url, $repo (из git remote; поменять — в .env)" } fill_gitea # Папка с музыкой пуста — почти всегда это несмонтированный диск: api отдавал бы ошибки воспроизведения. LIBRARY_HOST="$(sed -n 's/^LIBRARY_HOST=//p' .env)" if [ -n "$LIBRARY_HOST" ] && [ -z "$(ls -A "$LIBRARY_HOST" 2>/dev/null)" ] && [ "${PIPU_ALLOW_EMPTY_LIBRARY:-}" != 1 ]; then echo "LIBRARY_HOST=$LIBRARY_HOST пуст или не существует — диск с музыкой не смонтирован?" >&2 echo " проверьте: findmnt $LIBRARY_HOST; смонтируйте (sudo mount -a) и повторите." >&2 echo " Если папка и должна быть пустой: PIPU_ALLOW_EMPTY_LIBRARY=1 ./update.sh ${1:-}" >&2 exit 1 fi REF="${1:-}" if [ -n "${PIPU_UPDATE_SOURCE:-}" ]; then [[ "${PIPU_UPDATE_COMMIT:-}" =~ ^[a-f0-9]{40,64}$ ]] || { echo 'Invalid commit' >&2; exit 1; } [[ "${PIPU_UPDATE_REF:-}" =~ ^refs/(heads/main|tags/v?[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?)$ ]] || { echo 'Invalid ref' >&2; exit 1; } git -c protocol.file.allow=never -c protocol.ext.allow=never fetch --quiet -- "$PIPU_UPDATE_SOURCE" "$PIPU_UPDATE_REF" FETCHED="$(git rev-parse 'FETCH_HEAD^{commit}')" [ "$FETCHED" = "$PIPU_UPDATE_COMMIT" ] || { echo 'Source changed; select update again' >&2; exit 1; } else git fetch --tags --prune --quiet origin fi if [ -z "$REF" ]; then REF="$(git tag -l 'v*' --sort=-v:refname | grep -v -- '-rc' | head -1)" [ -n "$REF" ] || { echo "в репозитории нет релизных тегов; укажите версию или main" >&2; exit 1; } elif [[ "$REF" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then REF="v$REF" fi # Откат на более старую версию: миграции БД назад не применяются, старый api может не понять новую схему. CURRENT="$(sed -n 's/^PIPU_VERSION=//p' .env)" CURRENT_COMMIT="$(sed -n 's/^PIPU_COMMIT=//p' .env)" TARGET="${REF#v}" core() { [[ "$1" =~ ^([0-9]+\.[0-9]+\.[0-9]+) ]] && echo "${BASH_REMATCH[1]}"; } cur_core="$(core "$CURRENT" || true)" tgt_core="$(core "$TARGET" || true)" # сравниваем только MAJOR.MINOR.PATCH: переход 0.2.0-rc.1 → 0.2.0 откатом не считается if [ -n "$cur_core" ] && [ -n "$tgt_core" ] && [ "$cur_core" != "$tgt_core" ] && [ "$(printf '%s\n%s\n' "$cur_core" "$tgt_core" | sort -V | head -1)" = "$tgt_core" ]; then echo "внимание: $CURRENT → $TARGET — это откат. Миграции БД не откатываются;" >&2 echo " копию базы сделайте внешними средствами и проверьте, что старая версия совместима со схемой." >&2 if [ "${PIPU_ALLOW_DOWNGRADE:-}" != 1 ]; then echo " Чтобы продолжить: PIPU_ALLOW_DOWNGRADE=1 ./update.sh $REF" >&2 exit 1 fi fi echo "→ переключаемся на $REF" # Refuse to overwrite local source changes. git diff --quiet && git diff --cached --quiet || { echo 'Repository has local changes' >&2; exit 1; } if [ -n "${PIPU_UPDATE_COMMIT:-}" ]; then git checkout --quiet --detach "$PIPU_UPDATE_COMMIT" elif git show-ref --verify --quiet "refs/remotes/origin/$REF"; then git checkout --quiet "$REF" git merge --ff-only --quiet "origin/$REF" else git checkout --quiet --detach "$REF" fi # Use the target revision's setup, even when this updater was copied before # checkout. Existing deployments must gain new host services during upgrades. if [ -f ./install-management.sh ]; then bash ./install-management.sh --ensure fi if [ -n "${PIPU_UPDATE_COMMIT:-}" ]; then if [ "$REF" = main ]; then VERSION="main-$(git rev-parse --short HEAD)"; else VERSION="$REF"; fi else VERSION="$(git describe --tags --always --dirty)" fi VERSION="${VERSION#v}" COMMIT="$(git rev-parse HEAD)" if grep -q '^PIPU_COMMIT=' .env; then sed -i "s|^PIPU_COMMIT=.*|PIPU_COMMIT=$COMMIT|" .env else echo "PIPU_COMMIT=$COMMIT" >> .env fi sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$VERSION|" .env echo "→ версия $VERSION, сборка образов (первая сборка — 5–15 минут)" compose config --quiet if ! compose build --pull; then # A build failure leaves running containers untouched; keep their version in .env. sed -i "s|^PIPU_VERSION=.*|PIPU_VERSION=$CURRENT|" .env sed -i "s|^PIPU_COMMIT=.*|PIPU_COMMIT=$CURRENT_COMMIT|" .env echo 'Build failed; running containers were not replaced' >&2 exit 1 fi compose up -d --remove-orphans docker image prune -f >/dev/null echo "→ ждём готовности api" for _ in $(seq 1 60); do # 127.0.0.1, а не localhost: в alpine localhost — сначала ::1, а nginx слушает только IPv4 if compose exec -T nginx wget -qO- http://127.0.0.1/readyz >/dev/null 2>&1; then echo "pipu $VERSION запущен" exit 0 fi sleep 2 done echo "api не стал готов за 2 минуты: docker compose logs api" >&2 exit 1