Files
SkinbaseNova/scripts/rollback-production.sh

360 lines
9.6 KiB
Bash

#!/bin/bash
set -euo pipefail
remote_folder="${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}"
remote_server="${REMOTE_SERVER:-klevze@server3.klevze.si}"
remote_release_root="${REMOTE_RELEASE_ROOT:-${REMOTE_FOLDER:-/opt/www/virtual/SkinbaseNova}.releases}"
remote_shared_root="${REMOTE_SHARED_ROOT:-${REMOTE_RELEASE_ROOT:-/opt/www/virtual/SkinbaseNova.releases}/shared}"
php_bin="${PHP_BIN:-php}"
composer_bin="${COMPOSER_BIN:-composer}"
ssh_bin="${SSH_BIN:-ssh}"
dry_run=0
list_only=0
rollback_release_id=""
use_previous=0
skip_maintenance=0
usage() {
cat <<'EOF'
Usage: bash scripts/rollback-production.sh [options]
Options:
--previous Switch to the previous retained release on the production server.
--release-id ID Switch to a specific retained release ID.
--list List retained releases and exit.
--dry-run Preview the release switch without changing the active release.
--no-maintenance Skip php artisan down/up during the switch.
--help Show this help.
Environment overrides:
REMOTE_FOLDER, REMOTE_SERVER, REMOTE_RELEASE_ROOT, REMOTE_SHARED_ROOT, PHP_BIN, COMPOSER_BIN, SSH_BIN
Notes:
- Rollback changes the active server release; it does not re-upload files from local.
- Database migrations are not reversed automatically.
EOF
}
log_step() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' -> %s\n' "$1"
}
die() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
require_command() {
local command_name="$1"
local description="$2"
command -v "$command_name" >/dev/null 2>&1 || die "$description is required but was not found in PATH ($command_name)."
}
sanitize_release_fragment() {
local value="$1"
value="${value//[^A-Za-z0-9._-]/-}"
value="${value#-}"
value="${value%-}"
printf '%s' "$value"
}
while [[ $# -gt 0 ]]; do
case "$1" in
--previous)
use_previous=1
;;
--release-id)
shift
rollback_release_id="$(sanitize_release_fragment "${1:?Missing value for --release-id}")"
;;
--release-id=*)
rollback_release_id="$(sanitize_release_fragment "${1#*=}")"
;;
--list)
list_only=1
;;
--dry-run)
dry_run=1
;;
--no-maintenance)
skip_maintenance=1
;;
--help|-h)
usage
exit 0
;;
*)
die "Unknown option: $1"
;;
esac
shift
done
require_command "$ssh_bin" "SSH"
if [[ "$list_only" -eq 0 && "$use_previous" -eq 0 && -z "$rollback_release_id" ]]; then
die "Choose either --previous or --release-id, or use --list to inspect retained releases."
fi
if [[ "$use_previous" -eq 1 && -n "$rollback_release_id" ]]; then
die "Use either --previous or --release-id, not both."
fi
if [[ "$list_only" -eq 1 ]]; then
log_step "Retained releases on $remote_server"
"$ssh_bin" "$remote_server" \
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
'bash -s' <<'EOF'
set -euo pipefail
current_release="unknown"
if [[ -f "${REMOTE_RELEASE_ROOT}/current-release.txt" ]]; then
current_release="$(cat "${REMOTE_RELEASE_ROOT}/current-release.txt")"
fi
printf 'Current release: %s\n' "$current_release"
if [[ ! -d "${REMOTE_RELEASE_ROOT}/releases" ]]; then
printf 'No retained releases found in %s\n' "$REMOTE_RELEASE_ROOT"
exit 0
fi
find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d | sort -r | while read -r path; do
release_id="$(basename "$path")"
marker=" "
if [[ "$release_id" == "$current_release" ]]; then
marker="*"
fi
printf '%s %s\n' "$marker" "$release_id"
done
EOF
exit 0
fi
log_step "Switching production release on $remote_server"
if [[ "$dry_run" -eq 1 ]]; then
log_info "Dry-run mode enabled; remote current release will not be changed"
fi
"$ssh_bin" "$remote_server" \
REMOTE_FOLDER="$(printf '%q' "$remote_folder")" \
REMOTE_RELEASE_ROOT="$(printf '%q' "$remote_release_root")" \
REMOTE_SHARED_ROOT="$(printf '%q' "$remote_shared_root")" \
PHP_BIN="$(printf '%q' "$php_bin")" \
COMPOSER_BIN="$(printf '%q' "$composer_bin")" \
ROLLBACK_RELEASE_ID="$(printf '%q' "$rollback_release_id")" \
USE_PREVIOUS="$use_previous" \
DRY_RUN="$dry_run" \
SKIP_MAINTENANCE="$skip_maintenance" \
'bash -s' <<'EOF'
set -euo pipefail
current_link="${REMOTE_RELEASE_ROOT}/current"
log_step() {
printf '\n==> %s\n' "$1"
}
log_info() {
printf ' -> %s\n' "$1"
}
log_warn() {
printf 'WARN: %s\n' "$1" >&2
}
die() {
printf 'ERROR: %s\n' "$1" >&2
exit 1
}
current_release_id() {
if [[ -L "$current_link" ]]; then
basename "$(readlink "$current_link")"
return 0
fi
printf '%s\n' ''
}
resolve_target_release() {
local current_release
local target_release
local -a releases=()
[[ -d "${REMOTE_RELEASE_ROOT}/releases" ]] || die "No retained releases exist under ${REMOTE_RELEASE_ROOT}/releases"
current_release="$(current_release_id)"
if [[ "$USE_PREVIOUS" -eq 1 ]]; then
mapfile -t releases < <(find "${REMOTE_RELEASE_ROOT}/releases" -mindepth 1 -maxdepth 1 -type d | sort -r)
for release_path in "${releases[@]}"; do
target_release="$(basename "$release_path")"
if [[ "$target_release" != "$current_release" ]]; then
printf '%s\n' "$target_release"
return 0
fi
done
die "No previous retained release is available."
fi
printf '%s\n' "$ROLLBACK_RELEASE_ID"
}
ensure_dir() {
mkdir -p "$1"
}
ensure_php_runtime_dir() {
local target_dir="$1"
local -a privileged_cmd=()
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
privileged_cmd=(sudo -n)
elif [[ "$(id -u)" -eq 0 ]]; then
privileged_cmd=()
fi
if [[ ! -d "$target_dir" ]]; then
if [[ ${#privileged_cmd[@]} -gt 0 ]]; then
"${privileged_cmd[@]}" mkdir -p "$target_dir"
else
mkdir -p "$target_dir"
fi
fi
if [[ ${#privileged_cmd[@]} -gt 0 || "$(id -u)" -eq 0 ]]; then
"${privileged_cmd[@]}" chown -R skinbase:skinbase "$target_dir"
"${privileged_cmd[@]}" chmod 770 "$target_dir"
return
fi
chmod 770 "$target_dir" >/dev/null 2>&1 || true
}
link_shared_paths() {
local target_release="$1"
ensure_dir "$target_release/public" "$target_release/var"
rm -f "$target_release/.env"
ln -sfn "${REMOTE_SHARED_ROOT}/.env" "$target_release/.env"
rm -rf "$target_release/storage"
ln -sfn "${REMOTE_SHARED_ROOT}/storage" "$target_release/storage"
rm -rf "$target_release/public/files"
ln -sfn "${REMOTE_SHARED_ROOT}/public/files" "$target_release/public/files"
rm -rf "$target_release/public/sitemaps"
ln -sfn "${REMOTE_SHARED_ROOT}/public/sitemaps" "$target_release/public/sitemaps"
rm -rf "$target_release/var/php-tmp"
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-tmp" "$target_release/var/php-tmp"
rm -rf "$target_release/var/php-sessions"
ln -sfn "${REMOTE_SHARED_ROOT}/var/php-sessions" "$target_release/var/php-sessions"
}
bring_app_up() {
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$REMOTE_FOLDER/artisan" ]]; then
"$PHP_BIN" "$REMOTE_FOLDER/artisan" up >/dev/null 2>&1 || true
fi
}
target_release="$(resolve_target_release)"
[[ -n "$target_release" ]] || die "Unable to resolve a target release."
target_release_path="${REMOTE_RELEASE_ROOT}/releases/${target_release}"
[[ -d "$target_release_path" ]] || die "Retained release not found: ${target_release_path}"
current_release="$(current_release_id)"
[[ -n "$current_release" ]] || die "No active current release is configured under ${current_link}"
if [[ "$target_release" == "$current_release" ]]; then
die "Target release is already active: ${target_release}"
fi
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-tmp"
ensure_php_runtime_dir "${REMOTE_SHARED_ROOT}/var/php-sessions"
link_shared_paths "$target_release_path"
[[ -f "${REMOTE_SHARED_ROOT}/.env" ]] || die "Shared production .env is missing at ${REMOTE_SHARED_ROOT}/.env"
[[ -f "${target_release_path}/artisan" ]] || die "Target release is missing artisan: ${target_release_path}/artisan"
log_info "Current release: ${current_release}"
log_info "Target release: ${target_release}"
log_info "Target path: ${target_release_path}"
if [[ "$DRY_RUN" -eq 1 ]]; then
exit 0
fi
trap bring_app_up EXIT
if [[ "$SKIP_MAINTENANCE" -eq 0 && -f "$REMOTE_FOLDER/artisan" ]]; then
log_step "Enabling maintenance mode"
"$PHP_BIN" "$REMOTE_FOLDER/artisan" down --retry=60 || true
fi
if [[ ! -f "${target_release_path}/vendor/autoload.php" ]]; then
log_step "Installing Composer dependencies in target release"
(
cd "$target_release_path"
"$COMPOSER_BIN" install --no-dev --prefer-dist --optimize-autoloader --no-interaction
)
fi
log_step "Switching current release to ${target_release}"
ln -sfn "$target_release_path" "$current_link"
ln -sfn "$current_link" "$REMOTE_FOLDER"
cd "$REMOTE_FOLDER"
log_step "Refreshing caches"
"$PHP_BIN" artisan view:clear
"$PHP_BIN" artisan optimize:clear
"$PHP_BIN" artisan optimize
"$PHP_BIN" artisan view:cache
if [[ "$SKIP_MAINTENANCE" -eq 0 ]]; then
log_step "Bringing application back online"
"$PHP_BIN" artisan up
trap - EXIT
fi
if ! "$PHP_BIN" artisan homepage:warm-guest-cache; then
log_warn "Homepage guest cache warm failed during rollback."
fi
if ! "$PHP_BIN" artisan posts:warm-trending; then
log_warn "Post trending cache warm failed during rollback."
fi
log_step "Restarting queue workers"
"$PHP_BIN" artisan queue:restart || true
cat > "${REMOTE_RELEASE_ROOT}/current-release.json" <<JSON
{
"release_id": "${target_release}",
"rolled_back_from_release_id": "${current_release}",
"rolled_back_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"remote_folder": "${REMOTE_FOLDER}",
"release_path": "${target_release_path}"
}
JSON
printf '%s\n' "${target_release}" > "${REMOTE_RELEASE_ROOT}/current-release.txt"
log_step "Release switch complete"
EOF