#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" SCRIPT="$ROOT_DIR/scripts/release-backend.sh" DEV_SETUP="$ROOT_DIR/docs/DEV_SETUP.md" bash -n "$SCRIPT" for remote_block in \ REMOTE_INSPECT \ REMOTE_BACKUP \ REMOTE_ACTIVATE \ REMOTE_RESTORE \ REMOTE_ROLLBACK_INSPECT \ REMOTE_ROLLBACK_SAFETY; do awk -v marker="$remote_block" ' index($0, "<<" SQ marker SQ) { capture = 1; next } capture && $0 == marker { exit } capture { print } ' SQ="'" "$SCRIPT" | bash -n done extract_remote_block() { local marker="$1" awk -v marker="$marker" ' index($0, "<<" SQ marker SQ) { capture = 1; next } capture && $0 == marker { exit } capture { print } ' SQ="'" "$SCRIPT" } run_remote_block_locally() { local marker="$1" shift { cat <<'FAKE_SYSTEMD' systemctl() { case "$1" in restart) return 0 ;; is-active) printf 'active\n'; return 0 ;; status) return 0 ;; *) return 0 ;; esac } FAKE_SYSTEMD extract_remote_block "$marker" } | bash -s -- "$@" } test_tmp_base="${TMPDIR:-/tmp}" test_tmp="$(mktemp -d "$test_tmp_base/release-backend-test.XXXXXX")" test_tmp="$(cd "$test_tmp" && pwd -P)" case "$test_tmp" in "$test_tmp_base"/release-backend-test.*) ;; *) echo "FAIL: unsafe test temp directory: $test_tmp" >&2 exit 1 ;; esac trap 'rm -rf -- "$test_tmp"' EXIT mkdir -p "$test_tmp/app" "$test_tmp/backups" target="$test_tmp/app/ruoyi-admin.jar" printf 'old-backend\n' > "$target" old_sha="$(sha256sum "$target" | awk '{print $1}')" candidate="$test_tmp/app/candidate.jar" printf 'candidate-backend\n' > "$candidate" candidate_sha="$(sha256sum "$candidate" | awk '{print $1}')" backup_output="$( run_remote_block_locally REMOTE_BACKUP \ "$target" "$test_tmp/backups" "wygj-aihr.service" \ "$old_sha" "$candidate_sha" \ "0000000000000000000000000000000000000000" )" backup_dir="$(sed -n 's/^backup_dir=//p' <<<"$backup_output")" [[ "$backup_dir" == "$test_tmp"/backups/backend-* ]] [[ "$(sha256sum "$backup_dir/ruoyi-admin.jar" | awk '{print $1}')" == "$old_sha" ]] [[ -f "$backup_dir/release-manifest.txt" ]] run_remote_block_locally REMOTE_ACTIVATE \ "$target" "$candidate" "wygj-aihr.service" "$old_sha" "$candidate_sha" [[ "$(sha256sum "$target" | awk '{print $1}')" == "$candidate_sha" ]] [[ ! -e "$candidate" ]] run_remote_block_locally REMOTE_RESTORE \ "$backup_dir/ruoyi-admin.jar" "$target" "wygj-aihr.service" \ "$old_sha" "$candidate_sha" [[ "$(sha256sum "$target" | awk '{print $1}')" == "$old_sha" ]] printf 'concurrent-backend\n' > "$target" concurrent_sha="$(sha256sum "$target" | awk '{print $1}')" if run_remote_block_locally REMOTE_RESTORE \ "$backup_dir/ruoyi-admin.jar" "$target" "wygj-aihr.service" \ "$old_sha" "$candidate_sha"; then echo 'FAIL: restore accepted a concurrently changed target' >&2 exit 1 fi [[ "$(sha256sum "$target" | awk '{print $1}')" == "$concurrent_sha" ]] safety_output="$( run_remote_block_locally REMOTE_ROLLBACK_SAFETY \ "$target" "$test_tmp/backups" "wygj-aihr.service" \ "$concurrent_sha" "$old_sha" )" safety_dir="$(sed -n 's/^safety_dir=//p' <<<"$safety_output")" [[ "$safety_dir" == "$test_tmp"/backups/backend-rollback-safety-* ]] [[ "$(sha256sum "$safety_dir/ruoyi-admin.jar" | awk '{print $1}')" == "$concurrent_sha" ]] [[ -f "$safety_dir/rollback-manifest.txt" ]] grep -Fq 'REMOTE_SSH="YCWY"' "$SCRIPT" grep -Fq 'REMOTE_PATH="/opt/wygj/app/ruoyi-admin.jar"' "$SCRIPT" grep -Fq 'REMOTE_SERVICE="wygj-aihr.service"' "$SCRIPT" grep -Fq 'mode="${1:-plan}"' "$SCRIPT" grep -Fq 'plan and rollback-plan are read-only' "$SCRIPT" grep -Fq 'require_approval "DEPLOY_BACKEND:$expected_sha256"' "$SCRIPT" grep -Fq 'require_approval "ROLLBACK_BACKEND:$expected_sha256"' "$SCRIPT" grep -Fq 'deploy and rollback require a clean Git worktree' "$SCRIPT" grep -Fq 'run_post_deploy_preflight' "$SCRIPT" grep -Fq 'PUBLIC_HEALTH_READY_TIMEOUT_SECONDS=90' "$SCRIPT" grep -Fq 'wait_for_public_health' "$SCRIPT" grep -Fq 'if ! wait_for_public_health || ! run_post_deploy_preflight; then' "$SCRIPT" grep -Fq 'post-deploy preflight failed; restoring' "$SCRIPT" grep -Fq 'rollback health failed; restoring safety copy' "$SCRIPT" grep -Fq 'expected_target_sha="$5"' "$SCRIPT" grep -Fq 'concurrent target change' "$SCRIPT" grep -Fq 'remote backend release requires the fixed root deployment account' "$SCRIPT" grep -Fq 'cp -a "$target" "$backup_dir/ruoyi-admin.jar"' "$SCRIPT" grep -Fq 'mv -T "$staging" "$target"' "$SCRIPT" grep -Fq 'RELEASE_VERIFY_REMOTE_BACKEND=true' "$SCRIPT" grep -Fq 'RELEASE_VERIFY_REMOTE_SCHEMA=true' "$SCRIPT" if grep -Eq '(^|[[:space:]])rm([[:space:]]|$)' "$SCRIPT"; then echo 'FAIL: backend release script must not delete release or backup files' >&2 exit 1 fi deploy_without_approval="$( bash "$SCRIPT" deploy \ --artifact missing.jar \ --artifact-commit HEAD \ --expected-sha256 0000000000000000000000000000000000000000000000000000000000000000 \ 2>&1 || true )" grep -Fq 'explicit approval required' <<<"$deploy_without_approval" if grep -Fq 'release JAR not found' <<<"$deploy_without_approval"; then echo 'FAIL: deploy touched artifact or remote planning before approval' >&2 exit 1 fi rollback_without_approval="$( bash "$SCRIPT" rollback \ --backup-dir /opt/wygj/backups/backend-20260729000000-000000000000 \ --expected-sha256 0000000000000000000000000000000000000000000000000000000000000000 \ 2>&1 || true )" grep -Fq 'explicit approval required' <<<"$rollback_without_approval" if grep -Fq 'read-only rollback inspection failed' <<<"$rollback_without_approval"; then echo 'FAIL: rollback contacted the remote before approval' >&2 exit 1 fi grep -Fq './scripts/release-backend.sh plan' "$DEV_SETUP" grep -Fq './scripts/release-backend.sh deploy' "$DEV_SETUP" grep -Fq './scripts/release-backend.sh rollback-plan' "$DEV_SETUP" echo 'PASS: backend release and rollback require explicit approval and preserve recoverable backups'