619 lines
23 KiB
Bash
619 lines
23 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REMOTE_SSH="YCWY"
|
|
REMOTE_PATH="/opt/wygj/app/ruoyi-admin.jar"
|
|
REMOTE_SERVICE="wygj-aihr.service"
|
|
REMOTE_URL="https://peilian.njzhmj.top"
|
|
REMOTE_BACKUP_ROOT="/opt/wygj/backups"
|
|
PREFLIGHT_SCRIPT="$ROOT_DIR/scripts/release-preflight.sh"
|
|
MIN_FREE_RESERVE_BYTES=$((512 * 1024 * 1024))
|
|
PUBLIC_HEALTH_READY_TIMEOUT_SECONDS=90
|
|
|
|
fail() {
|
|
echo "release-backend: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage:
|
|
./scripts/release-backend.sh plan \
|
|
--artifact <release-jar> \
|
|
--artifact-commit <release-commit> \
|
|
--expected-sha256 <sha256>
|
|
|
|
./scripts/release-backend.sh deploy \
|
|
--artifact <release-jar> \
|
|
--artifact-commit <release-commit> \
|
|
--expected-sha256 <sha256> \
|
|
--approval DEPLOY_BACKEND:<sha256>
|
|
|
|
./scripts/release-backend.sh rollback-plan \
|
|
--backup-dir /opt/wygj/backups/backend-<timestamp>-<hash-prefix> \
|
|
--expected-sha256 <backup-jar-sha256>
|
|
|
|
./scripts/release-backend.sh rollback \
|
|
--backup-dir /opt/wygj/backups/backend-<timestamp>-<hash-prefix> \
|
|
--expected-sha256 <backup-jar-sha256> \
|
|
--approval ROLLBACK_BACKEND:<sha256>
|
|
|
|
plan and rollback-plan are read-only. deploy and rollback require an exact,
|
|
non-secret approval token and a clean Git worktree. The target is intentionally
|
|
fixed to YCWY:/opt/wygj/app/ruoyi-admin.jar and wygj-aihr.service.
|
|
EOF
|
|
}
|
|
|
|
mode="${1:-plan}"
|
|
case "$mode" in
|
|
plan|deploy|rollback-plan|rollback)
|
|
shift || true
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
usage >&2
|
|
fail "unsupported mode: $mode"
|
|
;;
|
|
esac
|
|
|
|
artifact=""
|
|
artifact_commit=""
|
|
expected_sha256=""
|
|
backup_dir=""
|
|
approval=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--artifact)
|
|
[[ $# -ge 2 ]] || fail "--artifact requires a value"
|
|
artifact="$2"
|
|
shift 2
|
|
;;
|
|
--artifact-commit)
|
|
[[ $# -ge 2 ]] || fail "--artifact-commit requires a value"
|
|
artifact_commit="$2"
|
|
shift 2
|
|
;;
|
|
--expected-sha256)
|
|
[[ $# -ge 2 ]] || fail "--expected-sha256 requires a value"
|
|
expected_sha256="${2,,}"
|
|
shift 2
|
|
;;
|
|
--backup-dir)
|
|
[[ $# -ge 2 ]] || fail "--backup-dir requires a value"
|
|
backup_dir="$2"
|
|
shift 2
|
|
;;
|
|
--approval)
|
|
[[ $# -ge 2 ]] || fail "--approval requires a value"
|
|
approval="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
fail "unknown argument: $1"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
require_sha256() {
|
|
local value="$1"
|
|
local label="$2"
|
|
[[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "$label must be a 64-character SHA-256"
|
|
}
|
|
|
|
require_clean_worktree() {
|
|
local status
|
|
status="$(git -C "$ROOT_DIR" status --porcelain)"
|
|
[[ -z "$status" ]] || fail "deploy and rollback require a clean Git worktree"
|
|
}
|
|
|
|
require_approval() {
|
|
local expected="$1"
|
|
[[ "$approval" == "$expected" ]] \
|
|
|| fail "explicit approval required; rerun with --approval $expected"
|
|
}
|
|
|
|
file_epoch() {
|
|
if stat -f %m "$1" >/dev/null 2>&1; then
|
|
stat -f %m "$1"
|
|
else
|
|
stat -c %Y "$1"
|
|
fi
|
|
}
|
|
|
|
artifact_commit_sha=""
|
|
artifact_sha256=""
|
|
artifact_bytes=""
|
|
artifact_module_name=""
|
|
remote_current_sha256=""
|
|
remote_current_bytes=""
|
|
|
|
validate_local_artifact() {
|
|
[[ -n "$artifact" ]] || fail "--artifact is required"
|
|
[[ -n "$artifact_commit" ]] || fail "--artifact-commit is required"
|
|
require_sha256 "$expected_sha256" "--expected-sha256"
|
|
|
|
if [[ "$artifact" != /* && ! "$artifact" =~ ^[A-Za-z]:[/\\] ]]; then
|
|
artifact="$ROOT_DIR/$artifact"
|
|
fi
|
|
[[ -f "$artifact" ]] || fail "release JAR not found: $artifact"
|
|
|
|
artifact_commit_sha="$(git -C "$ROOT_DIR" rev-parse --verify "${artifact_commit}^{commit}" 2>/dev/null)" \
|
|
|| fail "artifact commit is not a valid commit: $artifact_commit"
|
|
git -C "$ROOT_DIR" merge-base --is-ancestor "$artifact_commit_sha" HEAD \
|
|
|| fail "artifact commit must be an ancestor of HEAD: $artifact_commit_sha"
|
|
|
|
local commit_epoch artifact_epoch
|
|
commit_epoch="$(git -C "$ROOT_DIR" show -s --format=%ct "$artifact_commit_sha")"
|
|
artifact_epoch="$(file_epoch "$artifact")"
|
|
[[ "$artifact_epoch" -ge "$commit_epoch" ]] \
|
|
|| fail "release JAR is older than its artifact commit: $artifact"
|
|
|
|
artifact_sha256="$(sha256sum "$artifact" | awk '{print tolower($1)}')"
|
|
[[ "$artifact_sha256" == "$expected_sha256" ]] \
|
|
|| fail "release JAR SHA-256 mismatch: expected $expected_sha256, got $artifact_sha256"
|
|
artifact_bytes="$(stat -c %s "$artifact")"
|
|
|
|
artifact_module_name="$(
|
|
unzip -Z1 "$artifact" |
|
|
sed -nE 's#BOOT-INF/lib/(ruoyi-aihr-[^/]+\.jar)#\1#p' |
|
|
head -1
|
|
)"
|
|
[[ -n "$artifact_module_name" ]] || fail "release JAR does not contain ruoyi-aihr"
|
|
}
|
|
|
|
remote_value() {
|
|
local key="$1"
|
|
local text="$2"
|
|
printf '%s\n' "$text" | sed -n "s/^${key}=//p" | head -1
|
|
}
|
|
|
|
inspect_remote_target() {
|
|
local output
|
|
output="$(
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$REMOTE_PATH" "$REMOTE_SERVICE" "$REMOTE_BACKUP_ROOT" <<'REMOTE_INSPECT'
|
|
set -euo pipefail
|
|
target="$1"
|
|
service="$2"
|
|
backup_root="$3"
|
|
[[ -f "$target" && ! -L "$target" ]]
|
|
[[ "$(readlink -f "$target")" == "$target" ]]
|
|
[[ -d "$backup_root" && -w "$backup_root" ]]
|
|
printf 'target_type=%s\n' "$(stat -c %F "$target")"
|
|
printf 'target_realpath=%s\n' "$(readlink -f "$target")"
|
|
printf 'remote_user=%s\n' "$(id -un)"
|
|
printf 'target_sha256=%s\n' "$(sha256sum "$target" | awk '{print tolower($1)}')"
|
|
printf 'target_bytes=%s\n' "$(stat -c %s "$target")"
|
|
printf 'target_mode=%s\n' "$(stat -c '%a %U:%G' "$target")"
|
|
printf 'free_bytes=%s\n' "$(df --output=avail -B1 "$(dirname "$target")" | tail -1 | tr -d ' ')"
|
|
printf 'service_active=%s\n' "$(systemctl is-active "$service")"
|
|
printf 'service_exec_start=%s\n' "$(systemctl show "$service" --property=ExecStart --value --no-pager)"
|
|
REMOTE_INSPECT
|
|
)" || fail "read-only remote topology check failed"
|
|
|
|
[[ "$(remote_value target_type "$output")" == "regular file" ]] \
|
|
|| fail "remote backend target is not a regular file"
|
|
[[ "$(remote_value target_realpath "$output")" == "$REMOTE_PATH" ]] \
|
|
|| fail "remote backend target resolves outside the fixed path"
|
|
[[ "$(remote_value remote_user "$output")" == "root" ]] \
|
|
|| fail "remote backend release requires the fixed root deployment account"
|
|
[[ "$(remote_value service_active "$output")" == "active" ]] \
|
|
|| fail "remote service is not active before the operation"
|
|
[[ "$(remote_value service_exec_start "$output")" == *"$REMOTE_PATH"* ]] \
|
|
|| fail "remote service does not start from the fixed backend target"
|
|
|
|
remote_current_sha256="$(remote_value target_sha256 "$output")"
|
|
remote_current_bytes="$(remote_value target_bytes "$output")"
|
|
local free_bytes required_free
|
|
free_bytes="$(remote_value free_bytes "$output")"
|
|
[[ "$remote_current_sha256" =~ ^[0-9a-f]{64}$ ]] \
|
|
|| fail "remote target returned an invalid SHA-256"
|
|
[[ "$remote_current_bytes" =~ ^[0-9]+$ && "$free_bytes" =~ ^[0-9]+$ ]] \
|
|
|| fail "remote target returned invalid size metadata"
|
|
|
|
if [[ -n "$artifact_bytes" ]]; then
|
|
required_free=$((artifact_bytes + remote_current_bytes + MIN_FREE_RESERVE_BYTES))
|
|
[[ "$free_bytes" -ge "$required_free" ]] \
|
|
|| fail "remote filesystem free space is below backup + upload + 512 MiB reserve"
|
|
fi
|
|
|
|
printf '%s\n' "$output"
|
|
}
|
|
|
|
run_schema_preflight() {
|
|
RELEASE_REMOTE_URL="$REMOTE_URL" \
|
|
RELEASE_VERIFY_REMOTE_SCHEMA=true \
|
|
RELEASE_ARTIFACT_COMMIT="$artifact_commit_sha" \
|
|
"$PREFLIGHT_SCRIPT"
|
|
}
|
|
|
|
run_post_deploy_preflight() {
|
|
RELEASE_REMOTE_URL="$REMOTE_URL" \
|
|
RELEASE_VERIFY_REMOTE_BACKEND=true \
|
|
RELEASE_VERIFY_REMOTE_SCHEMA=true \
|
|
RELEASE_ARTIFACT_COMMIT="$artifact_commit_sha" \
|
|
RELEASE_LOCAL_BACKEND_PATH="$artifact" \
|
|
"$PREFLIGHT_SCRIPT"
|
|
}
|
|
|
|
run_public_health() {
|
|
local tenant_body mobile_body
|
|
curl -fsS --max-time 20 "$REMOTE_URL/" >/dev/null || {
|
|
echo "release-backend: root endpoint health check failed" >&2
|
|
return 1
|
|
}
|
|
tenant_body="$(curl -fsS --max-time 20 "$REMOTE_URL/prod-api/auth/tenant/list")" || {
|
|
echo "release-backend: tenant endpoint HTTP check failed" >&2
|
|
return 1
|
|
}
|
|
mobile_body="$(curl -fsS --max-time 20 "$REMOTE_URL/prod-api/api/aihr/mobile/home/user")" || {
|
|
echo "release-backend: mobile home endpoint HTTP check failed" >&2
|
|
return 1
|
|
}
|
|
printf '%s' "$tenant_body" |
|
|
LC_ALL=C grep -Eq '^[[:space:]]*\{[[:space:]]*"code"[[:space:]]*:[[:space:]]*200([[:space:]]*[,}])' \
|
|
|| {
|
|
echo "release-backend: tenant endpoint did not return business code 200" >&2
|
|
return 1
|
|
}
|
|
printf '%s' "$mobile_body" |
|
|
LC_ALL=C grep -Eq '^[[:space:]]*\{[[:space:]]*"code"[[:space:]]*:[[:space:]]*200([[:space:]]*[,}])' \
|
|
|| {
|
|
echo "release-backend: mobile home endpoint did not return business code 200" >&2
|
|
return 1
|
|
}
|
|
}
|
|
|
|
wait_for_public_health() {
|
|
local deadline=$((SECONDS + PUBLIC_HEALTH_READY_TIMEOUT_SECONDS))
|
|
while ((SECONDS < deadline)); do
|
|
if run_public_health >/dev/null 2>&1; then
|
|
echo "release-backend: public endpoints are ready"
|
|
return 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
echo "release-backend: public endpoints did not become ready within ${PUBLIC_HEALTH_READY_TIMEOUT_SECONDS}s" >&2
|
|
run_public_health
|
|
}
|
|
|
|
run_deploy_plan() {
|
|
validate_local_artifact
|
|
inspect_remote_target
|
|
run_schema_preflight
|
|
|
|
echo "mode=read-only-deploy-plan"
|
|
echo "artifact_commit=$artifact_commit_sha"
|
|
echo "artifact_path=$artifact"
|
|
echo "artifact_bytes=$artifact_bytes"
|
|
echo "artifact_sha256=$artifact_sha256"
|
|
echo "artifact_module=$artifact_module_name"
|
|
echo "remote_target=$REMOTE_SSH:$REMOTE_PATH"
|
|
echo "remote_current_sha256=$remote_current_sha256"
|
|
echo "remote_service=$REMOTE_SERVICE"
|
|
echo "approval_token=DEPLOY_BACKEND:$artifact_sha256"
|
|
if [[ "$remote_current_sha256" == "$artifact_sha256" ]]; then
|
|
fail "remote backend already matches the candidate; no deploy is needed"
|
|
fi
|
|
}
|
|
|
|
backup_remote_target() {
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$REMOTE_PATH" "$REMOTE_BACKUP_ROOT" "$REMOTE_SERVICE" \
|
|
"$remote_current_sha256" "$artifact_sha256" "$artifact_commit_sha" <<'REMOTE_BACKUP'
|
|
set -euo pipefail
|
|
target="$1"
|
|
backup_root="$2"
|
|
service="$3"
|
|
expected_current_sha="$4"
|
|
candidate_sha="$5"
|
|
artifact_commit="$6"
|
|
[[ -f "$target" && ! -L "$target" ]]
|
|
actual_current_sha="$(sha256sum "$target" | awk '{print tolower($1)}')"
|
|
[[ "$actual_current_sha" == "$expected_current_sha" ]]
|
|
timestamp="$(date +%Y%m%d%H%M%S)"
|
|
backup_dir="$backup_root/backend-$timestamp-${expected_current_sha:0:12}"
|
|
mkdir "$backup_dir"
|
|
cp -a "$target" "$backup_dir/ruoyi-admin.jar"
|
|
backup_sha="$(sha256sum "$backup_dir/ruoyi-admin.jar" | awk '{print tolower($1)}')"
|
|
[[ "$backup_sha" == "$expected_current_sha" ]]
|
|
printf 'created_at=%s\nservice=%s\ntarget=%s\nold_sha256=%s\ncandidate_sha256=%s\nartifact_commit=%s\n' \
|
|
"$(date --iso-8601=seconds)" "$service" "$target" "$expected_current_sha" "$candidate_sha" "$artifact_commit" \
|
|
> "$backup_dir/release-manifest.txt"
|
|
printf 'backup_dir=%s\nbackup_sha256=%s\n' "$backup_dir" "$backup_sha"
|
|
REMOTE_BACKUP
|
|
}
|
|
|
|
activate_remote_candidate() {
|
|
local staging_path="$1"
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$REMOTE_PATH" "$staging_path" "$REMOTE_SERVICE" \
|
|
"$remote_current_sha256" "$artifact_sha256" <<'REMOTE_ACTIVATE'
|
|
set -euo pipefail
|
|
target="$1"
|
|
staging="$2"
|
|
service="$3"
|
|
expected_current_sha="$4"
|
|
expected_candidate_sha="$5"
|
|
[[ -f "$target" && ! -L "$target" ]]
|
|
[[ -f "$staging" && ! -L "$staging" ]]
|
|
[[ "$(sha256sum "$target" | awk '{print tolower($1)}')" == "$expected_current_sha" ]]
|
|
[[ "$(sha256sum "$staging" | awk '{print tolower($1)}')" == "$expected_candidate_sha" ]]
|
|
chown --reference="$target" "$staging"
|
|
chmod --reference="$target" "$staging"
|
|
mv -T "$staging" "$target"
|
|
systemctl restart "$service"
|
|
for _ in $(seq 1 30); do
|
|
if [[ "$(systemctl is-active "$service" || true)" == "active" ]]; then
|
|
[[ "$(sha256sum "$target" | awk '{print tolower($1)}')" == "$expected_candidate_sha" ]]
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
systemctl status "$service" --no-pager >&2 || true
|
|
exit 1
|
|
REMOTE_ACTIVATE
|
|
}
|
|
|
|
restore_remote_backup() {
|
|
local source_jar="$1"
|
|
local expected_restore_sha="$2"
|
|
local expected_target_sha="$3"
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$source_jar" "$REMOTE_PATH" "$REMOTE_SERVICE" \
|
|
"$expected_restore_sha" "$expected_target_sha" <<'REMOTE_RESTORE'
|
|
set -euo pipefail
|
|
source_jar="$1"
|
|
target="$2"
|
|
service="$3"
|
|
expected_sha="$4"
|
|
expected_target_sha="$5"
|
|
[[ -f "$source_jar" && ! -L "$source_jar" ]]
|
|
[[ -f "$target" && ! -L "$target" ]]
|
|
[[ "$(sha256sum "$source_jar" | awk '{print tolower($1)}')" == "$expected_sha" ]]
|
|
[[ "$(sha256sum "$target" | awk '{print tolower($1)}')" == "$expected_target_sha" ]]
|
|
staging="${target}.restore-${expected_sha:0:12}-$(date +%Y%m%d%H%M%S)"
|
|
[[ ! -e "$staging" ]]
|
|
cp -a "$source_jar" "$staging"
|
|
chown --reference="$target" "$staging"
|
|
chmod --reference="$target" "$staging"
|
|
[[ "$(sha256sum "$staging" | awk '{print tolower($1)}')" == "$expected_sha" ]]
|
|
mv -T "$staging" "$target"
|
|
systemctl restart "$service"
|
|
for _ in $(seq 1 30); do
|
|
if [[ "$(systemctl is-active "$service" || true)" == "active" ]]; then
|
|
[[ "$(sha256sum "$target" | awk '{print tolower($1)}')" == "$expected_sha" ]]
|
|
exit 0
|
|
fi
|
|
sleep 2
|
|
done
|
|
systemctl status "$service" --no-pager >&2 || true
|
|
exit 1
|
|
REMOTE_RESTORE
|
|
}
|
|
|
|
get_remote_target_sha() {
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" \
|
|
sha256sum "$REMOTE_PATH" |
|
|
awk '{print tolower($1)}'
|
|
}
|
|
|
|
perform_deploy() {
|
|
local backup_output backup_path backup_sha staging_path after_failure_sha
|
|
backup_output="$(backup_remote_target)" || fail "remote backup failed; candidate was not activated"
|
|
printf '%s\n' "$backup_output"
|
|
backup_path="$(remote_value backup_dir "$backup_output")"
|
|
backup_sha="$(remote_value backup_sha256 "$backup_output")"
|
|
[[ "$backup_path" =~ ^/opt/wygj/backups/backend-[0-9]{14}-[0-9a-f]{12}$ ]] \
|
|
|| fail "remote backup returned an unsafe path"
|
|
[[ "$backup_sha" == "$remote_current_sha256" ]] \
|
|
|| fail "remote backup hash does not match the pre-deploy target"
|
|
|
|
staging_path="${REMOTE_PATH}.candidate-${artifact_sha256:0:12}-$(date +%Y%m%d%H%M%S)"
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" \
|
|
test ! -e "$staging_path" || fail "remote candidate staging path already exists"
|
|
if ! scp -q -o BatchMode=yes -o ConnectTimeout=10 -- "$artifact" "$REMOTE_SSH:$staging_path"; then
|
|
fail "candidate upload failed; production target was not changed; backup is $backup_path"
|
|
fi
|
|
|
|
if ! activate_remote_candidate "$staging_path"; then
|
|
after_failure_sha="$(get_remote_target_sha)" \
|
|
|| fail "candidate activation failed and the current target hash is unreadable; inspect $backup_path"
|
|
case "$after_failure_sha" in
|
|
"$artifact_sha256")
|
|
echo "release-backend: candidate activation failed after switch; restoring $backup_path" >&2
|
|
restore_remote_backup \
|
|
"$backup_path/ruoyi-admin.jar" "$remote_current_sha256" "$artifact_sha256" \
|
|
|| fail "automatic restore failed; use the recorded backup immediately: $backup_path"
|
|
wait_for_public_health \
|
|
|| fail "candidate activation failed; the original backend was restored but did not become ready"
|
|
fail "candidate activation failed and the original backend was restored"
|
|
;;
|
|
"$remote_current_sha256")
|
|
fail "candidate activation failed before switching the production target; backup is $backup_path"
|
|
;;
|
|
*)
|
|
fail "candidate activation saw a concurrent target change ($after_failure_sha); no automatic overwrite was attempted; inspect $backup_path"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if ! wait_for_public_health || ! run_post_deploy_preflight; then
|
|
after_failure_sha="$(get_remote_target_sha)" \
|
|
|| fail "post-deploy preflight failed and the current target hash is unreadable; inspect $backup_path"
|
|
case "$after_failure_sha" in
|
|
"$artifact_sha256")
|
|
echo "release-backend: post-deploy preflight failed; restoring $backup_path" >&2
|
|
restore_remote_backup \
|
|
"$backup_path/ruoyi-admin.jar" "$remote_current_sha256" "$artifact_sha256" \
|
|
|| fail "automatic restore failed; use the recorded backup immediately: $backup_path"
|
|
wait_for_public_health \
|
|
|| fail "original backend was restored but public health verification failed"
|
|
fail "post-deploy preflight failed and the original backend was restored"
|
|
;;
|
|
"$remote_current_sha256")
|
|
fail "post-deploy preflight failed after the original backend was already restored; inspect $backup_path"
|
|
;;
|
|
*)
|
|
fail "post-deploy preflight saw a concurrent target change ($after_failure_sha); no automatic overwrite was attempted; inspect $backup_path"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "mode=backend-deploy-complete"
|
|
echo "deployed_sha256=$artifact_sha256"
|
|
echo "rollback_backup=$backup_path"
|
|
echo "rollback_sha256=$remote_current_sha256"
|
|
echo "rollback_approval_token=ROLLBACK_BACKEND:$remote_current_sha256"
|
|
}
|
|
|
|
validate_backup_path() {
|
|
[[ "$backup_dir" =~ ^/opt/wygj/backups/backend-[0-9]{14}-[0-9a-f]{12}$ ]] \
|
|
|| fail "--backup-dir must be a release-backend backup directory"
|
|
require_sha256 "$expected_sha256" "--expected-sha256"
|
|
}
|
|
|
|
inspect_rollback() {
|
|
validate_backup_path
|
|
local output
|
|
output="$(
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$backup_dir/ruoyi-admin.jar" "$REMOTE_PATH" "$REMOTE_SERVICE" <<'REMOTE_ROLLBACK_INSPECT'
|
|
set -euo pipefail
|
|
backup_jar="$1"
|
|
target="$2"
|
|
service="$3"
|
|
[[ -f "$backup_jar" && ! -L "$backup_jar" ]]
|
|
[[ -f "$target" && ! -L "$target" ]]
|
|
printf 'backup_sha256=%s\n' "$(sha256sum "$backup_jar" | awk '{print tolower($1)}')"
|
|
printf 'current_sha256=%s\n' "$(sha256sum "$target" | awk '{print tolower($1)}')"
|
|
printf 'service_active=%s\n' "$(systemctl is-active "$service")"
|
|
REMOTE_ROLLBACK_INSPECT
|
|
)" || fail "read-only rollback inspection failed"
|
|
local backup_sha service_state
|
|
backup_sha="$(remote_value backup_sha256 "$output")"
|
|
service_state="$(remote_value service_active "$output")"
|
|
[[ "$backup_sha" == "$expected_sha256" ]] \
|
|
|| fail "backup JAR SHA-256 mismatch: expected $expected_sha256, got $backup_sha"
|
|
[[ "$service_state" == "active" ]] || fail "remote service is not active before rollback"
|
|
printf '%s\n' "$output"
|
|
echo "mode=read-only-rollback-plan"
|
|
echo "backup_dir=$backup_dir"
|
|
echo "approval_token=ROLLBACK_BACKEND:$expected_sha256"
|
|
}
|
|
|
|
perform_rollback() {
|
|
local current_sha safety_output safety_path safety_sha after_failure_sha
|
|
current_sha="$(get_remote_target_sha)" \
|
|
|| fail "cannot read the current remote backend hash"
|
|
require_sha256 "$current_sha" "remote current backend hash"
|
|
[[ "$current_sha" != "$expected_sha256" ]] \
|
|
|| fail "remote backend already matches the requested rollback JAR"
|
|
|
|
safety_output="$(
|
|
ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE_SSH" bash -s -- \
|
|
"$REMOTE_PATH" "$REMOTE_BACKUP_ROOT" "$REMOTE_SERVICE" \
|
|
"$current_sha" "$expected_sha256" <<'REMOTE_ROLLBACK_SAFETY'
|
|
set -euo pipefail
|
|
target="$1"
|
|
backup_root="$2"
|
|
service="$3"
|
|
current_sha="$4"
|
|
restore_sha="$5"
|
|
[[ "$(sha256sum "$target" | awk '{print tolower($1)}')" == "$current_sha" ]]
|
|
timestamp="$(date +%Y%m%d%H%M%S)"
|
|
safety_dir="$backup_root/backend-rollback-safety-$timestamp-${current_sha:0:12}"
|
|
mkdir "$safety_dir"
|
|
cp -a "$target" "$safety_dir/ruoyi-admin.jar"
|
|
[[ "$(sha256sum "$safety_dir/ruoyi-admin.jar" | awk '{print tolower($1)}')" == "$current_sha" ]]
|
|
printf 'created_at=%s\nservice=%s\ntarget=%s\nsafety_sha256=%s\nrestore_sha256=%s\n' \
|
|
"$(date --iso-8601=seconds)" "$service" "$target" "$current_sha" "$restore_sha" \
|
|
> "$safety_dir/rollback-manifest.txt"
|
|
printf 'safety_dir=%s\nsafety_sha256=%s\n' "$safety_dir" "$current_sha"
|
|
REMOTE_ROLLBACK_SAFETY
|
|
)" || fail "rollback safety backup failed; production target was not changed"
|
|
printf '%s\n' "$safety_output"
|
|
safety_path="$(remote_value safety_dir "$safety_output")"
|
|
safety_sha="$(remote_value safety_sha256 "$safety_output")"
|
|
[[ "$safety_path" =~ ^/opt/wygj/backups/backend-rollback-safety-[0-9]{14}-[0-9a-f]{12}$ ]] \
|
|
|| fail "rollback safety backup returned an unsafe path"
|
|
[[ "$safety_sha" == "$current_sha" ]] || fail "rollback safety backup hash mismatch"
|
|
|
|
if ! restore_remote_backup \
|
|
"$backup_dir/ruoyi-admin.jar" "$expected_sha256" "$current_sha"; then
|
|
after_failure_sha="$(get_remote_target_sha)" \
|
|
|| fail "rollback activation failed and the current target hash is unreadable; inspect $safety_path"
|
|
case "$after_failure_sha" in
|
|
"$expected_sha256")
|
|
echo "release-backend: rollback activation failed after switch; restoring safety copy $safety_path" >&2
|
|
restore_remote_backup \
|
|
"$safety_path/ruoyi-admin.jar" "$current_sha" "$expected_sha256" \
|
|
|| fail "rollback and safety restore both failed; use $safety_path immediately"
|
|
fail "rollback failed and the pre-rollback backend was restored"
|
|
;;
|
|
"$current_sha")
|
|
fail "rollback activation failed before switching the production target; safety backup is $safety_path"
|
|
;;
|
|
*)
|
|
fail "rollback activation saw a concurrent target change ($after_failure_sha); no automatic overwrite was attempted; inspect $safety_path"
|
|
;;
|
|
esac
|
|
fi
|
|
if ! wait_for_public_health; then
|
|
after_failure_sha="$(get_remote_target_sha)" \
|
|
|| fail "rollback health failed and the current target hash is unreadable; inspect $safety_path"
|
|
case "$after_failure_sha" in
|
|
"$expected_sha256")
|
|
echo "release-backend: rollback health failed; restoring safety copy $safety_path" >&2
|
|
restore_remote_backup \
|
|
"$safety_path/ruoyi-admin.jar" "$current_sha" "$expected_sha256" \
|
|
|| fail "health rollback and safety restore both failed; use $safety_path immediately"
|
|
fail "rollback health check failed and the pre-rollback backend was restored"
|
|
;;
|
|
"$current_sha")
|
|
fail "rollback health failed after the pre-rollback backend was already restored; inspect $safety_path"
|
|
;;
|
|
*)
|
|
fail "rollback health saw a concurrent target change ($after_failure_sha); no automatic overwrite was attempted; inspect $safety_path"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo "mode=backend-rollback-complete"
|
|
echo "restored_sha256=$expected_sha256"
|
|
echo "pre_rollback_safety_backup=$safety_path"
|
|
echo "pre_rollback_sha256=$current_sha"
|
|
}
|
|
|
|
case "$mode" in
|
|
plan)
|
|
run_deploy_plan
|
|
;;
|
|
deploy)
|
|
require_sha256 "$expected_sha256" "--expected-sha256"
|
|
require_approval "DEPLOY_BACKEND:$expected_sha256"
|
|
require_clean_worktree
|
|
run_deploy_plan
|
|
perform_deploy
|
|
;;
|
|
rollback-plan)
|
|
inspect_rollback
|
|
;;
|
|
rollback)
|
|
require_sha256 "$expected_sha256" "--expected-sha256"
|
|
require_approval "ROLLBACK_BACKEND:$expected_sha256"
|
|
require_clean_worktree
|
|
inspect_rollback
|
|
perform_rollback
|
|
;;
|
|
esac
|