Files
sbnews/scripts/deploy-server.sh
T
2026-07-11 11:19:43 +08:00

700 lines
20 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./scripts/deploy-server.sh --remote-dir <remote_dir> [options]
Modes:
1. Full deploy (no commit ids)
./scripts/deploy-server.sh --remote-dir /www/wwwroot/api.sbnews.net
2. Incremental deploy (compare two commits)
./scripts/deploy-server.sh \
--from <old_commit> \
--to <new_commit> \
--remote-dir /www/wwwroot/api.sbnews.net
3. Auto incremental deploy (compare latest two commits)
./scripts/deploy-server.sh \
--auto-incremental \
--remote-dir /www/wwwroot/api.sbnews.net
Options:
--source-dir <dir> Local source directory, default: server
--remote-dir <dir> Remote install directory, required
--host <ssh_alias> SSH host/alias, default: sbnews
--ssh-bin <cmd> SSH command, default: ssh. Use ssh.exe in WSL to reuse Windows SSH config
--auto-incremental Incremental deploy using HEAD~1 as --from and HEAD as --to
--from <commit> Base commit for incremental deploy
--to <commit> Target commit for incremental deploy
--remote-tmp <dir> Remote temp directory, default: /tmp
--post-install <cmd> Run command on server after extract/install
--sudo Use sudo on server when extracting/removing files
--keep-remote-temp Keep remote temp files after install
--include-vendor Include vendor directory in full package
--include-env Include .env in full package
--include-runtime Include runtime directory in full package
--include-uploads Include public/uploads directory in full package
--include-public-builds Include public/admin and public/mobile in full package
--include-crawler-data Include crawler data/logs/venv cache directories in full package
--dry-run Build package and print changed/deleted files, do not upload
-h, --help Show this help
Notes:
- Full deploy packages only these server paths by default:
app, config, extend, public/dongqiudi-crawler, route.
- Auto incremental deploy compares HEAD~1 and HEAD.
- Incremental deploy compares two commit ids and packages exact file contents from --to.
- If server is a root gitlink without server/.git metadata, incremental deploy falls back
to packaging the current working tree for the default server include paths.
- Deleted files between --from and --to are removed on the server.
- Upload uses ssh stdin instead of scp: ssh <host> "cat > archive" < local.tar.gz
EOF
}
now_ms() {
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
elif command -v python >/dev/null 2>&1; then
python - <<'PY'
import time
print(int(time.time() * 1000))
PY
else
printf '%s000\n' "$(date +%s)"
fi
}
format_duration() {
local ms="$1"
local seconds=$((ms / 1000))
local millis=$((ms % 1000))
local minutes=$((seconds / 60))
seconds=$((seconds % 60))
if [[ "$minutes" -gt 0 ]]; then
printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis"
else
printf '%d.%03ds' "$seconds" "$millis"
fi
}
DEPLOY_START_MS="$(now_ms)"
DEPLOY_LAST_LOG_MS="$DEPLOY_START_MS"
log() {
local current_ms
local total_ms
local step_ms
current_ms="$(now_ms)"
total_ms=$((current_ms - DEPLOY_START_MS))
step_ms=$((current_ms - DEPLOY_LAST_LOG_MS))
DEPLOY_LAST_LOG_MS="$current_ms"
printf '[deploy][+%s][total %s] %s\n' "$(format_duration "$step_ms")" "$(format_duration "$total_ms")" "$*"
}
fail() {
printf '[deploy][error] %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail "missing command: $1"
}
normalize_dir() {
local value="$1"
value="${value%/}"
printf '%s' "$value"
}
safe_remote_single_quote() {
printf "%s" "$1" | sed "s/'/'\\\\''/g"
}
SOURCE_DIR="server"
REMOTE_DIR=""
HOST="sbnews"
SSH_BIN="ssh"
AUTO_INCREMENTAL=0
FROM_COMMIT=""
TO_COMMIT=""
REMOTE_TMP="/tmp"
POST_INSTALL=""
USE_SUDO=0
KEEP_REMOTE_TEMP=0
INCLUDE_VENDOR=0
INCLUDE_ENV=0
INCLUDE_RUNTIME=0
INCLUDE_UPLOADS=0
INCLUDE_PUBLIC_BUILDS=0
INCLUDE_CRAWLER_DATA=0
DRY_RUN=0
FULL_INCLUDE_PATHS=(
"app"
"config"
"extend"
"public/dongqiudi-crawler"
"route"
)
while [[ $# -gt 0 ]]; do
case "$1" in
--source-dir)
[[ $# -ge 2 ]] || fail "--source-dir requires a value"
SOURCE_DIR="$2"
shift 2
;;
--remote-dir)
[[ $# -ge 2 ]] || fail "--remote-dir requires a value"
REMOTE_DIR="$2"
shift 2
;;
--host)
[[ $# -ge 2 ]] || fail "--host requires a value"
HOST="$2"
shift 2
;;
--ssh-bin)
[[ $# -ge 2 ]] || fail "--ssh-bin requires a value"
SSH_BIN="$2"
shift 2
;;
--auto-incremental)
AUTO_INCREMENTAL=1
shift
;;
--from)
[[ $# -ge 2 ]] || fail "--from requires a value"
FROM_COMMIT="$2"
shift 2
;;
--to)
[[ $# -ge 2 ]] || fail "--to requires a value"
TO_COMMIT="$2"
shift 2
;;
--remote-tmp)
[[ $# -ge 2 ]] || fail "--remote-tmp requires a value"
REMOTE_TMP="$2"
shift 2
;;
--post-install)
[[ $# -ge 2 ]] || fail "--post-install requires a value"
POST_INSTALL="$2"
shift 2
;;
--sudo)
USE_SUDO=1
shift
;;
--keep-remote-temp)
KEEP_REMOTE_TEMP=1
shift
;;
--include-vendor)
INCLUDE_VENDOR=1
shift
;;
--include-env)
INCLUDE_ENV=1
shift
;;
--include-runtime)
INCLUDE_RUNTIME=1
shift
;;
--include-uploads)
INCLUDE_UPLOADS=1
shift
;;
--include-public-builds)
INCLUDE_PUBLIC_BUILDS=1
shift
;;
--include-crawler-data)
INCLUDE_CRAWLER_DATA=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
fail "unknown argument: $1"
;;
esac
done
[[ -n "$REMOTE_DIR" ]] || {
usage
fail "--remote-dir is required"
}
SOURCE_DIR="$(normalize_dir "$SOURCE_DIR")"
REMOTE_DIR="$(normalize_dir "$REMOTE_DIR")"
REMOTE_TMP="$(normalize_dir "$REMOTE_TMP")"
if [[ "$AUTO_INCREMENTAL" -eq 1 ]]; then
[[ -z "$FROM_COMMIT" && -z "$TO_COMMIT" ]] || fail "--auto-incremental cannot be used with --from/--to"
FROM_COMMIT="HEAD~1"
TO_COMMIT="HEAD"
MODE="incremental"
elif [[ -n "$FROM_COMMIT" || -n "$TO_COMMIT" ]]; then
[[ -n "$FROM_COMMIT" && -n "$TO_COMMIT" ]] || fail "incremental deploy requires both --from and --to"
MODE="incremental"
else
MODE="full"
fi
require_cmd git
require_cmd tar
require_cmd "$SSH_BIN"
if [[ -n "$POST_INSTALL" ]]; then
require_cmd base64
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
SOURCE_ABS="$REPO_ROOT/$SOURCE_DIR"
cd "$REPO_ROOT"
[[ -d "$SOURCE_ABS" ]] || fail "source directory not found: $SOURCE_DIR"
GIT_CWD="$REPO_ROOT"
GIT_PATH_PREFIX="$SOURCE_DIR"
SOURCE_IS_GIT_REPO=0
SOURCE_IS_GITLINK=0
if SOURCE_GIT_TOP="$(git -C "$SOURCE_ABS" rev-parse --show-toplevel 2>/dev/null)" && [[ "$(cd "$SOURCE_GIT_TOP" && pwd -P)" == "$SOURCE_ABS" ]]; then
GIT_CWD="$SOURCE_ABS"
GIT_PATH_PREFIX=""
SOURCE_IS_GIT_REPO=1
elif ! git -C "$REPO_ROOT" rev-parse --show-toplevel >/dev/null 2>&1; then
[[ "$MODE" == "full" ]] || fail "incremental deploy requires git metadata in $SOURCE_DIR or repo root"
fi
if [[ "$SOURCE_IS_GIT_REPO" -ne 1 ]] && git -C "$REPO_ROOT" ls-files -s -- "$SOURCE_DIR" | awk '{print $1}' | grep -qx '160000'; then
SOURCE_IS_GITLINK=1
fi
if [[ "$MODE" == "incremental" ]]; then
git -C "$GIT_CWD" rev-parse --verify "${FROM_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --from commit: $FROM_COMMIT"
git -C "$GIT_CWD" rev-parse --verify "${TO_COMMIT}^{commit}" >/dev/null 2>&1 || fail "invalid --to commit: $TO_COMMIT"
FROM_COMMIT="$(git -C "$GIT_CWD" rev-parse "$FROM_COMMIT")"
TO_COMMIT="$(git -C "$GIT_CWD" rev-parse "$TO_COMMIT")"
fi
mkdir -p "$REPO_ROOT/.tmp"
WORK_DIR="$(mktemp -d "$REPO_ROOT/.tmp/deploy-server.XXXXXX")"
PACKAGE_ROOT="$WORK_DIR/package"
PAYLOAD_DIR="$PACKAGE_ROOT/payload"
EXTRACT_DIR="$WORK_DIR/extract"
ARCHIVE_PATH="$WORK_DIR/${SOURCE_DIR//\//-}-${MODE}.tar.gz"
DELETED_PATHS_FILE="$PACKAGE_ROOT/.deleted-paths.txt"
DEPLOY_INFO_FILE="$PACKAGE_ROOT/.deploy-info.txt"
CHANGED_LOG="$WORK_DIR/changed-paths.txt"
DELETED_LOG="$WORK_DIR/deleted-paths.txt"
cleanup_local() {
if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]]; then
chmod -R u+rwX "$WORK_DIR" 2>/dev/null || true
rm -rf "$WORK_DIR" || true
fi
}
trap cleanup_local EXIT
mkdir -p "$PAYLOAD_DIR" "$EXTRACT_DIR"
touch "$DELETED_PATHS_FILE" "$CHANGED_LOG" "$DELETED_LOG"
copy_full_payload() {
local tar_args=()
tar_args+=(--exclude=.git)
tar_args+=(--exclude=.idea)
tar_args+=(--exclude=.vscode)
tar_args+=(--exclude=.tmp)
tar_args+=(--exclude=.gitignore)
tar_args+=(--exclude='*.log')
tar_args+=(--exclude='release-*.zip')
tar_args+=(--exclude='*.zip')
tar_args+=(--exclude='__pycache__')
tar_args+=(--exclude='*.pyc')
if [[ "$INCLUDE_ENV" -ne 1 ]]; then
tar_args+=(--exclude=.env)
fi
if [[ "$INCLUDE_VENDOR" -ne 1 ]]; then
tar_args+=(--exclude=vendor)
fi
if [[ "$INCLUDE_RUNTIME" -ne 1 ]]; then
tar_args+=(--exclude=runtime)
fi
if [[ "$INCLUDE_UPLOADS" -ne 1 ]]; then
tar_args+=(--exclude=public/uploads)
fi
if [[ "$INCLUDE_PUBLIC_BUILDS" -ne 1 ]]; then
tar_args+=(--exclude=public/admin)
tar_args+=(--exclude=public/mobile)
fi
if [[ "$INCLUDE_CRAWLER_DATA" -ne 1 ]]; then
tar_args+=(--exclude=public/dongqiudi-crawler/data)
tar_args+=(--exclude=public/dongqiudi-crawler/logs)
tar_args+=(--exclude=public/dongqiudi-crawler/venv)
tar_args+=(--exclude=public/dongqiudi-crawler/__pycache__)
fi
log "building full payload from current working tree: $SOURCE_DIR"
log "full include paths: ${FULL_INCLUDE_PATHS[*]}"
(
cd "$SOURCE_ABS"
tar "${tar_args[@]}" -cf - "${FULL_INCLUDE_PATHS[@]}"
) | (
cd "$PAYLOAD_DIR"
tar -xf -
)
}
strip_source_prefix() {
local path="$1"
if [[ -z "$GIT_PATH_PREFIX" ]]; then
printf '%s' "$path"
else
printf '%s' "${path#"$GIT_PATH_PREFIX"/}"
fi
}
is_under_source_dir() {
local path="$1"
if [[ -z "$GIT_PATH_PREFIX" ]]; then
is_deploy_included_path "$path"
return $?
fi
[[ "$path" == "$GIT_PATH_PREFIX/"* ]] || return 1
is_deploy_included_path "$(strip_source_prefix "$path")"
}
is_deploy_included_path() {
local rel="$1"
local include
for include in "${FULL_INCLUDE_PATHS[@]}"; do
if [[ "$rel" == "$include" || "$rel" == "$include/"* ]]; then
return 0
fi
done
return 1
}
short_commit() {
git -C "$GIT_CWD" rev-parse --short "$1" 2>/dev/null || printf '%s' "$1"
}
print_incremental_file_list() {
local title="$1"
local prefix="$2"
local file="$3"
log "$title"
if [[ -s "$file" ]]; then
sort -u "$file" | sed '/^$/d' | sed "s/^/ $prefix /"
else
printf ' (none)\n'
fi
}
print_update_log() {
local from_short=""
local to_short=""
local commit_lines=""
if [[ "$MODE" == "incremental" ]]; then
from_short="$(short_commit "$FROM_COMMIT")"
to_short="$(short_commit "$TO_COMMIT")"
log "update log:"
log "commit range: $from_short -> $to_short"
if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then
log "$SOURCE_DIR is a root gitlink, showing root repository commits for the selected range"
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)"
elif [[ -n "$GIT_PATH_PREFIX" ]]; then
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" -- "$GIT_PATH_PREFIX" 2>/dev/null || true)"
else
commit_lines="$(git -C "$GIT_CWD" log --no-merges --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' "$FROM_COMMIT..$TO_COMMIT" 2>/dev/null || true)"
fi
log "commits:"
if [[ -n "$commit_lines" ]]; then
printf '%s\n' "$commit_lines"
else
printf ' (none)\n'
fi
print_incremental_file_list "changed files:" "+" "$CHANGED_LOG"
print_incremental_file_list "deleted files:" "-" "$DELETED_LOG"
else
log "update log:"
log "mode: full deploy from current working tree"
log "full include paths: ${FULL_INCLUDE_PATHS[*]}"
commit_lines="$(git -C "$GIT_CWD" log -1 --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:' * %h %ad %an %s' 2>/dev/null || true)"
log "latest commit:"
if [[ -n "$commit_lines" ]]; then
printf '%s\n' "$commit_lines"
else
printf ' (none)\n'
fi
fi
}
build_incremental_payload() {
local raw_diff="$WORK_DIR/diff-name-status.txt"
local archive_include="$WORK_DIR/incremental.tar"
local diff_args=()
if [[ "$SOURCE_IS_GITLINK" -eq 1 ]]; then
log "$SOURCE_DIR is tracked as a root gitlink; exact file-level diff is unavailable without $SOURCE_DIR/.git"
log "building fallback payload from current working tree include paths: ${FULL_INCLUDE_PATHS[*]}"
printf '%s\n' "${FULL_INCLUDE_PATHS[@]}" > "$CHANGED_LOG"
copy_full_payload
return
fi
if [[ -n "$GIT_PATH_PREFIX" ]]; then
diff_args+=(-- "$GIT_PATH_PREFIX")
fi
git -C "$GIT_CWD" diff --name-status --find-renames "$FROM_COMMIT" "$TO_COMMIT" "${diff_args[@]}" > "$raw_diff"
while IFS=$'\t' read -r status path1 path2; do
[[ -n "${status:-}" ]] || continue
case "$status" in
D)
is_under_source_dir "$path1" || continue
strip_source_prefix "$path1" >> "$DELETED_LOG"
;;
R*|C*)
[[ -n "${path2:-}" ]] || continue
is_under_source_dir "$path2" || continue
printf '%s\n' "$path2" >> "$CHANGED_LOG"
if [[ "$status" == R* ]] && is_under_source_dir "$path1"; then
strip_source_prefix "$path1" >> "$DELETED_LOG"
fi
;;
*)
is_under_source_dir "$path1" || continue
printf '%s\n' "$path1" >> "$CHANGED_LOG"
;;
esac
done < "$raw_diff"
mapfile -t changed_paths < <(sort -u "$CHANGED_LOG" | sed '/^$/d')
mapfile -t deleted_paths < <(sort -u "$DELETED_LOG" | sed '/^$/d')
if [[ "${#changed_paths[@]}" -eq 0 && "${#deleted_paths[@]}" -eq 0 ]]; then
log "no changed files detected between $FROM_COMMIT and $TO_COMMIT under $SOURCE_DIR"
exit 0
fi
if [[ "${#deleted_paths[@]}" -gt 0 ]]; then
printf '%s\n' "${deleted_paths[@]}" > "$DELETED_PATHS_FILE"
fi
if [[ "${#changed_paths[@]}" -gt 0 ]]; then
log "building incremental payload from commit $TO_COMMIT"
git -C "$GIT_CWD" archive --format=tar --output="$archive_include" "$TO_COMMIT" -- "${changed_paths[@]}"
tar -xf "$archive_include" -C "$EXTRACT_DIR"
if [[ "$SOURCE_IS_GIT_REPO" -eq 1 ]]; then
(
cd "$EXTRACT_DIR"
tar -cf - .
) | (
cd "$PAYLOAD_DIR"
tar -xf -
)
elif [[ -d "$EXTRACT_DIR/$SOURCE_DIR" ]]; then
(
cd "$EXTRACT_DIR/$SOURCE_DIR"
tar -cf - .
) | (
cd "$PAYLOAD_DIR"
tar -xf -
)
fi
fi
}
if [[ "$MODE" == "full" ]]; then
copy_full_payload
else
build_incremental_payload
fi
print_update_log
{
printf 'mode=%s\n' "$MODE"
printf 'source_dir=%s\n' "$SOURCE_DIR"
printf 'host=%s\n' "$HOST"
printf 'remote_dir=%s\n' "$REMOTE_DIR"
printf 'built_at=%s\n' "$(date '+%Y-%m-%d %H:%M:%S')"
if [[ "$MODE" == "incremental" ]]; then
printf 'auto_incremental=%s\n' "$AUTO_INCREMENTAL"
printf 'source_is_gitlink=%s\n' "$SOURCE_IS_GITLINK"
printf 'from_commit=%s\n' "$FROM_COMMIT"
printf 'to_commit=%s\n' "$TO_COMMIT"
printf 'changed_count=%s\n' "$(sort -u "$CHANGED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')"
printf 'deleted_count=%s\n' "$(sort -u "$DELETED_LOG" | sed '/^$/d' | wc -l | tr -d ' ')"
fi
} > "$DEPLOY_INFO_FILE"
log "packing archive: $ARCHIVE_PATH"
(
cd "$PACKAGE_ROOT"
tar -czf "$ARCHIVE_PATH" .
)
log "package size: $(du -h "$ARCHIVE_PATH" | awk '{print $1}')"
if [[ "$DRY_RUN" -eq 1 ]]; then
log "dry-run enabled, skip upload/install"
exit 0
fi
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
REMOTE_WORK_DIR="$REMOTE_TMP/${SOURCE_DIR//\//-}-deploy-$TIMESTAMP-$$"
REMOTE_ARCHIVE="$REMOTE_TMP/${SOURCE_DIR//\//-}-${MODE}-$TIMESTAMP.tar.gz"
REMOTE_TMP_Q="$(safe_remote_single_quote "$REMOTE_TMP")"
REMOTE_ARCHIVE_Q="$(safe_remote_single_quote "$REMOTE_ARCHIVE")"
log "uploading archive to $HOST:$REMOTE_ARCHIVE"
"$SSH_BIN" "$HOST" "mkdir -p '$REMOTE_TMP_Q' && cat > '$REMOTE_ARCHIVE_Q'" < "$ARCHIVE_PATH"
POST_INSTALL_B64=""
if [[ -n "$POST_INSTALL" ]]; then
POST_INSTALL_B64="$(printf '%s' "$POST_INSTALL" | base64 | tr -d '\r\n')"
fi
log "installing on server: $REMOTE_DIR"
"$SSH_BIN" "$HOST" \
"REMOTE_DIR='$(safe_remote_single_quote "$REMOTE_DIR")' REMOTE_ARCHIVE='$(safe_remote_single_quote "$REMOTE_ARCHIVE")' REMOTE_WORK_DIR='$(safe_remote_single_quote "$REMOTE_WORK_DIR")' KEEP_REMOTE_TEMP='$KEEP_REMOTE_TEMP' POST_INSTALL_B64='$POST_INSTALL_B64' USE_SUDO='$USE_SUDO' bash -s" <<'REMOTE_SCRIPT'
set -euo pipefail
remote_now_ms() {
if command -v python3 >/dev/null 2>&1; then
python3 - <<'PY'
import time
print(int(time.time() * 1000))
PY
elif command -v python >/dev/null 2>&1; then
python - <<'PY'
import time
print(int(time.time() * 1000))
PY
else
printf '%s000\n' "$(date +%s)"
fi
}
remote_format_duration() {
local ms="$1"
local seconds=$((ms / 1000))
local millis=$((ms % 1000))
local minutes=$((seconds / 60))
seconds=$((seconds % 60))
if [[ "$minutes" -gt 0 ]]; then
printf '%dm%02d.%03ds' "$minutes" "$seconds" "$millis"
else
printf '%d.%03ds' "$seconds" "$millis"
fi
}
REMOTE_START_MS="$(remote_now_ms)"
REMOTE_LAST_LOG_MS="$REMOTE_START_MS"
remote_log() {
local current_ms
local total_ms
local step_ms
current_ms="$(remote_now_ms)"
total_ms=$((current_ms - REMOTE_START_MS))
step_ms=$((current_ms - REMOTE_LAST_LOG_MS))
REMOTE_LAST_LOG_MS="$current_ms"
printf '[deploy][remote][+%s][total %s] %s\n' "$(remote_format_duration "$step_ms")" "$(remote_format_duration "$total_ms")" "$*"
}
run_priv() {
if [[ "${USE_SUDO:-0}" == "1" ]]; then
sudo "$@"
else
"$@"
fi
}
tar_extract_flags=(--no-same-owner --no-same-permissions --no-overwrite-dir)
remote_log "prepare remote work dir: $REMOTE_WORK_DIR"
mkdir -p "$REMOTE_WORK_DIR"
remote_log "extract uploaded archive"
tar -xzf "$REMOTE_ARCHIVE" -C "$REMOTE_WORK_DIR"
remote_log "ensure remote dir: $REMOTE_DIR"
run_priv mkdir -p "$REMOTE_DIR"
if [[ -d "$REMOTE_WORK_DIR/payload" ]]; then
remote_log "install payload into remote dir"
tar -cf - -C "$REMOTE_WORK_DIR/payload" . | run_priv tar "${tar_extract_flags[@]}" -xf - -C "$REMOTE_DIR"
fi
if [[ -f "$REMOTE_WORK_DIR/.deleted-paths.txt" ]]; then
remote_log "apply deleted paths"
while IFS= read -r rel; do
[[ -n "$rel" ]] || continue
case "$rel" in
*".."*|/*)
echo "[deploy][remote][warn] skip unsafe delete path: $rel" >&2
continue
;;
esac
run_priv rm -rf "$REMOTE_DIR/$rel"
done < "$REMOTE_WORK_DIR/.deleted-paths.txt"
fi
if [[ -n "${POST_INSTALL_B64:-}" ]]; then
remote_log "run post-install command"
POST_INSTALL_CMD="$(printf '%s' "$POST_INSTALL_B64" | base64 -d)"
cd "$REMOTE_DIR"
if [[ "${USE_SUDO:-0}" == "1" ]]; then
sudo bash -lc "$POST_INSTALL_CMD"
else
bash -lc "$POST_INSTALL_CMD"
fi
fi
if [[ "${KEEP_REMOTE_TEMP:-0}" != "1" ]]; then
remote_log "clean remote temp files"
rm -rf "$REMOTE_WORK_DIR" "$REMOTE_ARCHIVE"
fi
remote_log "remote install finished"
REMOTE_SCRIPT
log "deploy finished"
log "mode: $MODE"
log "source: $SOURCE_DIR"
log "remote: $HOST:$REMOTE_DIR"