Files
bssapp/deploy/deploy.sh
2026-04-03 15:47:22 +03:00

371 lines
9.3 KiB
Bash
Raw 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.
#!/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
}
ensure_node20_for_ui_build() {
local required_major=20
local nvm_dir="${NVM_DIR:-$HOME/.nvm}"
if [[ -s "$nvm_dir/nvm.sh" ]]; then
# shellcheck disable=SC1090
source "$nvm_dir/nvm.sh"
nvm install "$required_major" >/dev/null
nvm use "$required_major" >/dev/null
fi
if ! command -v node >/dev/null 2>&1; then
echo "ERROR: node command not found"
return 1
fi
local node_version
node_version="$(node -v 2>/dev/null || true)"
local node_major
node_major="$(echo "$node_version" | sed -E 's/^v([0-9]+).*/\1/')"
if [[ -z "$node_major" || "$node_major" -lt "$required_major" ]]; then
echo "ERROR: Node.js >=${required_major} required for UI build. Current: ${node_version:-unknown}"
echo "Hint: install nvm and run: nvm install ${required_major} && nvm alias default ${required_major}"
return 1
fi
echo "UI build runtime: node=$node_version npm=$(npm -v)"
}
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"
ensure_node20_for_ui_build
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 >/dev/null 2>&1 &
exit 0