Watch
1
0
Fork
You've already forked Seyyed_arc
0
forked from hesabix/arc
Seyyed_arc/update.sh

499 lines
19 KiB
Shell
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Hesabix update script: pull from repo, migrate backend, restart services, rebuild frontend, reload nginx.
# pip / Flutter mirrors: scripts/mirror_config.sh (saved in ${APP_ROOT}/.deploy_env from deploy).
# Run via: hesabix -update [-source URL] [-branch NAME]
# PostgreSQL pgvector: scripts/ensure_pgvector.sh (idempotent, non-fatal) before Alembic migrations.
# AI voice (local STT/TTS): scripts/ensure_voice_chat.sh — prompts if deps missing (INSTALL_VOICE in .deploy_env).
# Requires: API_DOMAIN, UI_DOMAIN, BRANCH, REPO_URL in env or in ${APP_ROOT}/.deploy_env
# Web build API URL: https if /etc/letsencrypt/live/<API_DOMAIN> exists; else http unless API_PUBLIC_SCHEME is set in env (custom TLS).
set -euo pipefail
APP_ROOT="${APP_ROOT:-/opt/hesabix}"
LOG_FILE="${APP_ROOT}/update.log"
CHECK_MARK=$'\xE2\x9C\x94'
CROSS_MARK=$'\xE2\x9D\x8C'
log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"; }
log_ok() { echo "${CHECK_MARK} $*" | tee -a "${LOG_FILE}"; }
log_err() { echo "${CROSS_MARK} $*" >&2; echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >> "${LOG_FILE}"; }
UPDATE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/mirror_config.sh
if [[ -r "${UPDATE_SCRIPT_DIR}/scripts/mirror_config.sh" ]]; then
# shellcheck disable=SC1091
source "${UPDATE_SCRIPT_DIR}/scripts/mirror_config.sh"
fi
# shellcheck source=scripts/hesabix_python.sh
if [[ -r "${UPDATE_SCRIPT_DIR}/scripts/hesabix_python.sh" ]]; then
# shellcheck disable=SC1091
source "${UPDATE_SCRIPT_DIR}/scripts/hesabix_python.sh"
fi
# If normal checkout/pull fails (local generated file, merge, diverged branch), realign the clone with remote.
hesabix_force_sync_origin() {
log_info "Force sync with origin/${BRANCH}: git reset --hard (local changes and commits in this clone will be discarded)."
git fetch origin --prune
if ! git show-ref -q "origin/${BRANCH}"; then
log_err "origin/${BRANCH} not found after fetch."
return 1
fi
if git show-ref -q "refs/heads/${BRANCH}"; then
git checkout -f "${BRANCH}"
else
git checkout -b "${BRANCH}" "origin/${BRANCH}"
fi
if ! git reset --hard "origin/${BRANCH}"; then
log_err "git reset --hard origin/${BRANCH} failed."
return 1
fi
return 0
}
# Same logic as deploy.sh: Flutter PATH for new shells (/etc/profile.d + idempotent line in bash.bashrc)
persist_flutter_path_in_profile_d() {
local f="/etc/profile.d/hesabix-flutter.sh"
if [[ ! -x /opt/flutter/bin/flutter && ! -x /snap/bin/flutter ]]; then
return 0
fi
cat > "${f}" <<'PROFILE'
# Hesabix: Flutter در PATH برای شِل‌های login (deploy.sh / update.sh) — ترجیحاً دستی ویرایش نشود.
if [ -x /opt/flutter/bin/flutter ]; then
case ":${PATH}:" in
*:/opt/flutter/bin:*) ;;
*) PATH="/opt/flutter/bin${PATH:+:$PATH}"; export PATH ;;
esac
fi
if [ -x /snap/bin/flutter ]; then
case ":${PATH}:" in
*:/snap/bin:*) ;;
*) PATH="/snap/bin${PATH:+:$PATH}"; export PATH ;;
esac
fi
PROFILE
chmod 644 "${f}" 2>/dev/null || true
log_ok "Flutter برای شِل‌های login در PATH: ${f}"
local marker="# hesabix-flutter-PATH (deploy.sh)"
if [[ -f /etc/bash.bashrc ]] && ! grep -qF "${marker}" /etc/bash.bashrc 2>/dev/null; then
printf '\n%s\n[ -r /etc/profile.d/hesabix-flutter.sh ] && . /etc/profile.d/hesabix-flutter.sh\n' "${marker}" >> /etc/bash.bashrc
log_ok "شِل تعاملی bash: منبع ${f} به /etc/bash.bashrc اضافه شد."
fi
}
configure_pip_hesabix_mirror() {
if declare -F hesabix_configure_pip_mirror >/dev/null 2>&1; then
hesabix_configure_pip_mirror
return 0
fi
if ! command -v python3 >/dev/null 2>&1; then
return 0
fi
python3 -m pip config --user set global.index "https://p.mirror.hesabix.ir/simple" 2>/dev/null || true
python3 -m pip config --user set global.index-url "https://p.mirror.hesabix.ir/simple" 2>/dev/null || true
python3 -m pip config --user set global.trusted-host "p.mirror.hesabix.ir" 2>/dev/null || true
log_info "pip user config: Hesabix PyPI (p.mirror.hesabix.ir/simple)"
}
ensure_api_journalctl_env() {
local dropin_dir="/etc/systemd/system/hesabix-api.service.d"
local dropin_file="${dropin_dir}/10-journalctl-env.conf"
mkdir -p "${dropin_dir}"
cat > "${dropin_file}" <<'EOF'
[Service]
Environment=HESABIX_ALLOW_SUDO_JOURNALCTL=1
EOF
systemctl daemon-reload
log_info "Ensured systemd env: HESABIX_ALLOW_SUDO_JOURNALCTL=1 for hesabix-api."
}
if [[ $EUID -ne 0 ]]; then
log_err "Please run as root (e.g. sudo hesabix -update)"
exit 1
fi
if [[ ! -d "${APP_ROOT}/app/.git" ]]; then
log_err "Hesabix app not found at ${APP_ROOT}/app. Run deploy.sh first."
exit 1
fi
# Load saved deploy config (.deploy_env)
if [[ -f "${APP_ROOT}/.deploy_env" ]]; then
set -a
# shellcheck source=/dev/null
source "${APP_ROOT}/.deploy_env"
set +a
fi
for v in API_DOMAIN UI_DOMAIN BRANCH REPO_URL; do
if [[ -z "${!v:-}" ]]; then
log_err "Missing $v. Set it in env or in ${APP_ROOT}/.deploy_env"
exit 1
fi
done
if [[ ! -f "${APP_ROOT}/.db_password" ]]; then
log_err "Missing ${APP_ROOT}/.db_password"
exit 1
fi
DB_PASSWORD=$(cat "${APP_ROOT}/.db_password")
export DB_PASSWORD
echo "==========================================" | tee -a "${LOG_FILE}"
log_info "Hesabix update started (repo=${REPO_URL}, branch=${BRANCH})"
echo "==========================================" | tee -a "${LOG_FILE}"
# --- 1. Update from repo ---
log_info "Step 1: Updating source from repository..."
cd "${APP_ROOT}/app"
current_remote=$(git remote get-url origin 2>/dev/null || true)
if [[ "${current_remote}" != "${REPO_URL}" ]]; then
git remote set-url origin "${REPO_URL}"
fi
git fetch origin --prune
if ! git show-ref -q "origin/${BRANCH}"; then
log_err "شاخه origin/${BRANCH} روی remote نیست. BRANCH و REPO_URL را در ${APP_ROOT}/.deploy_env بررسی کنید."
exit 1
fi
if git checkout -B "${BRANCH}" "origin/${BRANCH}" && git pull origin "${BRANCH}" --ff-only; then
:
else
log_info "به‌روزرسانی معمولی Git ناموفق (مثلاً تغییرات محلی یا هم‌نشانی نشدن شاخه). در حال بازیابی با reset --hard..."
if ! hesabix_force_sync_origin; then
exit 1
fi
fi
log_ok "Source updated."
# --- 2. Backend: pip, migrations, restart services ---
log_info "Step 2: Backend – install deps, migrations, restart services..."
configure_pip_hesabix_mirror
ensure_api_journalctl_env
api_dir="${APP_ROOT}/app/hesabixAPI"
if [[ ! -d "${api_dir}/.venv" ]]; then
log_err "Backend venv not found. Run full deploy first."
exit 1
fi
cd "${api_dir}"
if declare -F hesabix_resolve_backend_python >/dev/null 2>&1; then
backend_python=""
if ! backend_python=$(hesabix_resolve_backend_python); then
log_info "Python >= 3.11 not found; installing packages..."
if ! hesabix_install_backend_python_packages || ! backend_python=$(hesabix_resolve_backend_python); then
log_err "hesabix-api requires Python >= 3.11."
exit 1
fi
fi
if ! hesabix_ensure_backend_venv "${api_dir}" "${backend_python}"; then
log_err "Failed to ensure backend virtualenv with ${backend_python}"
exit 1
fi
if [[ "${HESABIX_VENV_RECREATED:-0}" == "1" ]]; then
log_info "Backend virtualenv rebuilt with Python >= 3.11."
fi
fi
# shellcheck disable=SC1091
source .venv/bin/activate
if declare -F hesabix_apply_pip_mirror_env >/dev/null 2>&1; then
hesabix_apply_pip_mirror_env
else
export PIP_INDEX_URL="${PIP_INDEX_URL:-https://p.mirror.hesabix.ir/simple}"
export PIP_TRUSTED_HOST="${PIP_TRUSTED_HOST:-p.mirror.hesabix.ir}"
fi
log_info "Installing backend deps from PyPI: ${PIP_INDEX_URL}"
pip install --upgrade pip setuptools wheel -q
pip install -e . -q
ensure_voice="${APP_ROOT}/app/scripts/ensure_voice_chat.sh"
if [[ -f "${ensure_voice}" ]]; then
chmod +x "${ensure_voice}" 2>/dev/null || true
log_info "AI voice chat (optional local STT/TTS)..."
if bash "${ensure_voice}" --update; then
log_ok "Voice chat prerequisites check completed."
else
log_info "Voice chat deps skipped or failed (non-fatal)."
fi
fi
ensure_pgvector="${APP_ROOT}/app/scripts/ensure_pgvector.sh"
if [[ -f "${ensure_pgvector}" ]]; then
chmod +x "${ensure_pgvector}" 2>/dev/null || true
log_info "Ensuring PostgreSQL pgvector package (optional)..."
if bash "${ensure_pgvector}"; then
log_ok "pgvector package check completed."
else
log_info "pgvector package not installed (non-fatal)."
fi
fi
fixup_db="${APP_ROOT}/app/scripts/hesabix_fixup_db_privileges.sh"
if [[ -f "${fixup_db}" ]]; then
chmod +x "${fixup_db}" 2>/dev/null || true
log_info "Ensuring hesabix owns public schema objects (Alembic/API access)..."
bash "${fixup_db}"
fi
# Ensure alembic_version.version_num is VARCHAR(255) for long revision IDs (fixes StringDataRightTruncation)
log_info "Ensuring alembic_version schema compatibility..."
PGPASSWORD="${DB_PASSWORD}" psql -h 127.0.0.1 -U hesabix -d hesabix -tAc "
DO \$\$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='public' AND table_name='alembic_version') THEN
ALTER TABLE public.alembic_version ALTER COLUMN version_num TYPE VARCHAR(255);
ELSE
CREATE TABLE public.alembic_version (version_num VARCHAR(255) PRIMARY KEY);
END IF;
EXCEPTION WHEN OTHERS THEN NULL;
END \$\$;
" 2>/dev/null || true
log_info "Running Alembic migrations..."
if ! alembic upgrade head; then
log_err "Migrations failed."
exit 1
fi
log_ok "Migrations done."
ensure_secrets="${APP_ROOT}/app/scripts/ensure_api_production_secrets.sh"
if [[ -f "${ensure_secrets}" ]]; then
chmod +x "${ensure_secrets}" 2>/dev/null || true
log_info "Ensuring API production secrets in .env..."
if bash "${ensure_secrets}"; then
log_ok "API production secrets verified."
else
log_err "Failed to ensure API production secrets."
exit 1
fi
fi
chown -R www-data:www-data "${api_dir}"
systemctl daemon-reload
systemctl restart hesabix-api hesabix-rq-worker hesabix-notification-moderation
sleep 3
for svc in hesabix-api; do
if ! systemctl is-active --quiet "$svc"; then
log_err "Service $svc failed to start. Check: journalctl -u $svc"
exit 1
fi
done
log_ok "Backend services restarted."
# --- 3. Flutter: update SDK, build web, deploy (PATH دائمی: /etc/profile.d/hesabix-flutter.sh) ---
log_info "Step 3: Flutter – update SDK, build web, deploy..."
export PATH="/opt/flutter/bin:/snap/bin:${PATH:-}"
ensure_flutter_sdk="${APP_ROOT}/app/scripts/ensure_flutter_sdk_for_update.sh"
if [[ -f "${ensure_flutter_sdk}" ]]; then
chmod +x "${ensure_flutter_sdk}" 2>/dev/null || true
if declare -F hesabix_apply_flutter_mirror_env >/dev/null 2>&1; then
hesabix_apply_flutter_mirror_env
else
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://f.mirror.hesabix.ir/pub}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://f.mirror.hesabix.ir/gcs}"
fi
log_info "Ensuring Flutter/Dart SDK (mirror fallbacks; git upgrade only if HESABIX_UPDATE_FLUTTER_SDK=1)..."
if ! bash "${ensure_flutter_sdk}"; then
log_err "Flutter/Dart SDK not ready. Set HESABIX_UPDATE_FLUTTER_SDK=0 or fix mirror access, then retry."
exit 1
fi
else
if [[ -d /opt/flutter ]]; then
(cd /opt/flutter && git fetch --depth 1 origin stable 2>/dev/null && git reset --hard origin/stable 2>/dev/null) || true
fi
fi
if ! command -v flutter >/dev/null 2>&1; then
log_err "Flutter not in PATH. Ensure Flutter is installed (e.g. run deploy.sh once)."
exit 1
fi
persist_flutter_path_in_profile_d
if declare -F hesabix_resolve_flutter_storage_base_url >/dev/null 2>&1; then
hesabix_resolve_flutter_storage_base_url || true
elif declare -F hesabix_apply_flutter_mirror_env >/dev/null 2>&1; then
hesabix_apply_flutter_mirror_env
else
export PUB_HOSTED_URL="${PUB_HOSTED_URL:-https://f.mirror.hesabix.ir/pub}"
export FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-https://f.mirror.hesabix.ir/gcs}"
fi
log_info "Flutter pub/storage: PUB_HOSTED_URL=${PUB_HOSTED_URL} FLUTTER_STORAGE_BASE_URL=${FLUTTER_STORAGE_BASE_URL}"
app_dir="${APP_ROOT}/app"
build_script="${app_dir}/build_web.sh"
if [[ ! -f "${build_script}" ]]; then
log_err "build_web.sh not found: ${build_script}"
exit 1
fi
chmod +x "${build_script}"
# shellcheck disable=SC1091
if [[ -r "${app_dir}/scripts/api_public_scheme.sh" ]]; then
source "${app_dir}/scripts/api_public_scheme.sh"
fi
if ! declare -F hesabix_resolve_api_public_scheme >/dev/null 2>&1; then
hesabix_resolve_api_public_scheme() {
if [[ -n "${API_DOMAIN:-}" ]] && [[ -d "/etc/letsencrypt/live/${API_DOMAIN}" ]]; then
printf '%s' "https"; return 0
fi
local s="${API_PUBLIC_SCHEME:-}"
s="${s,,}"
case "$s" in http|https) printf '%s' "$s"; return 0 ;; esac
printf '%s' "http"
}
fi
api_scheme="$(hesabix_resolve_api_public_scheme)"
api_url="${api_scheme}://${API_DOMAIN}"
cd "${app_dir}"
if ! env PATH="/opt/flutter/bin:/snap/bin:$PATH" \
PUB_HOSTED_URL="${PUB_HOSTED_URL:-}" FLUTTER_STORAGE_BASE_URL="${FLUTTER_STORAGE_BASE_URL:-}" \
SKIP_NGINX_ENSURE=1 \
bash build_web.sh --mode release --api-base-url "${api_url}" --clean --install-deps; then
log_err "Frontend build failed."
exit 1
fi
# اطمینان از nginx برای UI در «گام ۴» انجام می‌شود (یک‌بار، idempotent)؛ اینجا عمداً SKIP_NGINX_ENSURE تا دوباره‌کاری نشود.
build_output="${app_dir}/hesabixUI/hesabix_ui/build/web"
if [[ ! -f "${build_output}/index.html" ]]; then
log_err "Build output missing index.html."
exit 1
fi
mkdir -p "/var/www/${UI_DOMAIN}"
rsync -a --delete "${build_output}/" "/var/www/${UI_DOMAIN}/"
chown -R www-data:www-data "/var/www/${UI_DOMAIN}"
log_ok "Frontend built and deployed to /var/www/${UI_DOMAIN}."
# --- 4. Nginx: ensure client_max_body_size 1g for database restore, then reload ---
log_info "Step 4: Updating Nginx config and reloading..."
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
hesabix_nginx_bin() {
if command -v nginx >/dev/null 2>&1; then
command -v nginx
return 0
fi
if [[ -x /usr/sbin/nginx ]]; then
echo /usr/sbin/nginx
return 0
fi
return 1
}
ensure_ai_chat_sse_location() {
local conf="$1"
[[ -f "$conf" ]] || return 0
if grep -q 'location \^~ /api/v1/ai/chat/' "$conf"; then
return 0
fi
python3 - "$conf" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
text = path.read_text()
needle = " location /api/ {\n"
if "location ^~ /api/v1/ai/chat/" in text or needle not in text:
raise SystemExit(0)
block = """ # AI chat SSE: stream chunks immediately (no proxy buffering)
location ^~ /api/v1/ai/chat/ {
proxy_pass http://127.0.0.1:8000/api/v1/ai/chat/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 600;
proxy_connect_timeout 60;
proxy_send_timeout 600;
client_max_body_size 1g;
proxy_buffering off;
proxy_request_buffering off;
proxy_cache off;
gzip off;
add_header X-Accel-Buffering no always;
}
"""
path.write_text(text.replace(needle, block + needle, 1))
PY
}
if [[ -f /etc/nginx/sites-available/hesabix-api.conf ]]; then
if grep -q 'client_max_body_size' /etc/nginx/sites-available/hesabix-api.conf; then
sed -i 's/client_max_body_size [0-9]*[kmgKMG]*/client_max_body_size 1g/' /etc/nginx/sites-available/hesabix-api.conf
else
sed -i '/proxy_send_timeout 300;/a\ client_max_body_size 1g;' /etc/nginx/sites-available/hesabix-api.conf
fi
log_info "Ensured client_max_body_size 1g (database restore uploads)."
fi
ensure_ai_chat_sse_location /etc/nginx/sites-available/hesabix-api.conf
ensure_ai_chat_sse_location /etc/nginx/sites-available/hesabix-ui.conf
log_info "Ensured AI chat SSE nginx location (no proxy buffering)."
ensure_voice_ws_nginx() {
local conf="$1"
[[ -f "$conf" ]] || return 0
if grep -q 'location /ws/' "$conf"; then
if ! grep -q 'proxy_read_timeout 86400' "$conf" 2>/dev/null; then
sed -i '/location \/ws\//,/^[[:space:]]*}/ s/proxy_read_timeout [0-9]*;/proxy_read_timeout 86400;/' "$conf" 2>/dev/null || true
fi
return 0
fi
python3 - "$conf" <<'PY'
from pathlib import Path
import sys
path = Path(sys.argv[1])
text = path.read_text()
if "location /ws/" in text:
raise SystemExit(0)
block = """
# WebSocket (/ws/ai/voice, notifications, ...)
location /ws/ {
proxy_pass http://127.0.0.1:8000/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
}
"""
needle = " location /api/ {\n"
if needle not in text:
raise SystemExit(0)
path.write_text(text.replace(needle, block + needle, 1))
PY
}
ensure_voice_ws_nginx /etc/nginx/sites-available/hesabix-api.conf
ensure_voice_ws_nginx /etc/nginx/sites-available/hesabix-ui.conf
log_info "Ensured WebSocket nginx location (/ws/ai/voice)."
# UI: version.json، service worker، flutter_bootstrap.js و main.dart.js بدون کش یک‌سالهٔ immutable
# اگر هر دو location ورودی JS از قبل باشد، اسکریپت بلافاصله خارج می‌شود و فایل nginx را دست نمی‌زند.
ensure_ui_nginx="${app_dir}/scripts/ensure_nginx_ui_version_probe.sh"
if [[ -f "${ensure_ui_nginx}" ]]; then
chmod +x "${ensure_ui_nginx}" 2>/dev/null || true
log_info "Step 4 (UI): Ensuring nginx cache rules for Flutter web (idempotent)..."
if bash "${ensure_ui_nginx}"; then
log_ok "Nginx UI version/SW/entry-JS rules verified or updated."
else
log_info "Nginx UI ensure skipped or failed (e.g. no hesabix-ui.conf on this host); non-fatal."
fi
else
log_info "ensure_nginx_ui_version_probe.sh not found at ${ensure_ui_nginx}; skipped."
fi
NGINX_BIN="$(hesabix_nginx_bin)" || {
log_err "nginx not found in PATH or /usr/sbin/nginx"
exit 1
}
nginx_test_out="$("$NGINX_BIN" -t 2>&1)" || nginx_test_rc=$?
if [[ -n "${nginx_test_out:-}" ]]; then
echo "${nginx_test_out}" | tee -a "${LOG_FILE}"
fi
if [[ "${nginx_test_rc:-0}" -ne 0 ]]; then
log_err "Nginx config test failed (${NGINX_BIN} -t exit ${nginx_test_rc})."
exit 1
fi
if ! systemctl reload nginx; then
log_err "systemctl reload nginx failed."
exit 1
fi
log_ok "Nginx reloaded."
# --- 5. Optional health check ---
if command -v curl >/dev/null 2>&1; then
if curl -sSf --connect-timeout 5 "https://${API_DOMAIN}/api/v1/health" >/dev/null 2>&1 || \
curl -sSf --connect-timeout 5 "http://127.0.0.1:8000/api/v1/health" >/dev/null 2>&1; then
log_ok "API health check passed."
else
log_info "API health check skipped or failed (non-fatal)."
fi
fi
echo "==========================================" | tee -a "${LOG_FILE}"
log_ok "Hesabix update completed."
echo "==========================================" | tee -a "${LOG_FILE}"