From 1d25045c064f6c1c21eef30d904d2f3204e3e8e0 Mon Sep 17 00:00:00 2001 From: Mohamadhosein Moazennia Date: Fri, 20 Feb 2026 11:20:06 +0330 Subject: [PATCH] chore(scripts): consolidate deployment tooling and cleanup artifacts --- .gitignore | 3 +- replace-custom-panel.sh | 152 ------------------------------------ scripts/common.sh | 20 +++++ scripts/deploy_on_server.sh | 100 ++++++++++++++++++++++++ scripts/package_local.sh | 45 +++++++++++ scripts/upload_to_server.sh | 75 ++++++++++++++++++ 6 files changed, 242 insertions(+), 153 deletions(-) delete mode 100755 replace-custom-panel.sh create mode 100755 scripts/common.sh create mode 100755 scripts/deploy_on_server.sh create mode 100755 scripts/package_local.sh create mode 100755 scripts/upload_to_server.sh diff --git a/.gitignore b/.gitignore index 8fa4eeb0..3e995f48 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Ignore temporary files tmp/ *.tar.gz +3x-ui-custom-*.zip # Ignore build and distribution directories backup/ @@ -37,4 +38,4 @@ x-ui.db docker-compose.override.yml # Ignore .env (Environment Variables) file -.env \ No newline at end of file +.env diff --git a/replace-custom-panel.sh b/replace-custom-panel.sh deleted file mode 100755 index 5da17863..00000000 --- a/replace-custom-panel.sh +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -# Replace a Docker-based 3x-ui instance with a custom build from current repo. -# - Backs up db/cert folders -# - Builds a custom image from current source -# - Replaces running container with docker compose -# - Saves rollback info - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" -SERVICE_NAME="${SERVICE_NAME:-3xui}" -CONTAINER_NAME="${CONTAINER_NAME:-3xui_app}" -BACKUP_ROOT="${BACKUP_ROOT:-$SCRIPT_DIR/backups}" -USE_COMPOSE_BUILD="${USE_COMPOSE_BUILD:-0}" - -timestamp="$(date +%F-%H%M%S)" -git_sha="$(git rev-parse --short HEAD 2>/dev/null || echo no-git)" -new_tag="3xui-custom:${git_sha}-${timestamp}" - -log() { - printf '[%s] %s\n' "$(date '+%F %T')" "$*" -} - -die() { - log "ERROR: $*" - exit 1 -} - -need_cmd() { - command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1" -} - -need_cmd docker -need_cmd cp -need_cmd mkdir -need_cmd date - -if docker compose version >/dev/null 2>&1; then - COMPOSE_CMD=(docker compose -f "$COMPOSE_FILE") -elif command -v docker-compose >/dev/null 2>&1; then - COMPOSE_CMD=(docker-compose -f "$COMPOSE_FILE") -else - die "Neither 'docker compose' nor 'docker-compose' is available" -fi - -[ -f "$COMPOSE_FILE" ] || die "Compose file not found: $COMPOSE_FILE" - -mkdir -p "$BACKUP_ROOT" -backup_dir="$BACKUP_ROOT/$timestamp" -mkdir -p "$backup_dir" - -log "Starting replacement using compose file: $COMPOSE_FILE" -log "Backup directory: $backup_dir" - -if [ -d "$SCRIPT_DIR/db" ]; then - cp -a "$SCRIPT_DIR/db" "$backup_dir/db" - log "Backed up db to $backup_dir/db" -else - log "No ./db directory found, skipping db backup" -fi - -if [ -d "$SCRIPT_DIR/cert" ]; then - cp -a "$SCRIPT_DIR/cert" "$backup_dir/cert" - log "Backed up cert to $backup_dir/cert" -else - log "No ./cert directory found, skipping cert backup" -fi - -old_image="$(docker inspect -f '{{.Config.Image}}' "$CONTAINER_NAME" 2>/dev/null || true)" -if [ -n "$old_image" ]; then - rollback_tag="3xui-custom:rollback-${timestamp}" - docker image tag "$old_image" "$rollback_tag" - log "Tagged current running image for rollback: $rollback_tag (from $old_image)" -else - rollback_tag="" - log "No running container named $CONTAINER_NAME found, proceeding as fresh deploy" -fi - -if [ "$USE_COMPOSE_BUILD" = "1" ]; then - log "Building via compose service '$SERVICE_NAME'" - "${COMPOSE_CMD[@]}" build "$SERVICE_NAME" -else - log "Building custom image from current repo: $new_tag" - docker build -t "$new_tag" . -fi - -override_file="$backup_dir/docker-compose.override.generated.yml" -if [ "$USE_COMPOSE_BUILD" = "0" ]; then - cat > "$override_file" < "$meta_file" - -log "Deployment metadata saved: $meta_file" -log "Replacement completed successfully." -echo -echo "Rollback quick reference:" -if [ -n "$rollback_tag" ]; then - cat < /tmp/3xui-rollback.yml </dev/null 2>&1; then + echo "Error: ${cmd} is required but not installed." + exit 1 + fi +} + +script_dir() { + cd "$(dirname "${BASH_SOURCE[0]}")" && pwd +} + +repo_root() { + local dir + dir="$(script_dir)" + cd "${dir}/.." && pwd +} diff --git a/scripts/deploy_on_server.sh b/scripts/deploy_on_server.sh new file mode 100755 index 00000000..52126317 --- /dev/null +++ b/scripts/deploy_on_server.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Server script: +# 1) Unzip uploaded bundle +# 2) Copy code into target app directory +# 3) Rebuild + restart Docker compose stack +# +# Usage: +# ./deploy_on_server.sh [app_dir] +# Example: +# ./deploy_on_server.sh 3x-ui-custom-20260219-120000.zip ~/panel + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMMON_SH="${SCRIPT_DIR}/common.sh" +if [[ -f "${COMMON_SH}" ]]; then + # shellcheck source=scripts/common.sh + . "${COMMON_SH}" +else + require_cmd() { + local cmd="$1" + if ! command -v "${cmd}" >/dev/null 2>&1; then + echo "Error: ${cmd} is required but not installed." + exit 1 + fi + } +fi + +APP_DIR="${2:-$HOME/panel}" + +ARCHIVE_NAME="${1:-}" +if [[ -z "${ARCHIVE_NAME}" ]]; then + echo "Usage: $0 " + exit 1 +fi + +if [[ ! -f "${ARCHIVE_NAME}" ]]; then + echo "Error: archive not found in current directory: ${ARCHIVE_NAME}" + exit 1 +fi + +require_cmd unzip +require_cmd docker +require_cmd mktemp + +compose_cmd() { + if docker compose version >/dev/null 2>&1; then + echo "docker compose" + return + fi + + if command -v docker-compose >/dev/null 2>&1; then + echo "docker-compose" + return + fi + + echo "" +} + +COMPOSE="$(compose_cmd)" +if [[ -z "${COMPOSE}" ]]; then + echo "Error: neither 'docker compose' nor 'docker-compose' is available." + exit 1 +fi + +WORK_DIR="$(pwd)" +TMP_EXTRACT_DIR="$(mktemp -d)" +trap 'rm -rf "${TMP_EXTRACT_DIR}"' EXIT + +echo "Extracting bundle to temp dir..." +unzip -oq "${ARCHIVE_NAME}" -d "${TMP_EXTRACT_DIR}" + +mkdir -p "${APP_DIR}" + +echo "Syncing code to ${APP_DIR}..." +if command -v rsync >/dev/null 2>&1; then + rsync -a --delete \ + --exclude "db/" \ + --exclude "cert/" \ + "${TMP_EXTRACT_DIR}/" "${APP_DIR}/" +else + find "${APP_DIR}" -mindepth 1 -maxdepth 1 \ + ! -name "db" \ + ! -name "cert" \ + -exec rm -rf {} + + cp -a "${TMP_EXTRACT_DIR}/." "${APP_DIR}/" +fi + +cd "${APP_DIR}" +mkdir -p db cert + +# Stop old container if present, then rebuild and run +${COMPOSE} down || true +${COMPOSE} build +${COMPOSE} up -d + +echo "Deployment complete." +${COMPOSE} ps +echo "App directory: ${APP_DIR}" +echo "Archive used: ${WORK_DIR}/${ARCHIVE_NAME}" diff --git a/scripts/package_local.sh b/scripts/package_local.sh new file mode 100755 index 00000000..2e406009 --- /dev/null +++ b/scripts/package_local.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Package current repo into a timestamped zip bundle. +# +# Usage: +# ./scripts/package_local.sh [archive_prefix] +# Example: +# ./scripts/package_local.sh 3x-ui-custom +# +# Output: +# Prints the created archive file name on the last line. + +ARCHIVE_PREFIX="${1:-3x-ui-custom}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +# shellcheck source=scripts/common.sh +. "${SCRIPT_DIR}/common.sh" + +require_cmd zip + +TIMESTAMP="$(date +%Y%m%d-%H%M%S)" +ARCHIVE_NAME="${ARCHIVE_PREFIX}-${TIMESTAMP}.zip" +ARCHIVE_PATH="${REPO_ROOT}/${ARCHIVE_NAME}" + +cd "${REPO_ROOT}" + +echo "Creating archive: ${ARCHIVE_PATH}" +zip -r "${ARCHIVE_PATH}" . \ + -x "./.git/*" \ + -x "./.github/*" \ + -x "./.opencode/*" \ + -x "./.playwright-cli/*" \ + -x "./.tmpdb/*" \ + -x "./.tmplogs/*" \ + -x "./node_modules/*" \ + -x "./tmp/*" \ + -x "./output/*" \ + -x "./backups/*" \ + -x "./*.zip" \ + -x "./.DS_Store" + +echo "Created: ${ARCHIVE_NAME}" +echo "${ARCHIVE_NAME}" diff --git a/scripts/upload_to_server.sh b/scripts/upload_to_server.sh new file mode 100755 index 00000000..713993eb --- /dev/null +++ b/scripts/upload_to_server.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Upload bundle + deploy script to remote server. +# +# Usage: +# ./scripts/upload_to_server.sh [remote_port] [remote_base_dir] +# Example: +# ./scripts/upload_to_server.sh 3x-ui-custom-20260219-120000.zip root 203.0.113.10 22 /opt/3x-ui-deploy + +ARCHIVE_INPUT="${1:-}" +REMOTE_USER="${2:-}" +REMOTE_HOST="${3:-}" +REMOTE_PORT="${4:-22}" +REMOTE_BASE_DIR="${5:-/home/$REMOTE_USER/panel}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DEPLOY_SCRIPT_LOCAL="${SCRIPT_DIR}/deploy_on_server.sh" +COMMON_SCRIPT_LOCAL="${SCRIPT_DIR}/common.sh" +# shellcheck source=scripts/common.sh +. "${COMMON_SCRIPT_LOCAL}" + +usage() { + echo "Usage: $0 [remote_port] [remote_base_dir]" + echo "Example: $0 3x-ui-custom-20260219-120000.zip root 203.0.113.10 22 /opt/3x-ui-deploy" +} + +if [[ "${ARCHIVE_INPUT}" == "-h" || "${ARCHIVE_INPUT}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ -z "${ARCHIVE_INPUT}" || -z "${REMOTE_USER}" || -z "${REMOTE_HOST}" ]]; then + echo "Error: archive, remote_user, and remote_host are required." + usage + exit 1 +fi + +if [[ ! -f "${DEPLOY_SCRIPT_LOCAL}" ]]; then + echo "Error: ${DEPLOY_SCRIPT_LOCAL} not found." + exit 1 +fi + +require_cmd scp +require_cmd ssh + +if [[ "${ARCHIVE_INPUT}" = /* ]]; then + ARCHIVE_PATH="${ARCHIVE_INPUT}" +else + ARCHIVE_PATH="${REPO_ROOT}/${ARCHIVE_INPUT}" +fi + +if [[ ! -f "${ARCHIVE_PATH}" ]]; then + echo "Error: archive not found: ${ARCHIVE_PATH}" + exit 1 +fi + +ARCHIVE_NAME="$(basename "${ARCHIVE_PATH}")" + +echo "Ensuring remote directory exists: ${REMOTE_BASE_DIR}" +ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p '${REMOTE_BASE_DIR}'" + +echo "Uploading archive: ${ARCHIVE_NAME}" +scp -P "${REMOTE_PORT}" "${ARCHIVE_PATH}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/" + +echo "Uploading deploy script..." +scp -P "${REMOTE_PORT}" "${DEPLOY_SCRIPT_LOCAL}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/" +echo "Uploading common script..." +scp -P "${REMOTE_PORT}" "${COMMON_SCRIPT_LOCAL}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_BASE_DIR}/" + +echo +echo "Upload complete." +echo "Next, run on server:" +echo "ssh -p ${REMOTE_PORT} ${REMOTE_USER}@${REMOTE_HOST} 'cd ${REMOTE_BASE_DIR} && chmod +x deploy_on_server.sh common.sh && ./deploy_on_server.sh ${ARCHIVE_NAME}'"