#!/bin/bash set -euo pipefail umask 022 export NODE_OPTIONS="--max_old_space_size=4096" export CI="true" export npm_config_progress="false" export npm_config_loglevel="warn" export FORCE_COLOR="0" LOG_FILE="/var/log/bssapp_deploy.log" APP_DIR="/opt/bssapp" LOCK_FILE="/tmp/bssapp_deploy.lock" RUNTIME_BACKUP_DIR="" RUNTIME_PRESERVE_FILES=( ".env" "mail.env" "svc/.env" "svc/mail.env" "svc/fonts" "svc/public" "deploy/deploy.sh" "scripts/deploy.sh" ) log_step() { echo "== $1 ==" } backup_runtime_files() { RUNTIME_BACKUP_DIR="$(mktemp -d /tmp/bssapp-runtime.XXXXXX)" for rel in "${RUNTIME_PRESERVE_FILES[@]}"; do src="$APP_DIR/$rel" dst="$RUNTIME_BACKUP_DIR/$rel" if [[ -e "$src" ]]; then mkdir -p "$(dirname "$dst")" cp -a "$src" "$dst" fi done } restore_runtime_files() { [[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]] || return 0 find "$RUNTIME_BACKUP_DIR" -mindepth 1 -print -quit | grep -q . || return 0 cp -a "$RUNTIME_BACKUP_DIR/." "$APP_DIR/" } cleanup_runtime_backup() { if [[ -n "$RUNTIME_BACKUP_DIR" && -d "$RUNTIME_BACKUP_DIR" ]]; then rm -rf "$RUNTIME_BACKUP_DIR" fi } ensure_runtime_env_files() { [[ -f "$APP_DIR/.env" ]] || touch "$APP_DIR/.env" [[ -f "$APP_DIR/mail.env" ]] || touch "$APP_DIR/mail.env" [[ -f "$APP_DIR/svc/.env" ]] || touch "$APP_DIR/svc/.env" [[ -f "$APP_DIR/svc/mail.env" ]] || touch "$APP_DIR/svc/mail.env" } ensure_pdf_fonts() { local font_dir="$APP_DIR/svc/fonts" local sys_font_dir="/usr/share/fonts/truetype/dejavu" mkdir -p "$font_dir" if [[ ! -f "$font_dir/DejaVuSans.ttf" && -f "$sys_font_dir/DejaVuSans.ttf" ]]; then cp -a "$sys_font_dir/DejaVuSans.ttf" "$font_dir/DejaVuSans.ttf" fi if [[ ! -f "$font_dir/DejaVuSans-Bold.ttf" && -f "$sys_font_dir/DejaVuSans-Bold.ttf" ]]; then cp -a "$sys_font_dir/DejaVuSans-Bold.ttf" "$font_dir/DejaVuSans-Bold.ttf" fi if [[ ! -f "$font_dir/DejaVuSans.ttf" || ! -f "$font_dir/DejaVuSans-Bold.ttf" ]]; then echo "ERROR: Required PDF fonts missing in $font_dir" return 1 fi } ensure_ui_permissions() { local ui_root="$APP_DIR/ui/dist/spa" if [[ ! -d "$ui_root" ]]; then echo "ERROR: UI build output not found at $ui_root" return 1 fi chmod 755 /opt "$APP_DIR" "$APP_DIR/ui" "$APP_DIR/ui/dist" "$ui_root" find "$ui_root" -type d -exec chmod 755 {} \; find "$ui_root" -type f -exec chmod 644 {} \; } clean_ui_build_artifacts() { cd "$APP_DIR/ui" # dist'i silmiyoruz -> eski chunklar kısa süre kalabilir, ChunkLoadError azalır rm -rf .quasar node_modules/.cache || true } purge_nginx_ui_cache() { rm -rf /var/cache/nginx/* || true } purge_cdn_html_cache() { local zone_id="${CF_ZONE_ID:-}" local api_token="${CF_API_TOKEN:-}" local site_url="${SITE_URL:-https://ss.baggi.com.tr}" local purge_urls="${CDN_PURGE_URLS:-$site_url/,$site_url/index.html}" if [[ -z "$zone_id" || -z "$api_token" ]]; then echo "CDN purge skipped: CF_ZONE_ID / CF_API_TOKEN not set." return 0 fi IFS=',' read -r -a url_array <<< "$purge_urls" if [[ ${#url_array[@]} -eq 0 ]]; then echo "CDN purge skipped: no URLs configured." return 0 fi local files_json="" local sep="" local url="" for raw in "${url_array[@]}"; do url="$(echo "$raw" | xargs)" [[ -n "$url" ]] || continue files_json="${files_json}${sep}\"${url}\"" sep="," done if [[ -z "$files_json" ]]; then echo "CDN purge skipped: URL list resolved to empty." return 0 fi local payload payload="{\"files\":[${files_json}]}" local response response="$(curl -sS -X POST "https://api.cloudflare.com/client/v4/zones/${zone_id}/purge_cache" \ -H "Authorization: Bearer ${api_token}" \ -H "Content-Type: application/json" \ --data "$payload" || true)" if echo "$response" | grep -q '"success":true'; then echo "CDN HTML purge completed." return 0 fi echo "WARN: CDN purge may have failed. Response: $response" return 0 } extract_app_js_name() { local source="$1" echo "$source" | grep -oE 'app\.[a-f0-9]+\.js' | head -n1 || true } verify_live_ui_hash() { local site_url="${SITE_URL:-https://ss.baggi.com.tr}" local fail_on_mismatch="${FAIL_ON_UI_HASH_MISMATCH:-false}" local local_index="$APP_DIR/ui/dist/spa/index.html" if [[ ! -f "$local_index" ]]; then echo "WARN: local index not found for hash verify: $local_index" return 0 fi local local_app local_app="$(extract_app_js_name "$(cat "$local_index")")" if [[ -z "$local_app" ]]; then echo "WARN: local app hash parse failed." return 0 fi local live_html live_html="$(curl -sS -H "Cache-Control: no-cache" -H "Pragma: no-cache" "${site_url}/" || true)" if [[ -z "$live_html" ]]; then echo "WARN: live index fetch failed for ${site_url}/" return 0 fi local live_app live_app="$(extract_app_js_name "$live_html")" if [[ -z "$live_app" ]]; then echo "WARN: live app hash parse failed." return 0 fi if [[ "$local_app" == "$live_app" ]]; then echo "UI HASH OK: ${local_app}" return 0 fi echo "WARN: UI hash mismatch local=${local_app} live=${live_app}" if [[ "$fail_on_mismatch" == "true" ]]; then echo "ERROR: FAIL_ON_UI_HASH_MISMATCH=true and UI hash mismatch detected." return 1 fi return 0 } ensure_ui_readable_by_nginx() { local ui_index="$APP_DIR/ui/dist/spa/index.html" if [[ ! -f "$ui_index" ]]; then echo "ERROR: UI index not found at $ui_index" return 1 fi if id -u www-data >/dev/null 2>&1; then if ! su -s /bin/sh -c "test -r '$ui_index'" www-data; then echo "ERROR: www-data cannot read $ui_index" namei -l "$ui_index" || true return 1 fi fi } build_api_binary() { if ! command -v go >/dev/null 2>&1; then echo "ERROR: go command not found" return 1 fi export GOPATH="${GOPATH:-/var/cache/bssapp-go}" export GOMODCACHE="${GOMODCACHE:-$GOPATH/pkg/mod}" export GOCACHE="${GOCACHE:-/var/cache/bssapp-go-build}" mkdir -p "$GOPATH" "$GOMODCACHE" "$GOCACHE" cd "$APP_DIR/svc" GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o "$APP_DIR/svc/bssapp" ./main.go chmod +x "$APP_DIR/svc/bssapp" } restart_services() { systemctl daemon-reload || true systemctl restart bssapp if ! systemctl is-active --quiet bssapp; then echo "ERROR: bssapp service failed to start" return 1 fi if systemctl cat nginx >/dev/null 2>&1; then if ! nginx -t; then echo "ERROR: nginx config test failed" return 1 fi systemctl reload nginx if ! systemctl is-active --quiet nginx; then echo "ERROR: nginx service failed to start" return 1 fi else echo "WARN: nginx service not found; frontend may be unreachable." fi } run_deploy() { trap cleanup_runtime_backup EXIT exec 9>"$LOCK_FILE" if ! flock -n 9; then echo "[$(date '+%F %T')] Deploy already running. Skipping new request." return 0 fi echo "==============================" echo "[DEPLOY START] $(date '+%F %T')" echo "==============================" cd "$APP_DIR" log_step "GIT SYNC" backup_runtime_files git fetch origin git reset --hard origin/master git clean -fdx \ -e .env \ -e mail.env \ -e svc/.env \ -e svc/mail.env \ -e svc/fonts \ -e svc/public \ -e deploy/deploy.sh \ -e scripts/deploy.sh \ -e svc/bssapp restore_runtime_files echo "DEPLOY COMMIT: $(git rev-parse --short HEAD)" log_step "BUILD UI" cd "$APP_DIR/ui" clean_ui_build_artifacts npm ci --no-audit --no-fund --include=optional npm i -D --no-audit --no-fund sass-embedded@1.93.2 npm run build log_step "ENSURE UI PERMISSIONS" ensure_ui_permissions ensure_ui_readable_by_nginx log_step "BUILD API" build_api_binary log_step "ENSURE ENV FILES" ensure_runtime_env_files log_step "ENSURE PDF FONTS" ensure_pdf_fonts log_step "RESTART SERVICES" restart_services log_step "PURGE NGINX CACHE" purge_nginx_ui_cache log_step "PURGE CDN HTML CACHE (OPTIONAL)" purge_cdn_html_cache log_step "VERIFY LIVE UI HASH" verify_live_ui_hash echo "[DEPLOY FINISHED] $(date '+%F %T')" } if [[ "${1:-}" == "--run" ]]; then mkdir -p "$(dirname "$LOG_FILE")" if command -v logger >/dev/null 2>&1; then run_deploy 2>&1 | tee -a "$LOG_FILE" >(logger -t bssapp-deploy -p user.info) exit ${PIPESTATUS[0]} else run_deploy >>"$LOG_FILE" 2>&1 exit $? fi fi nohup /bin/bash "$0" --run /dev/null 2>&1 & exit 0