#!/usr/bin/env bash set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$script_dir/.." && pwd)" ssh_control_path="" cleanup() { local exit_code=$? if [[ -n "$ssh_control_path" ]] && ssh -o ControlPath="$ssh_control_path" -O exit "$REMOTE_HOST" >/dev/null 2>&1; then : fi exit "$exit_code" } trap cleanup EXIT if [[ $# -gt 1 ]]; then echo "Usage: $0 [deploy-config-file]" >&2 exit 1 fi config_path="${1:-$repo_root/deploy/vserver.env}" if [[ ! -f "$config_path" ]]; then echo "Missing deploy config: $config_path" >&2 echo "Copy deploy/vserver.env.example to deploy/vserver.env and adjust it first." >&2 exit 1 fi set -a source "$config_path" set +a REMOTE_HOST="${REMOTE_HOST:-myvserver}" REMOTE_APP_DIR="${REMOTE_APP_DIR:-/opt/rolemasterdb}" REMOTE_DATA_DIR="${REMOTE_DATA_DIR:-$REMOTE_APP_DIR/data}" IMAGE_NAME="${IMAGE_NAME:-rolemasterdb}" CONTAINER_NAME="${CONTAINER_NAME:-rolemasterdb}" HOST_BIND_ADDRESS="${HOST_BIND_ADDRESS:-}" HOST_PORT="${HOST_PORT:-8080}" CONTAINER_PORT="${CONTAINER_PORT:-8080}" REMOTE_USE_SUDO="${REMOTE_USE_SUDO:-0}" REMOTE_ENV_FILE="${REMOTE_ENV_FILE:-}" RELEASE_PREFIX="${RELEASE_PREFIX:-rolemasterdb}" release_id="$(date -u +%Y%m%d%H%M%S)" artifact_root="$repo_root/artifacts/deploy/$release_id" stage_dir="$artifact_root/stage" tarball_path="$artifact_root/${RELEASE_PREFIX}-${release_id}.tar.gz" publish_dir="$repo_root/src/RolemasterDb.App/bin/Release/net10.0/publish" seed_db_path="$repo_root/src/RolemasterDb.App/rolemaster.db" import_artifacts_source_dir="$repo_root/src/RolemasterDb.App/import-artifacts" dockerfile_path="$repo_root/deploy/vserver.Dockerfile" project_path="$repo_root/src/RolemasterDb.App/RolemasterDb.App.csproj" remote_release_dir="$REMOTE_APP_DIR/releases/$release_id" remote_tarball_path="$remote_release_dir/release.tar.gz" ssh_control_path="/tmp/rolemasterdb-deploy-${USER:-$(id -un)}-$(date -u +%Y%m%d%H%M%S)-$$.sock" if [[ ! -f "$seed_db_path" ]]; then echo "Seed database not found: $seed_db_path" >&2 exit 1 fi if [[ ! -f "$dockerfile_path" ]]; then echo "Dockerfile not found: $dockerfile_path" >&2 exit 1 fi if [[ ! -d "$import_artifacts_source_dir" ]]; then echo "Import artifacts directory not found: $import_artifacts_source_dir" >&2 exit 1 fi mkdir -p "$artifact_root" echo "Opening shared SSH connection to $REMOTE_HOST..." ssh \ -o ControlMaster=yes \ -o ControlPersist=10m \ -o ControlPath="$ssh_control_path" \ -Nf \ "$REMOTE_HOST" echo "Publishing RolemasterDb.App..." rm -rf "$publish_dir" mkdir -p \ "$publish_dir/wwwroot/components/layout" \ "$publish_dir/wwwroot/components/shared" env DOTNET_CLI_HOME="${DOTNET_CLI_HOME:-/tmp}" \ dotnet publish "$project_path" -c Release echo "Copying full import-artifacts tree into publish output..." rm -rf "$publish_dir/import-artifacts" cp -a "$import_artifacts_source_dir" "$publish_dir/import-artifacts" echo "Preparing deploy bundle..." rm -rf "$stage_dir" mkdir -p "$stage_dir/publish" "$stage_dir/seed" cp -a "$publish_dir/." "$stage_dir/publish/" cp "$seed_db_path" "$stage_dir/seed/rolemaster.db" cp "$dockerfile_path" "$stage_dir/Dockerfile" tar -C "$stage_dir" -czf "$tarball_path" . echo "Creating remote release directory..." ssh \ -o ControlPath="$ssh_control_path" \ "$REMOTE_HOST" bash -s -- "$remote_release_dir" "$REMOTE_DATA_DIR" <<'EOF' set -euo pipefail release_dir="$1" data_dir="$2" mkdir -p "$release_dir" "$data_dir" EOF echo "Uploading bundle to $REMOTE_HOST..." scp \ -o ControlPath="$ssh_control_path" \ "$tarball_path" "$REMOTE_HOST:$remote_tarball_path" echo "Building image and restarting container on $REMOTE_HOST..." remote_env_file_arg="${REMOTE_ENV_FILE:-__ROLEMASTERDB_EMPTY__}" ssh \ -o ControlPath="$ssh_control_path" \ "$REMOTE_HOST" bash -s -- \ "$release_id" \ "$REMOTE_APP_DIR" \ "$REMOTE_DATA_DIR" \ "$IMAGE_NAME" \ "$CONTAINER_NAME" \ "$HOST_BIND_ADDRESS" \ "$HOST_PORT" \ "$CONTAINER_PORT" \ "$REMOTE_USE_SUDO" \ "$remote_env_file_arg" <<'EOF' set -euo pipefail release_id="$1" remote_app_dir="$2" remote_data_dir="$3" image_name="$4" container_name="$5" host_bind_address="$6" host_port="$7" container_port="$8" remote_use_sudo="$9" remote_env_file="${10}" if [[ "$remote_env_file" == "__ROLEMASTERDB_EMPTY__" ]]; then remote_env_file="" fi release_dir="$remote_app_dir/releases/$release_id" tarball_path="$release_dir/release.tar.gz" docker_cmd=(docker) if [[ "$remote_use_sudo" == "1" ]]; then docker_cmd=(sudo docker) fi tar -xzf "$tarball_path" -C "$release_dir" rm -f "$tarball_path" if [[ ! -f "$remote_data_dir/rolemaster.db" ]]; then cp "$release_dir/seed/rolemaster.db" "$remote_data_dir/rolemaster.db" fi "${docker_cmd[@]}" build \ -t "$image_name:$release_id" \ -t "$image_name:latest" \ "$release_dir" "${docker_cmd[@]}" rm -f "$container_name" >/dev/null 2>&1 || true docker_run_args=( run -d --name "$container_name" --restart unless-stopped -e "ASPNETCORE_ENVIRONMENT=Production" -e "ASPNETCORE_URLS=http://+:$container_port" -e "ConnectionStrings__RolemasterDb=Data Source=/app/data/rolemaster.db" -v "$remote_data_dir:/app/data" ) if [[ -n "$host_bind_address" ]]; then docker_run_args+=(-p "$host_bind_address:$host_port:$container_port") else docker_run_args+=(-p "$host_port:$container_port") fi if [[ -n "$remote_env_file" ]]; then docker_run_args+=(--env-file "$remote_env_file") fi docker_run_args+=("$image_name:$release_id") "${docker_cmd[@]}" "${docker_run_args[@]}" ln -sfn "$release_dir" "$remote_app_dir/current" echo "Deployment complete." echo "Release: $release_id" echo "Container: $container_name" if [[ -n "$host_bind_address" ]]; then echo "Published at: $host_bind_address:$host_port -> $container_port" else echo "Published at: $host_port -> $container_port" fi EOF echo "Local bundle ready at $tarball_path"