506 lines
19 KiB
Bash
Executable File
506 lines
19 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 激活 prepare-staging-rc.sh 已构建的 commit-pinned RC。
|
||
# 本机只负责经 Tailscale 直连下发;所有起停、证据和回滚都在 mini-desktop 完成。
|
||
set -euo pipefail
|
||
|
||
RC_COMMIT="${RC_COMMIT:-}"
|
||
RC_HOST="${RC_HOST:-root@100.64.0.7}"
|
||
RC_ROOT="${RC_ROOT:-/root/game-staging/releases}"
|
||
ACTIVATION_TIMEOUT_SEC="${ACTIVATION_TIMEOUT_SEC:-180}"
|
||
if [ -n "${GENERATION_GATE_EVIDENCE_REF:-}" ] && [ -n "${GENERATION_GATE_REF:-}" ] \
|
||
&& [ "$GENERATION_GATE_EVIDENCE_REF" != "$GENERATION_GATE_REF" ]; then
|
||
printf 'GENERATION_GATE_EVIDENCE_REF 与 GENERATION_GATE_REF 冲突,拒绝选择其一继续。\n' >&2
|
||
exit 2
|
||
fi
|
||
GENERATION_GATE_EVIDENCE_REF="${GENERATION_GATE_EVIDENCE_REF:-${GENERATION_GATE_REF:-}}"
|
||
# 新变量表达真实语义;旧变量仅兼容受控 hash + RC 内相对路径格式。
|
||
if [ -n "${STORAGE_AUDIT_REF:-}" ] && [ -n "${OBJECT_SNAPSHOT_REF:-}" ] \
|
||
&& [ "$STORAGE_AUDIT_REF" != "$OBJECT_SNAPSHOT_REF" ]; then
|
||
printf 'STORAGE_AUDIT_REF 与旧 OBJECT_SNAPSHOT_REF 冲突,拒绝选择其一继续。\n' >&2
|
||
exit 2
|
||
fi
|
||
STORAGE_AUDIT_REF="${STORAGE_AUDIT_REF:-${OBJECT_SNAPSHOT_REF:-}}"
|
||
SSH_BIN="${SSH_BIN:-ssh}"
|
||
|
||
if [[ ! "$RC_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
|
||
printf 'RC_COMMIT 必须是完整 40 位小写提交哈希。\n' >&2
|
||
exit 2
|
||
fi
|
||
if [[ ! "$ACTIVATION_TIMEOUT_SEC" =~ ^[0-9]+$ ]] || [ "$ACTIVATION_TIMEOUT_SEC" -lt 30 ]; then
|
||
printf 'ACTIVATION_TIMEOUT_SEC 必须是至少 30 秒的整数。\n' >&2
|
||
exit 2
|
||
fi
|
||
validate_evidence_ref() {
|
||
local ref="$1" path component
|
||
[[ "$ref" =~ ^sha256:[0-9a-f]{64}:[A-Za-z0-9._/-]+$ ]] || return 1
|
||
path="${ref#sha256:}"
|
||
path="${path#*:}"
|
||
[ -n "$path" ] && [[ "$path" != /* ]] || return 1
|
||
IFS='/' read -r -a components <<<"$path"
|
||
for component in "${components[@]}"; do
|
||
[ -n "$component" ] && [ "$component" != '.' ] && [ "$component" != '..' ] \
|
||
&& [ "$component" != '.git' ] || return 1
|
||
done
|
||
}
|
||
if [ -z "$GENERATION_GATE_EVIDENCE_REF" ] || ! validate_evidence_ref "$GENERATION_GATE_EVIDENCE_REF"; then
|
||
printf 'GENERATION_GATE_EVIDENCE_REF 必填,格式为 sha256:<64hex>:<安全相对路径>。\n' >&2
|
||
exit 2
|
||
fi
|
||
if [ -n "$STORAGE_AUDIT_REF" ] \
|
||
&& [[ ! "$STORAGE_AUDIT_REF" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]]; then
|
||
printf 'STORAGE_AUDIT_REF 非法:只接受 sha256:<64hex>:evidence/storage-audit.tsv;留空则远端实时生成。\n' >&2
|
||
exit 2
|
||
fi
|
||
|
||
printf '激活 staging RC:host=%s commit=%s\n' "$RC_HOST" "$RC_COMMIT"
|
||
printf '注意:该操作会执行数据库迁移并切换五个 staging 服务。\n'
|
||
|
||
"$SSH_BIN" -o ProxyCommand=none "$RC_HOST" bash -s -- \
|
||
"$RC_COMMIT" "$RC_ROOT" "$ACTIVATION_TIMEOUT_SEC" "$STORAGE_AUDIT_REF" \
|
||
"$GENERATION_GATE_EVIDENCE_REF" <<'REMOTE_SCRIPT'
|
||
set -Eeuo pipefail
|
||
|
||
commit="$1"
|
||
root="$2"
|
||
timeout_sec="$3"
|
||
expected_storage_audit_ref="$4"
|
||
expected_generation_gate_ref="$5"
|
||
release="$root/$commit"
|
||
active_pointer="$root/active"
|
||
stable_prefix="gda-staging"
|
||
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
|
||
short="${commit:0:12}"
|
||
candidate_prefix="gda-rc-${short}-${run_id}"
|
||
evidence="$release/evidence/activation-$run_id"
|
||
activation_log="$evidence/activation.log"
|
||
env_file="/root/huijing-dev.env"
|
||
switch_started=0
|
||
pointer_changed=0
|
||
previous_was_active=0
|
||
previous="none"
|
||
previous_valid=0
|
||
|
||
mkdir -p "$evidence"
|
||
chmod 700 "$evidence"
|
||
umask 077
|
||
exec > >(tee -a "$activation_log") 2>&1
|
||
|
||
log_step() {
|
||
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1"
|
||
}
|
||
|
||
unit_name() {
|
||
printf '%s-%s' "$1" "$2"
|
||
}
|
||
|
||
stop_stack() {
|
||
local prefix="$1" service unit
|
||
for service in backend studio admin cheap-agent cheap-worker; do
|
||
unit="$(unit_name "$prefix" "$service")"
|
||
systemctl stop "$unit" >/dev/null 2>&1 || true
|
||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||
done
|
||
}
|
||
|
||
capture_journals() {
|
||
local prefix="$1" label="$2" service unit
|
||
mkdir -p "$evidence/journal-$label"
|
||
for service in backend studio admin cheap-agent cheap-worker; do
|
||
unit="$(unit_name "$prefix" "$service")"
|
||
journalctl -u "$unit" --no-pager -n 300 >"$evidence/journal-$label/$service.log" 2>&1 || true
|
||
done
|
||
}
|
||
|
||
port_listening() {
|
||
local port="$1"
|
||
ss -H -ltn | awk -v suffix=":$port" '$4 ~ suffix"$" {found=1} END {exit !found}'
|
||
}
|
||
|
||
wait_port() {
|
||
local name="$1" port="$2" deadline=$((SECONDS + timeout_sec))
|
||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||
if port_listening "$port"; then
|
||
log_step "$name readiness PASS port=$port"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
log_step "$name readiness FAIL port=$port"
|
||
return 1
|
||
}
|
||
|
||
wait_url() {
|
||
local name="$1" url="$2" deadline=$((SECONDS + timeout_sec)) code
|
||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||
code="$(curl -sS --noproxy '*' --connect-timeout 2 --max-time 5 -o /dev/null -w '%{http_code}' "$url" 2>/dev/null || true)"
|
||
if [ "$code" = "200" ]; then
|
||
log_step "$name readiness PASS http=200"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
log_step "$name readiness FAIL url=$url"
|
||
return 1
|
||
}
|
||
|
||
wait_json_health() {
|
||
local name="$1" url="$2" field="$3" expected="$4" deadline=$((SECONDS + timeout_sec)) body
|
||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||
body="$(curl -sS --noproxy '*' --connect-timeout 2 --max-time 5 "$url" 2>/dev/null || true)"
|
||
if HEALTH_FIELD="$field" HEALTH_EXPECTED="$expected" python3 -c '
|
||
import json,os,sys
|
||
value=json.load(sys.stdin)
|
||
for part in os.environ["HEALTH_FIELD"].split("."):
|
||
value=value.get(part) if isinstance(value,dict) else None
|
||
assert str(value).lower() == os.environ["HEALTH_EXPECTED"].lower()
|
||
' <<<"$body" >/dev/null 2>&1; then
|
||
log_step "$name readiness PASS"
|
||
return 0
|
||
fi
|
||
sleep 2
|
||
done
|
||
log_step "$name readiness FAIL url=$url"
|
||
return 1
|
||
}
|
||
|
||
run_unit() {
|
||
local unit="$1" launcher="$2"
|
||
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||
systemd-run --quiet --collect --unit="$unit" \
|
||
--property=Restart=on-failure --property=RestartSec=2s \
|
||
--property=TimeoutStopSec=30s "$launcher"
|
||
}
|
||
|
||
write_launchers() {
|
||
local target="$1" label="$2" backend_port="$3" studio_port="$4" admin_port="$5"
|
||
local agent_port="$6" worker_port="$7" discovery_enabled="$8"
|
||
local launch_dir="$evidence/launchers-$label" quoted_target
|
||
quoted_target="$(printf '%q' "$target")"
|
||
mkdir -p "$launch_dir"
|
||
|
||
cat >"$launch_dir/backend.sh" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
set -a
|
||
source /root/huijing-dev.env
|
||
set +a
|
||
export SPRING_PROFILES_ACTIVE=staging
|
||
export AIGC_WORKER_URL=http://127.0.0.1:${worker_port}/generate
|
||
exec java -jar ${quoted_target}/game-cloud/huijing-server/target/huijing-server.jar \\
|
||
--server.port=${backend_port} --spring.cloud.nacos.discovery.enabled=${discovery_enabled}
|
||
EOF
|
||
|
||
cat >"$launch_dir/studio.sh" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
cd ${quoted_target}/game-studio
|
||
exec node node_modules/vite/bin/vite.js preview \\
|
||
--host 0.0.0.0 --port ${studio_port} --strictPort --outDir dist
|
||
EOF
|
||
|
||
cat >"$launch_dir/admin.sh" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
cd ${quoted_target}/game-admin
|
||
exec node node_modules/vite/bin/vite.js preview \\
|
||
--host 0.0.0.0 --port ${admin_port} --strictPort --outDir dist-stage
|
||
EOF
|
||
|
||
cat >"$launch_dir/cheap-agent.sh" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
set -a
|
||
source /root/huijing-dev.env
|
||
set +a
|
||
export NO_PROXY=100.64.0.8,localhost,127.0.0.1
|
||
export no_proxy=100.64.0.8,localhost,127.0.0.1
|
||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
|
||
export CHEAP_SERVICE_PORT=${agent_port}
|
||
cd ${quoted_target}/cheap-worker
|
||
exec .venv/bin/python cheap_service_app.py
|
||
EOF
|
||
|
||
cat >"$launch_dir/cheap-worker.sh" <<EOF
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
set -a
|
||
source /root/huijing-dev.env
|
||
set +a
|
||
: "\${AIGC_CALLBACK_SECRET:?AIGC_CALLBACK_SECRET 未配置}"
|
||
export NO_PROXY=100.64.0.8,localhost,127.0.0.1
|
||
export no_proxy=100.64.0.8,localhost,127.0.0.1
|
||
unset http_proxy https_proxy HTTP_PROXY HTTPS_PROXY
|
||
export CHEAP_SERVICE_URL=http://127.0.0.1:${agent_port}
|
||
cd ${quoted_target}/cheap-worker
|
||
exec .venv/bin/python worker_service.py --port ${worker_port} --callback-secret "\$AIGC_CALLBACK_SECRET"
|
||
EOF
|
||
|
||
chmod 700 "$launch_dir"/*.sh
|
||
printf '%s' "$launch_dir"
|
||
}
|
||
|
||
start_stack() {
|
||
local target="$1" label="$2" prefix="$3" backend_port="$4" studio_port="$5" admin_port="$6"
|
||
local agent_port="$7" worker_port="$8" discovery_enabled="$9" launch_dir
|
||
launch_dir="$(write_launchers "$target" "$label" "$backend_port" "$studio_port" "$admin_port" \
|
||
"$agent_port" "$worker_port" "$discovery_enabled")"
|
||
|
||
run_unit "$(unit_name "$prefix" cheap-agent)" "$launch_dir/cheap-agent.sh"
|
||
wait_port "$label cheap-agent" "$agent_port"
|
||
run_unit "$(unit_name "$prefix" cheap-worker)" "$launch_dir/cheap-worker.sh"
|
||
wait_json_health "$label cheap-worker" "http://127.0.0.1:$worker_port/health" ok true
|
||
run_unit "$(unit_name "$prefix" backend)" "$launch_dir/backend.sh"
|
||
wait_json_health "$label backend" "http://127.0.0.1:$backend_port/actuator/health" status UP
|
||
run_unit "$(unit_name "$prefix" studio)" "$launch_dir/studio.sh"
|
||
wait_url "$label studio" "http://127.0.0.1:$studio_port/"
|
||
run_unit "$(unit_name "$prefix" admin)" "$launch_dir/admin.sh"
|
||
wait_url "$label admin" "http://127.0.0.1:$admin_port/"
|
||
}
|
||
|
||
valid_prepared_release() {
|
||
local target="$1" expected="${2:-}"
|
||
[ -d "$target/.git" ] || return 1
|
||
[ -f "$target/evidence/artifact-paths.env" ] || return 1
|
||
[ -f "$target/game-cloud/huijing-server/target/huijing-server.jar" ] || return 1
|
||
[ -d "$target/game-studio/dist" ] || return 1
|
||
[ -d "$target/game-admin/dist-stage" ] || return 1
|
||
[ -x "$target/cheap-worker/.venv/bin/python" ] || return 1
|
||
if [ -n "$expected" ]; then
|
||
[ "$(git -C "$target" rev-parse HEAD)" = "$expected" ] || return 1
|
||
fi
|
||
}
|
||
|
||
verify_generation_gate_evidence() {
|
||
local ref="$1" expected_commit="$2" hash_and_path expected_hash relative_path actual_hash object_type
|
||
hash_and_path="${ref#sha256:}"
|
||
expected_hash="${hash_and_path%%:*}"
|
||
relative_path="${hash_and_path#*:}"
|
||
|
||
# wrapper 已做路径语法校验;远端仍独立确认它是候选 commit 中的普通 tracked 文件。
|
||
object_type="$(git -C "$release" cat-file -t "$expected_commit:$relative_path" 2>/dev/null || true)"
|
||
[ "$object_type" = blob ] || {
|
||
log_step "生成门证据不是候选 commit 内的 tracked blob path=$relative_path"
|
||
return 1
|
||
}
|
||
[ -f "$release/$relative_path" ] && [ ! -L "$release/$relative_path" ] || {
|
||
log_step "生成门证据不是 RC 内普通文件 path=$relative_path"
|
||
return 1
|
||
}
|
||
actual_hash="$(sha256sum "$release/$relative_path" | awk '{print $1}')"
|
||
[ "$actual_hash" = "$expected_hash" ] || {
|
||
log_step "生成门证据 SHA-256 不匹配 path=$relative_path"
|
||
return 1
|
||
}
|
||
|
||
# 证据由 R1 执行方生成,激活器只验证,不创建或改写任何“通过”结论。
|
||
python3 - "$release/$relative_path" "$expected_commit" <<'PY'
|
||
import sys
|
||
|
||
path, expected_commit = sys.argv[1:]
|
||
values = {}
|
||
with open(path, encoding="utf-8") as handle:
|
||
for line_number, raw in enumerate(handle, 1):
|
||
line = raw.strip()
|
||
if not line or line.startswith("#"):
|
||
continue
|
||
if "=" not in line:
|
||
raise SystemExit(f"生成门证据第 {line_number} 行不是 key=value")
|
||
key, value = line.split("=", 1)
|
||
key, value = key.strip(), value.strip()
|
||
if not key or key in values:
|
||
raise SystemExit(f"生成门证据键为空或重复: {key!r}")
|
||
values[key] = value
|
||
|
||
required = {
|
||
"schema": "generation-r1-gate/1",
|
||
"rc_commit": expected_commit,
|
||
"r1_status": "PASS",
|
||
"actor_prompt_eval": "PASS",
|
||
"judge_prompt_eval": "PASS",
|
||
"game_content_gold": "PASS",
|
||
"live_prompt": "PASS",
|
||
"full_gate": "PASS",
|
||
"fresh_25": "PASS",
|
||
"historical_11": "PASS",
|
||
"production_shadow_20": "PASS",
|
||
"runtime_eligible": "true",
|
||
}
|
||
for key, expected in required.items():
|
||
actual = values.get(key)
|
||
if actual != expected:
|
||
raise SystemExit(f"生成门证据未通过 {key}: expected={expected} actual={actual!r}")
|
||
PY
|
||
|
||
printf 'generation_gate_ref=%s\ngeneration_gate_path=%s\n' "$ref" "$relative_path" \
|
||
>"$evidence/generation-gate-result.txt"
|
||
log_step "生成 R1 门证据校验 PASS path=$relative_path"
|
||
}
|
||
|
||
restore_active_pointer() {
|
||
local target="$1" temp="$root/.active-rollback-$run_id"
|
||
rm -f "$temp"
|
||
if [ "$target" = "none" ]; then
|
||
rm -f "$active_pointer"
|
||
else
|
||
ln -s "$target" "$temp"
|
||
mv -Tf "$temp" "$active_pointer"
|
||
fi
|
||
}
|
||
|
||
failure_handler() {
|
||
local rc=$?
|
||
trap - ERR
|
||
set +e
|
||
log_step "ACTIVATION FAIL rc=${rc},开始保留证据并恢复旧栈"
|
||
capture_journals "$candidate_prefix" candidate-failed
|
||
capture_journals "$stable_prefix" stable-failed
|
||
stop_stack "$candidate_prefix"
|
||
if [ "$switch_started" -eq 1 ]; then
|
||
stop_stack "$stable_prefix"
|
||
if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -eq 1 ]; then
|
||
log_step "恢复旧版本服务 target=$previous"
|
||
start_stack "$previous" rollback "$stable_prefix" 48080 4173 4174 8300 9501 true
|
||
rollback_rc=$?
|
||
printf 'rollback_service_rc=%s\n' "$rollback_rc" >"$evidence/rollback-result.txt"
|
||
else
|
||
printf 'rollback_service_rc=not-required\n' >"$evidence/rollback-result.txt"
|
||
fi
|
||
fi
|
||
if [ "$pointer_changed" -eq 1 ]; then
|
||
restore_active_pointer "$previous"
|
||
fi
|
||
printf 'failed commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$evidence" \
|
||
>"$release/RC_STATUS"
|
||
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
|
||
log_step "ACTIVATION_EVIDENCE=$evidence"
|
||
exit "$rc"
|
||
}
|
||
trap failure_handler ERR
|
||
|
||
log_step "校验 commit-pinned RC 与配置"
|
||
valid_prepared_release "$release" "$commit"
|
||
grep -q "^ready commit=$commit " "$release/RC_STATUS"
|
||
[ -z "$(git -C "$release" status --porcelain --untracked-files=no)" ]
|
||
|
||
# 生成门是数据库访问、Nacos/服务切换之前的硬前置;引用缺失、漂移或 R1 任一项不绿立即失败。
|
||
verify_generation_gate_evidence "$expected_generation_gate_ref" "$commit"
|
||
|
||
[ -f "$env_file" ]
|
||
case "$(stat -c '%a' "$env_file")" in
|
||
*00) ;;
|
||
*) log_step "拒绝读取权限过宽的 $env_file"; false ;;
|
||
esac
|
||
|
||
# 只校验键存在和非空,绝不输出值。
|
||
(
|
||
set -a
|
||
source "$env_file"
|
||
set +a
|
||
: "${MYSQL_ROOT_PASSWORD:?}" "${REDIS_PASSWORD:?}" "${NACOS_PASSWORD:?}"
|
||
: "${AIGC_CALLBACK_SECRET:?}" "${NEWAPI_KEY:?}"
|
||
[ "${AIGC_EXECUTOR_ENABLED:-}" = "true" ]
|
||
)
|
||
|
||
cd "$release"
|
||
sha256sum -c evidence/artifacts.sha256 >"$evidence/artifact-verification.txt"
|
||
|
||
log_step "生成并校验 DB-only 存储审计"
|
||
storage_audit_output="$(RC_RELEASE="$release" EXPECTED_STORAGE_AUDIT_REF="$expected_storage_audit_ref" \
|
||
bash "$release/deploy/generate-staging-storage-audit.sh")"
|
||
storage_audit_ref="$(printf '%s\n' "$storage_audit_output" | sed -n 's/^STORAGE_AUDIT_PASS ref=//p')"
|
||
[[ "$storage_audit_ref" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]]
|
||
cp "$release/evidence/storage-audit.tsv" "$evidence/storage-audit.tsv"
|
||
cp "$release/evidence/storage-audit.ref" "$evidence/storage-audit.ref"
|
||
printf 'storage_audit_ref=%s\n' "$storage_audit_ref" >"$evidence/storage-audit-result.txt"
|
||
log_step "$storage_audit_output"
|
||
|
||
if [ -L "$active_pointer" ]; then
|
||
previous="$(readlink -f "$active_pointer" || true)"
|
||
[ -n "$previous" ] || previous="none"
|
||
elif [ -L "$root/current" ]; then
|
||
# 首次迁移兼容旧 current 指针;成功后只维护 canonical active。
|
||
previous="$(readlink -f "$root/current" || true)"
|
||
[ -n "$previous" ] || previous="none"
|
||
fi
|
||
if [ "$previous" != "none" ] && valid_prepared_release "$previous"; then
|
||
previous_valid=1
|
||
fi
|
||
for service in backend studio admin cheap-agent cheap-worker; do
|
||
if systemctl is-active --quiet "$(unit_name "$stable_prefix" "$service")"; then
|
||
previous_was_active=1
|
||
fi
|
||
done
|
||
if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -ne 1 ]; then
|
||
log_step "已有受管服务运行,但 active/current 未指向可回滚 RC,拒绝切换"
|
||
false
|
||
fi
|
||
printf 'previous=%s\nprevious_was_active=%s\n' "$previous" "$previous_was_active" >"$evidence/activation-context.txt"
|
||
|
||
log_step "迁移前数据库快照"
|
||
docker inspect game-staging-mysql >/dev/null
|
||
docker exec game-staging-mysql sh -lc \
|
||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump -uroot --single-transaction --routines --events --databases ruoyi-vue-pro' \
|
||
| gzip -9 >"$evidence/pre-migration-ruoyi-vue-pro.sql.gz"
|
||
test -s "$evidence/pre-migration-ruoyi-vue-pro.sql.gz"
|
||
chmod 600 "$evidence/pre-migration-ruoyi-vue-pro.sql.gz"
|
||
|
||
log_step "在隔离临时 MySQL 容器中验证快照可恢复"
|
||
mysql_image="$(docker inspect game-staging-mysql --format '{{.Config.Image}}')"
|
||
DUMP_FILE="$evidence/pre-migration-ruoyi-vue-pro.sql.gz" \
|
||
EXPECTED_AUDIT_FILE="$evidence/storage-audit.tsv" MYSQL_IMAGE="$mysql_image" \
|
||
bash "$release/deploy/verify-mysql-dump-restore.sh" >"$evidence/mysql-restore-verify.log" 2>&1
|
||
grep -q '^RESTORE_VERIFY_PASS ' "$evidence/mysql-restore-verify.log"
|
||
log_step "数据库快照隔离恢复验证 PASS"
|
||
|
||
log_step "启动候选栈;backend 启动时先执行 Flyway migrations"
|
||
for port in 18080 14173 14174 18300 19501; do
|
||
if port_listening "$port"; then
|
||
log_step "候选端口已占用 port=$port"
|
||
false
|
||
fi
|
||
done
|
||
start_stack "$release" candidate "$candidate_prefix" 18080 14173 14174 18300 19501 false
|
||
|
||
docker exec game-staging-mysql sh -lc \
|
||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT installed_rank,version,success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 20"' \
|
||
>"$evidence/flyway-after-candidate.tsv"
|
||
flyway_failed_count="$(docker exec game-staging-mysql sh -lc \
|
||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT COUNT(*) FROM flyway_schema_history WHERE success <> 1"')"
|
||
[ "$flyway_failed_count" = "0" ]
|
||
|
||
log_step "候选栈执行只读 smoke"
|
||
BASE=http://127.0.0.1:18080 STUDIO_BASE=http://127.0.0.1:14173 \
|
||
bash "$release/deploy/smoke-test.sh" >"$evidence/candidate-smoke.log" 2>&1
|
||
capture_journals "$candidate_prefix" candidate-ready
|
||
|
||
log_step "候选栈全绿,开始切换正式五端口"
|
||
switch_started=1
|
||
capture_journals "$stable_prefix" pre-switch-old
|
||
stop_stack "$stable_prefix"
|
||
for port in 48080 4173 4174 8300 9501; do
|
||
if port_listening "$port"; then
|
||
log_step "正式端口仍被非受管进程占用 port=$port"
|
||
false
|
||
fi
|
||
done
|
||
|
||
start_stack "$release" stable "$stable_prefix" 48080 4173 4174 8300 9501 true
|
||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||
bash "$release/deploy/smoke-test.sh" >"$evidence/stable-smoke.log" 2>&1
|
||
capture_journals "$stable_prefix" stable-ready
|
||
|
||
log_step "正式栈全绿,原子切换 active 指针"
|
||
active_temp="$root/.active-$run_id"
|
||
ln -s "$release" "$active_temp"
|
||
mv -Tf "$active_temp" "$active_pointer"
|
||
pointer_changed=1
|
||
printf 'active commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$evidence" \
|
||
>"$release/RC_STATUS"
|
||
|
||
stop_stack "$candidate_prefix"
|
||
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
|
||
trap - ERR
|
||
log_step "RC_ACTIVATION_PASS commit=$commit active=$release"
|
||
log_step "ACTIVATION_EVIDENCE=$evidence"
|
||
REMOTE_SCRIPT
|